924060929 commented on PR #65851:
URL: https://github.com/apache/doris/pull/65851#issuecomment-5099452687

   ### Follow-up: how other engines split the two defaults, and what that 
suggests for the FE model
   
   Not a blocker — this is about the shape of the abstraction, since the two 
write-path issues I filed look like symptoms of one modelling decision. I 
checked Iceberg 1.10.1 bytecode, the Iceberg Spark connector, Trino's Iceberg 
plugin, and the spec.
   
   #### 1. The library only covers the read half
   
   In `iceberg-{api,core,parquet}-1.10.1`, `initialDefault` is referenced by 
readers, `writeDefault` by no writer at all:
   
   ```
   initialDefault -> SchemaUpdate, Schema, SchemaParser, NestedField, 
UnionByNameVisitor,
                     avro/ValueReaders, 
data/parquet/BaseParquetReaders$ReadBuilder   <- reader
   writeDefault   -> SchemaUpdate, SchemaParser, NestedField, 
UnionByNameVisitor      <- metadata only
   ```
   
   The read half is ~20 lines inside the reader builder (decompiled from 
`BaseParquetReaders$ReadBuilder.defaultReader`):
   
   ```java
   if (reader != null) return reader;                       // column present 
in the file
   if (field.initialDefault() != null) {
       return ParquetValueReaders.constant(
           convertConstant(field.type(), field.initialDefault()), 
definitionLevel);
   }
   if (field.isOptional()) { ... }                          // else NULL / 
required fails
   ```
   
   It is that short because it sits where the `NestedField` is already in hand. 
Doris pays a structural cost here that Spark and Trino do not — the reader is 
in BE (C++) while the schema lives in FE (Java) — so this PR has to rebuild it 
across a Thrift boundary. That part is unavoidable. The write half is on the 
engine in every case.
   
   #### 2. Nobody lets the two defaults share one slot
   
   | | `initial-default` (read) | `write-default` (write) | statement-scoped 
schema carrier |
   |---|---|---|---|
   | Iceberg lib | reader `constant` | metadata only | — |
   | Spark | `withExistenceDefaultValue` → `EXISTS_DEFAULT` | 
`withCurrentDefaultValue` → `CURRENT_DEFAULT` → `ResolveDefaultColumns` | 
`SparkTable` (one instance) |
   | Trino | page source `transforms.constantValue(...)` | 
`ColumnMetadata.defaultValue` | `IcebergTableHandle` (`snapshotId` + 
`tableSchemaJson`) |
   
   Spark just wires the two Iceberg defaults onto the two default slots Spark 
already had:
   
   ```java
   // iceberg spark/v4.0/.../spark/TypeToSparkType.java
   if (field.writeDefault() != null) {
       sparkField = 
sparkField.withCurrentDefaultValue(Literal$.MODULE$.create(writeDefault, 
type).sql());
   }
   if (field.initialDefault() != null) {
       sparkField = 
sparkField.withExistenceDefaultValue(Literal$.MODULE$.create(initialDefault, 
type).sql());
   }
   ```
   
   Trino has no such pair, so it routes them to two different layers. 
`write-default` is the only one that reaches column metadata; `initial-default` 
never does:
   
   ```java
   // IcebergUtil.getColumnMetadatas(Schema schema, ...) — recomputed from the 
Iceberg Schema every call
   .setNullable(column.isOptional())
   .setDefaultValue(formatIcebergDefaultAsSql(column.writeDefault(), 
column.type()))
   
   // IcebergPageSourceProvider — initial-default only as an execution-time 
constant
   Object initialDefault = getInitialDefault(tableSchema, 
column.getBaseColumnIdentity().getId());
   transforms.constantValue(writeNativeValue(column.getType(), initialDefault));
   ```
   
   Also worth noting: both engines derive nullability straight from 
`field.isOptional()`, so neither needs a separate required-field-id set 
alongside the defaults.
   
   #### 3. Where `Column.defaultValue` sits in that picture
   
   Doris uses one slot for both halves. FE treats it as the write default 
