morningman commented on code in PR #69536:
URL: https://github.com/apache/doris/pull/69536#discussion_r4368992831


##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java:
##########
@@ -2327,8 +2343,73 @@ private static List<Schema> metadataSchemaHistory(Table 
table) {
         return new ArrayList<>(table.schemas().values());
     }
 
-    private static void addHistoricalEqualityFields(
-            List<NestedField> fields, Set<Integer> missingFieldIds, Schema 
historicalSchema) {
+    /**
+     * A writer commits with the schema that is current at commit, so a key 
dropped before its delete commits loses
+     * the schema-only changes made before the drop from every snapshot. Field 
IDs are never reused and a field
+     * only relaxes compatibly, by promotion or from required to optional, so 
each field of the selected definition
+     * takes the widest promotion of its type and the optionality it has in 
any retained schema; neither depends on
+     * the order of the schemas. A field required in every retained schema 
stays required, and a promoted field
+     * keeps its defaults in the promoted type ({@link #promotedDefault}).
+     */
+    private static List<NestedField> withRetainedRelaxations(List<NestedField> 
fields, List<Schema> schemas) {
+        List<NestedField> relaxed = new ArrayList<>(fields.size());
+        for (NestedField field : fields) {
+            Type type = field.type();
+            boolean optional = field.isOptional();
+            for (Schema schema : schemas) {
+                NestedField retained = schema.findField(field.fieldId());
+                if (retained == null) {
+                    continue;
+                }
+                optional |= retained.isOptional();
+                if (type.isPrimitiveType() && retained.type().isPrimitiveType()
+                        && isWideningPromotion(type.asPrimitiveType(), 
retained.type().asPrimitiveType())) {
+                    type = retained.type();
+                }
+            }
+            if (type.isStructType()) {
+                type = 
Types.StructType.of(withRetainedRelaxations(type.asStructType().fields(), 
schemas));
+            }
+            relaxed.add(type.equals(field.type()) && optional == 
field.isOptional()
+                    ? field : 
Types.NestedField.from(field).ofType(type).isOptional(optional)
+                            
.withInitialDefault(promotedDefault(field.initialDefaultLiteral(), 
field.type(), type))
+                            
.withWriteDefault(promotedDefault(field.writeDefaultLiteral(), field.type(), 
type))
+                            .build());
+        }
+        return relaxed;
+    }
+
+    /**
+     * {@link TypeUtil#isPromotionAllowed} plus the v3 promotion of DATE to an 
unzoned TIMESTAMP or TIMESTAMP_NS,
+     * which the SDK's schema update does not implement yet but the format 
allows.
+     */
+    private static boolean isWideningPromotion(Type.PrimitiveType from, 
Type.PrimitiveType to) {
+        if (TypeUtil.isPromotionAllowed(from, to)) {
+            return true;
+        }
+        if (from.typeId() != Type.TypeID.DATE) {

Review Comment:
   Confirmed, and fixed in `9affde5af66`. The v3 promotion table lets `unknown` 
become any type, but Iceberg 1.12.0's `TypeUtil.isPromotionAllowed` and 
`SchemaUpdate` do not implement it, and `isWideningPromotion` only added DATE 
on top. In your history the only snapshot that records k is S1, written while k 
was `unknown`, so the carrier kept `unknown`. The scan dictionary encodes 
`unknown` as STRING, and FileScannerV2 then casts both keys to STRING: the DATE 
key becomes `2024-01-02` and the TIMESTAMP_NS tombstone `2024-01-02 
00:00:00.000000000`, so the deleted row stays visible. Master's reverse lookup 
took the retained TIMESTAMP_NS definition, so this PR introduced the 
regression. V1 compares in the delete file's own key type and was not affected.
   
   `isWideningPromotion` now treats `unknown` as promotable to any primitive 
type, so the recovered definition takes the widest retained promotion, 
TIMESTAMP_NS here. An `unknown` field only has null defaults, so no default 
conversion is involved. 
`equalityCarrierFollowsUnknownKeyPromotedWithoutASnapshot` replays your history 
(both promotions published as schema-only changes, a pending append and a 
pending delete committed after the drop) and checks the carrier and the scan 
dictionary; without the new rule the carrier stays `unknown`.
   
   If metadata cleanup has removed every schema that gave k a concrete type, 
the carrier has nothing to promote to and stays `unknown`, as on master. That 
is the same limitation as the cleanup case discussed earlier: the comparison 
type then has to come from the files, which I'm tracking as the FileScannerV2 
follow-up.
   



##########
fe/fe-connector/fe-connector-iceberg/src/main/java/org/apache/doris/connector/iceberg/IcebergScanPlanProvider.java:
##########
@@ -2327,8 +2341,55 @@ private static List<Schema> metadataSchemaHistory(Table 
table) {
         return new ArrayList<>(table.schemas().values());
     }
 
-    private static void addHistoricalEqualityFields(
-            List<NestedField> fields, Set<Integer> missingFieldIds, Schema 
historicalSchema) {
+    /**
+     * A writer commits with the schema that is current at commit, so a key 
dropped before its delete commits loses
+     * the schema-only changes made before the drop from every snapshot. Field 
IDs are never reused and a field
+     * only relaxes compatibly, by promotion or from required to optional, so 
each field of the selected definition
+     * takes the widest promotion of its type and the optionality it has in 
any retained schema; neither depends on
+     * the order of the schemas. A field required in every retained schema 
stays required.
+     */
+    private static List<NestedField> withRetainedRelaxations(List<NestedField> 
fields, List<Schema> schemas) {
+        List<NestedField> relaxed = new ArrayList<>(fields.size());
+        for (NestedField field : fields) {
+            Type type = field.type();
+            boolean optional = field.isOptional();
+            for (Schema schema : schemas) {
+                NestedField retained = schema.findField(field.fieldId());
+                if (retained == null) {
+                    continue;
+                }
+                optional |= retained.isOptional();
+                if (type.isPrimitiveType() && retained.type().isPrimitiveType()
+                        && isWideningPromotion(type.asPrimitiveType(), 
retained.type().asPrimitiveType())) {
+                    type = retained.type();
+                }
+            }
+            if (type.isStructType()) {
+                type = 
Types.StructType.of(withRetainedRelaxations(type.asStructType().fields(), 
schemas));
+            }
+            relaxed.add(type.equals(field.type()) && optional == 
field.isOptional()
+                    ? field : 
Types.NestedField.from(field).ofType(type).isOptional(optional).build());

Review Comment:
   You're right about the copied write default; fixed in `9affde5af66`. The 
rebuilt carrier field took the write default of the lineage definition. 
Converting a stale one outside the TIMESTAMP_NS range (1000-01-01 in your 
history) overflowed in `DateTimeUtil.microsToNanos` before any reader ran, so 
planning failed with `ArithmeticException: long overflow`. The carrier only 
feeds the scan dictionary, which carries the initial default and never a write 
default, so a rebuilt field now drops its write default instead of converting 
it. The initial default is still converted: it cannot change once the field is 
added, so the retained promoted definition already holds the same value. 
`equalityCarrierDropsStaleWriteDefaultOfPromotedDateKey` replays your history 
(write default cleared with `updateColumnDefault`, TIMESTAMP_NS published 
without a snapshot, the delete pending across the drop), and the two earlier 
default tests now expect no write default on the carrier.
   



-- 
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]

Reply via email to