Skip to content

Add REFRESH DDL command#238

Open
ryannedolan wants to merge 5 commits into
tabletrigger-input-watermarksfrom
ryannedolan/refresh-command
Open

Add REFRESH DDL command#238
ryannedolan wants to merge 5 commits into
tabletrigger-input-watermarksfrom
ryannedolan/refresh-command

Conversation

@ryannedolan

@ryannedolan ryannedolan commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • REFRESH DDL command

Details

Adds a  REFRESH  DDL command that fires the trigger(s) which produce a physical table, with optional window FROM...TO...:

  • Fire the trigger that produces this table
REFRESH "ADS"."MEMBERS";
  • Backfill it over a fixed historical window
REFRESH "ADS"."MEMBERS" FROM '2026-05-01' TO '2026-05-08';

It's the table-level counterpart to  FIRE TRIGGER  and reuses the same machinery.

Testing

Create a logical table with a rETL trigger:

0: Hoptimator> create table "LOGICAL-RETL".bar (key varchar, val varchar);
INFO Validating statement: CREATE TABLE `LOGICAL-RETL`.`BAR` (`KEY` VARCHAR, `VAL` VARCHAR)
INFO Added table BAR to schema LOGICAL-RETL
INFO Validating new table Source[LOGICAL-RETL.BAR]
INFO Validating deployable resources for table BAR
INFO Deploying create table Source[LOGICAL-RETL.BAR]
INFO Deploying tier offline (database CRD: ads-catalog-database) for table BAR
INFO Successfully snapshot K8s obj: TableTrigger:ads-catalog-database-bar-trigger
INFO Updated K8s obj: TableTrigger:ads-catalog-database-bar-trigger
INFO Deploying tier online (database CRD: ads-database) for table BAR
INFO Successfully snapshot K8s obj: LogicalTable:logical-retl-bar
INFO Created K8s obj: LogicalTable:logical-retl-bar
INFO Successfully snapshot K8s obj: TableTrigger:logical-bar-offline-trigger
INFO Created K8s obj: TableTrigger:logical-bar-offline-trigger
INFO Deployed offline-tier trigger logical-bar-offline-trigger for logical table BAR using JobTemplate retl-job-template
INFO Deployed table Source[LOGICAL-RETL.BAR]
INFO CREATE TABLE LOGICAL-RETL.BAR completed
No rows affected (0.428 seconds)

The resulting backfill trigger starts in PAUSED state, as specified by the logical database's table templates:

$ k get tabletriggers
NAME                               PAUSED   CATALOG       SCHEMA   TABLE
logical-bar-offline-trigger        true     ADS_CATALOG   ADS      BAR

Refreshing a physical table associated with the logical table causes the backfill trigger to fire and a job to run:

0: Hoptimator> refresh ads.bar from 2 days ago to 1 day ago;
INFO Validating statement: REFRESH `ADS`.`BAR` FROM '-2d' TO '-1d'
INFO Firing trigger logical-bar-offline-trigger
INFO Created K8s obj: Job:logical-bar-offline-trigger-retl-job-template-job-bf-bac0d6d0
INFO REFRESH ADS.BAR completed (1 trigger(s) fired)
No rows affected (0.184 seconds)

The job:

$ k get jobs
NAME                                       STATUS     COMPLETIONS   DURATION   AGE
logical-bar-offline-trigger-retl-job-template-job-bf-bac0d6d0   Complete   1/1           4s         4s

$  k logs logical-bar-offline-trigger-retl-job-template-job-bf-bac0dkvctq
logical-bar-offline-trigger-retl-job-template-trigger processing window [2026-07-13T22:25:18Z, 2026-07-14T22:25:18Z] at Wed Jul 15 22:25:19 UTC 2026

@ryannedolan
ryannedolan force-pushed the ryannedolan/refresh-command branch 2 times, most recently from 6ebb94c to 5edfb6b Compare July 15, 2026 22:17
@ryannedolan ryannedolan changed the title Implement REFRESH [MATERIALIZED VIEW | TABLE] to backfill via upstrea… Add REFRESH DDL command Jul 15, 2026
@ryannedolan
ryannedolan marked this pull request as ready for review July 15, 2026 22:28
ryannedolan and others added 5 commits July 15, 2026 17:37
…m triggers

REFRESH backfills a materialized view or logical table by firing every trigger
immediately upstream of it, reusing the FIRE TRIGGER machinery (including the
optional FROM ... TO ... backfill window). Rather than naming a trigger, you name
the object and Hoptimator discovers its upstream trigger(s).

