Skip to content

feat(plugin-iceberg): Support ALTER COLUMN SET/DROP NOT NULL - #28040

Open
Joe-Abraham wants to merge 2 commits into
prestodb:masterfrom
Joe-Abraham:alter_table_not_null
Open

Joe-Abraham wants to merge 2 commits into
prestodb:masterfrom
Joe-Abraham:alter_table_not_null

Conversation

@Joe-Abraham

@Joe-Abraham Joe-Abraham commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

Adds ALTER TABLE ... ALTER COLUMN ... SET NOT NULL and DROP NOT NULL to the Iceberg connector.

SET NOT NULL first checks that the column has no NULLs. It reads the NULL counts stored in the manifests at every branch head, without reading any data files. It fails if a file has NULLs in the column or has no NULL count for it. Delete files don't update those counts. So after you delete the NULL rows, run rewrite_data_files with rewrite-all, unless the delete removed whole files (for example, a whole partition).

One exception applies on Iceberg V3. Data files written before a column was added with DEFAULT have no NULL count for it, but they read the default. They are accepted while the table still has a snapshot from before the column was added.

Branches are read with the current schema. So rollback_to_snapshot, rollback_to_timestamp, set_current_snapshot, fast_forward and CREATE [OR REPLACE] BRANCH ... AS OF run the same check on the snapshot they move to.

Other rules:

  • Only top-level columns can be altered, and SET NOT NULL works only on primitive types.
  • Branches, metadata columns and named constraints are rejected.
  • DROP NOT NULL fails on a column that is already nullable (as in Hive) and on Iceberg identifier fields.

This PR also fixes DROP NOT NULL to check the drop-constraint privilege instead of the add-constraint privilege. It's an engine change, so it applies to Hive as well. hive.allow-add-constraint and hive.allow-drop-constraint are now listed in the Hive security docs.

It also makes UPDATE fail when it sets a NOT NULL column to NULL. Before, only INSERT and MERGE checked. On the Java path, such an UPDATE stored the NULL, and in a Parquet table the column could no longer be read. The planner now puts each SET value of a NOT NULL column through the check MERGE uses, on the Java and native paths. This is an engine change too.

Design change

The first version made SET NOT NULL metadata-only. To handle the old NULLs, it stopped trusting NOT NULL for pruning, aggregation folding and metadata deletes. That kept Presto correct but nothing else that reads the table. Checking the data up front keeps NOT NULL true for every reader. All of those workarounds are gone, and reads behave exactly as they do on master.

Motivation and Context

Part of #28156. Until now, NOT NULL could only be set in CREATE TABLE. Iceberg supports both directions, and the SPI hooks already exist. Native write enforcement landed in #28450.

Impact

  • Adds new SQL support to Iceberg. Existing queries and deletes are unchanged.
  • The rollback and branch commands above can now fail if the target snapshot has NULLs in a NOT NULL column.
  • DROP NOT NULL needs the drop-constraint privilege on every connector.
  • An UPDATE that sets a NOT NULL column to NULL now fails on every connector.

Known gaps:

  • A write that was already running when SET NOT NULL committed isn't checked.
  • A column without NULL counts (metrics mode none) can't be made NOT NULL once it has data.
  • On a V3 column added with DEFAULT, once the snapshots from before the column expire, SET NOT NULL fails until rewrite_data_files with rewrite-all rewrites the older files.

Test Plan

  • IcebergDistributedSmokeTestBase: set/drop, the NULL check (Parquet, ORC, partitions, deletes and rewrites, branches vs. tags, missing counts), branch moves, transactions (including a NULL committed after the transaction read the table) and error cases.
  • IcebergDistributedTestBase.testUpdateNullIntoNotNullColumn: UPDATE into NOT NULL columns.
  • TestIcebergV3: columns added with DEFAULT, including rollback to a snapshot from before the column, NULLs or missing counts in later files, a write default only, and expired snapshots.
  • TestRollbackToTimestampProcedure, TestFastForwardBranchProcedure, TestIcebergTableChangelog.
  • TestIcebergAlterColumnNotNull: native vs. Java, and native UPDATE into NOT NULL columns.
  • TestHiveIntegrationSmokeTest.testAlterColumnNotNullAccessControl.

Contributor checklist

  • Please make sure your submission complies with our contributing guide, in particular code style and commit standards.
  • PR description addresses the issue accurately and concisely. If the change is non-trivial, a GitHub Issue is referenced.
  • Documented new properties (with its default value), SQL syntax, functions, or other functionality.
  • If release notes are required, they follow the release notes guidelines.
  • Adequate tests were added if applicable.
  • CI passed.
  • If adding new dependencies, verified they have an OpenSSF Scorecard score of 5.0 or higher (or obtained explicit TSC approval for lower scores).

Release Notes

== RELEASE NOTES ==

General Changes
* Fix ``UPDATE`` to fail when it sets a ``NOT NULL`` column to ``NULL``.