(`BindSink`, `InsertUtils.generateDefaultExpression`), and BE treats it as the 
read fallback, filling from the **query-time** schema rather than the schema 
the segment was written with:
   
   ```cpp
   // be/src/storage/segment/segment.cpp:900
   if (!_column_meta_accessor->has_column_uid(unique_id)) {
       RETURN_IF_ERROR(new_default_iterator(tablet_column, iter));
       return Status::OK();
   }
   ```
   
   The spec draws exactly the line this single slot cannot express (L388-389): 
`initial-default` *must be set when a field is added and cannot change*, 
`write-default` *may change*.
   
   So `Column.defaultValue` is structurally the `write-default` slot — and it 
is the direct counterpart of Trino's `ColumnMetadata.defaultValue`, down to 
being an SQL string. The pre-PR code put `initial-default` there, which is what 
made both `INSERT` (should use `write-default`) and `DESC` (Trino shows 
`write-default`) wrong. Splitting them is right. My concern is only about what 
replaced it.
   
   #### 4. Suggestion: give `TableIf` a default provider instead of emptying 
the slot
   
   Today `IcebergUtils.parseField` stops populating `Column.defaultValue` but 
the callers that still read it are not redirected anywhere, so they silently 
degrade — `RewriteDefaultExpression` yields NULL for `DEFAULT(c)` in 
UPDATE/MERGE-MATCHED, and `DESC` shows nothing. Trino never hits this because 
it never stored a default anywhere else; there is exactly one entry point.
   
   A shape that would fit Doris:
   
   ```java
   Optional<Expression> writeDefaultOf(Column c);  // INSERT omitted column, 
DEFAULT(c), MERGE NOT MATCHED, DESC
   Optional<Expression> readDefaultOf(Column c);   // column absent from the 
file/segment
   ```
   
   - internal tables: both return `column.getDefaultValue()` — `TabletSchema` 
keeps serializing it, nothing changes
   - Iceberg: `writeDefaultOf` → pinned schema's `field.writeDefault()`, 
`readDefaultOf` → `field.initialDefault()`
   - every caller (`BindSink`, `InsertUtils`, `RewriteDefaultExpression`, 
`FileScanNode`, `DESC`) asks the interface
   
   That would fold in several loose ends at once: the UPDATE/MERGE `DEFAULT(c)` 
regression and the missing `DESC` default both disappear; the three 
`FileScanNode` hooks (`hasDefaultValue` / `getDefaultValueExpression` / 
`isColumnAllowNull`, which callers must combine in a specific order and which 
can disagree) collapse into one call returning a complete answer; and `DESC` 
would show `write-default`, matching Trino.
   
   #### 5. On pinning the schema — to be clear, I am not arguing against it
   
   Spark pins (`SparkTable`, one instance shared by analyzer and writer) and 
Trino pins (`IcebergTableHandle`, serialized to workers). 
`IcebergWriteSchemaContext` should exist. The narrower point is that both 
engines carry **one** instance with a single owner and therefore need no 
reconciliation, whereas here the same object is stored in the unbound sink, 
`StatementContext`, the logical sink, the physical sink, 
`IcebergInsertCommandContext` and the planner sink, kept in agreement by three 
different checks (`Preconditions` in `pinIcebergWriteSchema`, the `equals` 
guard in `bindDataSink`, and `validateCurrentSchema`). Consolidating on 
`StatementContext` as the single owner would also make the `refresh()`-related 
issue easier to reason about, since there would be one place that decides which 
schema this statement sees.
   
   References: [spec 
L326-337/L388-394](https://github.com/apache/iceberg/blob/main/format/spec.md), 
[TypeToSparkType](https://github.com/apache/iceberg/blob/main/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/TypeToSparkType.java),
 
[SparkTable](https://github.com/apache/iceberg/blob/main/spark/v4.0/spark/src/main/java/org/apache/iceberg/spark/source/SparkTable.java),
 [trino 
IcebergUtil](https://github.com/trinodb/trino/blob/master/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergUtil.java),
 [trino 
IcebergPageSourceProvider](https://github.com/trinodb/trino/blob/master/plugin/trino-iceberg/src/main/java/io/trino/plugin/iceberg/IcebergPageSourceProvider.java).


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