Skip to content

[spark-3.5] Consolidate the maintenance SQL extensions and wire them to the OFD/SE table properties - #28

Draft
mkuchenbecker wants to merge 6 commits into
mainfrom
claude/spark35-sql-maintenance-ddl
Draft

[spark-3.5] Consolidate the maintenance SQL extensions and wire them to the OFD/SE table properties#28
mkuchenbecker wants to merge 6 commits into
mainfrom
claude/spark35-sql-maintenance-ddl

Conversation

@mkuchenbecker

@mkuchenbecker mkuchenbecker commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Consolidates the four stacked spark-3.5 SQL-extension draft PRs (linkedin/openhouse linkedin#660, linkedin#661, linkedin#662, linkedin#663) onto one branch, and wires the resulting maintenance DDL to the table properties the OpenHouse maintenance jobs actually read. linkedin#661 and linkedin#662 were siblings off linkedin#660, so no branch carried all three verbs; the shared files (grammar, AST builder, parser gate, V2 strategy) are union-merged here.

Folded in Content
linkedin#660 spark-3.5 gets its own ANTLR grammar and the classes it borrowed from spark-3.1; drops the spark-3.1 dependency
linkedin#661 VACUUM table [REMOVE ORPHAN FILES] [RETAIN n HOURS]
linkedin#662 OPTIMIZE table [FULL] [REWRITE MANIFESTS], position-delete compaction, and the Iceberg ExtendedParser fix
linkedin#663 ANALYZE TABLE table COMPUTE CLUSTERING QUALITY

Changes

  • Client-facing API Changes
  • Bug Fixes
  • New Features
  • Documentation
  • Tests

Client-facing API changes. Three new SQL verbs on spark-3.5 (VACUUM, OPTIMIZE, ANALYZE TABLE ... COMPUTE CLUSTERING QUALITY). The VACUUM opt-in property changes from openhouse.vacuum.enabled to maintenance.vacuum.enabled; since the former could never be set on a real table (see below), nothing depends on it. VACUUM now returns (metric, value) rows reporting the retention windows it resolved.

Bug fixes. The commands previously invented their own property contract, which produced three defects:

  1. The VACUUM opt-in could never be set. The gate was openhouse.vacuum.enabled, but the /tables service treats openhouse.-prefixed keys as reserved and rejects any ALTER TABLE ... SET TBLPROPERTIES touching them, so VACUUM was permanently disabled on every real OpenHouse table. It passed CI only because the statement test runs against a Hadoop catalog. The gate moves to the maintenance. namespace.
  2. VACUUM ignored the snapshot-expiration policy. With no RETAIN it fell through to the Iceberg procedure defaults instead of the table's policies.history, so a hand-run VACUUM and the scheduled job disagreed about the same table. It now resolves maxAge x granularity and applies the versions cap as a second expiration, exactly as TableSnapshotsExpirationTask / Operations.expireSnapshots do, falling back to the job's own 3-day default.
  3. VACUUM ignored the orphan-file job's rules. It now takes that job's 7-day default and honors ofd.one_day_ttl.enabled, and refuses REMOVE ORPHAN FILES on a table configured for orphan backups: there the job moves orphans into the backup directory through a delete hook the stored procedure has no equivalent of, so running the procedure would destroy files the platform expects to remain recoverable and treat the backup directory itself as orphans.

VACUUM and OPTIMIZE also now refuse to run on a table opted out of platform maintenance (maintenance.disabled, or the per-job-type maintenance.SNAPSHOTS_EXPIRATION.disabled / maintenance.ORPHAN_FILES_DELETION.disabled / maintenance.DATA_COMPACTION.disabled), and VACUUM refuses replica tables, since the scheduled expiration job runs on primaries only.

The jobs app and the Spark extensions ship as separate artifacts and cannot share code, so the mirrored keys and defaults live in one place (MaintenanceProperties.scala) and are pinned by MaintenancePropertiesTest.

Documentation. docs/VACUUM.md updated for the new gate, the default-retention table, and the refusal cases. New docs/OPTIMIZE.md, which records a known gap: the scheduled data-compaction job bin-packs from the persisted data-layout strategies and does not read optimize.cluster.*, so it neither preserves the clustering OPTIMIZE establishes nor respects its incremental watermark.

The last commit reflows one javadoc comment in tables-test-fixtures — a pre-existing spotlessCheck violation on main, unrelated to these changes but enough to fail the build.

Testing Done

  • Added new tests for the changes made.
  • Updated existing tests to reflect the changes made.

Because the grammar now ships in the OpenHouse extension rather than in a patched Spark, the real-catalog integration tests no longer need a custom Spark build and are enabled (they were @Disabled): VacuumTestSpark3_5, OptimizeTestSpark3_5, AnalyzeClusteringTestSpark3_5.

Run on JDK 17 against stock spark-sql 3.5:

./gradlew :integrations:spark:spark-3.5:openhouse-spark-3.5-runtime_2.12:test
./gradlew :integrations:spark:spark-3.5:openhouse-spark-3.5-itest:statementTest   # also runs catalogTest
  • catalogTest72 tests, 0 failures (VacuumTestSpark3_5 6, OptimizeTestSpark3_5 2, AnalyzeClusteringTestSpark3_5 2, and no regressions in BranchTestSpark3_5 24, CatalogOperationTest 15, WapIdTest 10, RTASTest 5, …).
  • statementTest85 tests, 0 failures (VacuumStatementTest 14, OptimizeStatementTest 6, AnalyzeClusteringQualityStatementTest 5, plus the pre-existing policy/grant suites).
  • Runtime unit tests — MaintenancePropertiesTest (12), OptimizeTableTest, AnalyzeClusteringQualityExecTest.

What the new coverage pins:

  • MaintenancePropertiesTest — the resolver: policy window, granularity mapping, versions cap, malformed-policy failure, orphan defaults, disable switches, replica detection.
  • VacuumStatementTest — the new gate, the policy-driven default, the versions cap, the orphan defaults, and each refusal path.
  • VacuumTestSpark3_5 — against the embedded server, the parts only a real server can exercise: the maintenance. gate is settable while the openhouse. one is rejected, and the default window comes from the server-persisted policies.history set via ALTER TABLE ... SET POLICY (HISTORY ...).

Additional Information

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

The five feature commits are the four upstream PRs plus the wiring, each self-contained; the upstream PRs above are the smaller-PR breakdown this consolidates.

🤖 Generated with Claude Code

https://claude.ai/code/session_01V7Ydkx4dHrT8T2KPRosSko

Mike Kuchenbecker and others added 6 commits July 26, 2026 18:43
…park-3.1)

