Skip to content

Commit b67bea0

Browse files
authored
feat(migrations): keep old model names working with expiring compatibility views (#871)
* feat(migrations): keep old model names working with expiring compatibility views Model migrations now archive the old relation after the destination builds and create a view at the old name that reads the new relation, with column renames aliased back to their old names. Tables and views accept migrate_from and use the same migration path. State is recorded as append-only, deterministic-ID facts in a new _sqlbuild_old_name_views table; a required fact gates the new behaviour so moves recorded before the upgrade keep today's behaviour. A model that would replace a compatibility view is a planning error (M114). Configured with [migrations] old_name_views (default 30d) and the old_name_view model header; false keeps today's behaviour. Refs CHI-391 * feat(janitor): drop expired compatibility views and reject old-name reads The janitor lists compatibility views at old model names, drops them once they expire or when named with --drop-old-name-view, and records each drop as an append-only fact. General cleanup no longer archives a live compatibility view. P008 now also rejects literal Python SQL that reads a declared old name at compile time, and a discovered old name at plan time. Refs CHI-391 * fix(migrations): report old-name step failures as M117 A failure while archiving the old relation or creating its compatibility view now fails the model with M117 and its resume hint instead of the generic worker error. View publication is isolated in one step so later privilege handling has a single place to attach. Refs CHI-391 * feat(docs): document compatibility views at renamed models' old names Rewrites the migration contract that the old relation is never modified, documents old-name views, their retention and janitor handling, the [migrations] config and old_name_view header, and the upgrade note that janitor and builds should run the same version. Refs CHI-391 * test(migrations): cover old-name views end to end and every resume window Refs CHI-391 * fix(migrations): rebind Postgres compatibility views inside the promotion transaction Compatibility views bound to a model's relation are no longer dropped before the build and restored afterwards. The swap, build-aside rename, and column drop or type change of the destination now run in one transaction with the views' rebinding, so a crash at any point leaves each view present with its old or new definition. Views are replaced in place when their columns allow it, so views stacked on them outside SQLBuild keep working. Nested adapter transactions now join the outer transaction. Refs CHI-391 * feat(migrations): copy old relations' grants to compatibility views on every adapter The compatibility view at a renamed model's old name gets the privileges that were granted on the old relation, read from its archive with each warehouse's own mechanism: Snowflake SHOW GRANTS (skipping OWNERSHIP and shares, keeping grant option), PostgreSQL table ACLs, BigQuery table IAM bindings, Unity Catalog grants, and SQL Server object permissions. Nothing new is granted. The statements are recorded on view_created as the additive grants_copied column and reapplied whenever the view is re-created. Old-name views now run on BigQuery, Databricks and SQL Server too, so the supports_old_name_views capability and its plan note are removed. BigQuery cannot rename views, so an old view model is re-created at the archive name. Refs CHI-391 * feat(plan): show each old-name fact on its own row sqb plan nests the old name under each migration with one row each for the view expiry, column aliases, copied grants and the archive. The janitor lists compatibility views as a tree with their expiry or drop reason, and plan JSON gains old_name.grants_copied. Refs CHI-391 * docs(migrations): document copied grants and old-name views on every adapter Refs CHI-391 * test(migrations): cover copied grants, nested plan rows, and real crash windows Postgres crash tests now close the session at the fault, as a killed process would, so nothing after the fault can clean up; every crash point leaves the compatibility view readable. Refs CHI-391 * fix(migrations): read old-name view facts from the columns the state table has Readers inspect the stored _sqlbuild_old_name_views schema and select only the known columns it has, reading columns added later as NULL, as the additive state-schema rule requires. Refs CHI-391 * fix(migrations): apply the old-name skip checks when resuming pending steps A move whose archive or view is still pending no longer resumes when the old name is now a project destination, is a compatibility view of another move, or the model has turned old-name views off; nothing is archived or created and the plan shows the reason under 'left for janitor'. Refs CHI-391 * fix(migrations): keep compatibility view privileges exact across copies, rebuilds, and archives - Adapters read grants as structured facts and render them for the view's exposed columns. SQL Server now copies column-level GRANT and DENY, and PostgreSQL copies column privileges; a column the view does not expose is skipped. - Redefining a view keeps its current privileges instead of replaying the grants recorded at creation: CREATE OR REPLACE on PostgreSQL, COPY GRANTS on Snowflake, ALTER VIEW on SQL Server and Databricks. BigQuery, and a PostgreSQL view that must be dropped, re-read the view's grants just before and reapply them. - Builds redefine only views that exist; a recorded view that is absent is left for janitor's missing reconcile. - BigQuery copies a view's IAM bindings to the archive when archiving a view model. - BigQuery warns with M118 which principals kept access to the old name and also need read access on the destination table; nothing is granted. Refs CHI-391 * fix(janitor): never drop a relation the project built at a compatibility view's name When a recorded view's name is a project model's destination and that model has a fingerprint there newer than the view's creation, the relation is the project's: janitor runs no DDL, records the view as dropped with reason missing, and lists 'name now built by model:x'. Otherwise the view is still SQLBuild's, so expiry and --drop-old-name-view proceed, keeping the M114 remedy. Drops re-check this evidence just before running. Refs CHI-391 * fix(migrations): trust the recorded move as ownership evidence when resuming A resumed archive or view no longer requires the origin's own fingerprint, which the destination build of a same-model alias move supersedes; the recorded move and its required fact prove SQLBuild owned the origin. Resume keeps the name-reuse, compatibility-view, and disabled checks. Refs CHI-391 * fix(migrations): decide view ownership from definitions and reconcile view grants exactly - Janitor drops a recorded compatibility view only when the relation at the old name is a view whose stored definition matches the SQL SQLBuild renders for the move, or the SQL recorded on view_created (new additive view_sql column). Any other relation is left alone, listed as 'name now used by another relation', and the view is recorded as missing. Drops re-check just before running; fingerprints are no longer consulted. PostgreSQL compares through a rolled-back temporary view, since it rewrites stored definitions. - Every create, replace, or re-create of a compatibility view reconciles its privileges to the ones it had just before (or the old relation's, for a new view): missing grants are granted and grants added by default privileges or schema future grants are revoked. Owner privileges are never read or changed. - On adapters whose views bind by name at query time, a build skips redefining a view whose SQL is unchanged. Refs CHI-391 * fix(migrations): never redefine a compatibility view whose old name a project model now uses A recorded compatibility view whose old name is a project model's destination (for example after an interrupted early drop) is no longer refreshed, rebound, or released by builds of the destination. Before, when the project's model built at that name before the destination, the destination's refresh overwrote the project's view with the compatibility SQL; concurrent builds made that order likely. Refs CHI-391
1 parent 4ec1624 commit b67bea0

