delta-engine 0.2.0__tar.gz → 0.3.0__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.
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.github/workflows/ci.yaml +2 -1
- {delta_engine-0.2.0 → delta_engine-0.3.0}/CHANGELOG.md +38 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/PKG-INFO +2 -2
- {delta_engine-0.2.0 → delta_engine-0.3.0}/README.md +1 -1
- delta_engine-0.3.0/body.md +38 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/explanation-architecture.md +69 -39
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/explanation-safety-model.md +8 -6
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/how-to-configure-table.md +125 -30
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/how-to-deploy-metadata-only.md +18 -15
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/reference-api.md +16 -6
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/reference-limitations.md +19 -3
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/reference-safe-change-rules.md +23 -4
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/todo/todo.md +2 -2
- {delta_engine-0.2.0 → delta_engine-0.3.0}/notebooks/delta_engine_walkthrough.py +57 -39
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/reader.py +18 -1
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/sql/compile.py +20 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/api/delta_table.py +190 -69
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/dependency_resolution.py +13 -5
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/rendering.py +24 -3
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/model/column.py +0 -3
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/model/constraints.py +20 -9
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/model/table.py +19 -2
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/plan/__init__.py +4 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/plan/actions.py +18 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/plan/changes.py +26 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/plan/diff.py +21 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/sql/test_compile.py +26 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/test_reader.py +23 -5
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/test_reader_mappers.py +23 -2
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/api/test_delta_table.py +282 -43
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/api/test_foreign_key.py +227 -97
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_dependency_resolution.py +94 -9
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_engine.py +78 -18
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_rendering.py +34 -1
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_validation.py +90 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_foreign_key.py +53 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_table.py +42 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_table_aspect.py +8 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/plan/test_actions.py +30 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/plan/test_diff.py +63 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/e2e/test_engine_e2e.py +57 -4
- delta_engine-0.2.0/body.md +0 -28
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/CLAUDE.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/agents/ousterhout-design-reviewer.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/agents/test-reviewer.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/rules/coding-style.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/rules/documentation.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/rules/domain-architecture.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/rules/testing.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.claude/skills/add-tests/SKILL.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.github/pull_request_template.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.github/workflows/pr-title.yaml +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.github/workflows/publish-testpypi.yaml +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.github/workflows/release.yaml +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.gitignore +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/.pre-commit-config.yaml +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/LICENSE +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/conf.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/explanation-sync-lifecycle.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/how-to-add-action-type.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/how-to-handle-sync-failures.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/how-to-implement-adapter.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/how-to-preview-changes.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/index.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/installation.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/reference-data-types.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/docs/tutorial-getting-started.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/notebooks/README.md +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/notebooks/catalog_inspector.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/pyproject.toml +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/errors.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/executor.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/factory.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/log_config.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/sql/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/sql/dialect.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/sql/queries.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/adapters/databricks/sql/types.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/api/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/desired_tables.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/engine.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/errors.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/failures.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/ports.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/properties.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/report.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/application/validation.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/databricks.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/model/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/model/data_type.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/domain/model/qualified_name.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/py.typed +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/src/delta_engine/schema.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/sql/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/sql/test_dialect.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/sql/test_queries.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/sql/test_types.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/test_build_engine.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/test_errors.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/adapters/databricks/test_executor.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/api/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_desired_tables.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_errors.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_failures.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_ports.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_properties.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_public_api.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/application/test_report.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/config.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/conftest.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_column.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_data_type.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_primary_key.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/model/test_qualified_name.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/domain/plan/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/e2e/__init__.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/e2e/conftest.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/test_public_api.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/tests/test_public_imports.py +0 -0
- {delta_engine-0.2.0 → delta_engine-0.3.0}/uv.lock +0 -0
|
@@ -37,9 +37,10 @@ jobs:
|
|
|
37
37
|
schema="silver",
|
|
38
38
|
name="customers",
|
|
39
39
|
columns=[
|
|
40
|
-
Column("id", Integer(), nullable=False
|
|
40
|
+
Column("id", Integer(), nullable=False),
|
|
41
41
|
Column("name", String(), nullable=True),
|
|
42
42
|
],
|
|
43
|
+
primary_key=["id"],
|
|
43
44
|
)
|
|
44
45
|
|
|
45
46
|
desired = table.to_desired_table()
|
|
@@ -1,3 +1,41 @@
|
|
|
1
|
+
## v0.3.0 (2026-07-10)
|
|
2
|
+
|
|
3
|
+
### BREAKING CHANGE
|
|
4
|
+
|
|
5
|
+
- DeltaTable(metadata_only=True) is replaced by
|
|
6
|
+
DeltaTable(scope="metadata"). The metadata_only parameter is removed.
|
|
7
|
+
|
|
8
|
+
### Feat
|
|
9
|
+
|
|
10
|
+
- **api**: declare sync scope with a scope parameter
|
|
11
|
+
- declare foreign key pairings as an explicit mapping
|
|
12
|
+
- declare primary keys at table level
|
|
13
|
+
- canonicalize foreign key pair order in the domain
|
|
14
|
+
- derive clustering from Column.cluster_key in DeltaTable
|
|
15
|
+
- read observed clustering from DESCRIBE DETAIL clusteringColumns
|
|
16
|
+
- diff clustering by set and emit AlterClustering
|
|
17
|
+
- render clustering changes with an OPTIMIZE FULL hint
|
|
18
|
+
- compile CLUSTER BY for create and in-place clustering
|
|
19
|
+
- add AlterClustering action and SET_CLUSTERING phase
|
|
20
|
+
- carry clustered_by on table snapshots with invariants
|
|
21
|
+
- add CLUSTERING aspect and Column.cluster_key flag
|
|
22
|
+
|
|
23
|
+
### Fix
|
|
24
|
+
|
|
25
|
+
- reject a bare string primary_key at declaration
|
|
26
|
+
- **notebooks**: migrate walkthrough to the scope parameter
|
|
27
|
+
- update runtime-import CI smoke test to table-level primary_key
|
|
28
|
+
- drop OPTIMIZE FULL hint from clustering removal; note clustering in metadata-only docs
|
|
29
|
+
- reconcile clustering before dropping columns
|
|
30
|
+
- **test**: make DESCRIBE DETAIL reader fakes support asDict()
|
|
31
|
+
|
|
32
|
+
### Refactor
|
|
33
|
+
|
|
34
|
+
- **api**: clean up scope resolution and structure-gated validation
|
|
35
|
+
- wrap ForeignKey.columns in a read-only view; drop stale inference wording
|
|
36
|
+
- drop redundant clustering guard and trim docstrings
|
|
37
|
+
- declare clustering with table-level clustered_by, not a per-column flag
|
|
38
|
+
|
|
1
39
|
## v0.2.0 (2026-07-09)
|
|
2
40
|
|
|
3
41
|
### Feat
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: delta-engine
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Framework for defining, validating, and evolving tables in a consistent, safe, and controlled way.
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tomoscorbin/delta-engine
|
|
6
6
|
Project-URL: Repository, https://github.com/Tomoscorbin/delta-engine
|
|
@@ -109,7 +109,7 @@ for the model, or jump to what you need:
|
|
|
109
109
|
- [Safe-change rules](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/reference-safe-change-rules.md) — changes the engine blocks at validation
|
|
110
110
|
- [API reference](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/reference-api.md)
|
|
111
111
|
|
|
112
|
-
**Architecture**
|
|
112
|
+
**Architecture**
|
|
113
113
|
|
|
114
114
|
- [Architecture](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/explanation-architecture.md) — layers, ports and adapters, design decisions
|
|
115
115
|
- [Implement a custom adapter](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/how-to-implement-adapter.md) — the `CatalogStateReader` and `PlanExecutor` ports
|
|
@@ -83,7 +83,7 @@ for the model, or jump to what you need:
|
|
|
83
83
|
- [Safe-change rules](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/reference-safe-change-rules.md) — changes the engine blocks at validation
|
|
84
84
|
- [API reference](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/reference-api.md)
|
|
85
85
|
|
|
86
|
-
**Architecture**
|
|
86
|
+
**Architecture**
|
|
87
87
|
|
|
88
88
|
- [Architecture](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/explanation-architecture.md) — layers, ports and adapters, design decisions
|
|
89
89
|
- [Implement a custom adapter](https://github.com/Tomoscorbin/delta-engine/blob/main/docs/how-to-implement-adapter.md) — the `CatalogStateReader` and `PlanExecutor` ports
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
## v0.3.0 (2026-07-10)
|
|
2
|
+
|
|
3
|
+
### BREAKING CHANGE
|
|
4
|
+
|
|
5
|
+
- DeltaTable(metadata_only=True) is replaced by
|
|
6
|
+
DeltaTable(scope="metadata"). The metadata_only parameter is removed.
|
|
7
|
+
|
|
8
|
+
### Feat
|
|
9
|
+
|
|
10
|
+
- **api**: declare sync scope with a scope parameter
|
|
11
|
+
- declare foreign key pairings as an explicit mapping
|
|
12
|
+
- declare primary keys at table level
|
|
13
|
+
- canonicalize foreign key pair order in the domain
|
|
14
|
+
- derive clustering from Column.cluster_key in DeltaTable
|
|
15
|
+
- read observed clustering from DESCRIBE DETAIL clusteringColumns
|
|
16
|
+
- diff clustering by set and emit AlterClustering
|
|
17
|
+
- render clustering changes with an OPTIMIZE FULL hint
|
|
18
|
+
- compile CLUSTER BY for create and in-place clustering
|
|
19
|
+
- add AlterClustering action and SET_CLUSTERING phase
|
|
20
|
+
- carry clustered_by on table snapshots with invariants
|
|
21
|
+
- add CLUSTERING aspect and Column.cluster_key flag
|
|
22
|
+
|
|
23
|
+
### Fix
|
|
24
|
+
|
|
25
|
+
- reject a bare string primary_key at declaration
|
|
26
|
+
- **notebooks**: migrate walkthrough to the scope parameter
|
|
27
|
+
- update runtime-import CI smoke test to table-level primary_key
|
|
28
|
+
- drop OPTIMIZE FULL hint from clustering removal; note clustering in metadata-only docs
|
|
29
|
+
- reconcile clustering before dropping columns
|
|
30
|
+
- **test**: make DESCRIBE DETAIL reader fakes support asDict()
|
|
31
|
+
|
|
32
|
+
### Refactor
|
|
33
|
+
|
|
34
|
+
- **api**: clean up scope resolution and structure-gated validation
|
|
35
|
+
- wrap ForeignKey.columns in a read-only view; drop stale inference wording
|
|
36
|
+
- drop redundant clustering guard and trim docstrings
|
|
37
|
+
- declare clustering with table-level clustered_by, not a per-column flag
|
|
38
|
+
|
|
@@ -65,7 +65,7 @@ through a sync.
|
|
|
65
65
|
| `CatalogState` | The result of reading one table: `TablePresent`, `TableAbsent`, or `ReadFailed`. |
|
|
66
66
|
| `TableDiff` | Typed facts describing how desired and observed state differ. It is either `TableMissing` or `TableDrift`. |
|
|
67
67
|
| `Change` | One atomic difference inside a `TableDrift` (`ColumnAdded`, `TableTagUnset`, …). Each names the `TableAspect` it belongs to and can lower itself to actions. |
|
|
68
|
-
| `TableAspect` | One managed aspect of a table: columns, table comment, properties, tags, partitioning, primary key, or foreign keys. Internal enum.
|
|
68
|
+
| `TableAspect` | One managed aspect of a table: columns, table comment, properties, tags, partitioning, clustering, primary key, or foreign keys. Internal enum. |
|
|
69
69
|
| `ValidationResult` | The application policy verdict for a diff. It says whether a drift is safe to plan in this run. |
|
|
70
70
|
| `ActionPlan` | The ordered, table-local actions that should be executed if the table is allowed to run. |
|
|
71
71
|
| `ResolveResult` | The foreign-key dependency order plus any FK-specific failures. |
|
|
@@ -364,9 +364,14 @@ every change in the drift belongs to a managed aspect, so
|
|
|
364
364
|
`TableDrift.plan()` naturally produces only the managed actions, with no
|
|
365
365
|
filtering logic needed.
|
|
366
366
|
|
|
367
|
-
The public API exposes named
|
|
368
|
-
to the metadata aspects (comments, tags, key constraints)
|
|
369
|
-
enum stays
|
|
367
|
+
The public API exposes named scopes only: `DeltaTable`'s `scope` parameter
|
|
368
|
+
maps `"metadata"` to the metadata aspects (comments, tags, key constraints)
|
|
369
|
+
and `"tags"` to table and column tags only. The `TableAspect` enum stays
|
|
370
|
+
internal.
|
|
371
|
+
|
|
372
|
+
`CLUSTERING` is not one of the metadata aspects: liquid clustering keys
|
|
373
|
+
change how data files are laid out on storage, so a `metadata_only` sync
|
|
374
|
+
never reconciles them, the same as `COLUMN_STRUCTURE` and `PARTITIONING`.
|
|
370
375
|
|
|
371
376
|
`diff_table(desired, observed)` produces a `TableDiff`:
|
|
372
377
|
|
|
@@ -382,6 +387,7 @@ aspects:
|
|
|
382
387
|
- table properties
|
|
383
388
|
- table tags
|
|
384
389
|
- partitioning
|
|
390
|
+
- clustering
|
|
385
391
|
- primary key
|
|
386
392
|
- foreign keys
|
|
387
393
|
|
|
@@ -429,6 +435,11 @@ The phase ordering exists because backend DDL has dependencies:
|
|
|
429
435
|
- Column nullability changes run before primary keys are set, because primary
|
|
430
436
|
key columns must be non-nullable.
|
|
431
437
|
- Foreign keys are set last, after the referenced primary key exists.
|
|
438
|
+
- Clustering keys are altered after columns are added (a new clustering key may
|
|
439
|
+
name a column this same sync is still adding) but before columns are dropped,
|
|
440
|
+
so a table is reclustered off a column before that column is removed — a sync
|
|
441
|
+
that both drops the live clustering-key column and reclusters elsewhere must
|
|
442
|
+
not drop it while it is still the active key.
|
|
432
443
|
|
|
433
444
|
The domain plan describes intent. The adapter compiler decides how each action
|
|
434
445
|
is rendered for its backend.
|
|
@@ -477,9 +488,9 @@ dependency's report, but it is not retroactively converted into
|
|
|
477
488
|
`DeltaTable` is the public declaration object, but the engine plans with
|
|
478
489
|
`DesiredTable`. The lowering boundary does several important things up front:
|
|
479
490
|
|
|
480
|
-
- rejects property keys the engine does not manage (valued or `None`)
|
|
481
|
-
|
|
482
|
-
- generates a primary-key constraint from
|
|
491
|
+
- rejects property keys the engine does not manage (valued or `None`) and
|
|
492
|
+
rejects invalid declared property values
|
|
493
|
+
- generates a primary-key constraint from the table-level `primary_key` argument
|
|
483
494
|
- lowers public `ForeignKey` declarations into domain `ForeignKeyConstraint`
|
|
484
495
|
values
|
|
485
496
|
- validates structural invariants such as non-empty columns, unique column
|
|
@@ -495,6 +506,7 @@ customers = DeltaTable(
|
|
|
495
506
|
schema="silver",
|
|
496
507
|
name="customers",
|
|
497
508
|
columns=[...],
|
|
509
|
+
primary_key=["id"],
|
|
498
510
|
)
|
|
499
511
|
|
|
500
512
|
orders = DeltaTable(
|
|
@@ -503,17 +515,16 @@ orders = DeltaTable(
|
|
|
503
515
|
name="orders",
|
|
504
516
|
columns=[...],
|
|
505
517
|
foreign_keys=[
|
|
506
|
-
ForeignKey(
|
|
518
|
+
ForeignKey(columns={"customer_id": "id"}, references=customers),
|
|
507
519
|
],
|
|
508
520
|
)
|
|
509
521
|
```
|
|
510
522
|
|
|
511
|
-
This object reference lets the API
|
|
512
|
-
referenced table's primary key
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
importing the referenced table.
|
|
523
|
+
This object reference lets the API validate the mapping against the
|
|
524
|
+
referenced table's actual primary key, and keeps the reference valid if the
|
|
525
|
+
target is renamed. The tradeoff is that the referenced table must be declared
|
|
526
|
+
in Python scope. Within one module that usually means defining the parent
|
|
527
|
+
before the child; across modules it means importing the referenced table.
|
|
517
528
|
|
|
518
529
|
This source-code order does not determine execution order. The engine sorts
|
|
519
530
|
prepared desired tables by qualified name for deterministic setup, then the
|
|
@@ -521,20 +532,31 @@ resolver topologically orders them by FK dependency before execution.
|
|
|
521
532
|
|
|
522
533
|
References by dotted name are intentionally not supported in this iteration. If
|
|
523
534
|
that becomes necessary, the API can be widened to accept a `QualifiedName` as an
|
|
524
|
-
additional branch. That would be backward-compatible
|
|
525
|
-
explicit
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
Partitioning is shaped by a related decision.
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
535
|
+
additional branch. That would be backward-compatible: the referenced columns are
|
|
536
|
+
already explicit in the `columns` mapping, so a bare name would only lose the
|
|
537
|
+
primary-key object the engine validates the mapping against.
|
|
538
|
+
|
|
539
|
+
Partitioning is shaped by a related decision. `primary_key` and
|
|
540
|
+
`partitioned_by` are both table-level lists of column names, but "order"
|
|
541
|
+
means something different for each. A primary key's declaration order only
|
|
542
|
+
controls how the constraint is rendered — identity and drift compare it as a
|
|
543
|
+
set, so `(a, b)` and `(b, a)` are the same key. Partition order is
|
|
544
|
+
significant instead: the order of names in `partitioned_by` sets the physical
|
|
545
|
+
directory nesting Delta writes, and that nesting can be different from the
|
|
546
|
+
order columns appear in the table. The differ compares that list positionally,
|
|
547
|
+
which is why reordering it is drift, not a no-op.
|
|
548
|
+
|
|
549
|
+
Clustering is the other physical layout, and it is declared the same way —
|
|
550
|
+
`clustered_by` is a table-level list on `DeltaTable`, the sibling of
|
|
551
|
+
`partitioned_by` (the two are mutually exclusive; a table has one layout
|
|
552
|
+
strategy). What differs is not the declaration shape but the _comparison_:
|
|
553
|
+
liquid clustering has no physical directory nesting, so key order carries no
|
|
554
|
+
meaning — Delta clusters by the key _set_. So the differ compares
|
|
555
|
+
`partitioned_by` positionally (reordering it is drift) but compares
|
|
556
|
+
`clustered_by` as a set (reordering the keys is a no-op). The general rule: a
|
|
557
|
+
physical layout is a table-level list (`partitioned_by`, `clustered_by`), and
|
|
558
|
+
whether order is significant is a property of the differ, not of the
|
|
559
|
+
declaration shape.
|
|
538
560
|
|
|
539
561
|
## Constraint names
|
|
540
562
|
|
|
@@ -544,7 +566,8 @@ For desired tables, the API layer generates names when a `DeltaTable` is lowered
|
|
|
544
566
|
to a `DesiredTable`:
|
|
545
567
|
|
|
546
568
|
- primary key: `{table}_pk`
|
|
547
|
-
- foreign key: `{table}_{local_columns}_fk
|
|
569
|
+
- foreign key: `{table}_{local_columns}_fk`, joining the local columns in
|
|
570
|
+
sorted order, so the name is independent of declaration order
|
|
548
571
|
|
|
549
572
|
For observed tables, the reader adapter reads constraint names from the catalog.
|
|
550
573
|
After that, names live on `PrimaryKeyConstraint` and `ForeignKeyConstraint`
|
|
@@ -604,22 +627,29 @@ dependency cost.
|
|
|
604
627
|
|
|
605
628
|
## Where to make changes
|
|
606
629
|
|
|
607
|
-
| Change | Main location | Notes
|
|
608
|
-
| ------------------------------ | -------------------------------------------------------------------------- |
|
|
609
|
-
| Add a new backend | `delta_engine.adapters` | Implement `CatalogStateReader` and `PlanExecutor`; keep backend exceptions inside the adapter.
|
|
630
|
+
| Change | Main location | Notes |
|
|
631
|
+
| ------------------------------ | -------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
632
|
+
| Add a new backend | `delta_engine.adapters` | Implement `CatalogStateReader` and `PlanExecutor`; keep backend exceptions inside the adapter. |
|
|
610
633
|
| Add a new change type | `delta_engine.domain.plan.changes` | Add a frozen dataclass with an `aspect` `ClassVar[TableAspect]` and an `actions()` method; add it to the `Change` union, then emit it from the relevant `_diff_*` helper in `delta_engine.domain.plan.diff`. If the change is currently unsupported, add a rule to `validation.py`. |
|
|
611
|
-
| Add a new action type | `delta_engine.domain.plan` and adapter compiler | Define the action and phase in `actions.py`, emit it from the relevant change's `actions()` method, then compile it in the backend adapter.
|
|
612
|
-
| Add a safety rule | `delta_engine.application.validation` | Rules inspect the `TableDrift` changes and return `ValidationFailure` values.
|
|
613
|
-
| Add a data type | `delta_engine.domain.model.data_type` and adapter type mapping | The domain type is backend-free; SQL names and Spark parsing live in the Databricks adapter.
|
|
614
|
-
| Change public declarations | `delta_engine.api`, surfaced only through `delta_engine.schema` | Keep public ergonomics in `delta_engine.schema` and lower choices into domain snapshots before the engine phases begin.
|
|
615
|
-
| Change FK ordering or blocking | `delta_engine.application.dependency_resolution` | Cross-table dependency policy lives in the application layer, not in the domain plan or SQL compiler.
|
|
616
|
-
| Change report output | `delta_engine.application.report` and `delta_engine.application.rendering` | Keep display formatting out of domain objects.
|
|
617
|
-
| Change Databricks SQL | `delta_engine.adapters.databricks.sql` | Compile domain actions to backend statements at the adapter boundary.
|
|
634
|
+
| Add a new action type | `delta_engine.domain.plan` and adapter compiler | Define the action and phase in `actions.py`, emit it from the relevant change's `actions()` method, then compile it in the backend adapter. |
|
|
635
|
+
| Add a safety rule | `delta_engine.application.validation` | Rules inspect the `TableDrift` changes and return `ValidationFailure` values. |
|
|
636
|
+
| Add a data type | `delta_engine.domain.model.data_type` and adapter type mapping | The domain type is backend-free; SQL names and Spark parsing live in the Databricks adapter. |
|
|
637
|
+
| Change public declarations | `delta_engine.api`, surfaced only through `delta_engine.schema` | Keep public ergonomics in `delta_engine.schema` and lower choices into domain snapshots before the engine phases begin. |
|
|
638
|
+
| Change FK ordering or blocking | `delta_engine.application.dependency_resolution` | Cross-table dependency policy lives in the application layer, not in the domain plan or SQL compiler. |
|
|
639
|
+
| Change report output | `delta_engine.application.report` and `delta_engine.application.rendering` | Keep display formatting out of domain objects. |
|
|
640
|
+
| Change Databricks SQL | `delta_engine.adapters.databricks.sql` | Compile domain actions to backend statements at the adapter boundary. |
|
|
618
641
|
|
|
619
642
|
## Architectural rules
|
|
620
643
|
|
|
621
644
|
- Keep PySpark and Databricks imports inside `delta_engine.adapters`.
|
|
622
645
|
- Keep the domain backend-free, immutable, and deterministic.
|
|
646
|
+
- Make immutability real, not conventional: frozen dataclasses copy their
|
|
647
|
+
collection fields in `__post_init__` — sequences into tuples, mappings into
|
|
648
|
+
read-only views (`MappingProxyType`), aspect sets into `frozenset` — so an
|
|
649
|
+
object cannot change after construction through a collection the caller
|
|
650
|
+
still holds. This applies at the public boundary too: `ForeignKey.columns`
|
|
651
|
+
and `Column.tags` copy what the user passed, so mutating the original
|
|
652
|
+
mapping later does not alter the declaration.
|
|
623
653
|
- Put orchestration, safety policy, dependency resolution, and failure
|
|
624
654
|
propagation in the application layer.
|
|
625
655
|
- Put backend normalization at adapter boundaries, such as lowercasing catalog
|
|
@@ -52,15 +52,17 @@ unmanaged aspect has drifted. It never silently reconciles something a
|
|
|
52
52
|
declaration didn't claim responsibility for, and it never silently ignores
|
|
53
53
|
drift it isn't allowed to fix — the sync fails and tells you.
|
|
54
54
|
|
|
55
|
-
There are
|
|
55
|
+
There are three public scopes, selected by `DeltaTable`'s `scope` parameter:
|
|
56
56
|
|
|
57
|
-
| Scope
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
| `
|
|
57
|
+
| Scope | Manages | Use for |
|
|
58
|
+
| ---------------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
|
|
59
|
+
| `"full"` (the default) | Everything: columns, comments, properties, tags, partitioning, primary and foreign keys | Tables this declaration owns end to end |
|
|
60
|
+
| `"metadata"` | Comments, tags, and key constraints only | Rolling out governance metadata with a hard guarantee that no schema change can slip in |
|
|
61
|
+
| `"tags"` | Table and column tags only | Tag governance for tables owned elsewhere, e.g. by a streaming pipeline |
|
|
61
62
|
|
|
62
63
|
See [how to deploy metadata only](how-to-deploy-metadata-only.md) for the
|
|
63
|
-
|
|
64
|
+
metadata scope in practice, and [tags](how-to-configure-table.md#manage-tags-only)
|
|
65
|
+
for tag-only declarations.
|
|
64
66
|
|
|
65
67
|
## Cross-table dependency blocking
|
|
66
68
|
|
|
@@ -18,6 +18,7 @@ partitioned. This page is the practical reference for configuring each aspect.
|
|
|
18
18
|
| Primary keys | The table's primary key | [Primary keys](#primary-keys), below |
|
|
19
19
|
| Foreign keys | Cross-table references and sync ordering | [Foreign keys](#foreign-keys), below |
|
|
20
20
|
| Partitioning | Partition columns, fixed at creation | [Partitioning](#partitioning), below |
|
|
21
|
+
| Clustering | Liquid clustering keys, reconciled in place | [Clustering](#clustering), below |
|
|
21
22
|
|
|
22
23
|
## Properties
|
|
23
24
|
|
|
@@ -100,7 +101,8 @@ orders = DeltaTable(
|
|
|
100
101
|
catalog="prod",
|
|
101
102
|
schema="sales",
|
|
102
103
|
name="orders",
|
|
103
|
-
columns=[Column("id", Integer(), nullable=False
|
|
104
|
+
columns=[Column("id", Integer(), nullable=False)],
|
|
105
|
+
primary_key=["id"],
|
|
104
106
|
properties={
|
|
105
107
|
Property.CHANGE_DATA_FEED: "true", # ensure it is set
|
|
106
108
|
Property.LOG_RETENTION_DURATION: None, # ensure it is absent
|
|
@@ -223,6 +225,36 @@ the next sync unless it is also declared.
|
|
|
223
225
|
|
|
224
226
|
As with table tags, keys are **case-sensitive** (`PII` and `pii` are distinct).
|
|
225
227
|
|
|
228
|
+
### Manage tags only
|
|
229
|
+
|
|
230
|
+
Use `scope="tags"` when the table is owned elsewhere — for example by a
|
|
231
|
+
streaming pipeline — but you still want delta-engine to reconcile Unity
|
|
232
|
+
Catalog tags. The declaration carries the same table shape as any other, but
|
|
233
|
+
only table tags and column tags are managed: columns, comments, properties,
|
|
234
|
+
partitioning, primary keys, and foreign keys are never changed.
|
|
235
|
+
|
|
236
|
+
```python
|
|
237
|
+
from delta_engine.schema import Column, DeltaTable, String
|
|
238
|
+
|
|
239
|
+
events = DeltaTable(
|
|
240
|
+
catalog="dev",
|
|
241
|
+
schema="silver",
|
|
242
|
+
name="streaming_events",
|
|
243
|
+
columns=[
|
|
244
|
+
Column("id", String()),
|
|
245
|
+
Column("email", String(), tags={"pii": "true"}),
|
|
246
|
+
],
|
|
247
|
+
tags={"domain": "events"},
|
|
248
|
+
scope="tags",
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
The live table must already exist. If a non-tag aspect drifts from the
|
|
253
|
+
declaration, validation fails before any tag SQL runs; update the declaration
|
|
254
|
+
to match the live table or use the full scope. Properties are the exception:
|
|
255
|
+
a restricted scope never compares them, so live table properties cannot fail
|
|
256
|
+
the sync.
|
|
257
|
+
|
|
226
258
|
### Requirements and limits
|
|
227
259
|
|
|
228
260
|
Column tags require Unity Catalog on Databricks Runtime 13.3 LTS or later and the
|
|
@@ -270,8 +302,9 @@ drift that the sync overwrites.
|
|
|
270
302
|
|
|
271
303
|
## Primary keys
|
|
272
304
|
|
|
273
|
-
Declare a primary key by
|
|
274
|
-
|
|
305
|
+
Declare a primary key by passing `primary_key` to `DeltaTable` — a list of
|
|
306
|
+
column names, in the order the constraint is rendered. Every primary key
|
|
307
|
+
column must be non-nullable.
|
|
275
308
|
|
|
276
309
|
```python
|
|
277
310
|
from delta_engine.schema import Column, DeltaTable, Integer, String
|
|
@@ -281,10 +314,11 @@ orders = DeltaTable(
|
|
|
281
314
|
schema="silver",
|
|
282
315
|
name="orders",
|
|
283
316
|
columns=[
|
|
284
|
-
Column("order_id", Integer(), nullable=False
|
|
317
|
+
Column("order_id", Integer(), nullable=False),
|
|
285
318
|
Column("customer_id", Integer(), nullable=False),
|
|
286
319
|
Column("status", String()),
|
|
287
320
|
],
|
|
321
|
+
primary_key=["order_id"],
|
|
288
322
|
)
|
|
289
323
|
```
|
|
290
324
|
|
|
@@ -295,8 +329,8 @@ exposed on the table object.
|
|
|
295
329
|
|
|
296
330
|
### Composite primary keys
|
|
297
331
|
|
|
298
|
-
|
|
299
|
-
|
|
332
|
+
List several column names in `primary_key`. The constraint covers them in
|
|
333
|
+
that order.
|
|
300
334
|
|
|
301
335
|
```python
|
|
302
336
|
order_items = DeltaTable(
|
|
@@ -304,10 +338,11 @@ order_items = DeltaTable(
|
|
|
304
338
|
schema="silver",
|
|
305
339
|
name="order_items",
|
|
306
340
|
columns=[
|
|
307
|
-
Column("order_id", Integer(), nullable=False
|
|
308
|
-
Column("line_number", Integer(), nullable=False
|
|
341
|
+
Column("order_id", Integer(), nullable=False),
|
|
342
|
+
Column("line_number", Integer(), nullable=False),
|
|
309
343
|
Column("product_id", Integer(), nullable=False),
|
|
310
344
|
],
|
|
345
|
+
primary_key=["order_id", "line_number"],
|
|
311
346
|
)
|
|
312
347
|
```
|
|
313
348
|
|
|
@@ -315,8 +350,8 @@ order_items = DeltaTable(
|
|
|
315
350
|
|
|
316
351
|
A primary key identifies a row, so a nullable key column is not a well-formed
|
|
317
352
|
table definition — and Databricks rejects a nullable primary key at execution
|
|
318
|
-
time regardless. The engine enforces this early:
|
|
319
|
-
|
|
353
|
+
time regardless. The engine enforces this early: naming a nullable column in
|
|
354
|
+
`primary_key` raises `ValueError` when the `DeltaTable` is constructed,
|
|
320
355
|
before any sync runs.
|
|
321
356
|
|
|
322
357
|
### Constraints are informational
|
|
@@ -354,9 +389,8 @@ table with the original error. See
|
|
|
354
389
|
|
|
355
390
|
## Foreign keys
|
|
356
391
|
|
|
357
|
-
Pass `foreign_keys` with one `ForeignKey` per constraint.
|
|
358
|
-
|
|
359
|
-
that table's primary key.
|
|
392
|
+
Pass `foreign_keys` with one `ForeignKey` per constraint. `columns` maps each
|
|
393
|
+
local column to the referenced-table primary-key column it references.
|
|
360
394
|
|
|
361
395
|
```python
|
|
362
396
|
from delta_engine.schema import Column, DeltaTable, ForeignKey, Long, String
|
|
@@ -366,9 +400,10 @@ customers = DeltaTable(
|
|
|
366
400
|
schema="silver",
|
|
367
401
|
name="customers",
|
|
368
402
|
columns=[
|
|
369
|
-
Column("id", Long(), nullable=False
|
|
403
|
+
Column("id", Long(), nullable=False),
|
|
370
404
|
Column("name", String()),
|
|
371
405
|
],
|
|
406
|
+
primary_key=["id"],
|
|
372
407
|
)
|
|
373
408
|
|
|
374
409
|
orders = DeltaTable(
|
|
@@ -376,22 +411,23 @@ orders = DeltaTable(
|
|
|
376
411
|
schema="silver",
|
|
377
412
|
name="orders",
|
|
378
413
|
columns=[
|
|
379
|
-
Column("order_id", Long(), nullable=False
|
|
414
|
+
Column("order_id", Long(), nullable=False),
|
|
380
415
|
Column("customer_id", Long(), nullable=False),
|
|
381
416
|
Column("status", String()),
|
|
382
417
|
],
|
|
418
|
+
primary_key=["order_id"],
|
|
383
419
|
foreign_keys=[
|
|
384
|
-
ForeignKey(
|
|
420
|
+
ForeignKey(columns={"customer_id": "id"}, references=customers),
|
|
385
421
|
],
|
|
386
422
|
)
|
|
387
423
|
```
|
|
388
424
|
|
|
389
425
|
Referencing the target `DeltaTable` object — rather than a dotted table name —
|
|
390
|
-
is what lets the engine
|
|
426
|
+
is what lets the engine validate the mapping against that table's actual primary
|
|
391
427
|
key, and keeps the reference valid if the target is renamed. The constraint
|
|
392
|
-
name is generated at lowering as `{table}_{local_columns}_fk
|
|
393
|
-
(`orders_customer_id_fk` above). The name cannot
|
|
394
|
-
never depends on it — a foreign key created outside the engine under a
|
|
428
|
+
name is generated at lowering as `{table}_{local_columns}_fk`, joining the
|
|
429
|
+
local columns in sorted order (`orders_customer_id_fk` above). The name cannot
|
|
430
|
+
be chosen, and drift matching never depends on it — a foreign key created outside the engine under a
|
|
395
431
|
different name still matches by content.
|
|
396
432
|
|
|
397
433
|
Generated names join local columns with underscores, so two foreign keys over
|
|
@@ -418,20 +454,20 @@ employees = DeltaTable(
|
|
|
418
454
|
schema="silver",
|
|
419
455
|
name="employees",
|
|
420
456
|
columns=[
|
|
421
|
-
Column("id", Long(), nullable=False
|
|
457
|
+
Column("id", Long(), nullable=False),
|
|
422
458
|
Column("manager_id", Long()),
|
|
423
459
|
],
|
|
460
|
+
primary_key=["id"],
|
|
424
461
|
foreign_keys=[
|
|
425
|
-
ForeignKey(
|
|
462
|
+
ForeignKey(columns={"manager_id": "id"}, references=Self),
|
|
426
463
|
],
|
|
427
464
|
)
|
|
428
465
|
```
|
|
429
466
|
|
|
430
467
|
### Composite foreign keys
|
|
431
468
|
|
|
432
|
-
For a composite primary key,
|
|
433
|
-
|
|
434
|
-
in that same order.
|
|
469
|
+
For a composite primary key, map each local column to the primary-key column
|
|
470
|
+
it references.
|
|
435
471
|
|
|
436
472
|
```python
|
|
437
473
|
customer_accounts = DeltaTable(
|
|
@@ -439,9 +475,10 @@ customer_accounts = DeltaTable(
|
|
|
439
475
|
schema="silver",
|
|
440
476
|
name="customer_accounts",
|
|
441
477
|
columns=[
|
|
442
|
-
Column("tenant_id", Long(), nullable=False
|
|
443
|
-
Column("id", Long(), nullable=False
|
|
478
|
+
Column("tenant_id", Long(), nullable=False),
|
|
479
|
+
Column("id", Long(), nullable=False),
|
|
444
480
|
],
|
|
481
|
+
primary_key=["tenant_id", "id"],
|
|
445
482
|
)
|
|
446
483
|
|
|
447
484
|
order_lines = DeltaTable(
|
|
@@ -449,20 +486,24 @@ order_lines = DeltaTable(
|
|
|
449
486
|
schema="silver",
|
|
450
487
|
name="order_lines",
|
|
451
488
|
columns=[
|
|
452
|
-
Column("order_line_id", Long(), nullable=False
|
|
489
|
+
Column("order_line_id", Long(), nullable=False),
|
|
453
490
|
Column("tenant_id", Long(), nullable=False),
|
|
454
491
|
Column("customer_id", Long(), nullable=False),
|
|
455
492
|
],
|
|
493
|
+
primary_key=["order_line_id"],
|
|
456
494
|
foreign_keys=[
|
|
457
495
|
ForeignKey(
|
|
458
|
-
|
|
459
|
-
local_columns=["tenant_id", "customer_id"],
|
|
496
|
+
columns={"tenant_id": "tenant_id", "customer_id": "id"},
|
|
460
497
|
references=customer_accounts,
|
|
461
498
|
),
|
|
462
499
|
],
|
|
463
500
|
)
|
|
464
501
|
```
|
|
465
502
|
|
|
503
|
+
The mapping states which primary-key column each local column references;
|
|
504
|
+
its order never matters, and a mapping that does not cover the referenced
|
|
505
|
+
table's primary key exactly fails when the `DeltaTable` is constructed.
|
|
506
|
+
|
|
466
507
|
### Dependency ordering
|
|
467
508
|
|
|
468
509
|
A foreign key can only be added once its referenced table exists with the
|
|
@@ -573,3 +614,57 @@ Partition columns also cannot be of complex type (`Array`, `Map`, `Struct`,
|
|
|
573
614
|
rejected when the `DeltaTable` is constructed. See
|
|
574
615
|
[safe-change rules](reference-safe-change-rules.md) for the full set of changes
|
|
575
616
|
the engine rejects.
|
|
617
|
+
|
|
618
|
+
## Clustering
|
|
619
|
+
|
|
620
|
+
Declare Delta liquid clustering keys with the `clustered_by` argument — a
|
|
621
|
+
table-level list of column names, the same shape as `partitioned_by`:
|
|
622
|
+
|
|
623
|
+
```python
|
|
624
|
+
from delta_engine.schema import Column, DeltaTable, String
|
|
625
|
+
|
|
626
|
+
events = DeltaTable(
|
|
627
|
+
catalog="dev",
|
|
628
|
+
schema="silver",
|
|
629
|
+
name="events",
|
|
630
|
+
columns=[
|
|
631
|
+
Column("region", String()),
|
|
632
|
+
Column("event_type", String()),
|
|
633
|
+
],
|
|
634
|
+
clustered_by=["region"],
|
|
635
|
+
)
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
`DeltaTable.clustered_by` exposes the declared tuple of clustering column
|
|
639
|
+
names, in declaration order. Key order does not matter — Delta clusters by the
|
|
640
|
+
key set — so reordering the keys is never treated as drift.
|
|
641
|
+
|
|
642
|
+
A table cannot declare both `partitioned_by` and `clustered_by` — Delta
|
|
643
|
+
supports one physical layout strategy per table — and a declaration is
|
|
644
|
+
limited to four clustering keys. Both are rejected when the `DeltaTable` is
|
|
645
|
+
constructed. See
|
|
646
|
+
[limitations](reference-limitations.md) for the unsupported key types.
|
|
647
|
+
|
|
648
|
+
### Clustering is reconciled in place
|
|
649
|
+
|
|
650
|
+
Unlike partitioning, clustering keys are not fixed at creation. The engine
|
|
651
|
+
changes the key set with `ALTER TABLE ... CLUSTER BY (...)` (or `CLUSTER BY
|
|
652
|
+
NONE` to remove clustering) whenever the declaration changes — no table
|
|
653
|
+
recreation required.
|
|
654
|
+
|
|
655
|
+
The `ALTER` is a metadata change: it sets the _target_ clustering keys but
|
|
656
|
+
does not rewrite existing data, so it stays cheap regardless of table size.
|
|
657
|
+
Liquid clustering still lays data out physically — it co-locates rows within
|
|
658
|
+
files rather than in partition directories — but existing files keep their
|
|
659
|
+
old clustering until a later `OPTIMIZE` (or `OPTIMIZE FULL` to recluster the
|
|
660
|
+
whole table) rewrites them. This is why partitioning is blocked while
|
|
661
|
+
clustering is not: changing partition columns would mean physically rewriting
|
|
662
|
+
every data file up front. See
|
|
663
|
+
[safe-change rules](reference-safe-change-rules.md) for the full contrast.
|
|
664
|
+
|
|
665
|
+
### Drift
|
|
666
|
+
|
|
667
|
+
The engine compares clustering keys by _set_, not by order: declaring the
|
|
668
|
+
same keys in a different order is not drift and plans nothing. This mirrors
|
|
669
|
+
primary keys, and is unlike partitioning, where order is a physical layout
|
|
670
|
+
decision.
|