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.
Files changed (102) hide show
  1. deltaplan-0.1.0a1/.gitignore +17 -0
  2. deltaplan-0.1.0a1/CHANGELOG.md +270 -0
  3. deltaplan-0.1.0a1/CLAUDE.md +134 -0
  4. deltaplan-0.1.0a1/CONTRIBUTING.md +130 -0
  5. deltaplan-0.1.0a1/LICENSE +202 -0
  6. deltaplan-0.1.0a1/PKG-INFO +155 -0
  7. deltaplan-0.1.0a1/README.md +122 -0
  8. deltaplan-0.1.0a1/SECURITY.md +38 -0
  9. deltaplan-0.1.0a1/action/upsert_comment.py +108 -0
  10. deltaplan-0.1.0a1/action.yml +119 -0
  11. deltaplan-0.1.0a1/docs/DESIGN.md +179 -0
  12. deltaplan-0.1.0a1/docs/ci.md +158 -0
  13. deltaplan-0.1.0a1/docs/cli.md +160 -0
  14. deltaplan-0.1.0a1/docs/contributing.md +80 -0
  15. deltaplan-0.1.0a1/docs/index.md +80 -0
  16. deltaplan-0.1.0a1/docs/installation.md +87 -0
  17. deltaplan-0.1.0a1/docs/safety.md +146 -0
  18. deltaplan-0.1.0a1/docs/security.md +29 -0
  19. deltaplan-0.1.0a1/docs/spec.md +475 -0
  20. deltaplan-0.1.0a1/docs/stylesheets/extra.css +56 -0
  21. deltaplan-0.1.0a1/docs/testing.md +114 -0
  22. deltaplan-0.1.0a1/examples/bundle/databricks.yml +26 -0
  23. deltaplan-0.1.0a1/examples/bundle/deltaplan.yml +13 -0
  24. deltaplan-0.1.0a1/examples/deltaplan.yml +33 -0
  25. deltaplan-0.1.0a1/examples/tables/big_orders.yml +13 -0
  26. deltaplan-0.1.0a1/examples/tables/order_band.yml +18 -0
  27. deltaplan-0.1.0a1/examples/tables/orders.yml +54 -0
  28. deltaplan-0.1.0a1/mise.toml +64 -0
  29. deltaplan-0.1.0a1/mkdocs.yml +91 -0
  30. deltaplan-0.1.0a1/pyproject.toml +99 -0
  31. deltaplan-0.1.0a1/src/deltaplan/__init__.py +0 -0
  32. deltaplan-0.1.0a1/src/deltaplan/bundle.py +343 -0
  33. deltaplan-0.1.0a1/src/deltaplan/cli.py +688 -0
  34. deltaplan-0.1.0a1/src/deltaplan/differ.py +750 -0
  35. deltaplan-0.1.0a1/src/deltaplan/executor.py +295 -0
  36. deltaplan-0.1.0a1/src/deltaplan/history.py +320 -0
  37. deltaplan-0.1.0a1/src/deltaplan/introspect.py +815 -0
  38. deltaplan-0.1.0a1/src/deltaplan/loader.py +1383 -0
  39. deltaplan-0.1.0a1/src/deltaplan/model/__init__.py +5 -0
  40. deltaplan-0.1.0a1/src/deltaplan/model/change.py +117 -0
  41. deltaplan-0.1.0a1/src/deltaplan/model/function.py +49 -0
  42. deltaplan-0.1.0a1/src/deltaplan/model/plan.py +213 -0
  43. deltaplan-0.1.0a1/src/deltaplan/model/table.py +322 -0
  44. deltaplan-0.1.0a1/src/deltaplan/model/types.py +299 -0
  45. deltaplan-0.1.0a1/src/deltaplan/model/view.py +48 -0
  46. deltaplan-0.1.0a1/src/deltaplan/planner.py +1748 -0
  47. deltaplan-0.1.0a1/src/deltaplan/planning.py +385 -0
  48. deltaplan-0.1.0a1/src/deltaplan/py.typed +0 -0
  49. deltaplan-0.1.0a1/src/deltaplan/render/__init__.py +4 -0
  50. deltaplan-0.1.0a1/src/deltaplan/render/json.py +475 -0
  51. deltaplan-0.1.0a1/src/deltaplan/render/labels.py +184 -0
  52. deltaplan-0.1.0a1/src/deltaplan/render/markdown.py +265 -0
  53. deltaplan-0.1.0a1/src/deltaplan/render/rich.py +240 -0
  54. deltaplan-0.1.0a1/src/deltaplan/sql.py +137 -0
  55. deltaplan-0.1.0a1/src/deltaplan/typeparser.py +270 -0
  56. deltaplan-0.1.0a1/tests/conftest.py +23 -0
  57. deltaplan-0.1.0a1/tests/fake_warehouse.py +1280 -0
  58. deltaplan-0.1.0a1/tests/helpers.py +146 -0
  59. deltaplan-0.1.0a1/tests/integration/conftest.py +65 -0
  60. deltaplan-0.1.0a1/tests/integration/test_live_apply.py +241 -0
  61. deltaplan-0.1.0a1/tests/integration/test_live_governance.py +162 -0
  62. deltaplan-0.1.0a1/tests/integration/test_live_round_trip.py +177 -0
  63. deltaplan-0.1.0a1/tests/snapshots/test_differ.ambr +241 -0
  64. deltaplan-0.1.0a1/tests/snapshots/test_history.ambr +61 -0
  65. deltaplan-0.1.0a1/tests/snapshots/test_planner.ambr +171 -0
  66. deltaplan-0.1.0a1/tests/snapshots/test_render_markdown.ambr +64 -0
  67. deltaplan-0.1.0a1/tests/snapshots/test_render_rich.ambr +67 -0
  68. deltaplan-0.1.0a1/tests/unit/test_action.py +168 -0
  69. deltaplan-0.1.0a1/tests/unit/test_bookkeeping.py +65 -0
  70. deltaplan-0.1.0a1/tests/unit/test_bundle.py +408 -0
  71. deltaplan-0.1.0a1/tests/unit/test_cli.py +520 -0
  72. deltaplan-0.1.0a1/tests/unit/test_cli_apply.py +294 -0
  73. deltaplan-0.1.0a1/tests/unit/test_column_tags.py +157 -0
  74. deltaplan-0.1.0a1/tests/unit/test_connecting.py +75 -0
  75. deltaplan-0.1.0a1/tests/unit/test_convergence.py +424 -0
  76. deltaplan-0.1.0a1/tests/unit/test_differ.py +248 -0
  77. deltaplan-0.1.0a1/tests/unit/test_examples.py +29 -0
  78. deltaplan-0.1.0a1/tests/unit/test_executor.py +428 -0
  79. deltaplan-0.1.0a1/tests/unit/test_fake_warehouse.py +37 -0
  80. deltaplan-0.1.0a1/tests/unit/test_foreign_keys.py +186 -0
  81. deltaplan-0.1.0a1/tests/unit/test_functions.py +432 -0
  82. deltaplan-0.1.0a1/tests/unit/test_generation.py +298 -0
  83. deltaplan-0.1.0a1/tests/unit/test_grants.py +190 -0
  84. deltaplan-0.1.0a1/tests/unit/test_history.py +191 -0
  85. deltaplan-0.1.0a1/tests/unit/test_hooks_and_backfills.py +133 -0
  86. deltaplan-0.1.0a1/tests/unit/test_introspect.py +261 -0
  87. deltaplan-0.1.0a1/tests/unit/test_loader.py +449 -0
  88. deltaplan-0.1.0a1/tests/unit/test_masks_and_filters.py +223 -0
  89. deltaplan-0.1.0a1/tests/unit/test_name_case.py +128 -0
  90. deltaplan-0.1.0a1/tests/unit/test_planfile.py +136 -0
  91. deltaplan-0.1.0a1/tests/unit/test_planner.py +521 -0
  92. deltaplan-0.1.0a1/tests/unit/test_planning.py +189 -0
  93. deltaplan-0.1.0a1/tests/unit/test_render_markdown.py +141 -0
  94. deltaplan-0.1.0a1/tests/unit/test_render_rich.py +136 -0
  95. deltaplan-0.1.0a1/tests/unit/test_rewrite_safety.py +200 -0
  96. deltaplan-0.1.0a1/tests/unit/test_schemas.py +86 -0
  97. deltaplan-0.1.0a1/tests/unit/test_spent_renames.py +60 -0
  98. deltaplan-0.1.0a1/tests/unit/test_table_renames.py +220 -0
  99. deltaplan-0.1.0a1/tests/unit/test_typeparser.py +197 -0
  100. deltaplan-0.1.0a1/tests/unit/test_unmodelled.py +82 -0
  101. deltaplan-0.1.0a1/tests/unit/test_views.py +306 -0
  102. deltaplan-0.1.0a1/uv.lock +1034 -0
@@ -0,0 +1,17 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .DS_Store
8
+
9
+ # tool caches
10
+ .pytest_cache/
11
+ .ruff_cache/
12
+
13
+ # mkdocs build output
14
+ site/
15
+
16
+ # local plan artefacts
17
+ plan.json
@@ -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
+ ```