Security Changes
* Fix ``ALTER TABLE ... ALTER COLUMN ... DROP NOT NULL`` to check the drop-constraint privilege. With ``hive.security=legacy``, it now requires ``hive.allow-drop-constraint=true``.

Iceberg Connector Changes
* Add support for ``ALTER TABLE ... ALTER COLUMN ... SET NOT NULL`` and ``DROP NOT NULL``. ``SET NOT NULL`` fails if the column contains ``NULL`` values. See :doc:`/connector/iceberg`.

Summary by Sourcery

Enable safe Iceberg NOT NULL constraint changes while enforcing nullability consistently across writes, snapshot operations and security checks.

New Features:

  • Add Iceberg support for ALTER COLUMN SET NOT NULL and DROP NOT NULL, including validation of existing data and schema constraints.
  • Validate required-column constraints when moving Iceberg branches or snapshots to prevent exposing NULL values.

Bug Fixes:

  • Use the drop-constraint privilege for ALTER COLUMN DROP NOT NULL.
  • Reject UPDATE assignments that set NOT NULL columns to NULL across execution paths.

Enhancements:

  • Support Iceberg V3 default-valued columns when validating NOT NULL changes.
  • Enforce top-level, primitive-column and identifier-field restrictions for nullability changes.

Documentation:

  • Document Iceberg nullability alteration behavior and Hive add/drop constraint security properties.

Tests:

  • Add coverage for Iceberg nullability changes across formats, branches, transactions, deletes, rewrites, defaults, snapshots and native execution.
  • Add access-control and UPDATE enforcement tests for NOT NULL constraints.

@prestodb-ci prestodb-ci added the from:IBM PR from IBM label Jun 22, 2026
@sourcery-ai

sourcery-ai Bot commented Jun 22, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds safe bidirectional NOT NULL alteration for Iceberg by validating manifest null counts across branch heads and snapshot transitions, extends engine enforcement and authorization to UPDATE and DROP NOT NULL, and provides comprehensive documentation and Java/native integration coverage.

Sequence diagram for Iceberg SET NOT NULL validation

sequenceDiagram
    participant User
    participant Presto as Presto
    participant Metadata as IcebergAbstractMetadata
    participant Table as IcebergTable
    participant Manifests as IcebergManifests

    User->>Presto: ALTER TABLE ALTER COLUMN SET NOT NULL
    Presto->>Metadata: addConstraint
    Metadata->>Table: updateSchema
    Metadata->>Manifests: findDataFileWithNullValues
    loop Each branch head
        Manifests-->>Metadata: NULL counts from data files
    end
    alt NULL values or missing NULL count
        Metadata-->>Presto: CONSTRAINT_VIOLATION
        Presto-->>User: Reject schema change
    else No violating files
        Metadata->>Table: requireColumn
        Table-->>Metadata: commit schema update
        Metadata-->>Presto: Success
        Presto-->>User: Column is NOT NULL
    end
Loading

Sequence diagram for UPDATE enforcement on NOT NULL columns

sequenceDiagram
    participant User
    participant Planner as QueryPlanner
    participant Writer as UpdateWriter

    User->>Planner: UPDATE SET not_null_column = expression
    Planner->>Planner: checkNotNullColumns
    Planner->>Planner: failIfNull
    alt Expression evaluates to NULL
        Planner-->>User: Fail with constraint error
    else Non-NULL value
        Planner->>Writer: Execute checked update
        Writer-->>User: Update succeeds
    end
Loading

Flow diagram for Iceberg snapshot and branch moves

flowchart TD
    A[Snapshot or branch move] --> B[Resolve target snapshot]
    B --> C[verifyNoNullValuesInRequiredColumns]
    C --> D[Read target data files with current schema]
    D --> E{Required column can hold NULL?}
    E -- Yes --> F[Reject move with CONSTRAINT_VIOLATION]
    E -- No --> G[Commit rollback, branch creation, or fast-forward]
Loading

File-Level Changes

Change Details Files
Implement Iceberg NOT NULL constraint alteration with metadata-only schema updates guarded by manifest null-statistics validation.
  • Add connector support for SET NOT NULL and DROP NOT NULL on top-level columns.
  • Scan every distinct branch head without reading data files; reject NULL counts, missing counts, and unsupported types or fields.
  • Handle Iceberg V3 default-value compatibility for files written before a column was added.
  • Validate required-column invariants when creating, replacing, fast-forwarding, rolling back, or selecting snapshots and branches.
  • Remove prior read-side workarounds and preserve transactional schema-update behavior.
presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java
presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergConnector.java
presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergUtil.java
presto-iceberg/src/main/java/com/facebook/presto/iceberg/procedure/FastForwardBranchProcedure.java
presto-iceberg/src/main/java/com/facebook/presto/iceberg/procedure/RollbackToSnapshotProcedure.java
presto-iceberg/src/main/java/com/facebook/presto/iceberg/procedure/RollbackToTimestampProcedure.java
presto-iceberg/src/main/java/com/facebook/presto/iceberg/procedure/SetCurrentSnapshotProcedure.java
Enforce NOT NULL values consistently for UPDATE operations and correct DROP NOT NULL authorization.
  • Use drop-constraint privilege checks for DROP NOT NULL while retaining add-constraint checks for SET NOT NULL.
  • Wrap UPDATE assignment expressions for required columns with the existing fail-on-NULL logic on Java and native execution paths.
  • Preserve assignment ordering and first-assignment semantics for repeated SET targets.
  • Document the Hive legacy security property and configure native test execution accordingly.
presto-main-base/src/main/java/com/facebook/presto/execution/AlterColumnNotNullTask.java
presto-main-base/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java
presto-docs/src/main/sphinx/connector/hive-security.rst
presto-native-execution/src/test/java/com/facebook/presto/nativeworker/PrestoNativeQueryRunnerUtils.java
Document the new Iceberg SQL behavior, validation requirements, limitations, and operational remedies.
  • Document supported ALTER COLUMN syntax and restrictions.
  • Describe manifest statistics, delete-file, rewrite-all, branch, snapshot, and V3 default-value behavior.
  • Document Hive security properties and release-note changes.
presto-docs/src/main/sphinx/connector/iceberg.rst
presto-docs/src/main/sphinx/connector/hive-security.rst
Add broad integration and execution coverage for nullability changes and invariant enforcement.
  • Test set/drop operations, existing NULLs, missing metrics, deletes and rewrites, partitions, branches versus tags, snapshot movement, transactions, schema reordering, metadata columns, identifier fields, complex types, and mixed-case names.
  • Test V3 default and write-default behavior, snapshot expiration, and rollback compatibility.
  • Test Java/native parity, native UPDATE planning and writes, Hive access control, changelog restrictions, and UPDATE-to-NULL failures.
presto-hive/src/test/java/com/facebook/presto/hive/TestHiveIntegrationSmokeTest.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedSmokeTestBase.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/IcebergDistributedTestBase.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergLogicalPlanner.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergTableChangelog.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/TestIcebergV3.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/procedure/TestFastForwardBranchProcedure.java
presto-iceberg/src/test/java/com/facebook/presto/iceberg/procedure/TestRollbackToTimestampProcedure.java
presto-native-tests/src/test/java/com/facebook/presto/nativetests/iceberg/TestIcebergAlterColumnNotNull.java

Assessment against linked issues

Issue Objective Addressed Explanation
#28156 Implement Iceberg support for ALTER COLUMN SET NOT NULL and DROP NOT NULL, including validation that existing data satisfies SET NOT NULL. ✅
#28156 Ensure native workers enforce NOT NULL constraints on writes for Hive and Iceberg, with the expected error message. ❌ The PR adds native Iceberg tests, but the diff does not fix the native TableWriteNode argument ordering or the exception translator’s stray leading space. It also does not add native Hive enforcement coverage. The UPDATE planner changes address a separate write path.
#28156 Add dual-runner native-versus-Java coverage for SET/DROP NOT NULL across both Hive and Iceberg, including DDL/schema visibility, write enforcement, and idempotent drop behavior. ❌ The new dual-runner tests cover Iceberg, but the PR does not add equivalent native-versus-Java coverage for Hive.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch from f60b643 to 57ca3ba Compare June 24, 2026 04:50
@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch 2 times, most recently from 1727833 to 13992f5 Compare July 9, 2026 07:54
@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch 2 times, most recently from 68d80a5 to 1c903d1 Compare July 15, 2026 11:17
@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch from 1c903d1 to 2f03f4a Compare July 22, 2026 09:57
Copilot AI lite review requested due to automatic review settings July 22, 2026 09:57

This comment was marked as resolved.

@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch from 2f03f4a to c540b29 Compare August 3, 2026 10:40
Copilot AI review requested due to automatic review settings August 3, 2026 10:40

This comment was marked as resolved.

Copilot AI review requested due to automatic review settings August 4, 2026 08:23

This comment was marked as resolved.

@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch from 6b3d49d to d936008 Compare August 21, 2026 06:36
meta-codesync Bot pushed a commit to facebookincubator/axiom that referenced this pull request Sep 3, 2026
Summary:
## Why

