feat(datafusion): support ANALYZE TABLE on catalog-managed format tables - #815
Conversation
9ea2505 to
985e5a9
Compare
7eec84d to
fa132af
Compare
Split out of apache#591. A Format Table whose partitions the REST catalog manages had no way to report what those partitions hold. ANALYZE TABLE t [PARTITION (...)] COMPUTE STATISTICS [NOSCAN] measures the registered partitions from storage and reports the result through create_partitions_with_statistics with replaceStatistics, as Java PaimonAnalyzeFormatTablePartitionsCommand does: - NOSCAN stops at the listing: file count, byte size and the latest file modification time. A full ANALYZE also reads Parquet and ORC footers for row counts; a footer that cannot be read leaves the partition's row count unknown rather than short, and an empty partition holds exactly zero rows. - PARTITION (...) selects a leading run of partition values. A prefix with no registered partition, a partition at a custom location, FOR COLUMNS and CACHE METADATA are refused, as is a table whose partitions the catalog does not manage. - format-table.statistics.parallelism (default 8) bounds the listings and footer reads in flight. The collector lists each partition through the same helper the scan now uses, so a measurement counts exactly the files a scan of that partition reads and leaves committer staging trees out. Its streams own their items so that the future of every SQLContext statement stays Send.
Describe what NOSCAN and a full ANALYZE measure, how PARTITION selects partitions, what is refused, and format-table.statistics.parallelism.
Comment blocks stay within two lines; the reasoning they carried moves here. - ANALYZE measures the registered partitions from storage, and each measured field replaces what the catalog holds, so a table catches up with writers the catalog never saw. NOSCAN stops at what a listing gives (file count, byte size, last file creation time); a full ANALYZE also reads every file footer for its row count. - Analyzing never adds or removes a partition. There is no lock between its listing and the write, so a partition dropped in between can come back with its last measurement, the same last-writer-wins window every lock-free partition operation on these tables has. - `format-table.statistics.parallelism` is a session setting, not a table option, like the Java Spark connector option of the same name. Partition listings and footer reads share it, so it bounds one large partition as much as many small ones. - In a PARTITION clause a column named without a value means every value of it, and the valued columns must be a leading run of the keys: `PARTITION (dt = 'x', hour)` selects every hour of that day, while `PARTITION (hour = '00')` is rejected rather than widened. Values are spelled the way ADD PARTITION writes them, so `p = '01'` selects the INT partition registered as `1`. - The collector lists through the scan listing, so a measurement counts exactly the files a reader returns and leaves committer staging trees out. A listing failure aborts the whole collection, since a truncated listing looks like a partition that lost files. A partition holding nothing measures as an exact zero with no last file to date. The result is a whole-partition measurement that a catalog replaces rather than adds up, and it never decides that a partition should exist. - One file with an unknown row count makes its partition's count unknown: a sum missing a file, reported as exact, is worse than none. - The shared listing returns files whose own name is not hidden and ends with the format's extension, outside any entry that `is_hidden_below_partitions` skips.
fa132af to
c29f3a3
Compare
| let listings: Vec<Vec<FileStatus>> = futures::stream::iter(directories) | ||
| .map(|directory| async move { | ||
| // Each directory is a complete partition, so no partition level lies below it. | ||
| list_format_table_data_files(file_io, &directory, 0, format_extension).await |
There was a problem hiding this comment.
[P2] Propagate listing failures before replacing partition statistics
The reused helper returns an empty list after any listing error when file_io.exists(root) is false. In the locked OpenDAL 0.58.2, OSS/S3 exists performs HEAD on the exact key: table/dt=a can return 404 even when table/dt=a/part.parquet exists. If LIST or a subsequent page fails and HEAD returns 404, full ANALYZE treats the partition as empty and replaces its record count, file count, and byte size with zero; NOSCAN also replaces the latter two with zero.
I reproduced this with fault injection: listing returned one file and then an error, while stat on the partition prefix returned NotFound. collect() returned Ok with all three statistics set to zero.
Please treat only an actual NotFound from the listing as an empty partition and propagate other listing failures before writing statistics back to the catalog.
There was a problem hiding this comment.
Good catch, and thanks for the fault-injection repro. Only a not-found root now reads as an empty partition; any other listing failure is returned before statistics are written, which also covers the scan path (1f87213).
…listing A listing failure was read as an empty directory whenever `exists` on the root returned false. On OSS and S3 in the locked OpenDAL 0.58.2, `exists` is a HEAD on the exact key, so `table/dt=a` can report 404 while `table/dt=a/part.parquet` exists. A LIST or a later page that failed then made ANALYZE replace the partition's record count, file count and size with zero, and made a scan silently skip the partition's files. The listing now reads only a not-found error reported before anything was listed as an empty directory, as Java FormatTableScan.listDataFiles and FormatTablePartitionStatsCollector do with FileNotFoundException. Every other failure, including a not-found after entries were listed, is returned before any statistics are written back.
…em-table * upstream/main: feat(datafusion): support ANALYZE TABLE on catalog-managed format tables (apache#815) refactor(table): unify vector search scan and read APIs (apache#822) fix(rest): sign the query string the client actually sends (apache#821) feat(datafusion): add MSCK REPAIR TABLE for catalog-managed format tables (apache#817) feat(table): execute a primary-key vector search over engine-planned splits (apache#771) feat(datafusion): add SHOW, ADD and DROP PARTITION for catalog-managed format tables (apache#816) feat(table): read the registered partitions of a catalog-managed format table (apache#814) fix: report unsupported time travel instead of returning current data (apache#753) fix(file_index): skip pruning for narrowing integer schema changes (apache#806) fix(c): align append vector filter with core behavior (apache#793) fix(table): skip staging files and list format table partitions concurrently (apache#813) feat(rest): add partition registration, lookup and statistics APIs (apache#812) fix(datafusion): keep format table partition columns out of decoder filters (apache#811) fix(read): null-fill nested fields a data file predates (apache#805) fix(scan): prune NOT IN predicates using file stats (apache#795) fix(spec): accept the legacy first_not_null_value aggregate name (apache#794) fix(table): check batch arity instead of asserting it in debug builds (apache#801) fix(vindex): bound a deletion-vector position by its own source file (apache#802) fix(lumina): decide on the index size before parsing the metric (apache#803) # Conflicts: # crates/integrations/datafusion/src/physical_plan/scan.rs
Purpose
Last child PR split out of #591. A Format Table loaded from a REST catalog with
metastore.partitioned-table=truehas its partitions registered in the catalog, and the catalog can hold statistics for each of them: record count, file count, size and last file creation time. The Rust client had no way to measure a partition and report them. This addsANALYZE TABLEfor such tables, following the JavaPaimonAnalyzeFormatTablePartitionsCommandandFormatTablePartitionStatsCollector.#816 and #817 have merged, so this PR now applies to
mainon its own.This PR was rebased onto the restructured stack. ANALYZE itself is unchanged: its tests now use the shared helpers of #816, and a value in
PARTITION (...)is read by the sharedparse_format_partition_valuethat #816 introduced for partition literals.Brief change log
ANALYZE TABLE t [PARTITION (...)] COMPUTE STATISTICS [NOSCAN]on a Format Table with catalog-managed partitions measures the registered partitions from storage and reports them throughCatalog::create_partitions_with_statisticswithreplaceStatistics=true, so each measured field replaces what the catalog holds. It never adds or removes a partition.NOSCANreports the file count, total size and latest file modification time, and leaves the row count unknown (-1).PARTITION (...)selects a leading run of partition values; a column named without a value means every value of it. A prefix with no registered partition is an error.FOR COLUMNS,CACHE METADATA, a missingCOMPUTE STATISTICS, tables whose partitions the catalog does not manage, an empty or whitespace-only string inPARTITION (...)for a string column (it would name the default partition, as ADD and DROP in feat(datafusion): add SHOW, ADD and DROP PARTITION for catalog-managed format tables #816 refuse), and any selected partition at a custom location.format-table.statistics.parallelism(a session setting,SET 'paimon.<key>', as in Java; default 8, at least 1) bounds the listings and footer reads in flight.FormatTablePartitionStatsCollector, public inpaimon::table, does the measuring. A listing failure fails the whole collection, since a truncated listing cannot be told apart from a partition that lost files. Only a partition directory that the listing itself reports as not found counts as empty, as in Java; an existence check is not trusted, because on object stores a HEAD on the directory key returns 404 even when files lie below it.list_format_table_data_files, which the collector shares, so a measurement counts exactly the files a scan of that partition reads and leaves committer staging trees out (the rule from fix(table): skip staging files and list format table partitions concurrently #813). A scan reads the same files as before, except that a listing failure is no longer read as an empty directory when an existence check on the root returns false.read_file_row_countreads the row count of a Parquet or ORC file from its footer without decoding rows.crates/integrations/datafusion/src/format_partition_analyze.rsand reuses the partition-spec helpers offormat_partition_ddl.rsfrom feat(datafusion): add SHOW, ADD and DROP PARTITION for catalog-managed format tables #816;sql_context.rsonly dispatches it.Follow-up: using the reported row counts in DataFusion scan statistics, as apache/paimon#9351 does for Spark.
Tests
crates/integrations/datafusion/tests/rest_format_partition_sql.rs:PARTITIONselecting a leading run of values, and its errors;FOR COLUMNS, a blank string partition value, a partition at a custom location, a non-positive parallelism read as one, and a table without catalog-managed partitions.format_table_scan::tests::test_data_file_listing_returns_what_a_partition_scan_readsfor the shared listing, andtest_listing_failure_is_not_read_as_an_empty_directoryfor a listing that fails before or after returning files.SQLContext::sqlstaysSendnow covers the new statement too.Commands run on the top commit:
fmt and clippy pass.
paimon: 3000 tests pass.paimon-rest-server: 10 tests pass.paimon-datafusion: 781 pass, including the 16 tests inrest_format_partition_sql.rs, and 39 fail. The failures are the 39 tests that also fail by name onmainin this environment, because they read fixture tables provisioned bymake docker-up(two also need the lumina native library).Additions without a direct Java counterpart
read_file_row_countreads a Parquet or ORC file's row count from its footer. Java counts rows with the format'sSimpleStatsExtractor, which the Rust client does not have for Format Tables.ANALYZEmeasures in process with bounded concurrency, where Spark spreads the footer reads over executors (PaimonAnalyzeFormatTablePartitionsCommand.measureOnExecutors).API and Format
paimon::table::FormatTablePartitionStatsCollector.format-table.statistics.parallelism, read from theSQLContextsession.The storage format is unchanged.
Documentation
docs/src/sql.mdgains anANALYZE TABLEsubsection under Format Table Partitions and listsANALYZE TABLEin the SQL support scope.