Skip to content

feat(spark-3.5): [stacked] Add OPTIMIZE SQL extension (bin-pack + incremental clustering) - #662

Draft
mkuchenbecker wants to merge 2 commits into
mkuchenb/spark35-sql-standalonefrom
mkuchenb/spark35-sql-optimize
Draft

feat(spark-3.5): [stacked] Add OPTIMIZE SQL extension (bin-pack + incremental clustering)#662
mkuchenbecker wants to merge 2 commits into
mkuchenb/spark35-sql-standalonefrom
mkuchenb/spark35-sql-optimize

Conversation

@mkuchenbecker

Copy link
Copy Markdown
Contributor

OpenHouse spark-3.5 SQL Extensions Stack

PR Base Content
#660 main Standalone refactor (decouple spark-3.5 from spark-3.1)
vacuum #660 VACUUM
(this) #660 OPTIMIZE (bin-pack + incremental clustering)
analyze (this) ANALYZE … COMPUTE CLUSTERING QUALITY

Depends on #660. Independent of the VACUUM PR — bases directly on the standalone
refactor. The ANALYZE PR bases on this one. Review/merge #660 first.

Summary

Adds an OPTIMIZE command to the OpenHouse spark-3.5 SQL extensions:

OPTIMIZE <table> [FULL] [REWRITE MANIFESTS]
  • No optimize.cluster.keys → plain bin-pack compaction (system.rewrite_data_files);
    unchanged historical behavior.
  • Clustering configured → sort / z-order rewrite of the configured keys, incremental
    by default
    (only the forward slice of the leading key since the last run, tracked by an
    optimize.cluster.hwm-snapshot-id watermark); FULL reclusters up to the age floor.
    Snapshots younger than optimize.cluster.min-snapshot-age-minutes are held back;
    Iceberg partial progress bounds the snapshots committed per run.
  • Durable clustering state (optimize.cluster.config-id + optimize.cluster.state
    JSON intervals) advances atomically with the watermark in a single alterTable, so it
    survives snapshot expiration.
  • REWRITE MANIFESTS (system.rewrite_manifests) runs afterwards over the new layout.
  • Emits files_before / files_after / files_removed / snapshots_committed rows.

Thin sugar over the CALL path (OptimizeTableExec), with an OpenHouse-table guard
(openhouse.tableId). The extension parser wrapper now implements Iceberg's
ExtendedParser and delegates parseSortOrder, so the sort / z-order sort_order
argument parses (the wrapper is the session's outermost parser).

Changes

  • Client-facing API Changes
  • Internal API Changes
  • Bug Fixes
  • New Features
  • Performance Improvements
  • Code Style
  • Refactoring
  • Documentation
  • Tests

New non-reserved keywords OPTIMIZE / FULL / REWRITE / MANIFESTS.

Testing Done

  • Added new tests for the changes made.

  • OptimizeTableTest (pure helpers: parseClusterConfig / configId / parseState /
    advanceState) — 9 tests.

  • OptimizeStatementTest (real Iceberg, Hadoop catalog): bin-pack reduction, clustering
    writes durable state, incremental second run is a no-op, REWRITE MANIFESTS, FULL,
    non-OpenHouse rejection — 6 tests.

./gradlew :integrations:spark:spark-3.5:openhouse-spark-3.5-runtime_2.12:test            # 9/9
./gradlew :integrations:spark:spark-3.5:openhouse-spark-3.5-itest:statementTest --tests '*OptimizeStatementTest'   # 6/6
# both BUILD SUCCESSFUL

Additional Information

  • Large PR broken into smaller PRs, and PR plan linked in the description.

Mike Kuchenbecker and others added 2 commits July 31, 2026 16:25
…ring) for Iceberg

Adds an OPTIMIZE command to the OpenHouse spark-3.5 SQL extensions:

  OPTIMIZE <table> [FULL] [REWRITE MANIFESTS]

With no optimize.cluster.keys configured, runs plain bin-pack compaction
(system.rewrite_data_files). With clustering configured, runs a sort/z-order
rewrite of the configured keys, incremental by default (only the forward slice
of the leading key since the last run, tracked by an optimize.cluster.hwm-snapshot-id
watermark); FULL reclusters up to the age floor. Snapshots younger than
optimize.cluster.min-snapshot-age-minutes are held back; partial progress bounds
committed snapshots per run. Durable clustering state (config-id + JSON interval
state) is advanced atomically with the watermark in a single alterTable so it
survives snapshot expiration. REWRITE MANIFESTS runs afterwards over the new
layout. Emits files_before/after/removed and snapshots_committed output rows.

Implemented as thin sugar over the Iceberg stored-procedure CALL path
(OptimizeTableExec), with an OpenHouse-table guard (openhouse.tableId). The
extension parser wrapper now implements Iceberg's ExtendedParser and delegates
parseSortOrder, so the sort/z-order rewrite's sort_order argument parses (the
wrapper is the session's outermost parser). New non-reserved keywords
OPTIMIZE/FULL/REWRITE/MANIFESTS.

Tested by OptimizeTableTest (pure helpers) and OptimizeStatementTest (real
Iceberg, Hadoop catalog: bin-pack reduction, clustering writes durable state,
incremental second run is a no-op, REWRITE MANIFESTS, FULL, non-OpenHouse
rejection).

Co-authored-by: Copilot <[email protected]>
After the data rewrite, OPTIMIZE now calls system.rewrite_position_delete_files
to compact merge-on-read position delete files and drop dangling deletes (deletes
that no longer apply to any live data, e.g. because the data files they targeted
were rewritten). Runs after the data rewrite (so it also cleans deletes made
dangling by that rewrite) and before REWRITE MANIFESTS (so manifest compaction
sees the reduced delete-file set). No-op on copy-on-write or delete-free tables.

Tested by OptimizeStatementTest.testOptimizeCompactsMergeOnReadDeletesAndKeepsRowsCorrect:
a merge-on-read table with a position-delete, OPTIMIZE keeps the visible rows
correct across the rewrite.

Co-authored-by: Copilot <[email protected]>
@mkuchenbecker
mkuchenbecker force-pushed the mkuchenb/spark35-sql-optimize branch from 73088a6 to 1add93d Compare July 31, 2026 23:25
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.

1 participant