The native (Prestissimo) worker currently accepts NULLs into NOT NULL columns
silently, while the Java engine rejects them in `TableWriterOperator` before any
file is written — a data-integrity and Java-parity gap
(prestodb/presto#28156).

## What

Table writes can now enforce NOT NULL constraints on selected columns. Writing a
null into a column marked NOT NULL fails with, for example:
`NULL value not allowed for NOT NULL column: c1`.

`InsertTableHandle` carries the list of NOT NULL column names (empty means
unconstrained), so the constraint travels with the write-request descriptor and
round-trips through plan serialization. `TableWriteNode` validates at
construction that each name exists in the target schema. `TableWriter` maps each
constrained column to its input column and checks it with
`DecodedVector::hasNulls()`, which catches nulls behind dictionary or constant
encoding as well as flat vectors. The check is opt-in and only scans the listed
columns.

Part of prestodb/presto#28156. E2E protocol changes:
prestodb/presto#28040.

X-link: facebookincubator/velox#18135

Reviewed By: shrinidhijoshi

Differential Revision: D116681749

Pulled By: Yuhta

fbshipit-source-id: dc23981cc3e24cda03185c44137e32312e3f726a
@Joe-Abraham Joe-Abraham changed the title feat(plugin-iceberg): Support ALTER COLUMN SET NOT NULL feat(plugin-iceberg): Support ALTER COLUMN SET/DROP NOT NULL Sep 3, 2026
@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch 5 times, most recently from 6bac416 to cd4ed35 Compare September 8, 2026 07:30
@Joe-Abraham
Joe-Abraham requested a review from a team as a code owner September 30, 2026 05:54
@prestodb-ci
prestodb-ci requested review from a team and removed request for a team September 30, 2026 05:54
@prestodb-ci
prestodb-ci requested a review from NivinCS September 30, 2026 05:55
steveburnett
steveburnett previously approved these changes Sep 30, 2026

@steveburnett steveburnett left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM! (docs)

Pull branch, local doc build, looks good. Thanks!

@Joe-Abraham

Copy link
Copy Markdown
Contributor Author

@aditi-pandit @hantangwangd @nmahadevuni @imjalpreet Can you please have a look when you get some time, Thanks!

@nmahadevuni

nmahadevuni commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

Thanks @Joe-Abraham

Critical

  1. Time travel can still hide pre-existing NULLs. The skip decision uses the current schema (icebergTable.schema() / table.schema()), but useSnapshot(id) binds the filter against the snapshot's schema. I confirmed SnapshotScan.useSnapshot checks useSnapshotSchema(), which is on by default for data table scans. Repro:
    INSERT INTO t VALUES (1, NULL); -- S1
    ALTER TABLE t ALTER COLUMN c SET NOT NULL;
    INSERT INTO t VALUES (2, 5); -- S2, snapshot schema: c required
    ALTER TABLE t ALTER COLUMN c DROP NOT NULL;
    SELECT * FROM t FOR VERSION AS OF WHERE c IS NULL;
    The current schema says c is optional, so isNull(c) is pushed down. Iceberg binds it against S2's schema (c required) and folds it to alwaysFalse. Row 1 is silently dropped.

    The same mismatch affects canEnforceColumnConstraintInSpecs and IcebergFilterPushdown, which would also treat the domain as enforced. Fix: use the schema the scan actually binds to (SnapshotUtil.schemaFor(table, snapshotId), or tableScan.schema() after useSnapshot) in IcebergSplitManager, IcebergUtil.getTableScan, TableStatisticsMaker, the partition loader, and the two optimizer checks. Please add a test for this sequence.

Suggestions

  1. The new predicate skipping also affects columns created as NOT NULL. IS NULL on such a column used to prune every file; now it scans everything. That's correct and cheap enough, but c IN (...) OR c IS NULL on a required column also loses min/max pruning entirely. Mention it in the commit message or docs, or consider pushing only the non-null part when the column has never held NULLs (probably not worth it).
  2. Concurrent schema change. updateColumnNullability looks up the field, then calls updateSchema() on the same Table object. This matches addColumn/renameColumn, so it isn't new. With allowIncompatibleChanges(), though, a concurrent commit could slip in. This is acceptable as-is; just noting it.

Nits

  1. Message style: "Not Null constraint not found on column %s" uses different capitalization and doesn't quote the column name, unlike the other new messages ("Cannot %s NOT NULL on metadata column '%s'").
  2. Signature formatting: dropConstraint(ConnectorSession session, ConnectorTableHandle tableHandle, Optional constraintName, Optional columnName) and updateColumnNullability(...) are long one-line signatures. That's fine under 180 characters, but other recent methods in this file use one parameter per line.
  3. Random table names: the native test uses randomTableSuffix(). That's common in presto-native-tests, but it technically conflicts with the "no random values" rule in AGENTS.md. Fixed table names with DROP IF EXISTS would also work.
  4. Redundant check: verify(handle.getIcebergTableName().getTableType() == DATA, ...) is fine, but validateNoBranchSpecified already gives a user-facing error for branches. Consider whether system tables (e.g. "t$snapshots") should also get a PrestoException rather than a VerifyException.

Questions

  1. Does Presto's own time-travel path report column nullability from the snapshot schema or the current schema? This affects how the Critical issue shows up in DESCRIBE ... FOR VERSION AS OF.
  2. Is there a plan to support nested fields (ALTER COLUMN payload.x SET NOT NULL)? Iceberg supports it, and the current error would just be "column does not exist".

Testing Recommendations

  • Time travel: the sequence in Critical 1 run with pushdown_filter_enabled both true and false, and with a partitioned column.
  • Aggregation folding: SELECT count(c), min(c), max(c) FROM t WHERE c IS NULL OR c > 0 after SET NOT NULL with existing NULLs. This exercises the aggregation optimizer, which receives the skipped filter.
  • rewrite_data_files: run with filter => 'c IS NULL' on a required column that has old NULLs, and verify the row count is preserved afterwards.
  • Nested column paths: a predicate on a nested field of a required struct, to exercise schema.findField("a.b") in the subfield pushdown path.

@mohsaka mohsaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a nit otherwise looks good. Thanks!

Joe-Abraham added a commit to Joe-Abraham/presto that referenced this pull request Oct 9, 2026
@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch from 09d43af to 4f853af Compare October 9, 2026 06:57
Joe-Abraham added a commit to Joe-Abraham/presto that referenced this pull request Oct 9, 2026
…b#28040)

Implement addConstraint and dropConstraint in the Iceberg connector on
top of Iceberg's UpdateSchema, and declare the ALTER_COLUMN capability so
AlterColumnNotNullTask routes the statements to the connector. SET NOT
NULL maps to requireColumn() with allowIncompatibleChanges(), and DROP
NOT NULL maps to makeColumnOptional(). Only top-level columns can be
altered; named constraints, branch tables, and the _row_id and
_last_updated_sequence_number metadata columns are rejected.

SET NOT NULL does not rewrite data files, so files written before a
column became required can still hold NULLs, while Iceberg folds IS NULL
on a required field to false and IS NOT NULL to true without reading
data. A null-sensitive domain on a field that is required in one table
schema, but optional or absent in another (the field or an enclosing
struct), is therefore neither pushed into Iceberg scans and deletes nor
treated as enforced by partition pruning. Columns that are NOT NULL in
every schema keep full pushdown and metadata delete.

Part of prestodb#28156.
@Joe-Abraham
Joe-Abraham force-pushed the alter_table_not_null branch from 4f853af to f4f7f75 Compare October 9, 2026 07:31
mohsaka
mohsaka previously approved these changes Oct 9, 2026

@mohsaka mohsaka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM. Thanks!

@Joe-Abraham

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

sourcery-ai[bot]

This comment was marked as outdated.

@Joe-Abraham

Copy link
Copy Markdown
Contributor Author

@nmahadevuni Thanks for the detailed review. Here's how each point was addressed:

Critical 1 (time travel). Fixed. Rather than tracking the schema each scan binds against at every call site, isNullSensitiveOnRequiredField now treats a column as NULL-sensitive when it is required in any of the table's schemas. That set includes every schema a scan can bind against, so the snapshot case is covered, and canEnforceColumnConstraintInSpecs and IcebergFilterPushdown use the same check. testTimeTravelToSnapshotWithRequiredColumnAndExistingNullData runs your sequence on a regular column and on a partition column, with IS NULL, IS NOT NULL, IS NULL OR c = 5, and count(*). Looking at every schema also covers tables whose older schemas have been removed, for example by Spark's expire_snapshots with clean_expired_metadata; testNullPredicatesAfterOldSchemasAreRemoved covers that.

Suggestion 1 (columns created NOT NULL). Agreed, and it's documented now. The Iceberg docs say that IS NULL and IS NOT NULL on a NOT NULL column, including one declared in CREATE TABLE, are not used for file pruning, partition pruning, or metadata deletes, and the Impact section of the PR description calls out c = 1 OR c IS NULL. I didn't push only the non-NULL part: once old schemas are removed, the metadata can't show that a column never held NULLs.

Suggestion 2 (concurrent schema change). Left as is, matching addColumn and renameColumn.

Nits

  1. The message is the same as the Hive connector's, word for word, so DROP NOT NULL on a nullable column fails the same way on both connectors. I kept it for that reason.
  2. The signatures stay on one line to match the neighboring setColumnDefault, dropColumn, and renameColumn.
  3. randomTableSuffix() only keeps table names unique; the data each test writes and checks is fixed. The existing Iceberg smoke tests use it the same way.
  4. The verify on the table type matches the sibling column DDL (addColumn, dropColumn, renameColumn, setColumnDefault), and TestIcebergTableChangelog covers the $changelog case. I kept it consistent with them.

Question 1. A snapshot read uses the snapshot's schema: getTableHandle stores the schema from IcebergUtil.tryGetReadSchema, and the column metadata is built from it, so columns show the nullability they had at that snapshot. A tag is read with its snapshot's schema, and a branch with the current schema.

Testing recommendations

  • rewrite_data_files: testRewriteDataFilesWithNullPartitionFilterOnRequiredColumn uses c2 = 'foo' OR c2 IS NULL and c2 IS NOT NULL as the filter on a NOT NULL partition column, and checks that the call fails and that the data files and rows are unchanged, with pushdown_filter_enabled on (Java and native) and off (Java). testRewriteDataFilesWithNullsInNotNullColumn covers old NULLs in a NOT NULL column: the writer rejects them and nothing is rewritten.
  • Aggregation folding: testSetNotNullOnPartitionColumnWithExistingNullData runs count(*) with IS NULL and IS NOT NULL on a NOT NULL partition column with old NULLs, with aggregate pushdown on (the default). The skipped domain is never enforced, so the filter stays between the aggregation and the scan and folding doesn't run.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@prestodb-ci

This comment was marked as off-topic.

nmahadevuni
nmahadevuni previously approved these changes Oct 13, 2026

@nmahadevuni nmahadevuni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @Joe-Abraham , looks good. Please document the behavior change for v1 tables in the description so they can go in the commit message. Please fix the merge conflict.

@aditi-pandit aditi-pandit left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for the thorough work here. I traced each changed path (both pushdown modes, metadata delete, rewrite_data_files, aggregation folding, transaction staging, mixed-case names) and did not find a correctness bug inside Presto. Transaction staging matches addColumn/renameColumn, mixed-case handle names line up with findField, the verify in addConstraint is a real invariant (AddConstraintTask gates UNIQUE/PK by capability), and the access-control fix is well tested. Inline comments are mostly suggestions and tests.

Main design concern: SET NOT NULL without validating existing data

Iceberg's own Spark DDL docs state: "It is not possible to change a nullable column to a non-nullable column with SET NOT NULL because Iceberg doesn't know whether there is existing data with null values." This PR protects Presto's planner, but other engines (Spark, Trino, Flink) will still bind c IS NULL on the required field to alwaysFalse and silently drop the legacy NULL rows. The docs warning acknowledges this, but the statement still succeeds unchecked.

Have you considered validating before commit instead? For example, reject SET NOT NULL if any data file in the current snapshot has null_value_counts > 0 for the column, or has no metrics for it (or fall back to a scan). That keeps required truthful for every reader, and it would let most of the null-sensitive plumbing go: isNullSensitiveOnRequiredField plus the schemas argument on all toIcebergExpression callers, the metadata-delete verify, the rewrite_data_files guard, the v1 DELETE behavior change, and the lost pruning on columns declared NOT NULL at CREATE TABLE (see the inline comment on IcebergFilterPushdown). The remaining risk would be a concurrent writer still on the old schema, which is much narrower. If metadata-only is the intended semantics (as in Hive), it would help to explain in the description why that is acceptable for Iceberg when Iceberg itself refuses it.

Other tests worth adding

  • count(c), min(c), max(c) on a SET NOT NULL column with legacy NULLs (aggregation folding).
  • DELETE ... WHERE region IS NULL with pushdown_filter_enabled=true (native), to show the metadata-delete verify is unreachable.
  • SET NOT NULL twice: the metadata location / snapshot does not change.

TupleDomain<ColumnHandle> unenforcedConstraint = TupleDomain.withColumnDomains(Maps.filterKeys(constraint.getSummary().getDomains().get(), not(Predicates.in(partitionColumns))));
Map<ColumnHandle, Domain> constraintDomains = constraint.getSummary().getDomains().get();
Collection<Schema> schemas = icebergTable.schemas().values();
Predicate<ColumnHandle> enforcedByPartitionPruning = column -> partitionColumns.contains(column)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This now applies to every NOT NULL column, including ones declared NOT NULL at CREATE TABLE that never held NULLs. WHERE pk IS NOT NULL on a NOT NULL identity partition column used to be enforced and now keeps a filter, and IS NULL used to prune to zero files and now scans everything. The release notes mention this, but these tables pay the cost without using SET NOT NULL. Another argument for validating at SET time (see review body).

}