spark-3.5 previously borrowed the ANTLR grammar (via spark-3.1's generated-src
srcDir) and several classes (OpenHouseCatalog, IcebergCatalogMapper,
OpenhouseSparkSessionExtensions, OpenhouseSqlExtensionsAstBuilder,
OpenhouseDataSourceV2Strategy, GrantableResourceTypes, Principal) from the
spark-3.1 shadow jar. This coupling meant the SQL grammar could not evolve for
spark-3.5 without also changing spark-3.1.

Give spark-3.5 its own copy of the grammar and these classes, add the ANTLR
generation task (antlr4 4.7.1, matching spark-3.1), and drop the spark-3.1
module dependency. No behavior change; runtime + itest compile against the
standalone module.

Co-authored-by: Copilot <[email protected]>
Adds VACUUM <table> [REMOVE ORPHAN FILES] [RETAIN n HOURS] as an OpenHouse
Spark SQL extension: grammar, AST builder, logical plan, and exec node that
delegates to the catalog's expire_snapshots / remove_orphan_files procedures.
Orphan-file deletion is opt-in and runs first, since it needs no write quota.

Squashed from linkedin#661.
…ring)

Adds OPTIMIZE <table> [FULL] [REWRITE MANIFESTS]. With no clustering keys
configured this is a plain bin-pack compaction; with optimize.cluster.keys set
it is a sort / z-order rewrite that is incremental by default, tracked by a
watermark and interval state persisted as table properties. Also compacts
merge-on-read position delete files after the data rewrite, and makes the
extensions parser an Iceberg ExtendedParser so sort/zorder rewrites resolve
their sort order.

Squashed from linkedin#662.
…nsion

Read-only probe that reports how well a table is clustered to its current key
selection, using the optimize.cluster.* state OPTIMIZE persists plus manifest
metrics. Coverage and per-key depth are computed with distributed SQL over
metadata, so the command is safe on tables with very large file counts. Only
the COMPUTE CLUSTERING QUALITY variant is intercepted; ANALYZE TABLE ...
COMPUTE STATISTICS still goes to Spark.

Cherry-picked from linkedin#663.
… read

VACUUM and OPTIMIZE invented their own property contract instead of the one
the scheduled snapshot-expiration (SE) and orphan-file-deletion (OFD) jobs
respect. Three defects followed.

The VACUUM opt-in could never be set. The gate was `openhouse.vacuum.enabled`,
but the /tables service treats `openhouse.`-prefixed keys as reserved and
rejects any ALTER TABLE that touches them, so VACUUM was permanently disabled
on every real OpenHouse table; the statement test passed only because it runs
against a Hadoop catalog. The gate moves to `maintenance.vacuum.enabled`.

VACUUM ignored the expiration policy. With no RETAIN it fell through to the
Iceberg procedure defaults rather than the table's `policies.history`, so a
hand-run VACUUM and the scheduled job disagreed about the same table. It now
resolves maxAge x granularity and the versions cap the way
TableSnapshotsExpirationTask and Operations.expireSnapshots do, falling back
to the job's own 3-day default.

VACUUM ignored the orphan-file job's rules. It now takes that job's 7-day
default and honors `ofd.one_day_ttl.enabled`, and refuses REMOVE ORPHAN FILES
on a table configured for orphan backups: there the job moves orphans into the
backup directory through a delete hook the stored procedure has no equivalent
of, so running the procedure would destroy files the platform expects to
remain recoverable and treat the backup directory itself as orphans.

Both commands now also refuse to run on a table opted out of platform
maintenance (`maintenance.disabled`, `maintenance.<JOB_TYPE>.disabled`), and
VACUUM refuses replica tables, since the scheduled expiration job runs on
primaries only. VACUUM reports the windows it resolved as output rows.

The jobs app and the Spark extensions are separate artifacts and cannot share
code, so the mirrored keys and defaults live in MaintenanceProperties and are
pinned by MaintenancePropertiesTest.
Pre-existing formatting violation on main (from the delta-harness commit),
unrelated to this branch's changes but enough to fail `./gradlew spotlessCheck`
and therefore CI. Output of `spotlessApply`; comment text is unchanged.
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.

2 participants