deltaplan 0.1.0a1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deltaplan-0.1.0a1/.gitignore +17 -0
- deltaplan-0.1.0a1/CHANGELOG.md +270 -0
- deltaplan-0.1.0a1/CLAUDE.md +134 -0
- deltaplan-0.1.0a1/CONTRIBUTING.md +130 -0
- deltaplan-0.1.0a1/LICENSE +202 -0
- deltaplan-0.1.0a1/PKG-INFO +155 -0
- deltaplan-0.1.0a1/README.md +122 -0
- deltaplan-0.1.0a1/SECURITY.md +38 -0
- deltaplan-0.1.0a1/action/upsert_comment.py +108 -0
- deltaplan-0.1.0a1/action.yml +119 -0
- deltaplan-0.1.0a1/docs/DESIGN.md +179 -0
- deltaplan-0.1.0a1/docs/ci.md +158 -0
- deltaplan-0.1.0a1/docs/cli.md +160 -0
- deltaplan-0.1.0a1/docs/contributing.md +80 -0
- deltaplan-0.1.0a1/docs/index.md +80 -0
- deltaplan-0.1.0a1/docs/installation.md +87 -0
- deltaplan-0.1.0a1/docs/safety.md +146 -0
- deltaplan-0.1.0a1/docs/security.md +29 -0
- deltaplan-0.1.0a1/docs/spec.md +475 -0
- deltaplan-0.1.0a1/docs/stylesheets/extra.css +56 -0
- deltaplan-0.1.0a1/docs/testing.md +114 -0
- deltaplan-0.1.0a1/examples/bundle/databricks.yml +26 -0
- deltaplan-0.1.0a1/examples/bundle/deltaplan.yml +13 -0
- deltaplan-0.1.0a1/examples/deltaplan.yml +33 -0
- deltaplan-0.1.0a1/examples/tables/big_orders.yml +13 -0
- deltaplan-0.1.0a1/examples/tables/order_band.yml +18 -0
- deltaplan-0.1.0a1/examples/tables/orders.yml +54 -0
- deltaplan-0.1.0a1/mise.toml +64 -0
- deltaplan-0.1.0a1/mkdocs.yml +91 -0
- deltaplan-0.1.0a1/pyproject.toml +99 -0
- deltaplan-0.1.0a1/src/deltaplan/__init__.py +0 -0
- deltaplan-0.1.0a1/src/deltaplan/bundle.py +343 -0
- deltaplan-0.1.0a1/src/deltaplan/cli.py +688 -0
- deltaplan-0.1.0a1/src/deltaplan/differ.py +750 -0
- deltaplan-0.1.0a1/src/deltaplan/executor.py +295 -0
- deltaplan-0.1.0a1/src/deltaplan/history.py +320 -0
- deltaplan-0.1.0a1/src/deltaplan/introspect.py +815 -0
- deltaplan-0.1.0a1/src/deltaplan/loader.py +1383 -0
- deltaplan-0.1.0a1/src/deltaplan/model/__init__.py +5 -0
- deltaplan-0.1.0a1/src/deltaplan/model/change.py +117 -0
- deltaplan-0.1.0a1/src/deltaplan/model/function.py +49 -0
- deltaplan-0.1.0a1/src/deltaplan/model/plan.py +213 -0
- deltaplan-0.1.0a1/src/deltaplan/model/table.py +322 -0
- deltaplan-0.1.0a1/src/deltaplan/model/types.py +299 -0
- deltaplan-0.1.0a1/src/deltaplan/model/view.py +48 -0
- deltaplan-0.1.0a1/src/deltaplan/planner.py +1748 -0
- deltaplan-0.1.0a1/src/deltaplan/planning.py +385 -0
- deltaplan-0.1.0a1/src/deltaplan/py.typed +0 -0
- deltaplan-0.1.0a1/src/deltaplan/render/__init__.py +4 -0
- deltaplan-0.1.0a1/src/deltaplan/render/json.py +475 -0
- deltaplan-0.1.0a1/src/deltaplan/render/labels.py +184 -0
- deltaplan-0.1.0a1/src/deltaplan/render/markdown.py +265 -0
- deltaplan-0.1.0a1/src/deltaplan/render/rich.py +240 -0
- deltaplan-0.1.0a1/src/deltaplan/sql.py +137 -0
- deltaplan-0.1.0a1/src/deltaplan/typeparser.py +270 -0
- deltaplan-0.1.0a1/tests/conftest.py +23 -0
- deltaplan-0.1.0a1/tests/fake_warehouse.py +1280 -0
- deltaplan-0.1.0a1/tests/helpers.py +146 -0
- deltaplan-0.1.0a1/tests/integration/conftest.py +65 -0
- deltaplan-0.1.0a1/tests/integration/test_live_apply.py +241 -0
- deltaplan-0.1.0a1/tests/integration/test_live_governance.py +162 -0
- deltaplan-0.1.0a1/tests/integration/test_live_round_trip.py +177 -0
- deltaplan-0.1.0a1/tests/snapshots/test_differ.ambr +241 -0
- deltaplan-0.1.0a1/tests/snapshots/test_history.ambr +61 -0
- deltaplan-0.1.0a1/tests/snapshots/test_planner.ambr +171 -0
- deltaplan-0.1.0a1/tests/snapshots/test_render_markdown.ambr +64 -0
- deltaplan-0.1.0a1/tests/snapshots/test_render_rich.ambr +67 -0
- deltaplan-0.1.0a1/tests/unit/test_action.py +168 -0
- deltaplan-0.1.0a1/tests/unit/test_bookkeeping.py +65 -0
- deltaplan-0.1.0a1/tests/unit/test_bundle.py +408 -0
- deltaplan-0.1.0a1/tests/unit/test_cli.py +520 -0
- deltaplan-0.1.0a1/tests/unit/test_cli_apply.py +294 -0
- deltaplan-0.1.0a1/tests/unit/test_column_tags.py +157 -0
- deltaplan-0.1.0a1/tests/unit/test_connecting.py +75 -0
- deltaplan-0.1.0a1/tests/unit/test_convergence.py +424 -0
- deltaplan-0.1.0a1/tests/unit/test_differ.py +248 -0
- deltaplan-0.1.0a1/tests/unit/test_examples.py +29 -0
- deltaplan-0.1.0a1/tests/unit/test_executor.py +428 -0
- deltaplan-0.1.0a1/tests/unit/test_fake_warehouse.py +37 -0
- deltaplan-0.1.0a1/tests/unit/test_foreign_keys.py +186 -0
- deltaplan-0.1.0a1/tests/unit/test_functions.py +432 -0
- deltaplan-0.1.0a1/tests/unit/test_generation.py +298 -0
- deltaplan-0.1.0a1/tests/unit/test_grants.py +190 -0
- deltaplan-0.1.0a1/tests/unit/test_history.py +191 -0
- deltaplan-0.1.0a1/tests/unit/test_hooks_and_backfills.py +133 -0
- deltaplan-0.1.0a1/tests/unit/test_introspect.py +261 -0
- deltaplan-0.1.0a1/tests/unit/test_loader.py +449 -0
- deltaplan-0.1.0a1/tests/unit/test_masks_and_filters.py +223 -0
- deltaplan-0.1.0a1/tests/unit/test_name_case.py +128 -0
- deltaplan-0.1.0a1/tests/unit/test_planfile.py +136 -0
- deltaplan-0.1.0a1/tests/unit/test_planner.py +521 -0
- deltaplan-0.1.0a1/tests/unit/test_planning.py +189 -0
- deltaplan-0.1.0a1/tests/unit/test_render_markdown.py +141 -0
- deltaplan-0.1.0a1/tests/unit/test_render_rich.py +136 -0
- deltaplan-0.1.0a1/tests/unit/test_rewrite_safety.py +200 -0
- deltaplan-0.1.0a1/tests/unit/test_schemas.py +86 -0
- deltaplan-0.1.0a1/tests/unit/test_spent_renames.py +60 -0
- deltaplan-0.1.0a1/tests/unit/test_table_renames.py +220 -0
- deltaplan-0.1.0a1/tests/unit/test_typeparser.py +197 -0
- deltaplan-0.1.0a1/tests/unit/test_unmodelled.py +82 -0
- deltaplan-0.1.0a1/tests/unit/test_views.py +306 -0
- deltaplan-0.1.0a1/uv.lock +1034 -0
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0a1] - 2026-09-18
|
|
10
|
+
|
|
11
|
+
The first public release: an alpha. Everything in the design is built and tested
|
|
12
|
+
offline, against a fake warehouse that interprets deltaplan's own SQL. The live
|
|
13
|
+
suite has only just started running against a real workspace — its first run
|
|
14
|
+
found a wrong assumption about `information_schema`, fixed here — so expect more
|
|
15
|
+
of those before 0.1.0.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Table renames**: `renamed_from:` on a table plans `ALTER TABLE … RENAME TO`
|
|
20
|
+
as its first step, and the table's other changes follow under the new name.
|
|
21
|
+
The old name is never treated as an orphan, so a strict schema renames rather
|
|
22
|
+
than drops. `apply` checks for staleness under the name the table was read by.
|
|
23
|
+
- **Asset Bundles.** `bundle: databricks.yml` in `deltaplan.yml` takes the
|
|
24
|
+
targets from the bundle: names, the default, each target's workspace, and
|
|
25
|
+
its variables (defaults, overrides, `BUNDLE_VAR_*`, `${var.…}` and
|
|
26
|
+
`${bundle.target}` references, `include:` files). A `warehouse_id` lookup is
|
|
27
|
+
resolved by name once connected. Variables that only a workspace could
|
|
28
|
+
resolve are reported, with the reason, when a spec uses one.
|
|
29
|
+
- Specs may write a variable as `${var.name}`, as bundles do.
|
|
30
|
+
- A target can be marked `default: true`; `-t` is then optional.
|
|
31
|
+
- **SQL functions** (`function:` specs): parameters, return type, body,
|
|
32
|
+
comment and `EXECUTE` grants. Created before the tables and views that call
|
|
33
|
+
them, replaced when their definition changes (grants put back), never
|
|
34
|
+
dropped. Introspected from `information_schema.routines` and imported.
|
|
35
|
+
- **Foreign keys** (`foreign_key:` constraints), introspected, diffed, planned
|
|
36
|
+
and imported. They are planned after every table, so the table they reference
|
|
37
|
+
exists first, and matched by what they mean rather than only by name.
|
|
38
|
+
- Plan files now read back a table's hooks; they were written but dropped on
|
|
39
|
+
the way in.
|
|
40
|
+
- **Identity, generated and default columns** (`identity:`, `generated:`,
|
|
41
|
+
`default:`). `CREATE TABLE` has all three. Defaults can be set, changed and
|
|
42
|
+
dropped later, with the `allowColumnDefaults` feature enabled first as its own
|
|
43
|
+
step; identity and generated columns exist only from creation, so adding or
|
|
44
|
+
changing one on an existing table is a step deltaplan won't run, with the
|
|
45
|
+
reason. Rewrites carry defaults; tables with identity or generated columns
|
|
46
|
+
are never rewritten. Introspected and imported, so an imported table
|
|
47
|
+
re-creates faithfully.
|
|
48
|
+
- `plan` and `drift` note a `renamed_from` hint that has done its job and can
|
|
49
|
+
be deleted. (The design puts this in `validate`, which can't see the live
|
|
50
|
+
table.) A table with only notes still reads "No changes".
|
|
51
|
+
- **Backfills.** `using:` on a column being added fills the existing rows
|
|
52
|
+
(`UPDATE … WHERE col IS NULL`) before `SET NOT NULL` — so a NOT NULL column
|
|
53
|
+
can be added to a table with data. Without it, the plan warns and says what to
|
|
54
|
+
add.
|
|
55
|
+
- **Table hooks** (`hooks: {before, after}`), the design's simple pre/post SQL
|
|
56
|
+
hooks: run as written around a table's changes, only when it has some.
|
|
57
|
+
- **Schemas are created when a spec needs them**, once each, just before the
|
|
58
|
+
first table or view in them — so a fresh target plans from nothing. Catalogs
|
|
59
|
+
are never created, and schemas are never dropped. The history schema is
|
|
60
|
+
created the same way.
|
|
61
|
+
|
|
62
|
+
- **A target can name its workspace**: `profile:` on a target picks a
|
|
63
|
+
`~/.databrickscfg` profile, and `--profile` overrides it on every command that
|
|
64
|
+
connects — dev and prod are usually different workspaces.
|
|
65
|
+
- A runbook for the live test suite in the testing guide.
|
|
66
|
+
- The terminal plan ends with the same warnings the pull-request comment
|
|
67
|
+
raises: that it destroys something, or has steps `apply` will refuse.
|
|
68
|
+
- **Milestone 5 (governance) is complete.**
|
|
69
|
+
- **Views.** A spec with `view:` and a `query:` describes a view. The query is
|
|
70
|
+
what is compared — whitespace aside — and a change replaces the view, with
|
|
71
|
+
its tags and grants put back as they were, and the old definition as undo.
|
|
72
|
+
Views are planned after tables and after the views they read; a cycle is an
|
|
73
|
+
error. Views can be claimed, dropped in a strict schema, and `import`ed.
|
|
74
|
+
- A table is never turned into a view or the reverse; planning stops instead.
|
|
75
|
+
- Materialized views and streaming tables are recognised and skipped — they
|
|
76
|
+
report their storage as Delta, and would otherwise have been treated as
|
|
77
|
+
tables.
|
|
78
|
+
- **Column masks and row filters**, handled as security controls: set or
|
|
79
|
+
replaced when the spec declares them, never removed because a spec is silent,
|
|
80
|
+
inline in `CREATE TABLE` so a new table is never unprotected, refused up front
|
|
81
|
+
if the function is missing, and never rewritten — the staging copy would hold
|
|
82
|
+
possibly unmasked data.
|
|
83
|
+
- A step's precheck now carries its own `refusal`, so a refused step says
|
|
84
|
+
exactly why ("the masking function … does not exist", "ssn still has NULLs").
|
|
85
|
+
- **Grants** (`grants:` on a table). A principal the spec names gets exactly
|
|
86
|
+
the privileges listed — granted or revoked to match, each revoke with a
|
|
87
|
+
warning and its undo; principals it doesn't name are left alone. Privileges
|
|
88
|
+
are checked against a known list, because as keywords they can't be quoted.
|
|
89
|
+
A rewrite puts back grants to principals the spec doesn't name.
|
|
90
|
+
- **Column tags** (`tags:` on a column), additive like table tags. A rewrite
|
|
91
|
+
puts back the table and column tags the spec doesn't declare, so rebuilding a
|
|
92
|
+
table never diffs away what deltaplan doesn't manage.
|
|
93
|
+
|
|
94
|
+
### Fixed
|
|
95
|
+
|
|
96
|
+
- A new view was headed `~ update` and counted as a change rather than an add.
|
|
97
|
+
- `plan -o plan.txt` also printed the whole plan to stdout.
|
|
98
|
+
- **A rewrite could drop a column without `--allow-destructive`.** A rewrite
|
|
99
|
+
copies only the columns the spec lists, so a column the spec also removed went
|
|
100
|
+
with it — inside a step classed `rewrite`, which the flag doesn't gate. The
|
|
101
|
+
step that replaces the table is now `destructive` whenever the rewrite drops a
|
|
102
|
+
column or field, and names it.
|
|
103
|
+
- **A lossy conversion could NULL values silently.** The staging step now
|
|
104
|
+
checks that every row arrived and no converted column gained NULLs, and stops
|
|
105
|
+
the run — before the original table is touched — if either did.
|
|
106
|
+
- **A rewrite dropped properties and constraints nobody declared** — a
|
|
107
|
+
retention setting such as `delta.logRetentionDuration`, a CHECK or primary key
|
|
108
|
+
someone else added. The replacement now carries them across, as it already
|
|
109
|
+
did tags and grants; a view replace carries its properties the same way.
|
|
110
|
+
- **Partitioning, identity and generated columns, and column defaults went
|
|
111
|
+
unnoticed** — and a rewrite would have dropped them (an identity column coming
|
|
112
|
+
back as a plain BIGINT). They are now read from the catalog, reported as not
|
|
113
|
+
modelled, and a table that has any is never rewritten.
|
|
114
|
+
- **`import` wrote properties Delta maintains itself** — among them
|
|
115
|
+
`delta.columnMapping.maxColumnId`, which every later plan would then have set
|
|
116
|
+
back to a stale value as columns were added. Import now writes intent only,
|
|
117
|
+
`validate` refuses a spec that declares a Delta-maintained property, and
|
|
118
|
+
`delta.feature.*` flags are no longer reported as unmanaged.
|
|
119
|
+
- A failed postcheck reports what it means, not a generic message.
|
|
120
|
+
- The live suite skipped entirely unless `DATABRICKS_HOST` was set, so anyone
|
|
121
|
+
authenticating with a profile would never have run it. It now accepts any
|
|
122
|
+
source the Databricks SDK does, and says why when it skips.
|
|
123
|
+
- Failing to connect to a workspace is a message naming the profile, not a
|
|
124
|
+
traceback.
|
|
125
|
+
|
|
126
|
+
- **A differently-cased name could drop a live table.** A spec naming
|
|
127
|
+
`main.sales.Orders` against the live `orders`, in a strict schema, planned a
|
|
128
|
+
no-op create and a DROP of the real table. Names are now compared the way Unity
|
|
129
|
+
Catalog compares them: object names in lower case, column and field names
|
|
130
|
+
ignoring case.
|
|
131
|
+
- **The apply lock could expire under a long run.** It is now renewed before
|
|
132
|
+
every step, and a run that finds it has lost the lock stops rather than carry
|
|
133
|
+
on beside a second one.
|
|
134
|
+
- A precheck whose own query fails is recorded as a failed step, instead of
|
|
135
|
+
escaping as a traceback; warehouse errors during `apply` and `force-unlock` are
|
|
136
|
+
reported as messages.
|
|
137
|
+
|
|
138
|
+
### Before the first release
|
|
139
|
+
|
|
140
|
+
deltaplan was built in milestones before anything was published. Their
|
|
141
|
+
numbers were internal and never released; what each added is kept here.
|
|
142
|
+
|
|
143
|
+
#### Milestone 4
|
|
144
|
+
|
|
145
|
+
##### Added
|
|
146
|
+
|
|
147
|
+
- **Milestone 4 (CI) is complete.**
|
|
148
|
+
- `--format md`: the plan as a pull-request comment — summary, GitHub alerts for
|
|
149
|
+
anything destructive, expensive or impossible, a `diff` block per table so
|
|
150
|
+
additions and removals are coloured, the numbered steps with their risk, and
|
|
151
|
+
the SQL folded away. Falls back to leaving out the SQL, then to a summary
|
|
152
|
+
table, when a plan is too long for a comment.
|
|
153
|
+
- `deltaplan show plan.json`: render a saved plan in any format without a
|
|
154
|
+
warehouse — exactly what `apply` of that file would run.
|
|
155
|
+
- `deltaplan drift`: exits 0 in sync, 2 on drift, 1 on error. Drift is anything
|
|
156
|
+
`apply` would do; unmanaged objects are not drift.
|
|
157
|
+
- A GitHub Action (`uses: misja-pronk/deltaplan@v0`) that runs `plan` or
|
|
158
|
+
`drift`, writes the job summary, and comments on the pull request — updating
|
|
159
|
+
its own comment rather than adding one per push. Inputs reach its script
|
|
160
|
+
through the environment, never by interpolation, and a test holds it to that.
|
|
161
|
+
- CI lints the workflows with actionlint; releases move the major-version tag
|
|
162
|
+
the Action is used by.
|
|
163
|
+
|
|
164
|
+
#### Ownership and strict schemas
|
|
165
|
+
|
|
166
|
+
##### Added
|
|
167
|
+
|
|
168
|
+
- **Ownership is claimed.** A spec for a table deltaplan didn't create plans a
|
|
169
|
+
visible `CLAIM ownership` step that marks it managed — which is how an
|
|
170
|
+
`import`ed table is handed over on its first apply.
|
|
171
|
+
- **Strict schemas.** A managed table whose spec was deleted is dropped in a
|
|
172
|
+
strict schema (destructive, so `--allow-destructive` applies, with `UNDROP`
|
|
173
|
+
as the way back) and kept — but listed — in an additive one. Tables deltaplan
|
|
174
|
+
didn't create are never touched in either mode.
|
|
175
|
+
- `schemas:` in `deltaplan.yml` sets the mode per schema, as the design
|
|
176
|
+
specifies; the target's `mode` is the default.
|
|
177
|
+
- `history_schema` and `schemas:` keys may use target variables
|
|
178
|
+
(`${catalog}.deltaplan`), so one project file serves every catalog.
|
|
179
|
+
- `deltaplan plan --clone` adds a `SHALLOW CLONE` of each table before the first
|
|
180
|
+
step that could lose its data.
|
|
181
|
+
- `planning.py`: the specs-to-plan pipeline, out of the CLI, so `plan`, `drift`
|
|
182
|
+
and the GitHub Action share it.
|
|
183
|
+
|
|
184
|
+
##### Changed
|
|
185
|
+
|
|
186
|
+
- The plan summary counts destroyed tables; it was hard-coded to zero.
|
|
187
|
+
|
|
188
|
+
#### Milestone 3
|
|
189
|
+
|
|
190
|
+
##### Added
|
|
191
|
+
|
|
192
|
+
- **Milestone 3 (rewrites) is complete**: a table that can't be patched is
|
|
193
|
+
rebuilt, and `apply` runs it.
|
|
194
|
+
- A rewrite stages the converted data beside the table, **replaces** the table
|
|
195
|
+
from that staging table (keeping its identity and Delta history, so the
|
|
196
|
+
recorded restore point means something, and with no window where the table is
|
|
197
|
+
empty), puts back what a query result can't carry — `NOT NULL`, comments, tags,
|
|
198
|
+
constraints — with ordinary `ALTER`s, and drops the staging table.
|
|
199
|
+
- deltaplan writes the conversion where it honestly can: a cast between scalars,
|
|
200
|
+
`named_struct` matched **by name** rather than by position, `transform` over an
|
|
201
|
+
array of structs, and `CAST(NULL AS …)` for a column that didn't exist.
|
|
202
|
+
- `using:` on a column — a SQL expression over the live table — for conversions
|
|
203
|
+
deltaplan won't invent: a struct becoming an array, a map whose shape moved, or
|
|
204
|
+
any change that needs a decision rather than a cast.
|
|
205
|
+
- The plan file now carries both sides of each diff, so it records what was
|
|
206
|
+
compared and a rewrite knows what it is rebuilding into.
|
|
207
|
+
|
|
208
|
+
##### Changed
|
|
209
|
+
|
|
210
|
+
- `apply` no longer refuses plans containing rewrites. It still refuses any plan
|
|
211
|
+
with a step deltaplan couldn't generate, naming the step and what it needs.
|
|
212
|
+
|
|
213
|
+
##### Fixed
|
|
214
|
+
|
|
215
|
+
- Table-level changes (properties, tags) were rendered one level too deep, as
|
|
216
|
+
though they were nested inside a column.
|
|
217
|
+
|
|
218
|
+
#### Milestone 2
|
|
219
|
+
|
|
220
|
+
##### Added
|
|
221
|
+
|
|
222
|
+
- **Milestone 2 (apply) is complete**: `deltaplan apply plan.json` and
|
|
223
|
+
`deltaplan force-unlock`.
|
|
224
|
+
- Executor with the design's four promises: a fresh run refuses a stale plan
|
|
225
|
+
(recomputed state fingerprint), steps are skipped when the change they
|
|
226
|
+
implement is already true of the live table, a failed run resumes from the
|
|
227
|
+
history table instead of starting over, and a lock row per target keeps two
|
|
228
|
+
applies apart. A restore point is recorded before every destructive step.
|
|
229
|
+
- Run history in Delta tables (`runs`, `steps`, `lock`) in the schema named by
|
|
230
|
+
`history_schema`, created on first use.
|
|
231
|
+
- The plan file is now read as well as written, so `apply` consumes exactly what
|
|
232
|
+
`plan` produced — asserted by a round-trip test.
|
|
233
|
+
- A fake warehouse (`tests/fake_warehouse.py`) that interprets deltaplan's own
|
|
234
|
+
SQL against in-memory models, so `plan → apply → re-plan is empty` is asserted
|
|
235
|
+
offline for every kind of change. See [docs/testing.md](docs/testing.md).
|
|
236
|
+
|
|
237
|
+
##### Fixed
|
|
238
|
+
|
|
239
|
+
- The table features deltaplan enables itself as prerequisites
|
|
240
|
+
(`delta.columnMapping.mode`, `delta.enableTypeWidening`) are no longer reported
|
|
241
|
+
back as unmanaged properties after an apply.
|
|
242
|
+
|
|
243
|
+
#### Milestone 1
|
|
244
|
+
|
|
245
|
+
##### Added
|
|
246
|
+
|
|
247
|
+
- **Milestone 1 (read-only) is complete**: `validate`, `import` and `plan`.
|
|
248
|
+
- Type tree and parser for Databricks type strings, including nested
|
|
249
|
+
struct/array/map, decimals, backticked field names, and `not null` / `comment`
|
|
250
|
+
inside structs.
|
|
251
|
+
- YAML loader with `${var}` substitution per target, both type notations, and
|
|
252
|
+
errors that carry file, line and column — including for unknown keys.
|
|
253
|
+
- A `deltaplan.yml` project file: where specs live, and what each target
|
|
254
|
+
substitutes.
|
|
255
|
+
- Pure differ: recursive diff at Databricks' nested paths, declared renames via
|
|
256
|
+
`renamed_from`, kind-change detection, and opt-in column-order diffing.
|
|
257
|
+
- Pure planner: changes become ordered steps classified `meta` / `feature` /
|
|
258
|
+
`rewrite` / `destructive`, with column mapping and type widening inserted as
|
|
259
|
+
their own prerequisite steps, and a conservative widening matrix.
|
|
260
|
+
- Renderers: the terminal layout from the design document, and JSON for
|
|
261
|
+
`-o plan.json`.
|
|
262
|
+
- Introspection of live Unity Catalog state through `information_schema` and
|
|
263
|
+
`DESCRIBE DETAIL`, plus a live integration suite that asserts the Databricks
|
|
264
|
+
behaviour the planner relies on.
|
|
265
|
+
- Project scaffold: uv + hatchling packaging (src layout, Apache-2.0), mise tasks,
|
|
266
|
+
ruff + ty configuration, pytest with a `unit` / `integration` split, and CI for
|
|
267
|
+
lint, types, tests, docs and the built wheel.
|
|
268
|
+
- `docs/DESIGN.md` as the source of truth, plus a mkdocs-material site published to
|
|
269
|
+
GitHub Pages.
|
|
270
|
+
- A `deltaplan version` command, so the packaging is testable end to end.
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
`deltaplan`: declarative plan/apply for Databricks SQL tables. Read `docs/DESIGN.md` first; it is the source of truth. If code and design disagree, flag it instead of silently picking one.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
- Differ and planner are pure: no I/O, no SDK imports, no clock, no env.
|
|
8
|
+
- Domain model: frozen, slotted stdlib dataclasses with tuples. Pydantic/msgspec only in the loader.
|
|
9
|
+
- Never generate SQL by string-concatenating unquoted identifiers. One `quote_ident()` helper, used everywhere.
|
|
10
|
+
- Anything not modelled on a live table is reported as unmanaged, never diffed away.
|
|
11
|
+
- No destructive step without the `destructive` risk class.
|
|
12
|
+
- Every Databricks behaviour assumption gets a test and a link to the docs in the test docstring. If unsure about a behaviour, say so and add a `TODO(verify)` — do not guess.
|
|
13
|
+
- Small PR-sized commits, conventional commit messages.
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/deltaplan/
|
|
19
|
+
model/ types.py, table.py, change.py, plan.py
|
|
20
|
+
typeparser.py
|
|
21
|
+
loader.py
|
|
22
|
+
introspect.py
|
|
23
|
+
differ.py
|
|
24
|
+
planner.py
|
|
25
|
+
render/ rich.py, markdown.py, json.py
|
|
26
|
+
executor.py (milestone 2)
|
|
27
|
+
cli.py
|
|
28
|
+
tests/
|
|
29
|
+
unit/
|
|
30
|
+
integration/
|
|
31
|
+
snapshots/
|
|
32
|
+
docs/ DESIGN.md (source of truth) + the mkdocs site
|
|
33
|
+
examples/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
Tooling is mise + the Astral stack (uv, ruff, ty) — same as `isolinear`. Never use
|
|
39
|
+
pip/virtualenv, black/flake8/isort, or mypy.
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
mise install # pinned Python + uv
|
|
43
|
+
uv sync
|
|
44
|
+
uv run pytest tests/unit
|
|
45
|
+
uv run pytest -m integration # needs DATABRICKS_HOST / token / warehouse id
|
|
46
|
+
uv run ruff check . && uv run ruff format --check .
|
|
47
|
+
uv run ty check
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`mise run check` is the full gate (lint + format check + types + unit tests);
|
|
51
|
+
`mise tasks` lists the rest. Docs: `mise run docs` (mkdocs-material, published to
|
|
52
|
+
Pages). Releases are version-driven from `pyproject.toml` — see CONTRIBUTING.md.
|
|
53
|
+
|
|
54
|
+
## Milestone 1 (read-only) — done
|
|
55
|
+
|
|
56
|
+
All eight items are built, lint/type/test clean, with golden plans in
|
|
57
|
+
`tests/snapshots/` and a live suite in `tests/integration/`. Deliberate
|
|
58
|
+
departures from this document, each explained in the commit that made it:
|
|
59
|
+
`ty` instead of pyright; a hand-written validator in the loader rather than
|
|
60
|
+
Pydantic/msgspec, so errors carry file:line:column; `deltaplan.yml` invented for
|
|
61
|
+
the project/target config; nested YAML types extended from struct to array and
|
|
62
|
+
map; `sql.py` added for `quote_ident()`; rewrites classified but not generated (milestone 3); the Markdown
|
|
63
|
+
renderer deferred to milestone 4.
|
|
64
|
+
|
|
65
|
+
**Milestone 2 (apply) is done too**: `executor.py`, `history.py` (the design's
|
|
66
|
+
runs/steps/lock tables), `apply` and `force-unlock`. Two more departures worth
|
|
67
|
+
knowing: the design's per-step precheck/postcheck queries are replaced by
|
|
68
|
+
`differ.is_applied()`, which asks the *model* whether a change is already true
|
|
69
|
+
of the live table — it reuses tested code instead of inventing SQL we cannot
|
|
70
|
+
verify (`precheck` survives as a precondition guard, e.g. SET NOT NULL); and
|
|
71
|
+
`history.py` is a new module the layout above doesn't list, because putting the
|
|
72
|
+
history tables inside `executor.py` would have made one file do two jobs.
|
|
73
|
+
|
|
74
|
+
Testing without a workspace: `tests/fake_warehouse.py` is an in-memory catalog
|
|
75
|
+
that interprets deltaplan's own SQL, so plan → apply → re-plan can be asserted
|
|
76
|
+
offline for every change kind. It proves our SQL matches our intent; only
|
|
77
|
+
`tests/integration/` proves Databricks agrees. Read `docs/testing.md` before
|
|
78
|
+
adding a test, and keep the fake's `FakeSqlError` loud — a statement shape it
|
|
79
|
+
doesn't know must fail, not pass.
|
|
80
|
+
|
|
81
|
+
**Milestone 3 (rewrites) is done.** A table with a rewrite-class change is
|
|
82
|
+
rebuilt whole rather than patched: stage the converted data, REPLACE the table
|
|
83
|
+
from the staging table (identity and history kept, no empty window), put back
|
|
84
|
+
what a query result can't carry with ordinary ALTERs, drop the staging table.
|
|
85
|
+
Two departures worth knowing: the design's single `CREATE OR REPLACE TABLE …
|
|
86
|
+
AS SELECT` is staged in two statements, because a self-referencing RTAS is
|
|
87
|
+
unverified (TODO(verify) in `replace_table_sql`) and staging makes the expensive
|
|
88
|
+
step repeatable; and `using:` is a new spec hint for conversions deltaplan won't
|
|
89
|
+
invent. A rewrite cannot set NOT NULL on a nested field — it refuses rather than
|
|
90
|
+
dropping the constraint.
|
|
91
|
+
|
|
92
|
+
**The rest of the design's safety model is in too**: ownership claims (a spec
|
|
93
|
+
for someone else's table plans a visible `claim_table`), strict schemas (managed
|
|
94
|
+
tables whose spec is gone become `drop_table`, destructive), and
|
|
95
|
+
`plan --clone` for a SHALLOW CLONE before risky steps. The specs-to-plan
|
|
96
|
+
pipeline lives in `planning.py` (a module the layout above doesn't list),
|
|
97
|
+
because `plan`, `drift` and the Action all need it. One departure: the design
|
|
98
|
+
says the mode is per schema; `deltaplan.yml` has a per-schema `schemas:` map
|
|
99
|
+
*and* keeps the target's `mode` as the default for unlisted schemas.
|
|
100
|
+
|
|
101
|
+
**Milestone 4 (CI) is done**: `render/markdown.py`, `show`, `drift` (exit 0/2/1),
|
|
102
|
+
and a composite GitHub Action — `action.yml` at the repo root, with its comment
|
|
103
|
+
script in `action/upsert_comment.py` (stdlib only, tested against a fake API).
|
|
104
|
+
Change labels are shared by both renderers in `render/labels.py`. The Action
|
|
105
|
+
must never interpolate `${{ }}` into a `run:` script — `test_action.py`
|
|
106
|
+
enforces it.
|
|
107
|
+
|
|
108
|
+
**Milestone 5 (governance) is done**: column tags, grants (per principal),
|
|
109
|
+
column masks and row filters (additive, never removed, never rewritten), and
|
|
110
|
+
views (`model/view.py`; `Relation = Table | View`; tables and views share
|
|
111
|
+
`Securable`). Every milestone in DESIGN.md is built.
|
|
112
|
+
|
|
113
|
+
Since then, beyond the design: schema creation, hooks and backfills, identity /
|
|
114
|
+
generated / default columns, foreign keys, SQL functions (`model/function.py`;
|
|
115
|
+
`Relation = Table | View | Function`) and Asset Bundle targets (`bundle.py`, a
|
|
116
|
+
new module the layout doesn't list: it reads someone else's YAML leniently,
|
|
117
|
+
which `loader.py`'s strict validator shouldn't). External tables are out of
|
|
118
|
+
scope for now — managed tables only.
|
|
119
|
+
|
|
120
|
+
What remains is verification, not construction: nothing has run against a real
|
|
121
|
+
workspace. Every `TODO(verify)` in `src/` names an assumption the live suite in
|
|
122
|
+
`tests/integration/` is written to settle — run it (`uv run pytest -m
|
|
123
|
+
integration`) before trusting any of this with production tables.
|
|
124
|
+
|
|
125
|
+
1. ~~Scaffold: `pyproject.toml` (uv, src layout, Apache-2.0), ruff, ty, pytest, GitHub Actions for lint + unit tests, README stub, move `DESIGN.md` to `docs/`.~~ **Done.**
|
|
126
|
+
2. `model/types.py` + `typeparser.py`: type tree and parser for Databricks type strings incl. nested struct/array/map, decimal, backticked field names, `NOT NULL` and comments inside structs. Round-trip tests.
|
|
127
|
+
3. `model/table.py` + `loader.py`: YAML → model, both type notations, `${var}` substitution, strict unknown-key errors with file/line paths.
|
|
128
|
+
4. `differ.py`: recursive diff with paths, `renamed_from` handling, kind-change detection. Snapshot tests.
|
|
129
|
+
5. `planner.py`: changes → steps with risk classes and prerequisite steps (column mapping, type widening). SQL generation. Snapshot tests.
|
|
130
|
+
6. `render/rich.py`: the plan layout from the design doc, nested changes as a tree.
|
|
131
|
+
7. `introspect.py`: live state via `databricks-sdk` Statement Execution API. Integration tests with ephemeral schema.
|
|
132
|
+
8. `cli.py`: `validate`, `import`, `plan`.
|
|
133
|
+
|
|
134
|
+
Stop after each numbered item and summarise what was built and what was assumed.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Contributing to deltaplan
|
|
2
|
+
|
|
3
|
+
Thanks for your interest! Issues and pull requests are very welcome.
|
|
4
|
+
|
|
5
|
+
`deltaplan` is pre-alpha: the read-only milestone (`validate`, `import`, `plan`) is
|
|
6
|
+
still being built. [`docs/DESIGN.md`](docs/DESIGN.md) is the source of truth — if the
|
|
7
|
+
code and the design disagree, that is a bug in one of them, so please say which.
|
|
8
|
+
|
|
9
|
+
## Toolchain
|
|
10
|
+
|
|
11
|
+
deltaplan uses [`mise`](https://mise.jdx.dev) to pin tools and the all-Astral
|
|
12
|
+
stack — [`uv`](https://docs.astral.sh/uv/) (env / deps / run),
|
|
13
|
+
[`ruff`](https://docs.astral.sh/ruff/) (lint + format), and
|
|
14
|
+
[`ty`](https://docs.astral.sh/ty/) (type check).
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
mise install # installs the pinned Python + uv (optional but recommended)
|
|
18
|
+
uv sync # creates .venv and installs deps + dev tools
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Day-to-day
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
uv run deltaplan # run the CLI
|
|
25
|
+
uv run pytest tests/unit # fast tests, no workspace needed
|
|
26
|
+
uv run ruff check . && uv run ruff format . # lint + format
|
|
27
|
+
uv run ty check # type check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`mise run check` runs the whole gate (lint, format check, types, unit tests) in one
|
|
31
|
+
go; `mise tasks` lists the rest.
|
|
32
|
+
|
|
33
|
+
All of these run in CI on every push/PR — please make sure they're green before
|
|
34
|
+
opening a PR. New behaviour should come with a test.
|
|
35
|
+
|
|
36
|
+
## Architecture
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
spec (YAML) ─┐
|
|
40
|
+
├─> differ ─> changes ─> planner ─> plan (JSON) ─> renderer
|
|
41
|
+
live (UC) ───┘ │
|
|
42
|
+
└─> executor ─> history
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The dependency rule is simple: **the middle of the pipeline does no I/O.**
|
|
46
|
+
|
|
47
|
+
- **`model/`** — frozen, slotted stdlib dataclasses holding tuples, so everything is
|
|
48
|
+
hashable. No Pydantic, no SDK.
|
|
49
|
+
- **`loader.py` / `introspect.py`** — the only places that read YAML or talk to a
|
|
50
|
+
workspace. Validation happens at this edge and nowhere else.
|
|
51
|
+
- **`differ.py` / `planner.py`** — pure functions: no I/O, no SDK imports, no clock,
|
|
52
|
+
no environment. They must be unit-testable without a workspace.
|
|
53
|
+
- **`render/`** — rich / markdown / json views of the same `Plan` object, sharing
|
|
54
|
+
their wording through `render/labels.py`.
|
|
55
|
+
- **`planning.py`** — specs and a warehouse in, a plan out: the pipeline `plan`,
|
|
56
|
+
`drift` and the GitHub Action share.
|
|
57
|
+
- **`executor.py`** / **`history.py`** — the only places that run SQL that changes
|
|
58
|
+
anything.
|
|
59
|
+
- **`action.yml`** + **`action/`** — the GitHub Action, at the repo root so
|
|
60
|
+
`uses: misja-pronk/deltaplan@v0` finds it.
|
|
61
|
+
|
|
62
|
+
House rules worth repeating:
|
|
63
|
+
|
|
64
|
+
- Never build SQL by concatenating unquoted identifiers — there is one
|
|
65
|
+
`quote_ident()` helper, and it is used everywhere.
|
|
66
|
+
- Anything not modelled on a live table is reported as **unmanaged** and never
|
|
67
|
+
diffed away. Only tables deltaplan created can be drop candidates.
|
|
68
|
+
- No destructive step without the `destructive` risk class.
|
|
69
|
+
- Every Databricks behaviour assumption gets a test and a link to the docs in the
|
|
70
|
+
test docstring. If a behaviour is unclear, add a `TODO(verify)` and say so — don't
|
|
71
|
+
guess.
|
|
72
|
+
|
|
73
|
+
## Tests
|
|
74
|
+
|
|
75
|
+
Three layers, each honest about what it proves:
|
|
76
|
+
|
|
77
|
+
1. **Unit tests** (`tests/unit/`) — the pure middle of the pipeline, with golden plans
|
|
78
|
+
in `tests/snapshots/`.
|
|
79
|
+
2. **Convergence against a fake warehouse** (`tests/fake_warehouse.py`) — an in-memory
|
|
80
|
+
catalog that interprets the statements the planner generates, so `plan → apply →
|
|
81
|
+
re-plan is empty` can be asserted offline, for every kind of change. It proves our
|
|
82
|
+
SQL means what our changes mean; it cannot prove Databricks accepts it.
|
|
83
|
+
3. **Integration tests** (`tests/integration/`) — marked `@pytest.mark.integration`,
|
|
84
|
+
skipped without credentials, run nightly against a real workspace in an ephemeral
|
|
85
|
+
schema. The only source of truth about Databricks.
|
|
86
|
+
|
|
87
|
+
Every discovered Databricks limitation becomes a test in layer 3, with a link to the
|
|
88
|
+
documentation in its docstring. There is a fuller description in
|
|
89
|
+
[docs/testing.md](docs/testing.md).
|
|
90
|
+
|
|
91
|
+
## Commits & PRs
|
|
92
|
+
|
|
93
|
+
- Small, PR-sized commits with [conventional commit](https://www.conventionalcommits.org/)
|
|
94
|
+
messages (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`).
|
|
95
|
+
- Describe the *why*, not just the *what*.
|
|
96
|
+
- By contributing you agree your work is licensed under the project's
|
|
97
|
+
[Apache-2.0 License](LICENSE).
|
|
98
|
+
|
|
99
|
+
## Releasing
|
|
100
|
+
|
|
101
|
+
Releases are **version-driven**: the `version` in `pyproject.toml` is the single
|
|
102
|
+
source of truth, and merging a bump to `main` ships it. No manual tagging.
|
|
103
|
+
|
|
104
|
+
1. On a branch, bump the version:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
uv version --bump patch # or: minor / major — edits pyproject.toml + uv.lock
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
2. In `CHANGELOG.md`, rename the `## [Unreleased]` heading to `## [X.Y.Z]` (the
|
|
111
|
+
new version) and start a fresh, empty `## [Unreleased]` above it. Those notes
|
|
112
|
+
become the GitHub release body.
|
|
113
|
+
3. Open a PR. When it merges to `main`, the [`release`](.github/workflows/release.yml)
|
|
114
|
+
workflow builds the wheel + sdist, publishes to **PyPI** (via Trusted Publishing —
|
|
115
|
+
no API token), and creates the **`vX.Y.Z`** tag and GitHub release.
|
|
116
|
+
|
|
117
|
+
A merge that doesn't change the version is a no-op, and a version that's already
|
|
118
|
+
tagged or already on PyPI is skipped — so the workflow is safe to re-run.
|
|
119
|
+
|
|
120
|
+
> **One-time setup.** Releasing is deliberately dormant until deltaplan is public.
|
|
121
|
+
> Two things switch it on: a [PyPI Trusted Publisher](https://docs.pypi.org/trusted-publishers/)
|
|
122
|
+
> for repository `misja-pronk/deltaplan`, workflow `release.yml`, environment `pypi`
|
|
123
|
+
> (at <https://pypi.org/manage/account/publishing/>), and the repository variable
|
|
124
|
+
> `RELEASE_ENABLED=true`.
|
|
125
|
+
|
|
126
|
+
## Previewing the docs
|
|
127
|
+
|
|
128
|
+
```sh
|
|
129
|
+
uv run --group docs mkdocs serve
|
|
130
|
+
```
|