Grammar (regenerated via the Calcite codegen flow): REFRESH now takes an optional
MATERIALIZED VIEW / TABLE keyword and an optional FROM/TO window, collapsed into a
single SqlRefreshObject node (replacing the parse-only SqlRefreshMaterializedView).
The object-type keyword is optional; when given it is validated against the
resolved kind, so REFRESH TABLE on an MV (or vice versa) is rejected. REFRESH also
errors when the object is unknown or has no upstream triggers — a refresh that
silently fires nothing is a footgun.

Discovery is decoupled from Kubernetes via a new RefreshProvider SPI (resolved by
RefreshService in hoptimator-jdbc): given an object path it reports the object kind
and the names of upstream triggers. K8sRefreshProvider implements it by treating
the TableTriggers owned by the object's CRD (LogicalTable / materialized View) as
its upstream triggers. The DDL layer never touches the k8s API.

Tested: parser unit tests, executor unit tests (mocked RefreshService), K8s
provider unit tests, and a live logical-refresh.id integration script. Docs updated
(ddl-reference REFRESH section, extending/index SPI table).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…shProvider

The graph layer already resolves an identifier to its kind (GraphService.resolve
returns View for a materialized view, LogicalTable for a logical table) and builds
the dependency graph with the object's owned triggers attached (OWNER_OF edges to
Trigger nodes). REFRESH's discovery is exactly that data, so drop the dedicated
RefreshProvider SPI + K8sRefreshProvider and resolve via GraphService instead.

RefreshService now lives entirely in hoptimator-jdbc: it maps the GraphTarget to a
kind and collects the root's OWNER_OF trigger nodes as the immediately-upstream
triggers. RefreshTarget moves into the jdbc package (no longer a cross-module SPI
type). No behavior change — the K8s graph builder already discovered owned triggers
by CRD ownership, the same signal the old provider used.

Tested: RefreshService unit tests (kind mapping + trigger extraction over synthetic
graphs), unchanged executor tests, and the live logical-refresh.id script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
REFRESH <logical table> FROM a TO b backfills the table's offline-tier trigger over
the given window. Add LogicalTableBackfillTest: it seeds the trigger's watermark to
simulate processed history (a backfill is capped at the watermark), refreshes a
window behind it, and asserts a one-off backfill Job is created over exactly
[from, to] (WINDOW_START/WINDOW_END) and runs immediately (not suspended).

Adds testCompileOnly findbugs.annotations to the module for the (test-only)
SuppressFBWarnings on the intentionally-dynamic backfill-window DDL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix stale references left after paring back — the bespoke RefreshProvider SPI was
removed in favor of GraphService/GraphProvider-based discovery.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pivot from refreshing logical objects to refreshing PHYSICAL tables. Hoptimator
doesn't distinguish logical from physical, and a consumer always reads a specific
physical tier -- so 'refresh the tier I read' is what customers actually want, and
it's unambiguous. This collapses the earlier complexity: no kind resolution, no
MATERIALIZED VIEW/TABLE keyword, no tiers-in-DDL, no recursion.

Grammar: REFRESH <table> [FROM a TO b] (regenerated; dropped the kind keyword).
Discovery: RefreshService resolves the identifier and reads the producing trigger(s)
off the one-hop dependency graph (trigger -> table edges). A logical table is
rejected with a hint to refresh a tier; a physical table with no producer errors.
Reuses the graph SPI end to end; the DDL layer never touches Kubernetes.

Also fixes a real blocker: backfill Job names for real trigger names exceeded the
63-char Kubernetes limit. backfillJobName now shortens an over-long base to a
readable prefix + a hash of the full base (deterministic, unique, valid).

Grounded with PhysicalTableRefreshTest (integration): a reverse-ETL logical table
(logical-retl) whose offline trigger writes the physical table ADS.CAMPAIGNS; the
test seeds a watermark, REFRESHes ADS.CAMPAIGNS FROM a TO b, and asserts a backfill
Job over exactly [from, to] -- plus the logical-table-rejected and unknown-table
error paths. Unit tests: parser, executor, RefreshService.producingTriggers, and
backfillJobName length capping. Removed the obsolete logical-object REFRESH script.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ryannedolan
ryannedolan force-pushed the ryannedolan/refresh-command branch from 5edfb6b to ba83ee4 Compare July 15, 2026 22:37

@srnand srnand left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

niceee!

throws SQLException {
String identifier = String.join(".", path);
GraphTarget target = GraphService.resolve(identifier, connection);
if (target instanceof GraphTarget.LogicalTable) {

@srnand srnand Jul 16, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

do we want to allow REFRESH on view objects ? since GraphTarget could be a MV as well it seems (GraphTarget.View). need to probably add another condition here i guess since we only want to refresh physical tables.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good call, will fix.

* has zero or one producing trigger. The DDL layer never touches Kubernetes — the graph is built by
* a pluggable {@code GraphProvider}.
*/
final class RefreshService {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We can probably just add these methods to GraphService. Not much here otherwise.

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