github-actions[bot] commented on code in PR #69488:
URL: https://github.com/apache/doris/pull/69488#discussion_r4355508080


##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java:
##########
@@ -476,15 +489,62 @@ private static Plan normalizePlanWithoutLock(LogicalPlan 
plan, TableIf table,
     }
 
     static List<Column> connectorWriteSchema(TableIf table, boolean full) {
+        return pinnedConnectorWriteSchema(table).orElseGet(() -> 
table.getBaseSchema(full));
+    }
+
+    private static Optional<List<Column>> pinnedConnectorWriteSchema(TableIf 
table) {
         ConnectContext context = ConnectContext.get();
-        if (context != null && context.getStatementContext() != null) {
-            Optional<List<Column>> pinned =
-                    
context.getStatementContext().getConnectorWriteSchema(table.getId());
-            if (pinned.isPresent()) {
-                return pinned.get();
-            }
+        if (context == null || context.getStatementContext() == null) {
+            return Optional.empty();
+        }
+        return 
context.getStatementContext().getConnectorWriteSchema(table.getId());
+    }
+
+    /**
+     * The pinned writer schema of a target whose column defaults apply to 
writes only. The read schema of such a
+     * table carries no defaults, so DEFAULT(column) in an INSERT into it must 
resolve against this schema.
+     */
+    private static Optional<List<Column>> writeOnlyDefaultColumns(TableIf 
table) {
+        if (!(table instanceof PluginDrivenExternalTable)
+                || !((PluginDrivenExternalTable) 
table).hasWriteOnlyDefaults()) {
+            return Optional.empty();
         }
-        return table.getBaseSchema(full);
+        return pinnedConnectorWriteSchema(table);
+    }
+
+    /** Resolve DEFAULT(column) in one inserted value by name against the 
target's pinned writer schema. */
+    private static NamedExpression resolveDefaultReferences(NamedExpression 
value,
+            Optional<List<Column>> writeOnlyDefaultColumns, TableIf table, 
List<String> targetNameParts) {
+        if (!writeOnlyDefaultColumns.isPresent()) {
+            return value;
+        }
+        return (NamedExpression) 
ConnectorWriteSchemaUtils.resolveDefaultReferences(value,
+                writeOnlyDefaultColumns.get(), (ExternalTable) table, 
ConnectContext.get(), targetNameParts, null);
+    }
+
+    /**
+     * Resolve DEFAULT(column) in the select lists of an INSERT ... SELECT by 
name against the target's pinned
+     * writer schema, before the source relations are bound, so it names a 
target column whatever the source is.
+     */
+    private static Plan resolveSelectDefaultReferences(Plan query, 
Optional<List<Column>> writeOnlyDefaultColumns,
+            TableIf table, List<String> targetNameParts) {
+        return query.rewriteUp(plan -> {

Review Comment:
   [P2] Normalize defaults in every SELECT output shape and nested query
   
   With target `t(c INT)` having a write-default, `INSERT INTO t(c) SELECT 
DEFAULT(c) FROM (SELECT 1 AS k) s GROUP BY k` parses as `Sink(t.c) -> 
Aggregate(groupBy=k, output=DEFAULT(c)) -> Alias(s) -> OneRow(k=1)`. The lambda 
handles only Project/OneRow outputs, so it leaves this DEFAULT untouched and 
binding fails with unknown column `c` against source `s.k`; removing GROUP BY 
resolves the same target reference successfully. CTE definitions and 
scalar-subquery plans also escape this traversal: command-level WITH is 
attached after normalization, CTE definitions are `extraPlans()`, and subquery 
plans are not expression children. Resolve target defaults in aggregate/repeat 
outputs and explicitly traverse these separately owned query plans, with 
grouped/CTE/subquery coverage.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/rules/expression/rules/RewriteDefaultExpression.java:
##########
@@ -82,6 +87,15 @@ private static Expression 
rewrite(ExpressionMatchingContext<Default> context) {
                     + column.getName() + "'");
         }
 
+        Optional<TableIf> originalTable = slotRef.getOriginalTable();
+        if (originalTable.filter(PluginDrivenExternalTable.class::isInstance)
+                .filter(table -> ((PluginDrivenExternalTable) 
table).hasWriteOnlyDefaults())
+                .isPresent()) {
+            column = 
pinnedWriteColumn(context.cascadesContext.getStatementContext(), 
originalTable.get(), column)

Review Comment:
   [P2] Derive the result type after selecting the writer column
   
   `targetType` is computed from the read column before this substitution and 
remains unchanged afterward. The read schema can still be cached as INT after 
an external Iceberg promotion to LONG, while `pinAndGet` independently loads 
current LONG writer columns and a new write-default such as `4900000001`. Then 
`UPDATE t SET other = 'x' WHERE DEFAULT(score) = 4900000001` produces 
`Filter(CAST(4900000001 AS INT) = 4900000001)` above the cached INT scan, 
instead of using the BIGINT writer default, causing overflow/error or an 
incorrect predicate. The writer fences compare the current writer 
columns/identity and do not validate this stale predicate type. Compute the 
type from the selected writer column and add a same-field-ID 
read-INT/write-BIGINT test.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java:
##########
@@ -476,15 +489,62 @@ private static Plan normalizePlanWithoutLock(LogicalPlan 
plan, TableIf table,
     }
 
     static List<Column> connectorWriteSchema(TableIf table, boolean full) {
+        return pinnedConnectorWriteSchema(table).orElseGet(() -> 
table.getBaseSchema(full));
+    }
+
+    private static Optional<List<Column>> pinnedConnectorWriteSchema(TableIf 
table) {
         ConnectContext context = ConnectContext.get();
-        if (context != null && context.getStatementContext() != null) {
-            Optional<List<Column>> pinned =
-                    
context.getStatementContext().getConnectorWriteSchema(table.getId());
-            if (pinned.isPresent()) {
-                return pinned.get();
-            }
+        if (context == null || context.getStatementContext() == null) {
+            return Optional.empty();
+        }
+        return 
context.getStatementContext().getConnectorWriteSchema(table.getId());
+    }
+
+    /**
+     * The pinned writer schema of a target whose column defaults apply to 
writes only. The read schema of such a
+     * table carries no defaults, so DEFAULT(column) in an INSERT into it must 
resolve against this schema.
+     */
+    private static Optional<List<Column>> writeOnlyDefaultColumns(TableIf 
table) {
+        if (!(table instanceof PluginDrivenExternalTable)
+                || !((PluginDrivenExternalTable) 
table).hasWriteOnlyDefaults()) {
+            return Optional.empty();
         }
-        return table.getBaseSchema(full);
+        return pinnedConnectorWriteSchema(table);
+    }
+
+    /** Resolve DEFAULT(column) in one inserted value by name against the 
target's pinned writer schema. */
+    private static NamedExpression resolveDefaultReferences(NamedExpression 
value,
+            Optional<List<Column>> writeOnlyDefaultColumns, TableIf table, 
List<String> targetNameParts) {
+        if (!writeOnlyDefaultColumns.isPresent()) {
+            return value;
+        }
+        return (NamedExpression) 
ConnectorWriteSchemaUtils.resolveDefaultReferences(value,

Review Comment:
   [P1] Preserve the referenced column type before binding its consumers
   
   For an Iceberg `t(c INT)` with write-default `-1`, `INSERT INTO t(c) SELECT 
bit_count(DEFAULT(c))` should evaluate `bit_count` on an INT and write 32. This 
new route instead produces `OneRow(bit_count(-1:TINYINT))`: 
`ConnectorWriteSchemaUtils.resolveDefaultReferences` parses the default SQL 
without casting it to the referenced column type, and the parser chooses 
TINYINT for `-1`. `BitCount` has width-specific signatures, and both its FE 
evaluator and BE implementation return 8 for TINYINT `-1`, versus 32 for INT 
`-1`. Casting the completed expression to the sink's INT cannot restore the 
lost value; VALUES expressions have the same problem. Cast each substituted 
default to its referenced writer column's type before binding surrounding 
operators/functions, as the bound DEFAULT rewrite does.



##########
fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/insert/InsertUtils.java:
##########
@@ -476,15 +489,62 @@ private static Plan normalizePlanWithoutLock(LogicalPlan 
plan, TableIf table,
     }
 
     static List<Column> connectorWriteSchema(TableIf table, boolean full) {
+        return pinnedConnectorWriteSchema(table).orElseGet(() -> 
table.getBaseSchema(full));
+    }
+
+    private static Optional<List<Column>> pinnedConnectorWriteSchema(TableIf 
table) {
         ConnectContext context = ConnectContext.get();
-        if (context != null && context.getStatementContext() != null) {
-            Optional<List<Column>> pinned =
-                    
context.getStatementContext().getConnectorWriteSchema(table.getId());
-            if (pinned.isPresent()) {
-                return pinned.get();
-            }
+        if (context == null || context.getStatementContext() == null) {
+            return Optional.empty();
+        }
+        return 
context.getStatementContext().getConnectorWriteSchema(table.getId());
+    }
+
+    /**
+     * The pinned writer schema of a target whose column defaults apply to 
writes only. The read schema of such a
+     * table carries no defaults, so DEFAULT(column) in an INSERT into it must 
resolve against this schema.
+     */
+    private static Optional<List<Column>> writeOnlyDefaultColumns(TableIf 
table) {
+        if (!(table instanceof PluginDrivenExternalTable)
+                || !((PluginDrivenExternalTable) 
table).hasWriteOnlyDefaults()) {
+            return Optional.empty();
         }
-        return table.getBaseSchema(full);
+        return pinnedConnectorWriteSchema(table);
+    }
+
+    /** Resolve DEFAULT(column) in one inserted value by name against the 
target's pinned writer schema. */
+    private static NamedExpression resolveDefaultReferences(NamedExpression 
value,
+            Optional<List<Column>> writeOnlyDefaultColumns, TableIf table, 
List<String> targetNameParts) {
+        if (!writeOnlyDefaultColumns.isPresent()) {
+            return value;
+        }
+        return (NamedExpression) 
ConnectorWriteSchemaUtils.resolveDefaultReferences(value,
+                writeOnlyDefaultColumns.get(), (ExternalTable) table, 
ConnectContext.get(), targetNameParts, null);

Review Comment:
   [P2] Accept fully qualified target column references
   
   `INSERT INTO ctl.db.t(c) SELECT DEFAULT(ctl.db.t.c)` now reaches 
`ConnectorWriteSchemaUtils.resolveDefaultReferences`, which passes the 
four-part name to `UpdateCommand.checkAssignmentColumn`. That assignment-LHS 
validator accepts only one, two, or three name parts and throws `column in 
assignment list is invalid` for this unambiguous target reference. Ordinary 
Nereids column binding explicitly supports `catalog.db.table.column`, and this 
form should also resolve against the pinned target schema. Use qualification 
validation that includes the catalog component rather than applying the 
narrower UPDATE-assignment grammar; cover the fully qualified form in SELECT 
and VALUES.



##########
fe/fe-connector/fe-connector-hive/src/main/java/org/apache/doris/connector/hive/HiveConnectorMetadata.java:
##########
@@ -625,7 +625,8 @@ public ConnectorTableSchema getTableSchema(
                     ConnectorCapability.SUPPORTS_NESTED_COLUMN_PRUNE,
                     ConnectorCapability.SUPPORTS_FIELD_ID_ACCESS_PATH,
                     ConnectorCapability.SUPPORTS_STORAGE_PREDICATE_PRUNING,
-                    ConnectorCapability.SUPPORTS_NESTED_COLUMN_SCHEMA_CHANGE));
+                    ConnectorCapability.SUPPORTS_NESTED_COLUMN_SCHEMA_CHANGE,
+                    ConnectorCapability.SUPPORTS_WRITE_ONLY_DEFAULTS));

Review Comment:
   [P2] Reflect this capability for snapshot schemas as well
   
   The two-argument `getTableSchema` reflects this set, but the snapshot 
overload still returns the sibling schema directly. For `SELECT DEFAULT(c) FROM 
hms_catalog.db.iceberg_t FOR VERSION AS OF <snapshot_id>`, 
`PluginDrivenMvccExternalTable.loadSnapshot` publishes that unreflected 
historical schema. `tableCapabilities()` reads the ambient pinned schema, while 
the Hive catalog itself does not declare write-only defaults, so 
`hasWriteOnlyDefaults()` is false and the bound rewrite returns NULL for a 
nullable column instead of rejecting this read-only use. Direct Iceberg 
catalogs retain the guard through their connector-wide capability. Apply the 
same reflection to `getTableSchema(session, handle, snapshot)` and test that 
overload.



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