Skip to content

[wip]feat: support column default values (initial-default / write-default)#731

Draft
huan233usc wants to merge 1 commit into
apache:mainfrom
huan233usc:feat/column-default-values
Draft

[wip]feat: support column default values (initial-default / write-default)#731
huan233usc wants to merge 1 commit into
apache:mainfrom
huan233usc:feat/column-default-values

Conversation

@huan233usc

@huan233usc huan233usc commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Closes #730 (item 2 of #637).

This end-to-end proof of concept has been split into four smaller pull requests:

  1. feat(schema): represent, serialize and validate v3 column default values (1/4) #746 — Schema model, JSON serialization, and v3 validation
  2. feat(parquet): apply column default values when reading missing fields (2/4) #792 — Parquet read-path support
  3. feat(avro): apply column default values when reading missing fields #800 — Avro read-path support
  4. feat: support v3 column default values in UpdateSchema (3/4) #793 — Schema-evolution support in UpdateSchema

Please review the split pull requests above. This PR is retained only as the original end-to-end proof of concept.

@huan233usc
huan233usc marked this pull request as draft June 11, 2026 21:07
@huan233usc huan233usc changed the title feat: support column default values (initial-default / write-default) [wip]feat: support column default values (initial-default / write-default) Jun 11, 2026
@huan233usc
huan233usc force-pushed the feat/column-default-values branch 5 times, most recently from bb5f2d0 to 1b2be81 Compare June 12, 2026 07:24
return std::cref(*write_default_);
}

