morningman commented on code in PR #69571:
URL: https://github.com/apache/doris/pull/69571#discussion_r4367353301
##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -2292,6 +2299,63 @@ static FileStoreTable legacyOrcSplitTable(FileStoreTable
table, DataSplit split)
return base;
}
+ /** Positions in the partition row of the partition columns that hold
binary values. */
+ private static int[] binaryPartitionFields(Table table) {
+ RowType partitionType = table.rowType().project(table.partitionKeys());
+ return java.util.stream.IntStream.range(0,
partitionType.getFieldCount()).filter(field -> {
+ DataTypeRoot typeRoot =
partitionType.getTypeAt(field).getTypeRoot();
+ return typeRoot == DataTypeRoot.BINARY || typeRoot ==
DataTypeRoot.VARBINARY;
+ }).toArray();
+ }
+
+ /**
+ * Paimon finds the files of a partition under the directory its values
name. With {@code partition.legacy-name}
+ * = true, the default, it names a partition after the string of each
value, which for a non-NULL binary value
+ * is the Java array object ({@code [B@1b6d3586}) rather than its bytes,
and every resolution of the name
+ * differs. A file written under such a name, or looked up under one, is
never found: Paimon's own read fails,
+ * while the BE skips a missing file of an external table and would
silently drop the partition's rows. A file
+ * with an external path is found by the path its metadata records.
Without a schema dictionary (system tables
+ * other than {@code $ro}, which the JNI reader reads with Paimon's own
reader) only the lookup is checked.
+ */
+ private void checkBinaryPartitionFiles(PaimonTableHandle handle, Table
table, DataSplit split,
+ int[] binaryPartitionFields, Supplier<Table> schemaTable,
+ Map<FileStoreTable, Map<Long, Boolean>>
legacyPartitionNameSchemas) {
+ BinaryRow partition = split.partition();
+ OptionalInt nonNullField = Arrays.stream(binaryPartitionFields)
+ .filter(field -> !partition.isNullAt(field)).findFirst();
+ if (!nonNullField.isPresent()) {
+ return;
+ }
+ int binaryField = nonNullField.getAsInt();
+ FileStoreTable splitTable = schemaTable.get() == null
+ ? null : legacyOrcSplitTable((FileStoreTable)
schemaTable.get(), split);
+ boolean legacyLookup = CoreOptions.fromMap((splitTable == null ? table
: splitTable).options())
+ .legacyPartitionName();
+ for (DataFileMeta file : split.dataFiles()) {
Review Comment:
Confirmed, and fixed in `65a1682d334`. With `index-file-in-data-file-dir =
true`, a deletion vector records an absolute path only if an external data path
is configured when it is written (`DeletionFileWriter.result`). Otherwise
Paimon resolves it under the partition's directory each time a split is planned
(`IndexInDataFileDirPathFactory.toPath`). Under legacy names that directory is
the identity string of a fresh array. So once the external paths are removed, a
deletion vector written by a DELETE is never found, while its data file keeps
its absolute path. Both native readers then get NOT_FOUND when they open the
deletion vector (V1 `_init_deletion_vector`, V2
`TableReader::_parse_delete_predicates`), and `FileScanner` / `FileScannerV2`
skip the whole data range, surviving rows included. Paimon's own reader, JNI
and Rust fail instead.
For a split with a non-NULL binary partition value on a table that keeps
index files in data directories, every deletion vector the split carries must
now exist when the split is planned, whether or not its data file has an
external path. A missing one fails planning, naming the deletion vector file,
its data file, the partition and the option. The option is immutable, so the
reading table's value is the one every writer used. Deletion vectors under the
table's `index/` directory do not depend on partition names and are not
checked. The existence check runs inside the catalog authentication, once per
deletion vector file and scan.
`PaimonScanPlanProviderTest#externalBinaryPartitionFileWithLocalDeletionVectorFailsScanPlanning`
replays your sequence through the Paimon API: two rows written to one file
with external paths, the paths removed, then one row deleted by persisting a
deletion vector the way `PaimonSparkWriter.persistDeletionVectors` does it.
Without the new check that spli
t plans. `#externalBinaryPartitionDeletionVectorAtExternalPathStaysReadable`
keeps the external paths, so the deletion vector records its absolute path and
the split still plans.
master before this PR and branch-4.2 drop these rows too, since neither
checks binary partitions at all. A BE-side change that makes a missing Paimon
deletion vector an error instead of a skipped range, which would also cover
deletion vectors lost in other ways, is tracked separately.
##########
fe/fe-connector/fe-connector-paimon/src/main/java/org/apache/doris/connector/paimon/PaimonScanPlanProvider.java:
##########
@@ -2292,6 +2299,63 @@ static FileStoreTable legacyOrcSplitTable(FileStoreTable
table, DataSplit split)
return base;
}
+ /** Positions in the partition row of the partition columns that hold
binary values. */
+ private static int[] binaryPartitionFields(Table table) {
+ RowType partitionType = table.rowType().project(table.partitionKeys());
+ return java.util.stream.IntStream.range(0,
partitionType.getFieldCount()).filter(field -> {
+ DataTypeRoot typeRoot =
partitionType.getTypeAt(field).getTypeRoot();
+ return typeRoot == DataTypeRoot.BINARY || typeRoot ==
DataTypeRoot.VARBINARY;
+ }).toArray();
+ }
+
+ /**
+ * Paimon finds the files of a partition under the directory its values
name. With {@code partition.legacy-name}
+ * = true, the default, it names a partition after the string of each
value, which for a non-NULL binary value
+ * is the Java array object ({@code [B@1b6d3586}) rather than its bytes,
and every resolution of the name
+ * differs. A file written under such a name, or looked up under one, is
never found: Paimon's own read fails,
+ * while the BE skips a missing file of an external table and would
silently drop the partition's rows. A file
+ * with an external path is found by the path its metadata records.
Without a schema dictionary (system tables
+ * other than {@code $ro}, which the JNI reader reads with Paimon's own
reader) only the lookup is checked.
+ */
+ private void checkBinaryPartitionFiles(PaimonTableHandle handle, Table
table, DataSplit split,
+ int[] binaryPartitionFields, Supplier<Table> schemaTable,
+ Map<FileStoreTable, Map<Long, Boolean>>
legacyPartitionNameSchemas) {
+ BinaryRow partition = split.partition();
+ OptionalInt nonNullField = Arrays.stream(binaryPartitionFields)
+ .filter(field -> !partition.isNullAt(field)).findFirst();
+ if (!nonNullField.isPresent()) {
+ return;
+ }
+ int binaryField = nonNullField.getAsInt();
+ FileStoreTable splitTable = schemaTable.get() == null
+ ? null : legacyOrcSplitTable((FileStoreTable)
schemaTable.get(), split);
+ boolean legacyLookup = CoreOptions.fromMap((splitTable == null ? table
: splitTable).options())
+ .legacyPartitionName();
+ for (DataFileMeta file : split.dataFiles()) {
+ if (file.externalPath().isPresent()) {
+ continue;
+ }
+ if (legacyLookup || (splitTable != null &&
legacyPartitionNameSchemas
+ .computeIfAbsent(splitTable, ignored -> new HashMap<>())
+ .computeIfAbsent(file.schemaId(), id ->
withSchemaHistoryAuthentication(() -> CoreOptions
Review Comment:
Confirmed. `FileStoreTable.copy` keeps the schema ID (`TableSchema.copy`),
so a file written with a dynamic `partition.legacy-name = false` (`SET
spark.paimon.partition.legacy-name = false`, a Flink hint, or `table.copy`)
refers to a schema whose persisted option is still `true`. Nothing in the
metadata records the directory a file was written to: `DataFileMeta` has only
the file name and an optional external path, and dynamic options are not
persisted. The schema's option is therefore only a hint. Failing on it alone
rejected files that are readable once the table is altered to `false`, and
master reads them.
Fixed in `65a1682d334`. The file's schema still selects which files to look
at, but planning now fails only if such a file is also missing at its planned
path (`bucketPath/fileName`, the path the native reader is given). In a table
whose option was switched after legacy writes, the file is missing and the
error stays. In your sequence the file is there and the scan plans. This costs
one existence call per flagged file, and none for tables whose files were
written under the current naming.
`PaimonScanPlanProviderTest#binaryPartitionFilesWrittenWithDynamicNamesStayReadable`
writes through `table.copy({"partition.legacy-name": "false"})` on a table
that keeps the default: planning fails as a legacy lookup until the option is
altered to `false`, and then plans the file under the bytes of its value.
Without the existence check it fails with the error you describe.
The inverse case is not covered: a file written with a dynamic
`partition.legacy-name = true` while the schema says `false` is still looked up
under its bytes, and the BE skips it. Catching that would take an existence
call for every file of every binary partition on every scan. master and
branch-4.2 behave the same.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]