Skip to Content
We are live but in Staging 🎉

Maintenance — API Reference

Package: dodil.tables.v1 · Service: Tables

Three maintenance RPCs plus one SQL statement cover the operational lifecycle of a table. The plane drains the WAL into Delta automatically; you reach for these for explicit file packing, space reclamation, and rollback.

SurfaceKindWhen to call
Tables.OptimizeTableRPCAfter heavy writes — bin-pack small files into larger ones
Tables.VacuumTableRPCReclaim space — remove superseded files past retention
Tables.CompactRPCForce the WAL to drain into Delta now, instead of waiting for the tick
RESTORE … TO VERSION AS OF nSQL, via Tables.ExecuteRoll a table back to an earlier committed version

There is no History RPC and no Restore RPC. dodil.tables.v1 has neither, and no /history or /restore route exists on any door. Rollback is the RESTORE statement; the commit log has no read API at all. Verified against dodil-tables/proto/api/tables.proto:30-125 and crates/adapter/src/rest.rs:1073-1096.

The RPC-shaped calls reach the plane over the tables gRPC door (table-rpc.<region>.dodil.io:443) or the CLI. They are not REST routes on the tables HTTP door, which serves only /v1/sql/*, /v1/residency/*, /v1/observe/*, /v1/vector/* and /v1/graph/*.

OptimizeTable

Compacts small Delta files into larger ones. Two modes, chosen by whether you pass z_order_columns:

  • Bin-pack (default, z_order_columns empty) — coalesce small files per partition.
  • Z-order — rewrite files clustered by the given columns, improving read locality for queries that filter on them.

Request

# Bin-pack dodil data table optimize events -b kb-prod # Z-order dodil data table optimize events -b kb-prod \ --z-order-column event_type --z-order-column user_id

Note the field names: db_id and table, not bucket and table_name. There is no target_file_size_mb field — the target size is not caller-tunable.

Response

{ "table_name": "events", "version": 44, "optimize_type": "compact", "files_added": 1, "files_removed": 12 }

Six fields — there are no partitions_optimized, total_considered_files, total_files_skipped, num_batches, bytes_added or bytes_removed counters. files_removed − files_added is the packing win.

When to call: after large bulk writes, nightly batches, migrations. Routinely-written HTAP tables stay reasonably packed on their own.

VacuumTable

Permanently delete Delta files that are no longer referenced and are older than the retention window. This is destructive: vacuumed files are gone, and any version that depended on them can no longer be read or restored.

Request

# Dry run first — always dodil data table vacuum events -b kb-prod --dry-run # Real run (168 h is both the default and the floor) dodil data table vacuum events -b kb-prod --retention-hours 168

Four fields. There is no disable_retention_check — no bypass flag exists on the request, on the CLI, or anywhere in the plane. The auto-maintenance loop passes enforce_retention: true unconditionally and clamps the configured window with max(configured, 168); MIN_VACUUM_RETENTION_HOURS is a hard 168. Sources: proto/api/tables.proto:568-573, crates/compactor/src/maintenance/config.rs:20, runner.rs:411, cli-shell/cli-k3/cmd/table.go:714-715.

Response

{ "table_name": "events", "version": 44, "dry_run": false, "files_deleted": 12 }

The plane returns a count, never a path list — there is no files_deleted_paths field. A dry run reports what would be deleted under the same shape.

Vacuum destroys the history RESTORE depends on. Once the files backing version N are vacuumed, RESTORE … TO VERSION AS OF N fails and any time-travel read of that version fails with it. Take the restore target you might need before you vacuum, and remember that the 168-hour floor is the only protection you get — there is nothing to relax and nothing to tighten below it.

Compact

Force the WAL to drain into Delta now. The plane does this on a background tick; call it manually for:

  • After bulk writes — materialise the rows in Delta for analytical reads immediately.
  • Before maintenanceCompact then OptimizeTable is the canonical post-batch sequence.
  • Tests and e2e checks — drain the log so a warehouse read is deterministic.

Request

dodil data table compact events -b kb-prod

Two fields. There is no batch_size — the drain sizes itself.

Response

{ "wal_entries_processed": 247, "wal_unique_keys": 189, "drained_high_ulid": "01JXQ7…" }
FieldMeaning
drained_high_ulidThe highest ULID now in Delta. Absent when nothing drained.
wal_entries_processedWAL blobs read this tick
wal_unique_keysDistinct PKs after newest-per-key dedup — lower than wal_entries_processed is normal
fence_abortedA competing drain held the fence; this call did nothing
truncatedThe drain hit its internal cap and the log has more — re-run until it returns false

There are no rows_merged, rows_rejected, tombstones_seen or last_drain_target_version fields. For standing backlog, read GetDatabaseStatswal_segments, wal_entries_estimate, wal_high_ulid, and per-table wal_backlog_entries / rejected_rows (proto/api/tables.proto:770-823).

RESTORE

Rollback is a SQL statement, not an RPC. It is DDL-class: it lowers to the same spec_json compactor route TRUNCATE uses, so it rides Tables.Execute and needs no endpoint of its own (crates/tables-sql/src/restore.rs:1-27).

RESTORE TABLE events TO VERSION AS OF 40; RESTORE TABLE events TO TIMESTAMP AS OF '2026-05-27T08:00:00Z';

TABLE is optional. The two anchors are mutually exclusive. The timestamp form requires an offset (Z or +02:00) — a naive local timestamp has no defined meaning across a fleet spanning regions, so it is rejected rather than guessed. It resolves to the last version committed at or before that instant.

psql "postgresql://$USER:$TOKEN@pg.uk-lon-1.dodil.io:5432/kb-prod?sslmode=require" \ -c "RESTORE TABLE events TO VERSION AS OF 40"

A restore is itself a forward commit — it publishes the target version’s file set as a new version, so the restore is restorable and history is preserved (until you vacuum it away).

The two things a restore does that surprise people

1. It destroys the tail, and wipes the WAL to make that stick. Rows written after the target version are gone — that is the operation, not a side effect. The compactor also wipes the un-drained WAL, because otherwise those rows would fold back in on the next drain. If the wipe fails, the response carries a loud warning:

RESTORE committed, but the WAL wipe reported '<err>' — rows written after the restored version may resurrect on the next drain; re-run the RESTORE

Re-run it. Source: crates/coordinator/src/execute/ddl_stmt.rs:120-128,167-180, crates/delta-store/src/restore.rs.

2. Secondary indexes are NOT rolled back. An index is a derived table with its own history and no version corresponding to the base table’s target. Read-time revalidation and backfill reconcile them afterwards, exactly as they do after any other out-of-band change. When the table has indexes, the response says so:

secondary indexes (<cols>) were NOT rolled back — an index has its own history with no counterpart to the base's restored version; read-time revalidation and backfill reconcile them

Source: ddl_stmt.rs:181-193.

Restoring a table that does not exist is a FAILED_PRECONDITION, not a silent no-op. The plane also clears the table’s cached row-count estimate rather than leaving a stale one that would skew join planning.

Finding a restore target

There is no commit-log read API — no History RPC, no SHOW HISTORY statement, no route. The plane’s SHOW vocabulary is SHOW DATABASES, SHOW TABLES, SHOW GRAPHS, SHOW VECTOR INDEXES and SHOW MIGRATIONS; none of them list Delta commits.

What you can get:

  • The current versionDdlResult.version on any DDL, and TableStats.delta_version from GetDatabaseStats.
  • A point-in-time anchor — use TO TIMESTAMP AS OF instead of a version number when you know when rather than which.
  • A single historical rowGetRow accepts as_of_version, a pure historical snapshot with no live overlay (proto/api/tables.proto:292-294). It is a PK point lookup, not a scan.

Recording the version returned by each write is the practical answer; the plane does not keep a queryable log for you.

See also