SchemaField SchemaField::WithInitialDefault(Literal initial_default) const {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative to those with methods are make it in constructor

@huan233usc
huan233usc force-pushed the feat/column-default-values branch 8 times, most recently from 1613523 to c1a3762 Compare June 14, 2026 23:13
Implements Iceberg v3 column default values (apache#730, item 2 of apache#637):

- Schema model: SchemaField carries optional `initial-default` and
  `write-default` literals, with validation that defaults are primitive
  and match the field type.
- JSON serde: parse and write the two fields using single-value
  serialization (resolves the TODO in struct field serialization).
- Read path: Project() maps a missing column with an initial-default to
  FieldProjection::Kind::kDefault carrying the literal (for both required
  and optional columns, per spec), and the Parquet and Avro readers
  materialize it as a constant column via a new Literal-to-Arrow helper.
  This resolves the default-value TODO in schema projection.
- Schema evolution: Add*Column accept an optional default value (used as
  both initial-default and write-default); a required column with a
  default no longer needs AllowIncompatibleChanges(); RequireColumn()
  accepts columns added with a default in the same update (resolves the
  defaulted-add TODO); UpdateColumnDefault() updates the write-default of
  an existing column; doc/rename/type-promotion updates preserve defaults
  (promotion casts them to the new type).
- Format version gating: Schema::Validate() rejects schemas with default
  values below v3, using the existing kMinFormatVersionDefaultValues.

Writers consume complete Arrow arrays, so applying write-default to
omitted columns remains the engine's responsibility (as in Java); the
library stores, validates, and serializes it.
@huan233usc

Copy link
Copy Markdown
Contributor Author

Splitting this into smaller reviewable PRs; keeping this one open as the end-to-end proof-of-concept. Part 1/4 (schema foundation): #746. Read-path (Parquet, then Avro) and schema-evolution parts will follow, stacked on #746.

huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 15, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 15, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 15, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 16, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 16, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 16, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 18, 2026
First of a multi-part split of column default value support (apache#730) — the
schema foundation the read and evolution paths build on. Purely additive;
no read/write behavior change on its own.

- SchemaField carries `initial-default` / `write-default` (immutable
  std::shared_ptr<const Literal>) with copy-preserving WithInitialDefault /
  WithWriteDefault modifiers; getters return optional<reference_wrapper>.
- JSON serde reads/writes `initial-default` / `write-default` via the
  existing single-value serialization.
- Schema::Validate rejects default values below format v3 and validates
  they are non-null primitive literals matching the field type.
- Generic schema projection maps a column missing from a data file with an
  initial-default to FieldProjection::Kind::kDefault.

Read-path application (Parquet/Avro) and schema evolution follow in separate
PRs. See apache#731 for the full end-to-end proof-of-concept.
wgtmac pushed a commit that referenced this pull request Jun 29, 2026
Part 1 of a multi-part split of #730 (column default values, item 2 of
#637). The full
end-to-end implementation is in #731, kept open as the proof-of-concept;
this series
lands it in reviewable pieces.

This PR is the **schema foundation** — representing, serializing and
validating v3
column default values. It is purely additive and changes no read or
write behavior on
its own.

## What's in this PR

- **`SchemaField`** carries `initial-default` / `write-default`, stored
as
`std::shared_ptr<const Literal>` (immutable payload shared across
copies, like the
adjacent `type_`; the C++ analog of Java's `final Literal<?>`). They are
set via the
constructor. Getters return `std::optional<std::reference_wrapper<const
Literal>>` for
reading (the `Schema::FindFieldByName` idiom); `initial_default_ptr()` /
`write_default_ptr()` expose the shared pointer so a rebuilt field (e.g.
ID
  reassignment) shares the value instead of copying it.
- **JSON serde**: parse/write `initial-default` / `write-default` using
the existing
  single-value serialization (all primitive types).
- **`Schema::Validate`**: version-gates the `initial-default` to format
v3
(`kMinFormatVersionDefaultValues`) — it reinterprets how existing data
files are read,
so it requires the v3 reader contract. The `write-default` only affects
values written
going forward and is **not** version-gated (matching Java's
`Schema.checkCompatibility`,
which gates only the initial default). Both defaults are otherwise
validated to be
  non-null primitive literals matching the field type.
- **Generic projection**: a column missing from a data file with an
`initial-default`
maps to `FieldProjection::Kind::kDefault` carrying the literal (the
per-format readers
  consume this in the follow-up PRs).

## Follow-ups (stacked on this PR)

- read path — Parquet (`literal_util` + parquet
projection/materialization)
- read path — Avro
- schema evolution (`UpdateSchema` add/update column defaults)

## Testing

Added tests
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 29, 2026
…s (2/4)

When a column is present in the read schema but missing from a Parquet data
file (written before the column existed), fill it with the column's v3
initial-default instead of null. Adds a shared Arrow materializer
(arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a
kDefault projection branch in the Parquet schema/data projection paths.

Part 2 of the v3 column-default-values work (POC apache#731), built on the schema
support merged in apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jun 30, 2026
…s (2/4)

When a column is present in the read schema but missing from a Parquet data
file (written before the column existed), fill it with the column's v3
initial-default instead of null. Adds a shared Arrow materializer
(arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a
kDefault projection branch in the Parquet schema/data projection paths.

The materializer wraps the storage array in the extension type for extension
types such as `arrow.uuid` (compute::Cast has no storage->extension kernel).

Part 2 of the v3 column-default-values work (POC apache#731), built on the schema
support merged in apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 1, 2026
…s (2/4)

When a column is present in the read schema but missing from a Parquet data
file (written before the column existed), fill it with the column's v3
initial-default instead of null. Adds a shared Arrow materializer
(arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a
kDefault projection branch in the Parquet schema/data projection paths.

The materializer wraps the storage array in the extension type for extension
types such as `arrow.uuid` (compute::Cast has no storage->extension kernel).

Part 2 of the v3 column-default-values work (POC apache#731), built on the schema
support merged in apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 1, 2026
…s (2/4)

When a column is present in the read schema but missing from a Parquet data
file (written before the column existed), fill it with the column's v3
initial-default instead of null. Adds a shared Arrow materializer
(arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a
kDefault projection branch in the Parquet schema/data projection paths.

The materializer wraps the storage array in the extension type for extension
types such as `arrow.uuid` (compute::Cast has no storage->extension kernel).

Part 2 of the v3 column-default-values work (POC apache#731), built on the schema
support merged in apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 1, 2026
…s (2/4)

When a column is present in the read schema but missing from a Parquet data
file (written before the column existed), fill it with the column's v3
initial-default instead of null. Adds a shared Arrow materializer
(arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a
kDefault projection branch in the Parquet schema/data projection paths.

The materializer wraps the storage array in the extension type for extension
types such as `arrow.uuid` (compute::Cast has no storage->extension kernel).

Part 2 of the v3 column-default-values work (POC apache#731), built on the schema
support merged in apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 1, 2026
…s (2/4)

When a column is present in the read schema but missing from a Parquet data
file (written before the column existed), fill it with the column's v3
initial-default instead of null. Adds a shared Arrow materializer
(arrow/literal_util) that turns a Literal into an Arrow scalar/array, and a
kDefault projection branch in the Parquet schema/data projection paths.

The materializer wraps the storage array in the extension type for extension
types such as `arrow.uuid` (compute::Cast has no storage->extension kernel).

Part 2 of the v3 column-default-values work (POC apache#731), built on the schema
support merged in apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 1, 2026
AddColumn / AddRequiredColumn now accept an optional default value, used as
both the initial-default and write-default of the new column; a non-null
default also lets a required column be added (or an added column be made
required) without AllowIncompatibleChanges(). UpdateColumnDefault sets or
clears the write-default. Defaults are cast to the column type (rejecting
uncastable or out-of-range values) and preserved across rename / doc / type
updates and nested field-id reassignment.

Part 4 of the v3 column-default-values work (POC apache#731), built on apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 1, 2026
AddColumn / AddRequiredColumn now accept an optional default value, used as
both the initial-default and write-default of the new column; a non-null
default also lets a required column be added (or an added column be made
required) without AllowIncompatibleChanges(). UpdateColumnDefault sets or
clears the write-default. Defaults are cast to the column type (rejecting
uncastable or out-of-range values) and preserved across rename / doc / type
updates and nested field-id reassignment.

Part 4 of the v3 column-default-values work (POC apache#731), built on apache#746.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 2, 2026
AddColumn / AddRequiredColumn now accept an optional default value, used as
both the initial-default and write-default of the new column; a non-null
default also lets a required column be added (or an added column be made
required) without AllowIncompatibleChanges(). UpdateColumnDefault sets or
clears the write-default. Defaults are cast to the column type (rejecting
uncastable or out-of-range values) and preserved across rename / doc / type
updates and nested field-id reassignment.

Part 4 of the v3 column-default-values work (POC apache#731), built on apache#746.
wgtmac pushed a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 11, 2026
AddColumn / AddRequiredColumn now accept an optional default value, used as
both the initial-default and write-default of the new column; a non-null
default also lets a required column be added (or an added column be made
required) without AllowIncompatibleChanges(). UpdateColumnDefault sets or
clears the write-default. Defaults are cast to the column type (rejecting
uncastable or out-of-range values) and preserved across rename / doc / type
updates and nested field-id reassignment.

Part 4 of the v3 column-default-values work (POC apache#731), built on apache#746.
wgtmac pushed a commit that referenced this pull request Jul 11, 2026
## What

Part 3 of 4 of Iceberg v3 column default-value support (POC #731), built
on the
schema-layer support merged in #746. Independent of the read-path PRs
(#792 and
the Avro follow-up).

Adds default-value handling to `UpdateSchema` (schema evolution).

## Changes

- **`AddColumn` / `AddRequiredColumn`** take an optional
`default_value`. When
provided it is set as both the column's `initial-default` and
`write-default`.
  A non-null default also lets a required column be added without
`AllowIncompatibleChanges()` — rows written before the change read the
default
  instead of null.
- **`UpdateColumnDefault(name, default)`** (new) sets, or clears with
`std::nullopt`, a column's `write-default`; the `initial-default` is
fixed when
  the column is added.
- Defaults are **cast to the column type** (rejecting uncastable or
out-of-range
values) and **preserved** across rename / doc / type-promotion updates
and
  nested field-id reassignment.
- `RequireColumn` may now mark a column that was added with a default
required.

The `SchemaField` constructor stores defaults verbatim (it does not
coerce
them), so the cast/promotion is performed explicitly at each evolution
site —
the same effect as Java, where `NestedField`'s constructor runs
`castDefault`.
Same-scale decimal precision widening is handled directly (the unscaled
value is
unchanged), since `Literal::CastTo` does not cast between decimal types.

## Tests

13 cases in `update_schema_test.cc`: add optional/required/nested column
with a
default, mismatched/narrowing rejection, `UpdateColumnDefault`
(set / clear / cast-to-type / pre-existing column),
require-after-default, and
preservation across doc updates and type promotion — including
same-scale
decimal precision promotion.

## Stack

1. #746 — schema: represent / serialize / validate (merged)
2. #792 — read path: Parquet
3. read path: Avro (follows)
4. **this PR** — schema evolution: `addColumn` / `updateColumnDefault`
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 12, 2026
…3/4)

When a column is present in the read schema but missing from an Avro data file
(written before the column existed), fill it with the column's v3
initial-default instead of null. Reuses the shared arrow/literal_util
materializer (merged in apache#792) and adds AppendDefaultToBuilder for the row-by-row
Avro decode paths, plus a kDefault projection branch in the Avro schema/data
projection.

Part 3 of the v3 column-default-values work (POC apache#731), built on the schema
support in apache#746 and the Parquet read path in apache#792.
huan233usc added a commit to huan233usc/iceberg-cpp that referenced this pull request Jul 20, 2026
…3/4)

When a column is present in the read schema but missing from an Avro data file
(written before the column existed), fill it with the column's v3
initial-default instead of null. Reuses the shared arrow/literal_util
materializer (merged in apache#792) and adds AppendDefaultToBuilder for the row-by-row
Avro decode paths, plus a kDefault projection branch in the Avro schema/data
projection.

Part 3 of the v3 column-default-values work (POC apache#731), built on the schema
support in apache#746 and the Parquet read path in apache#792.
@manuzhang manuzhang added this to the 0.4.0 milestone Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support column default values (initial-default / write-default)

2 participants