weaverstack 0.1.1__py3-none-any.whl
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.
- weaver/__init__.py +59 -0
- weaver/build_bundle/__init__.py +109 -0
- weaver/build_bundle/aliases.py +325 -0
- weaver/build_bundle/bundle.py +359 -0
- weaver/build_bundle/catalogue_actions.py +275 -0
- weaver/build_bundle/changes.py +186 -0
- weaver/build_bundle/endpoints.py +83 -0
- weaver/build_bundle/executors/__init__.py +69 -0
- weaver/build_bundle/executors/alias.py +202 -0
- weaver/build_bundle/executors/base.py +132 -0
- weaver/build_bundle/executors/folder.py +71 -0
- weaver/build_bundle/executors/load_file.py +205 -0
- weaver/build_bundle/executors/spark_case.py +26 -0
- weaver/build_bundle/executors/spark_schema.py +60 -0
- weaver/build_bundle/executors/spark_sql.py +59 -0
- weaver/build_bundle/executors/spark_sql_batch.py +57 -0
- weaver/build_bundle/executors/spark_table.py +213 -0
- weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
- weaver/build_bundle/executors/tsql.py +81 -0
- weaver/build_bundle/incremental.py +288 -0
- weaver/build_bundle/installer.py +384 -0
- weaver/build_bundle/models.py +288 -0
- weaver/build_bundle/payloads.py +34 -0
- weaver/build_bundle/physical.py +625 -0
- weaver/build_bundle/planner.py +389 -0
- weaver/build_bundle/prune.py +620 -0
- weaver/build_bundle/report.py +108 -0
- weaver/build_bundle/stages.py +196 -0
- weaver/build_bundle/targets.py +272 -0
- weaver/build_bundle/workflow.py +585 -0
- weaver/catalogue/__init__.py +73 -0
- weaver/catalogue/builtin.py +238 -0
- weaver/catalogue/claims.py +121 -0
- weaver/catalogue/projection.py +437 -0
- weaver/catalogue/reader.py +152 -0
- weaver/catalogue/reconcile.py +231 -0
- weaver/catalogue/render.py +410 -0
- weaver/catalogue/state.py +660 -0
- weaver/catalogue/tables.py +648 -0
- weaver/config.py +178 -0
- weaver/declaration/__init__.py +171 -0
- weaver/declaration/columns.py +223 -0
- weaver/declaration/ddl.py +266 -0
- weaver/declaration/dependencies.py +544 -0
- weaver/declaration/graph.py +240 -0
- weaver/declaration/item_dependencies.py +292 -0
- weaver/declaration/load.py +191 -0
- weaver/declaration/metadata.py +1405 -0
- weaver/declaration/model.py +448 -0
- weaver/declaration/references.py +294 -0
- weaver/declaration/repository.py +959 -0
- weaver/declaration/schemas.py +135 -0
- weaver/declaration/source.py +674 -0
- weaver/declaration/spark_load.py +759 -0
- weaver/declaration/sql_shaping.py +591 -0
- weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
- weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
- weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
- weaver/declaration/templates/load/column_metadata.sql +40 -0
- weaver/declaration/templates/load/full_replace_body.sql +21 -0
- weaver/declaration/templates/load/install_load_procedure.sql +27 -0
- weaver/declaration/templates/load/load_procedure.sql +48 -0
- weaver/declaration/templates/load/primary_key_body.sql +113 -0
- weaver/declaration/tsql_ddl.py +468 -0
- weaver/declaration/tsql_load.py +417 -0
- weaver/declaration/warehouse_type_mapping.yml +93 -0
- weaver/diagnostics.py +247 -0
- weaver/errors.py +61 -0
- weaver/etl.py +469 -0
- weaver/fabric/__init__.py +107 -0
- weaver/fabric/auth.py +137 -0
- weaver/fabric/capacity.py +143 -0
- weaver/fabric/client.py +147 -0
- weaver/fabric/environment.py +460 -0
- weaver/fabric/livy.py +478 -0
- weaver/fabric/notebooks.py +201 -0
- weaver/fabric/onelake.py +263 -0
- weaver/fabric/resolution.py +344 -0
- weaver/fabric/resources.py +245 -0
- weaver/fabric/session.py +148 -0
- weaver/fabric/shortcuts.py +120 -0
- weaver/fabric/sql.py +118 -0
- weaver/fabric/store.py +198 -0
- weaver/initialise.py +209 -0
- weaver/lakehouse.py +386 -0
- weaver/load.py +474 -0
- weaver/load_execution.py +483 -0
- weaver/load_plan.py +912 -0
- weaver/load_report.py +330 -0
- weaver/load_resolution.py +386 -0
- weaver/locations.py +164 -0
- weaver/objects.py +392 -0
- weaver/operations.py +757 -0
- weaver/physical_wipe.py +369 -0
- weaver/push.py +76 -0
- weaver/resolution.py +292 -0
- weaver/runtime/__init__.py +30 -0
- weaver/runtime/folder_load.py +402 -0
- weaver/runtime/load_contract.py +245 -0
- weaver/runtime/load_result.py +104 -0
- weaver/runtime/spark_load.py +152 -0
- weaver/runtime/table_load.py +497 -0
- weaver/spark/__init__.py +49 -0
- weaver/spark/catalogue.py +245 -0
- weaver/spark/destination.py +195 -0
- weaver/spark/session.py +84 -0
- weaver/spark/tokens.py +138 -0
- weaver/sql/__init__.py +40 -0
- weaver/sql/authentication.py +38 -0
- weaver/sql/connection.py +90 -0
- weaver/sql/errors.py +25 -0
- weaver/sql/execution.py +123 -0
- weaver/sql/pool.py +174 -0
- weaver/sql/wipe.py +156 -0
- weaver/store.py +209 -0
- weaver/targets.py +257 -0
- weaver/task_logging.py +215 -0
- weaver/unbind.py +74 -0
- weaver/workspaces.py +175 -0
- weaver_cli/__init__.py +12 -0
- weaver_cli/__main__.py +7 -0
- weaver_cli/main.py +626 -0
- weaverstack-0.1.1.dist-info/METADATA +113 -0
- weaverstack-0.1.1.dist-info/RECORD +127 -0
- weaverstack-0.1.1.dist-info/WHEEL +4 -0
- weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
- weaverstack-0.1.1.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"""Render one item's frozen physical prune, drop, schema and build stages.
|
|
2
|
+
|
|
3
|
+
Every function here plans for exactly one logical item against exactly one bound
|
|
4
|
+
target. That is the shape multi-item build needs: the item graph orders the items,
|
|
5
|
+
and inside an item the document graph orders the work, so nothing here reaches
|
|
6
|
+
across items or chooses a sequence number.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
|
+
from typing import Mapping
|
|
14
|
+
|
|
15
|
+
from ..declaration.metadata import DELTA_TARGET, FOLDER, SQL_TARGET, TABLE, VIEW
|
|
16
|
+
from ..declaration.model import WeaverItemId
|
|
17
|
+
from ..errors import BuildError
|
|
18
|
+
from ..spark.tokens import object_token
|
|
19
|
+
from ..etl import FILE_TYPE, PROCEDURE_TYPE, item_load_artefacts
|
|
20
|
+
from .changes import (
|
|
21
|
+
FILE as FILE_KIND,
|
|
22
|
+
FOLDER as FOLDER_KIND,
|
|
23
|
+
SCHEMA as SCHEMA_KIND,
|
|
24
|
+
STORED_PROCEDURE as PROCEDURE_KIND,
|
|
25
|
+
TABLE as TABLE_KIND,
|
|
26
|
+
VIEW as VIEW_KIND,
|
|
27
|
+
added as change_added,
|
|
28
|
+
removed as change_removed,
|
|
29
|
+
)
|
|
30
|
+
from .models import (
|
|
31
|
+
BUILD_FOLDER,
|
|
32
|
+
BUILD_PROCEDURE,
|
|
33
|
+
DELETE_FILE,
|
|
34
|
+
DROP_PROCEDURE,
|
|
35
|
+
WRITE_FILE,
|
|
36
|
+
BUILD_TABLE,
|
|
37
|
+
BUILD_VIEW,
|
|
38
|
+
CREATE_SCHEMA,
|
|
39
|
+
DROP_FOLDER,
|
|
40
|
+
DROP_TABLE,
|
|
41
|
+
DROP_VIEW,
|
|
42
|
+
BuildAction,
|
|
43
|
+
BuildBatch,
|
|
44
|
+
)
|
|
45
|
+
from .payloads import sha256_hex
|
|
46
|
+
from .prune import managed_sets, render_inventory_prune
|
|
47
|
+
from .stages import BUILD, DROP, LOAD, PRUNE, SCHEMA, PlannedStage
|
|
48
|
+
from .targets import WAREHOUSE_TARGET
|
|
49
|
+
|
|
50
|
+
_OBJECT_KIND = {TABLE: BUILD_TABLE, VIEW: BUILD_VIEW}
|
|
51
|
+
_DROP_KIND = {FOLDER: DROP_FOLDER, TABLE: DROP_TABLE, VIEW: DROP_VIEW}
|
|
52
|
+
_DECLARATION_KIND = {"folder": FOLDER, "table": TABLE, "view": VIEW}
|
|
53
|
+
|
|
54
|
+
#: A Weaver document kind, and a Registry object type, as the change vocabulary
|
|
55
|
+
#: spells them. Two mappings rather than one because the two inputs are
|
|
56
|
+
#: different: a build knows what it declares, a drop knows what is installed.
|
|
57
|
+
_CHANGE_KIND_FOR_KIND = {FOLDER: FOLDER_KIND, TABLE: TABLE_KIND, VIEW: VIEW_KIND}
|
|
58
|
+
_CHANGE_KIND_FOR_TYPE = {
|
|
59
|
+
"folder": FOLDER_KIND,
|
|
60
|
+
"table": TABLE_KIND,
|
|
61
|
+
"view": VIEW_KIND,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#: Kinds that change Delta storage, and therefore leave a Lakehouse's SQL
|
|
65
|
+
#: analytics endpoint describing something that is no longer there.
|
|
66
|
+
DELTA_MUTATING_KINDS = frozenset(
|
|
67
|
+
{
|
|
68
|
+
BUILD_TABLE,
|
|
69
|
+
BUILD_VIEW,
|
|
70
|
+
DROP_TABLE,
|
|
71
|
+
DROP_VIEW,
|
|
72
|
+
"prune_table",
|
|
73
|
+
"prune_view",
|
|
74
|
+
"prune_schema",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _slug(value) -> str:
|
|
80
|
+
"""An identity as something safe to name a payload file and an action after.
|
|
81
|
+
|
|
82
|
+
Separators, spaces and the shape marker's colon all go: a payload path must
|
|
83
|
+
stay relative and inside the bundle, and a colon reads as a drive letter on
|
|
84
|
+
one of the platforms a bundle is unpacked on.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return str(value).replace("/", "--").replace(" ", "-").replace(":", "-")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def item_prune_stage(
|
|
91
|
+
repository,
|
|
92
|
+
selected_ids,
|
|
93
|
+
*,
|
|
94
|
+
item: WeaverItemId,
|
|
95
|
+
target,
|
|
96
|
+
inventory,
|
|
97
|
+
) -> PlannedStage | None:
|
|
98
|
+
"""Freeze one item's authoritative repository/inventory diff.
|
|
99
|
+
|
|
100
|
+
The keep-set is derived here rather than handed in, and the load artefacts
|
|
101
|
+
are why. They contribute the ``_`` schema a Warehouse's generated procedures
|
|
102
|
+
live in, which no document declares — so a caller that did not think to pass
|
|
103
|
+
them would produce a prune that drops the schema the same build just created.
|
|
104
|
+
A destructive default is not something to leave reachable, and the repository
|
|
105
|
+
is already here, so nothing has to be remembered.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
documents = {
|
|
109
|
+
str(identity): repository.source_documents[identity]
|
|
110
|
+
for identity in selected_ids
|
|
111
|
+
if identity.item == item
|
|
112
|
+
}
|
|
113
|
+
target_kind = SQL_TARGET if target.kind == WAREHOUSE_TARGET else DELTA_TARGET
|
|
114
|
+
managed = managed_sets(
|
|
115
|
+
documents,
|
|
116
|
+
target_kind,
|
|
117
|
+
alias_destinations=[
|
|
118
|
+
alias.destination
|
|
119
|
+
for alias in repository.aliases
|
|
120
|
+
if alias.destination.item == item
|
|
121
|
+
],
|
|
122
|
+
load_identities=[
|
|
123
|
+
artefact.identity for artefact in item_load_artefacts(repository, item=item)
|
|
124
|
+
],
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
payloads: dict[str, bytes] = {}
|
|
128
|
+
actions, changes = render_inventory_prune(target, inventory, managed, payloads)
|
|
129
|
+
if not actions:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
# Two items pruning a same-named object share the merged stage's payload
|
|
133
|
+
# directory, so each item's frozen drops carry its own prefix.
|
|
134
|
+
item_slug = _slug(item)
|
|
135
|
+
return PlannedStage(
|
|
136
|
+
phase=PRUNE,
|
|
137
|
+
slug="item-prune",
|
|
138
|
+
description="prune unmanaged objects by logical item",
|
|
139
|
+
payloads={f"{item_slug}-{name}": data for name, data in payloads.items()},
|
|
140
|
+
changes={
|
|
141
|
+
target.id: tuple(
|
|
142
|
+
replace(change, action_id=f"{item_slug}-{change.action_id}")
|
|
143
|
+
for change in changes
|
|
144
|
+
)
|
|
145
|
+
},
|
|
146
|
+
batches=(
|
|
147
|
+
BuildBatch(
|
|
148
|
+
id=f"item-prune-{item_slug}",
|
|
149
|
+
target_id=target.id,
|
|
150
|
+
actions=tuple(
|
|
151
|
+
_prefixed(action, item_slug) for action in actions
|
|
152
|
+
),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _prefixed(action: BuildAction, item_slug: str) -> BuildAction:
|
|
159
|
+
return replace(
|
|
160
|
+
action,
|
|
161
|
+
id=f"{item_slug}-{action.id}",
|
|
162
|
+
payload=None if action.payload is None else f"{item_slug}-{action.payload}",
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def item_drop_stages(
|
|
167
|
+
repository,
|
|
168
|
+
selected_for_drop,
|
|
169
|
+
*,
|
|
170
|
+
item: WeaverItemId,
|
|
171
|
+
target,
|
|
172
|
+
registered,
|
|
173
|
+
) -> tuple[PlannedStage, ...]:
|
|
174
|
+
"""One item's managed drops, dependants before dependencies."""
|
|
175
|
+
|
|
176
|
+
selected = {identity for identity in selected_for_drop if identity.item == item}
|
|
177
|
+
if not selected:
|
|
178
|
+
return ()
|
|
179
|
+
graph = repository.dependency_graph.subgraph({str(value) for value in selected})
|
|
180
|
+
identities = {str(identity): identity for identity in selected}
|
|
181
|
+
stages = []
|
|
182
|
+
for index, layer in enumerate(reversed(graph.layers())):
|
|
183
|
+
payloads: dict[str, bytes] = {}
|
|
184
|
+
changes: list = []
|
|
185
|
+
actions = []
|
|
186
|
+
for node in sorted(layer):
|
|
187
|
+
identity = identities[node]
|
|
188
|
+
installed = registered[identity].object_type
|
|
189
|
+
actions.append(_drop_action(identity, installed, target, payloads))
|
|
190
|
+
changes.append(
|
|
191
|
+
change_removed(
|
|
192
|
+
_CHANGE_KIND_FOR_TYPE[installed],
|
|
193
|
+
identity.object_id.qualified,
|
|
194
|
+
f"managed-drop-{_slug(identity)}",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
actions = tuple(actions)
|
|
198
|
+
stages.append(
|
|
199
|
+
PlannedStage(
|
|
200
|
+
phase=DROP,
|
|
201
|
+
index=index,
|
|
202
|
+
slug="managed-drop",
|
|
203
|
+
description="drop selected rebuild dependency layer",
|
|
204
|
+
payloads=payloads,
|
|
205
|
+
changes={target.id: tuple(changes)},
|
|
206
|
+
batches=(
|
|
207
|
+
BuildBatch(
|
|
208
|
+
id=f"managed-drop-{_slug(item)}",
|
|
209
|
+
target_id=target.id,
|
|
210
|
+
actions=actions,
|
|
211
|
+
),
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
return tuple(stages)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _drop_action(identity, installed_type, target, payloads) -> BuildAction:
|
|
219
|
+
try:
|
|
220
|
+
installed_kind = _DECLARATION_KIND[installed_type]
|
|
221
|
+
except KeyError as exc:
|
|
222
|
+
raise BuildError(
|
|
223
|
+
f"registered document {identity} has unsupported type {installed_type!r}"
|
|
224
|
+
) from exc
|
|
225
|
+
kind = _DROP_KIND[installed_kind]
|
|
226
|
+
action_slug = _slug(identity)
|
|
227
|
+
if installed_kind == FOLDER:
|
|
228
|
+
return BuildAction(
|
|
229
|
+
id=f"managed-drop-{action_slug}",
|
|
230
|
+
kind=kind,
|
|
231
|
+
resource_node_id=str(identity),
|
|
232
|
+
executor="folder",
|
|
233
|
+
payload=None,
|
|
234
|
+
payload_sha256=None,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
schema = identity.object_id.schema
|
|
238
|
+
name = identity.object_id.object
|
|
239
|
+
if target.kind == WAREHOUSE_TARGET:
|
|
240
|
+
keyword = "view" if installed_kind == VIEW else "table"
|
|
241
|
+
statement = f"drop {keyword} {_tsql_ident(schema)}.{_tsql_ident(name)};\n"
|
|
242
|
+
executor, extension = "tsql", ".sql"
|
|
243
|
+
else:
|
|
244
|
+
keyword = "VIEW" if installed_kind == VIEW else "TABLE"
|
|
245
|
+
statement = f"DROP {keyword} {object_token(schema, name)}\n"
|
|
246
|
+
executor, extension = "spark_sql", ".spark.sql"
|
|
247
|
+
content = statement.encode("utf-8")
|
|
248
|
+
filename = f"drop-{action_slug}{extension}"
|
|
249
|
+
payloads[filename] = content
|
|
250
|
+
return BuildAction(
|
|
251
|
+
id=f"managed-drop-{action_slug}",
|
|
252
|
+
kind=kind,
|
|
253
|
+
resource_node_id=str(identity),
|
|
254
|
+
executor=executor,
|
|
255
|
+
payload=filename,
|
|
256
|
+
payload_sha256=sha256_hex(content),
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _tsql_ident(name: str) -> str:
|
|
261
|
+
return "[" + name.replace("]", "]]") + "]"
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def item_schema_stage(
|
|
265
|
+
selected_ids,
|
|
266
|
+
*,
|
|
267
|
+
item: WeaverItemId,
|
|
268
|
+
target,
|
|
269
|
+
inventory,
|
|
270
|
+
extra_schemas=(),
|
|
271
|
+
) -> PlannedStage | None:
|
|
272
|
+
"""The schemas this item needs and its target does not already hold.
|
|
273
|
+
|
|
274
|
+
``extra_schemas`` carries the schemas the item's planned aliases land in.
|
|
275
|
+
They are the item's own declared schemas — an alias destination has to be —
|
|
276
|
+
but no *document* of the item need live in them, so a namespace an alias
|
|
277
|
+
depends on would otherwise never be created.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
present = {schema.casefold() for schema in inventory.schemas}
|
|
281
|
+
wanted = {
|
|
282
|
+
identity.object_id.schema
|
|
283
|
+
for identity in selected_ids
|
|
284
|
+
if identity.item == item and not identity.is_files
|
|
285
|
+
} | set(extra_schemas)
|
|
286
|
+
schemas = sorted(
|
|
287
|
+
schema for schema in wanted if schema.casefold() not in present
|
|
288
|
+
)
|
|
289
|
+
if not schemas:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
item_slug = _slug(item)
|
|
293
|
+
payloads: dict[str, bytes] = {}
|
|
294
|
+
actions = []
|
|
295
|
+
changes = []
|
|
296
|
+
for schema in schemas:
|
|
297
|
+
if target.kind == WAREHOUSE_TARGET:
|
|
298
|
+
content = f"create schema [{schema.replace(']', ']]')}];\n".encode("utf-8")
|
|
299
|
+
executor, extension = "tsql", ".sql"
|
|
300
|
+
else:
|
|
301
|
+
content = (json.dumps({"schema": schema}, sort_keys=True) + "\n").encode()
|
|
302
|
+
executor, extension = "spark_schema", ".schema.json"
|
|
303
|
+
filename = f"create-{item_slug}-{schema}{extension}"
|
|
304
|
+
payloads[filename] = content
|
|
305
|
+
action_id = f"schema-{item_slug}-{schema}"
|
|
306
|
+
actions.append(
|
|
307
|
+
BuildAction(
|
|
308
|
+
id=action_id,
|
|
309
|
+
kind=CREATE_SCHEMA,
|
|
310
|
+
resource_node_id=None,
|
|
311
|
+
executor=executor,
|
|
312
|
+
payload=filename,
|
|
313
|
+
payload_sha256=sha256_hex(content),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
changes.append(change_added(SCHEMA_KIND, schema, action_id))
|
|
317
|
+
return PlannedStage(
|
|
318
|
+
phase=SCHEMA,
|
|
319
|
+
slug="create-schemas",
|
|
320
|
+
description="create item-owned schemas",
|
|
321
|
+
payloads=payloads,
|
|
322
|
+
changes={target.id: tuple(changes)},
|
|
323
|
+
batches=(
|
|
324
|
+
BuildBatch(id=f"{item_slug}", target_id=target.id, actions=tuple(actions)),
|
|
325
|
+
),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass(frozen=True)
|
|
330
|
+
class RenderedAction:
|
|
331
|
+
"""One authored document turned into one action and its frozen payload.
|
|
332
|
+
|
|
333
|
+
The smallest unit the build has: a declaration in, an executable out. Kept as
|
|
334
|
+
a value rather than written straight into a stage's payload dict so that the
|
|
335
|
+
mapping from *declaration* to *bundle action* — which executor runs it, what
|
|
336
|
+
the payload is called, what its hash is — can be examined on its own, without
|
|
337
|
+
planning an item or generating a bundle to see it.
|
|
338
|
+
|
|
339
|
+
``payloads`` is empty for a folder, which is created rather than executed.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
action: BuildAction
|
|
343
|
+
payloads: Mapping[str, bytes]
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def render_document_build_action(identity, source) -> RenderedAction:
|
|
347
|
+
"""The build action and payload one declared document renders to.
|
|
348
|
+
|
|
349
|
+
This is where ``source.create_ddl()`` becomes something a bundle can carry:
|
|
350
|
+
the DDL says *what statement*, and this says what action runs it, under what
|
|
351
|
+
id, with which executor, and against which frozen bytes. The two are separate
|
|
352
|
+
claims and are worth failing separately.
|
|
353
|
+
"""
|
|
354
|
+
|
|
355
|
+
action_slug = _slug(identity)
|
|
356
|
+
if source.kind == FOLDER:
|
|
357
|
+
# A folder has no statement to run: it is a directory the installer
|
|
358
|
+
# creates, so there is nothing to freeze and nothing to hash.
|
|
359
|
+
return RenderedAction(
|
|
360
|
+
action=BuildAction(
|
|
361
|
+
id=f"folder-{action_slug}",
|
|
362
|
+
kind=BUILD_FOLDER,
|
|
363
|
+
resource_node_id=str(identity),
|
|
364
|
+
executor="folder",
|
|
365
|
+
payload=None,
|
|
366
|
+
payload_sha256=None,
|
|
367
|
+
),
|
|
368
|
+
payloads={},
|
|
369
|
+
)
|
|
370
|
+
ddl = source.create_ddl()
|
|
371
|
+
filename = f"{action_slug}{ddl.extension}"
|
|
372
|
+
content = ddl.content.encode("utf-8")
|
|
373
|
+
return RenderedAction(
|
|
374
|
+
action=BuildAction(
|
|
375
|
+
id=f"object-{action_slug}",
|
|
376
|
+
kind=_OBJECT_KIND[source.kind],
|
|
377
|
+
resource_node_id=str(identity),
|
|
378
|
+
executor=ddl.executor,
|
|
379
|
+
payload=filename,
|
|
380
|
+
payload_sha256=sha256_hex(content),
|
|
381
|
+
),
|
|
382
|
+
payloads={filename: content},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def render_load_build_action(artefact) -> RenderedAction:
|
|
387
|
+
"""The action and frozen payload one load artefact installs as.
|
|
388
|
+
|
|
389
|
+
The load half of :func:`render_document_build_action`, and the same claim:
|
|
390
|
+
what runs it, under what id, against which bytes. A file is written into the
|
|
391
|
+
runtime tree by the ``load_file`` executor; a procedure is a create-or-alter
|
|
392
|
+
run by ``tsql``, which needs no knowledge that it happens to be a procedure.
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
action_slug = _slug(artefact.identity)
|
|
396
|
+
if artefact.is_file:
|
|
397
|
+
filename = f"{action_slug}.payload"
|
|
398
|
+
executor, kind = "load_file", WRITE_FILE
|
|
399
|
+
else:
|
|
400
|
+
filename = f"{action_slug}.sql"
|
|
401
|
+
executor, kind = "tsql", BUILD_PROCEDURE
|
|
402
|
+
return RenderedAction(
|
|
403
|
+
action=BuildAction(
|
|
404
|
+
id=f"load-{action_slug}",
|
|
405
|
+
kind=kind,
|
|
406
|
+
resource_node_id=str(artefact.identity),
|
|
407
|
+
executor=executor,
|
|
408
|
+
payload=filename,
|
|
409
|
+
payload_sha256=sha256_hex(artefact.payload),
|
|
410
|
+
),
|
|
411
|
+
payloads={filename: artefact.payload},
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def item_load_stages(
|
|
416
|
+
artefacts,
|
|
417
|
+
selected_for_build,
|
|
418
|
+
*,
|
|
419
|
+
item: WeaverItemId,
|
|
420
|
+
target,
|
|
421
|
+
) -> tuple[PlannedStage, ...]:
|
|
422
|
+
"""One item's load layer: the last thing it does, and one barrier wide.
|
|
423
|
+
|
|
424
|
+
A single stage rather than dependency layers, because there are no
|
|
425
|
+
dependencies to express — nothing here runs anything, so a deployed module
|
|
426
|
+
and a generated procedure have no ordering between them. What they *do*
|
|
427
|
+
depend on is the item's structural work, and that is expressed by the layer
|
|
428
|
+
being last rather than by an edge.
|
|
429
|
+
|
|
430
|
+
Empty when the item has no selected load work, which is a phase with nothing
|
|
431
|
+
to do rather than an empty barrier: an unpopulated stage takes no sequence
|
|
432
|
+
number.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
selected = [
|
|
436
|
+
artefact
|
|
437
|
+
for artefact in artefacts
|
|
438
|
+
if artefact.identity.item == item and artefact.identity in selected_for_build
|
|
439
|
+
]
|
|
440
|
+
if not selected:
|
|
441
|
+
return ()
|
|
442
|
+
payloads: dict[str, bytes] = {}
|
|
443
|
+
actions = []
|
|
444
|
+
changes = []
|
|
445
|
+
for artefact in sorted(selected, key=lambda value: str(value.identity)):
|
|
446
|
+
rendered = render_load_build_action(artefact)
|
|
447
|
+
payloads.update(rendered.payloads)
|
|
448
|
+
actions.append(rendered.action)
|
|
449
|
+
changes.append(
|
|
450
|
+
change_added(
|
|
451
|
+
FILE_KIND if artefact.is_file else PROCEDURE_KIND,
|
|
452
|
+
artefact.target_path
|
|
453
|
+
if artefact.is_file
|
|
454
|
+
else artefact.identity.object_id.qualified,
|
|
455
|
+
rendered.action.id,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
return (
|
|
459
|
+
PlannedStage(
|
|
460
|
+
phase=LOAD,
|
|
461
|
+
slug="load",
|
|
462
|
+
description="install load artefacts",
|
|
463
|
+
payloads=payloads,
|
|
464
|
+
changes={target.id: tuple(changes)},
|
|
465
|
+
batches=(
|
|
466
|
+
BuildBatch(
|
|
467
|
+
id=f"{_slug(item)}", target_id=target.id, actions=tuple(actions)
|
|
468
|
+
),
|
|
469
|
+
),
|
|
470
|
+
),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def item_load_removals(
|
|
475
|
+
removed,
|
|
476
|
+
*,
|
|
477
|
+
item: WeaverItemId,
|
|
478
|
+
target,
|
|
479
|
+
registered,
|
|
480
|
+
) -> tuple[PlannedStage, ...]:
|
|
481
|
+
"""Frozen removals for load artefacts the source has stopped claiming.
|
|
482
|
+
|
|
483
|
+
Driven by the previous Registry rows rather than by a diff against the
|
|
484
|
+
target, and that is what makes a rename ordinary: the old identity is no
|
|
485
|
+
longer claimed, so its row names exactly what to remove and where, while the
|
|
486
|
+
new identity is simply new. Nothing has to notice that the two are related.
|
|
487
|
+
|
|
488
|
+
The removals ride in the item's load layer alongside its writes. They cannot
|
|
489
|
+
collide — an identity is either still claimed or not — and keeping them
|
|
490
|
+
together means everything the load layer does to a target is in one barrier.
|
|
491
|
+
"""
|
|
492
|
+
|
|
493
|
+
# Scoped by what the Registry says each removed object *is*, not by what its
|
|
494
|
+
# identity looks like. A removed table is removed by the inventory prune,
|
|
495
|
+
# which can see it; only the two the prune cannot see are handled here.
|
|
496
|
+
selected = sorted(
|
|
497
|
+
(
|
|
498
|
+
identity
|
|
499
|
+
for identity in removed
|
|
500
|
+
if identity.item == item
|
|
501
|
+
and registered[identity].object_type in (FILE_TYPE, PROCEDURE_TYPE)
|
|
502
|
+
),
|
|
503
|
+
key=str,
|
|
504
|
+
)
|
|
505
|
+
payloads: dict[str, bytes] = {}
|
|
506
|
+
actions = []
|
|
507
|
+
changes = []
|
|
508
|
+
for identity in selected:
|
|
509
|
+
object_type = registered[identity].object_type
|
|
510
|
+
action_slug = _slug(identity)
|
|
511
|
+
if object_type == FILE_TYPE:
|
|
512
|
+
actions.append(
|
|
513
|
+
BuildAction(
|
|
514
|
+
id=f"load-remove-{action_slug}",
|
|
515
|
+
kind=DELETE_FILE,
|
|
516
|
+
resource_node_id=str(identity),
|
|
517
|
+
executor="load_file",
|
|
518
|
+
payload=None,
|
|
519
|
+
payload_sha256=None,
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
changes.append(
|
|
523
|
+
change_removed(
|
|
524
|
+
FILE_KIND,
|
|
525
|
+
f"{identity.object_id.schema}/{identity.object_id.object}",
|
|
526
|
+
f"load-remove-{action_slug}",
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
continue
|
|
530
|
+
statement = (
|
|
531
|
+
"drop procedure if exists "
|
|
532
|
+
f"{_tsql_ident(identity.object_id.schema)}."
|
|
533
|
+
f"{_tsql_ident(identity.object_id.object)};\n"
|
|
534
|
+
)
|
|
535
|
+
content = statement.encode("utf-8")
|
|
536
|
+
filename = f"drop-{action_slug}.sql"
|
|
537
|
+
payloads[filename] = content
|
|
538
|
+
actions.append(
|
|
539
|
+
BuildAction(
|
|
540
|
+
id=f"load-remove-{action_slug}",
|
|
541
|
+
kind=DROP_PROCEDURE,
|
|
542
|
+
resource_node_id=str(identity),
|
|
543
|
+
executor="tsql",
|
|
544
|
+
payload=filename,
|
|
545
|
+
payload_sha256=sha256_hex(content),
|
|
546
|
+
)
|
|
547
|
+
)
|
|
548
|
+
changes.append(
|
|
549
|
+
change_removed(
|
|
550
|
+
PROCEDURE_KIND,
|
|
551
|
+
identity.object_id.qualified,
|
|
552
|
+
f"load-remove-{action_slug}",
|
|
553
|
+
)
|
|
554
|
+
)
|
|
555
|
+
if not actions:
|
|
556
|
+
return ()
|
|
557
|
+
return (
|
|
558
|
+
PlannedStage(
|
|
559
|
+
phase=LOAD,
|
|
560
|
+
slug="load",
|
|
561
|
+
description="install load artefacts",
|
|
562
|
+
payloads=payloads,
|
|
563
|
+
changes={target.id: tuple(changes)},
|
|
564
|
+
batches=(
|
|
565
|
+
BuildBatch(
|
|
566
|
+
id=f"remove-{_slug(item)}",
|
|
567
|
+
target_id=target.id,
|
|
568
|
+
actions=tuple(actions),
|
|
569
|
+
),
|
|
570
|
+
),
|
|
571
|
+
),
|
|
572
|
+
)
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def item_build_stages(
|
|
576
|
+
repository,
|
|
577
|
+
selected_for_build,
|
|
578
|
+
*,
|
|
579
|
+
item: WeaverItemId,
|
|
580
|
+
target,
|
|
581
|
+
) -> tuple[PlannedStage, ...]:
|
|
582
|
+
"""One item's declared documents, in forward dependency layers."""
|
|
583
|
+
|
|
584
|
+
selected = {identity for identity in selected_for_build if identity.item == item}
|
|
585
|
+
if not selected:
|
|
586
|
+
return ()
|
|
587
|
+
graph = repository.dependency_graph.subgraph({str(value) for value in selected})
|
|
588
|
+
identities = {str(identity): identity for identity in selected}
|
|
589
|
+
stages = []
|
|
590
|
+
for index, layer in enumerate(graph.layers()):
|
|
591
|
+
payloads: dict[str, bytes] = {}
|
|
592
|
+
actions = []
|
|
593
|
+
changes = []
|
|
594
|
+
for node in sorted(layer):
|
|
595
|
+
identity = identities[node]
|
|
596
|
+
source = repository.source_documents[identity]
|
|
597
|
+
rendered = render_document_build_action(identity, source)
|
|
598
|
+
payloads.update(rendered.payloads)
|
|
599
|
+
actions.append(rendered.action)
|
|
600
|
+
changes.append(
|
|
601
|
+
change_added(
|
|
602
|
+
_CHANGE_KIND_FOR_KIND[source.kind],
|
|
603
|
+
identity.object_id.qualified,
|
|
604
|
+
rendered.action.id,
|
|
605
|
+
)
|
|
606
|
+
)
|
|
607
|
+
actions = tuple(actions)
|
|
608
|
+
stages.append(
|
|
609
|
+
PlannedStage(
|
|
610
|
+
phase=BUILD,
|
|
611
|
+
index=index,
|
|
612
|
+
slug="build-objects",
|
|
613
|
+
description="build dependency layer",
|
|
614
|
+
payloads=payloads,
|
|
615
|
+
changes={target.id: tuple(changes)},
|
|
616
|
+
batches=(
|
|
617
|
+
BuildBatch(
|
|
618
|
+
id=f"{_slug(item)}", target_id=target.id, actions=actions
|
|
619
|
+
),
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
)
|
|
623
|
+
return tuple(stages)
|
|
624
|
+
|
|
625
|
+
|