Expression filter = toIcebergExpression(predicate);
Expression filter = toIcebergExpression(predicate, table.schemas().values());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Question: with pushdown_filter_enabled=true, there is no FilterNode, and the null-sensitive domain stays in getValidPredicate() while toIcebergExpression now drops it. If this rule can see that layout, count(*) WHERE region IS NULL would fold over all files and return a wrong count. Can you confirm the rule ordering makes this unreachable, or add a native test for it?

}
// Any domain left here is on a partition column, and reaches domainPredicate only when it admits or
// excludes only NULL on a NOT NULL column, which partition pruning cannot enforce.
String unenforcedPartitionColumns = layoutHandle.getDomainPredicate()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggestion: this walks getDomainPredicate().getDomains() a second time, and relies on the comment's invariant that only partition columns are left. Collecting the root names once and branching (any non-partition column -> existing error, else non-empty -> this error) is a single pass and does not depend on the invariant.

.orElse("");
if (!unenforcedPartitionColumns.isEmpty()) {
throw new PrestoException(NOT_SUPPORTED, format(
"rewrite_data_files does not support IS NULL or IS NOT NULL filters on NOT NULL partition column %s when pushdown_filter_enabled=true",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: with more than one column this reads "partition column a, b". Consider "column(s)" or pluralizing.

if (!required && icebergTable.schema().identifierFieldIds().contains(field.get().fieldId())) {
throw new PrestoException(NOT_SUPPORTED, format("Cannot drop NOT NULL on column '%s': it is an identifier field of table '%s'", columnName, tableName));
}
String fieldName = field.get().name();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

SET NOT NULL on a column that is already required still goes through updateSchema().commit(). Consider returning early when required && field.get().isRequired(), so it does not write new metadata or add a commit to the transaction. The docs say this case "does not change the schema"; a test that the metadata location stays the same would lock that in.

* by it when the field is required in any of {@code schemas}: a scan of a snapshot other than the current
* one binds against that snapshot's schema.
*/
public static boolean isNullSensitiveOnRequiredField(Collection<Schema> schemas, String columnName, Domain domain)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Matching by name across historical schemas is conservative, but a dropped and re-added column with the same name gives false positives, and renames make it fragile. For top-level columns, matching by field id (IcebergColumnHandle.getId(), with schema.findField(int)) is more robust. Name matching could stay for pushed-down subfields.

}
Expression expression = toIcebergExpression(TupleDomain.withColumnDomains(
ImmutableMap.of(LAST_UPDATED_SEQUENCE_NUMBER_COLUMN_HANDLE, domain)));
ImmutableMap.of(LAST_UPDATED_SEQUENCE_NUMBER_COLUMN_HANDLE, domain)), ImmutableList.of(LINEAGE_ONLY_SCHEMA));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: LAST_UPDATED_SEQUENCE_NUMBER is optional in LINEAGE_ONLY_SCHEMA, so this argument has no effect. A short comment would save readers from checking.