145 files changed

Lines changed: 9655 additions & 219 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎fensu.toml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,20 @@ contract = "src/sqlbuild/adapter/contract/classes/strict_adapter.py:StrictAdapte
278278
forbidden_owners = ["src/sqlbuild/adapter/contract/classes/base_adapter.py:BaseAdapter"]
279279
paths = ["src/sqlbuild/adapter/contract/classes/*", "src/sqlbuild/adapters/*/classes/*"]
280280
reason = "tests/unit/src/sqlbuild/adapter/contract/classes/strict_adapter/test_strict_adapter.py requires every first-class adapter to define each StrictAdapter method itself instead of inheriting it from BaseAdapter."
281+
282+
[[targets.python.rule_exceptions]]
283+
rule = "XSB051"
284+
path = "src/sqlbuild/executor/migrations/_helpers/old_name_views.py"
285+
symbols = [
286+
"present_old_name_views",
287+
"recreate_old_name_views",
288+
"refresh_old_name_views",
289+
"run_planned_old_name_steps",
290+
]
291+
reason = "Old-name steps re-read facts and physical state per move so a retry never repeats a completed step, and compatibility views are re-created in dependency order, so a chained view must read the columns of the view it selects from after that view is re-created. Each view's existence is checked right before it is redefined, and a model has only its own short chain of old names."
292+
293+
[[targets.python.rule_exceptions]]
294+
rule = "XSB051"
295+
path = "src/sqlbuild/executor/old_name_views/_helpers/janitor.py"
296+
symbols = ["plan_old_name_views", "apply_old_name_view_drops"]
297+
reason = "Janitor reads each compatibility view's stored definition right before deciding to drop it, so a relation that replaced the view is never dropped; a project has few live old names."

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/CONTENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Bundled copies of every page on the SQLBuild documentation site, matching the in
3838
- [Schemas](concepts/models/schemas.md) (`concepts/models/schemas`) - Declare model columns inline or reuse canonical inherited schemas.
3939
- [Type Enforcement](concepts/models/type-enforcement.md) (`concepts/models/type-enforcement`) - Understand declared model types, static checks, and runtime casting.
4040
- [Contracts](concepts/models/contracts.md) (`concepts/models/contracts`) - Validate required or exact model output schemas.
41-
- [Model migrations](concepts/models/migrations.md) (`concepts/models/migrations`) - Keep the history of an incremental or snapshot model when you rename it.
41+
- [Model migrations](concepts/models/migrations.md) (`concepts/models/migrations`) - Keep a renamed model's history, and keep its old name working for a while.
4242
- [Column migrations](concepts/models/column-migrations.md) (`concepts/models/column-migrations`) - Keep a column's history when you rename it in an incremental or snapshot model.
4343
- [Hooks](concepts/models/hooks.md) (`concepts/models/hooks`) - Run SQL or Python lifecycle hooks around model materialization.
4444
- [SQL Hooks](concepts/models/hooks/sql.md) (`concepts/models/hooks/sql`) - Define, parameterize, compile, and invoke reusable or inline SQL lifecycle hooks.

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/cli/janitor.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Online: https://sqlbuild.com/docs/cli/janitor/
1111
- Usage
1212
- Flags
1313
- Lifecycle
14+
- Compatibility views
1415
- Relation age
1516
- Audit record
1617
- Restoring an archive
@@ -34,6 +35,7 @@ sqb --project-dir <path> janitor [flags]
3435
| `--auto-approve` | Skip the confirmation prompt and run the planned actions immediately |
3536
| `--retention-days` | Override the configured retention period (days) before a stale relation is archived |
3637
| `--direct-state-history-versions` | Override how many state-history versions are kept per identity |
38+
| `--drop-old-name-view <name>` | Drop the [compatibility view](../concepts/models/migrations.md#old-names) at a renamed model's old name before it expires. Repeat for several views. Fails with `C503` if no compatibility view is recorded at the name |
3739

3840
## Lifecycle
3941

@@ -58,6 +60,20 @@ Long names are shortened with a short hash so they fit the adapter's identifier
5860

5961
Setting both `retention_days` and `archive_retention_days` to `0` archives and deletes stale relations in the same run.
6062

63+
## Compatibility views
64+
65+
When a model is renamed, SQLBuild keeps a [compatibility view](../concepts/models/migrations.md#old-names) at its old name. The janitor lists each one with the model it reads and its expiry:
66+
67+
```
68+
Old name views (1)
69+
└── prod.revenue -> model:daily_revenue
70+
└── expires 2026-10-28 (live; drop early with --drop-old-name-view prod.revenue)
71+
```
72+
73+
A compatibility view is never archived as a stale relation. Once it expires, or when it is named with `--drop-old-name-view`, the janitor drops it and records the drop in `_sqlbuild_old_name_views`. Before dropping, the janitor checks that the relation at the old name is still the view SQLBuild created there, by comparing its stored definition with the view's SQL. Any other relation at that name, such as a view a project model built after an interrupted early drop, is left alone: the janitor records the compatibility view as dropped and lists it as `record dropped (name now used by another relation, model:revenue)`, naming the model when the name is a project destination. A compatibility view that no longer exists is recorded as dropped too.
74+
75+
Run the same SQLBuild version for `sqb janitor` as for builds. An older janitor does not know compatibility views and archives them as stale relations.
76+
6177
## Relation age
6278

6379
`retention_days` is measured from the last time a relation was written, as reported by the warehouse. For a relation the project no longer builds, that is roughly when it stopped being built.

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/models/column-migrations.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ After the rename is recorded, `migrate_from` has no further effect, and SQLBuild
4646

4747
A declaration is rejected when compiling if:
4848

49-
- the model is not incremental or snapshot;
49+
- the model is not incremental or snapshot, unless it is a table or view model that declares its own `migrate_from`; there the column's `migrate_from` only names the old column in the [compatibility view](migrations.md#old-names) at the model's old name;
5050
- `migrate_from` names the column itself, or a qualified name instead of one column;
5151
- two columns declare the same `migrate_from`;
5252
- a declared column migrates from another column that itself declares `migrate_from`. Chains and swaps can't be renamed in place safely; rename through a name no other column uses, or rebuild with `--full-refresh`.

‎src/sqlbuild/.agents/skills/sqlbuild/references/docs/concepts/models/configuration.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Online: https://sqlbuild.com/docs/concepts/models/configuration/
2323
| `materialized` | `view`, `table`, `incremental`, `snapshot`, or a custom materialization name |
2424
| `tags` | Tags used by selectors |
2525
| `description` | Human-readable model description |
26-
| `columns` | Model-local column declarations, inherited-column audit augmentation, or column renames with `migrate_from` (incremental and snapshot models; see [Column migrations](column-migrations.md)) |
26+
| `columns` | Model-local column declarations, inherited-column audit augmentation, or column renames with `migrate_from` (incremental and snapshot models; see [Column migrations](column-migrations.md)). On a table or view model that declares its own `migrate_from`, a column `migrate_from` only names the old column in the [compatibility view](migrations.md#old-names) |
2727
| `model_schema` | Reusable column schema name |
2828
| `audits` | Model-level audit instances |
2929
| `enums` | Model-local enum declarations; names must begin with `_` |
@@ -81,8 +81,9 @@ Table promotion mode is a project setting rather than a `MODEL()` field. Staged
8181
| `full_refresh` | Optional model execution override: `false` always runs incrementally, `true` always full-refreshes, and omission follows the command |
8282
| `on_schema_change` | `append_new_columns`, `sync_all_columns`, `ignore`, or `fail` |
8383
| `replay_on_change` | `forward`, `full`, or `bounded-<duration>` |
84-
| `migrate_from` | Old model name (or `schema.name`) whose data this model takes over; see [Model migrations](migrations.md). Also valid on snapshot models |
85-
| `migrate_force` | `true` replaces a destination that already has its own build history during a migration |
84+
| `migrate_from` | Old model name (or `schema.name`) whose data this model takes over; see [Model migrations](migrations.md). Also valid on snapshot, table, and view models |
85+
| `migrate_force` | `true` replaces a destination that already has its own build history during a migration. Incremental and snapshot models only |
86+
| `old_name_view` | How long a [compatibility view](migrations.md#old-names) keeps the old name working after a migration, such as `7d`, or `false` for none. Overrides `[migrations] old_name_views`; valid on every model |
8687

8788
See [Incremental](../incremental.md) for full semantics.
8889

0 commit comments

Comments
 (0)