assertQueryFails(rewriteAll, "NULL value not allowed for NOT NULL column: c2");
assertHasDataFiles(loadTable(tableName).currentSnapshot(), 2);

assertUpdate("UPDATE " + tableName + " SET c2 = 0 WHERE c2 IS NULL", 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This covers an UPDATE that repairs the NULL. What about an UPDATE of a different column on a row with a legacy NULL, e.g. UPDATE t SET c1 = 9 WHERE c1 = 1? With merge-on-read the full row is rewritten with NULL in the NOT NULL column, so I'd expect the writer to reject it. If so, that blocks any update of those rows and is worth a test and a docs note next to the rewrite_data_files one.

// --- helpers ---

@Test
public void testColumnRequiredInEverySchema()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: the new tests land below the // --- helpers --- marker. Move them above it.

* Dropping ``NOT NULL`` on a column that does not have the constraint fails.

Unlike ``SET NOT NULL``, relaxing a column from required to optional is a compatible schema
change in Iceberg, so this direction is always safe.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"Always safe" conflicts with the identifier-field case a few lines up, where DROP NOT NULL fails. Suggest something like "is a compatible schema change in Iceberg, so existing data stays valid".

@Joe-Abraham

Copy link
Copy Markdown
Contributor Author

@sourcery-ai review

@prestodb-ci

prestodb-ci commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

AI session: claude/c04f5136-5f7a-4f82-9162-4e958ef5c350

The AI reviewer completed the code review and posted the findings in the pull request review timeline.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="presto-main-base/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java" line_range="1050-1052" />
<code_context>
+     */
+    private PlanAndMappings checkNotNullColumns(Update node, PlanAndMappings planAndMappings, List<String> columnNames, List<Expression> columnValues, List<ColumnMetadata> updatedColumns)
+    {
+        Map<String, ColumnMetadata> notNullColumns = updatedColumns.stream()
+                .filter(column -> !column.isNullable())
+                .collect(toImmutableMap(ColumnMetadata::getName, Function.identity()));
+        if (notNullColumns.isEmpty()) {
+            return planAndMappings;
</code_context>
<issue_to_address>
**Iceberg updates miss null checks**

When the UPDATE analyzer obtains the assigned column metadata through Iceberg’s per-column lookup for a NOT NULL column, iceberg UPDATE analysis supplies column metadata through `getColumnMetadata(session, tableHandle, columnHandle)`, which builds metadata without setting nullability; `ColumnMetadata` therefore defaults to nullable. The planner filters those columns out here, so an UPDATE can still write NULL into a NOT NULL Iceberg column.

Ensure the UPDATE analysis receives Iceberg nullability from the table schema, or have the connector’s per-column metadata lookup set nullability correctly.
</issue_to_address>

### Comment 2
<location path="presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergUtil.java" line_range="663" />
<code_context>
+            // Without the field in the snapshot's schema, no data file of the snapshot has the column, so every row reads
+            // the field's initial default, or NULL if it has none.
+            if (snapshotField != null) {
+                scan = scan.filter(Expressions.isNull(snapshotField.name()));
+            }
+            Optional<DataFile> file = findDataFileWithNullValues(scan, field);
</code_context>
<issue_to_address>
**Renamed snapshots are checked incorrectly**

When a required column is renamed after the target snapshot, especially when its old name is reused, `verifyNoNullValuesInRequiredColumns` builds a filter using `snapshotField.name()`, but `newScan()` resolves it against the current schema. The filter either fails to bind and rejects a safe move, or binds to a different column and prunes files containing NULLs, so the caller sees a rejected safe move or an invalid move that succeeds.

Resolve the filter to the required field by its field ID in the current schema before scanning.

Also at `presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergUtil.java:664`.
</issue_to_address>

### Comment 3
<location path="presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java" line_range="3150" />
<code_context>
+        }
+        String fieldName = field.get().name();
+        // The NULL check runs after the update is opened, so it reads the metadata the schema change commits on.
+        UpdateSchema updateSchema = icebergTable.updateSchema();
+        if (required) {
+            verifyNoNullValues(icebergTable, field.get(), columnName, tableName);
</code_context>
<issue_to_address>
**Failed alter blocks other-table writes**

When an explicit transaction runs SET NOT NULL on a column whose data fails validation, then attempts to write to a different table before ending the transaction, `updateSchema()` registers a transaction for this table before `verifyNoNullValues` can reject the change. The failed statement leaves that table in `txByTable`, so a later write to another table in the same transaction is rejected with a multi-table transaction conflict even though the ALTER made no change.

Avoid registering the table transaction until validation succeeds, or undo the empty transaction registration when validation fails.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +1050 to +1052
Map<String, ColumnMetadata> notNullColumns = updatedColumns.stream()
.filter(column -> !column.isNullable())
.collect(toImmutableMap(ColumnMetadata::getName, Function.identity()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High · Iceberg updates miss null checks

When the UPDATE analyzer obtains the assigned column metadata through Iceberg’s per-column lookup for a NOT NULL column, iceberg UPDATE analysis supplies column metadata through getColumnMetadata(session, tableHandle, columnHandle), which builds metadata without setting nullability; ColumnMetadata therefore defaults to nullable. The planner filters those columns out here, so an UPDATE can still write NULL into a NOT NULL Iceberg column.

Ensure the UPDATE analysis receives Iceberg nullability from the table schema, or have the connector’s per-column metadata lookup set nullability correctly.

Prompt for AI agents
In `presto-main-base/src/main/java/com/facebook/presto/sql/planner/QueryPlanner.java` at lines 1050-1052:

**Iceberg updates miss null checks**

When the UPDATE analyzer obtains the assigned column metadata through Iceberg’s per-column lookup for a NOT NULL column, iceberg UPDATE analysis supplies column metadata through `getColumnMetadata(session, tableHandle, columnHandle)`, which builds metadata without setting nullability; `ColumnMetadata` therefore defaults to nullable. The planner filters those columns out here, so an UPDATE can still write NULL into a NOT NULL Iceberg column.

Ensure the UPDATE analysis receives Iceberg nullability from the table schema, or have the connector’s per-column metadata lookup set nullability correctly.

Comment thread presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergUtil.java Outdated
}
String fieldName = field.get().name();
// The NULL check runs after the update is opened, so it reads the metadata the schema change commits on.
UpdateSchema updateSchema = icebergTable.updateSchema();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium · Failed alter blocks other-table writes

When an explicit transaction runs SET NOT NULL on a column whose data fails validation, then attempts to write to a different table before ending the transaction, updateSchema() registers a transaction for this table before verifyNoNullValues can reject the change. The failed statement leaves that table in txByTable, so a later write to another table in the same transaction is rejected with a multi-table transaction conflict even though the ALTER made no change.

Avoid registering the table transaction until validation succeeds, or undo the empty transaction registration when validation fails.

Prompt for AI agents
In `presto-iceberg/src/main/java/com/facebook/presto/iceberg/IcebergAbstractMetadata.java` at line 3150:

**Failed alter blocks other-table writes**

When an explicit transaction runs SET NOT NULL on a column whose data fails validation, then attempts to write to a different table before ending the transaction, `updateSchema()` registers a transaction for this table before `verifyNoNullValues` can reject the change. The failed statement leaves that table in `txByTable`, so a later write to another table in the same transaction is rejected with a multi-table transaction conflict even though the ALTER made no change.

Avoid registering the table transaction until validation succeeds, or undo the empty transaction registration when validation fails.

@prestodb-ci prestodb-ci left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No actionable findings found.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

from:IBM PR from IBM

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support ALTER TABLE ALTER COLUMN SET/DROP NOT NULL for Iceberg and the native (Prestissimo) worker

7 participants