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
weaver/__init__.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""The small notebook-facing Weaver interface.
|
|
2
|
+
|
|
3
|
+
Internal workspace, repository, target, storage, SQL, bundle, installer, and
|
|
4
|
+
control-plane composition seams live in their owning modules. Importing the
|
|
5
|
+
product namespace remains safe without PySpark, Fabric credentials, or the
|
|
6
|
+
optional desktop CLI.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from . import config as _config # establish declaration/workspace import order
|
|
12
|
+
from .errors import CommandError, ConfigError, IdentityError, WeaverError
|
|
13
|
+
from .operations import BuildResult, WipeReport, WipeResult, build, wipe
|
|
14
|
+
from .load import load
|
|
15
|
+
from .load_report import LoadMessage, LoadNodeReport, LoadResult, LoadRunReport
|
|
16
|
+
from .lakehouse import Lakehouse, default_lakehouse, lakehouse_for
|
|
17
|
+
from .objects import Folder, Table, View, WeaverObject
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _resolve_version() -> str:
|
|
21
|
+
"""Read the git-derived installed distribution version."""
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from importlib.metadata import version
|
|
25
|
+
|
|
26
|
+
return version("weaverstack")
|
|
27
|
+
except Exception: # pragma: no cover - never worth crashing an import over
|
|
28
|
+
return "0.0.0+unknown"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
__version__ = _resolve_version()
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"__version__",
|
|
35
|
+
# ordinary operations and results
|
|
36
|
+
"build",
|
|
37
|
+
"BuildResult",
|
|
38
|
+
"wipe",
|
|
39
|
+
"WipeReport",
|
|
40
|
+
"WipeResult",
|
|
41
|
+
"load",
|
|
42
|
+
"LoadRunReport",
|
|
43
|
+
"LoadNodeReport",
|
|
44
|
+
"LoadMessage",
|
|
45
|
+
"LoadResult",
|
|
46
|
+
# authored objects
|
|
47
|
+
"WeaverObject",
|
|
48
|
+
"Folder",
|
|
49
|
+
"Table",
|
|
50
|
+
"View",
|
|
51
|
+
"Lakehouse",
|
|
52
|
+
"default_lakehouse",
|
|
53
|
+
"lakehouse_for",
|
|
54
|
+
# common errors
|
|
55
|
+
"WeaverError",
|
|
56
|
+
"CommandError",
|
|
57
|
+
"ConfigError",
|
|
58
|
+
"IdentityError",
|
|
59
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Weaver build — planning a declaration into a bundle, and installing it.
|
|
2
|
+
|
|
3
|
+
The boundary is deliberate::
|
|
4
|
+
|
|
5
|
+
WeaverRepository -> planner -> BuildBundle -> installer -> workspaces
|
|
6
|
+
|
|
7
|
+
The planner owns every decision — item selection, ordering, executable
|
|
8
|
+
generation, certification. The installer owns execution only: it validates a
|
|
9
|
+
bundle and runs it, and never reads the source declaration, resolves a
|
|
10
|
+
dependency or selects a target.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .bundle import (
|
|
16
|
+
BuildBundle,
|
|
17
|
+
compute_bundle_id,
|
|
18
|
+
load_bundle,
|
|
19
|
+
plan_from_yaml,
|
|
20
|
+
plan_to_yaml,
|
|
21
|
+
write_bundle,
|
|
22
|
+
)
|
|
23
|
+
from .models import (
|
|
24
|
+
BuildAction,
|
|
25
|
+
BuildBatch,
|
|
26
|
+
BuildPlan,
|
|
27
|
+
BuildSequence,
|
|
28
|
+
OmittedNode,
|
|
29
|
+
)
|
|
30
|
+
from .installer import InstallationEnvironment, execute_action, install_bundle
|
|
31
|
+
from .incremental import BuildSelection, Impact, determine_impact
|
|
32
|
+
from .physical import RenderedAction, render_document_build_action
|
|
33
|
+
from .planner import PlannedItem, generate_item_build_bundle, plan_item_build
|
|
34
|
+
from .report import InstallationReport
|
|
35
|
+
from .workflow import (
|
|
36
|
+
BuildState,
|
|
37
|
+
ItemBuildResult,
|
|
38
|
+
build_item_repository,
|
|
39
|
+
build_item_repository_source,
|
|
40
|
+
build_uploaded_item_repository,
|
|
41
|
+
catalogue_items_for_build,
|
|
42
|
+
install_bundle_archive,
|
|
43
|
+
materialise_bundle_archive,
|
|
44
|
+
materialise_tree,
|
|
45
|
+
prepare_repository,
|
|
46
|
+
persist_bundle_archive,
|
|
47
|
+
timestamped_archive_name,
|
|
48
|
+
read_build_state,
|
|
49
|
+
validate_build_request,
|
|
50
|
+
)
|
|
51
|
+
from .targets import (
|
|
52
|
+
BoundTarget,
|
|
53
|
+
LakehouseBinding,
|
|
54
|
+
ItemBinding,
|
|
55
|
+
ItemBindings,
|
|
56
|
+
parse_item_binding,
|
|
57
|
+
effective_item_bindings,
|
|
58
|
+
WarehouseBinding,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
__all__ = [
|
|
62
|
+
"BoundTarget",
|
|
63
|
+
"LakehouseBinding",
|
|
64
|
+
"ItemBinding",
|
|
65
|
+
"ItemBindings",
|
|
66
|
+
"parse_item_binding",
|
|
67
|
+
"effective_item_bindings",
|
|
68
|
+
"WarehouseBinding",
|
|
69
|
+
"OmittedNode",
|
|
70
|
+
"BuildAction",
|
|
71
|
+
"BuildBatch",
|
|
72
|
+
"BuildSequence",
|
|
73
|
+
"BuildPlan",
|
|
74
|
+
"BuildBundle",
|
|
75
|
+
"Impact",
|
|
76
|
+
"BuildSelection",
|
|
77
|
+
"determine_impact",
|
|
78
|
+
# The three narrow seams: one document rendered, one item planned, one
|
|
79
|
+
# action executed. Each is the lowest layer that can answer its own
|
|
80
|
+
# question, so a failure localises there rather than in a whole build.
|
|
81
|
+
"RenderedAction",
|
|
82
|
+
"render_document_build_action",
|
|
83
|
+
"PlannedItem",
|
|
84
|
+
"plan_item_build",
|
|
85
|
+
"execute_action",
|
|
86
|
+
"compute_bundle_id",
|
|
87
|
+
"load_bundle",
|
|
88
|
+
"write_bundle",
|
|
89
|
+
"plan_to_yaml",
|
|
90
|
+
"plan_from_yaml",
|
|
91
|
+
"generate_item_build_bundle",
|
|
92
|
+
"InstallationEnvironment",
|
|
93
|
+
"install_bundle",
|
|
94
|
+
"InstallationReport",
|
|
95
|
+
"ItemBuildResult",
|
|
96
|
+
"BuildState",
|
|
97
|
+
"build_item_repository",
|
|
98
|
+
"build_item_repository_source",
|
|
99
|
+
"build_uploaded_item_repository",
|
|
100
|
+
"materialise_tree",
|
|
101
|
+
"prepare_repository",
|
|
102
|
+
"catalogue_items_for_build",
|
|
103
|
+
"read_build_state",
|
|
104
|
+
"validate_build_request",
|
|
105
|
+
"persist_bundle_archive",
|
|
106
|
+
"materialise_bundle_archive",
|
|
107
|
+
"install_bundle_archive",
|
|
108
|
+
"timestamped_archive_name",
|
|
109
|
+
]
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Planning each repository alias as one physical action.
|
|
2
|
+
|
|
3
|
+
An alias is the consuming item's own name for something another item produces.
|
|
4
|
+
It is owned by its destination item — the source stays the canonical producer —
|
|
5
|
+
so it is planned as part of that item's work, ahead of every document the item
|
|
6
|
+
declares, because those documents are written against the namespace it
|
|
7
|
+
establishes.
|
|
8
|
+
|
|
9
|
+
What an alias *becomes* depends on the destination it is bound to, and that is a
|
|
10
|
+
planning decision because it is a decision about the target kind:
|
|
11
|
+
|
|
12
|
+
=============================== ==============================================
|
|
13
|
+
Fabric or local Lakehouse a ``create_alias`` action — a OneLake shortcut
|
|
14
|
+
in Fabric, a filesystem link in the emulator
|
|
15
|
+
Fabric Warehouse a frozen view over the bound source
|
|
16
|
+
=============================== ==============================================
|
|
17
|
+
|
|
18
|
+
Only the last of the two is spelled out in SQL, because only there is the
|
|
19
|
+
statement itself the semantic decision. A shortcut and a link are two transports
|
|
20
|
+
for one frozen decision — this destination, that source — which is the same split
|
|
21
|
+
:mod:`weaver.build_bundle.executors.spark_schema` documents for ``LOCATION``.
|
|
22
|
+
|
|
23
|
+
Where the current bindings give an alias no physical form at all, it is left out
|
|
24
|
+
of the plan and recorded as an omission. That is deliberately the planner's
|
|
25
|
+
decision and never the installer's: the installer may only run an alias action
|
|
26
|
+
already frozen for it.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Iterable, Mapping
|
|
34
|
+
|
|
35
|
+
from ..declaration.model import WeaverDocumentId, WeaverItemId
|
|
36
|
+
from .models import (
|
|
37
|
+
CREATE_ALIAS,
|
|
38
|
+
OMIT_ALIAS_UNSUPPORTED,
|
|
39
|
+
BuildAction,
|
|
40
|
+
BuildBatch,
|
|
41
|
+
OmittedNode,
|
|
42
|
+
)
|
|
43
|
+
from .changes import (
|
|
44
|
+
FOLDER as FOLDER_KIND,
|
|
45
|
+
TABLE as TABLE_KIND,
|
|
46
|
+
VIEW as VIEW_KIND,
|
|
47
|
+
added,
|
|
48
|
+
)
|
|
49
|
+
from .payloads import sha256_hex
|
|
50
|
+
from .stages import ALIAS, PlannedStage
|
|
51
|
+
from .targets import BoundTarget, WAREHOUSE_TARGET
|
|
52
|
+
|
|
53
|
+
#: Where a Lakehouse alias is materialised, by whether it names a Files document.
|
|
54
|
+
TABLES_AREA = "Tables"
|
|
55
|
+
FILES_AREA = "Files"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _slug(value) -> str:
|
|
59
|
+
return str(value).replace("/", "--").replace(" ", "-")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def alias_node_id(destination: WeaverDocumentId) -> str:
|
|
63
|
+
"""How an alias is named in a plan.
|
|
64
|
+
|
|
65
|
+
Prefixed, because an alias destination is not a repository document: nothing
|
|
66
|
+
declares it, and no dependency layer contains it.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
return f"alias:{destination}"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(frozen=True)
|
|
73
|
+
class ItemAliasPlan:
|
|
74
|
+
"""One item's planned aliases, the schemas they need, and what was left out."""
|
|
75
|
+
|
|
76
|
+
stage: PlannedStage | None = None
|
|
77
|
+
schemas: tuple[str, ...] = ()
|
|
78
|
+
omitted: tuple[OmittedNode, ...] = ()
|
|
79
|
+
#: The destinations behind ``omitted``, as identities rather than node ids.
|
|
80
|
+
#: The caller needs them because an alias with no physical form must not be
|
|
81
|
+
#: certified as installed — see :attr:`omitted`.
|
|
82
|
+
omitted_destinations: tuple[WeaverDocumentId, ...] = ()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def plan_item_aliases(
|
|
86
|
+
repository,
|
|
87
|
+
*,
|
|
88
|
+
item: WeaverItemId,
|
|
89
|
+
target: BoundTarget,
|
|
90
|
+
target_by_item: Mapping[WeaverItemId, BoundTarget],
|
|
91
|
+
selected: Iterable[WeaverDocumentId],
|
|
92
|
+
) -> ItemAliasPlan:
|
|
93
|
+
"""Plan the aliases this build selected, against the current bindings.
|
|
94
|
+
|
|
95
|
+
``selected`` is what incremental selection chose to rebuild. An alias absent
|
|
96
|
+
from it is current — its declaration is unchanged, its destination is there,
|
|
97
|
+
and its source has not been rebuilt since — so it is left alone rather than
|
|
98
|
+
replaced, exactly as an unchanged document is.
|
|
99
|
+
|
|
100
|
+
Its *schema* is still reported. A retained alias lives in a namespace the
|
|
101
|
+
item must have, and a build that created only the schemas its rebuilt aliases
|
|
102
|
+
needed would leave the others homeless.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
aliases = sorted(
|
|
106
|
+
(alias for alias in repository.aliases if alias.destination.item == item),
|
|
107
|
+
key=lambda alias: str(alias.destination),
|
|
108
|
+
)
|
|
109
|
+
if not aliases:
|
|
110
|
+
return ItemAliasPlan()
|
|
111
|
+
|
|
112
|
+
chosen = set(selected)
|
|
113
|
+
omitted: list[OmittedNode] = []
|
|
114
|
+
omitted_destinations: list[WeaverDocumentId] = []
|
|
115
|
+
supported: list[tuple] = []
|
|
116
|
+
schemas: list[str] = []
|
|
117
|
+
|
|
118
|
+
for alias in aliases:
|
|
119
|
+
source_target = target_by_item.get(alias.source.item)
|
|
120
|
+
reason = _unsupported(alias, target=target, source_target=source_target)
|
|
121
|
+
if reason is not None:
|
|
122
|
+
omitted.append(
|
|
123
|
+
OmittedNode(
|
|
124
|
+
node_id=alias_node_id(alias.destination),
|
|
125
|
+
reason=OMIT_ALIAS_UNSUPPORTED,
|
|
126
|
+
detail=reason,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
omitted_destinations.append(alias.destination)
|
|
130
|
+
continue
|
|
131
|
+
schemas.append(alias.destination.object_id.schema)
|
|
132
|
+
if alias.destination in chosen:
|
|
133
|
+
supported.append((alias, source_target))
|
|
134
|
+
|
|
135
|
+
stage = None
|
|
136
|
+
if supported:
|
|
137
|
+
item_slug = _slug(item)
|
|
138
|
+
payloads: dict[str, bytes] = {}
|
|
139
|
+
action = _alias_action(
|
|
140
|
+
supported, item=item, target=target, payloads=payloads
|
|
141
|
+
)
|
|
142
|
+
# One action stands for every alias the item consumes, so it produces
|
|
143
|
+
# several changes. Each names what the alias physically *is* at this
|
|
144
|
+
# binding — a folder under Files, a view in a Warehouse, a table in a
|
|
145
|
+
# Lakehouse — which is the same question the Registry row answers.
|
|
146
|
+
stage = PlannedStage(
|
|
147
|
+
phase=ALIAS,
|
|
148
|
+
slug="item-aliases",
|
|
149
|
+
description="materialise item-owned aliases",
|
|
150
|
+
payloads=payloads,
|
|
151
|
+
changes={
|
|
152
|
+
target.id: tuple(
|
|
153
|
+
added(
|
|
154
|
+
_alias_change_kind(alias.destination, target),
|
|
155
|
+
alias.destination.object_id.qualified,
|
|
156
|
+
action.id,
|
|
157
|
+
)
|
|
158
|
+
for alias, _source_target in supported
|
|
159
|
+
)
|
|
160
|
+
},
|
|
161
|
+
batches=(
|
|
162
|
+
BuildBatch(
|
|
163
|
+
id=f"item-aliases-{item_slug}",
|
|
164
|
+
target_id=target.id,
|
|
165
|
+
actions=(action,),
|
|
166
|
+
),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
return ItemAliasPlan(
|
|
170
|
+
stage=stage,
|
|
171
|
+
schemas=tuple(sorted(set(schemas))),
|
|
172
|
+
omitted=tuple(omitted),
|
|
173
|
+
omitted_destinations=tuple(omitted_destinations),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _unsupported(alias, *, target: BoundTarget, source_target: BoundTarget | None) -> str | None:
|
|
178
|
+
"""Why this alias has no physical form here, or None when it has one."""
|
|
179
|
+
|
|
180
|
+
if source_target is None:
|
|
181
|
+
return (
|
|
182
|
+
f"source item {alias.source.item} is not bound, so the alias has no "
|
|
183
|
+
"physical source to point at"
|
|
184
|
+
)
|
|
185
|
+
if alias.destination.is_files != alias.source.is_files:
|
|
186
|
+
return (
|
|
187
|
+
"an alias must stay in one namespace — a Files destination needs a "
|
|
188
|
+
"Files source, and a table destination a table source"
|
|
189
|
+
)
|
|
190
|
+
if target.kind != WAREHOUSE_TARGET and source_target.kind == WAREHOUSE_TARGET:
|
|
191
|
+
return (
|
|
192
|
+
"a Lakehouse alias is a OneLake shortcut, and there is no shortcut "
|
|
193
|
+
f"form for the Warehouse source {alias.source}"
|
|
194
|
+
)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _alias_change_kind(destination, target) -> str:
|
|
199
|
+
"""What an alias will look like to an inventory, which depends on binding.
|
|
200
|
+
|
|
201
|
+
The same rule :func:`weaver.build_bundle.prune.managed_sets` applies for the
|
|
202
|
+
keep-set, and for the same reason: an alias is a folder, a view or a table
|
|
203
|
+
according to what it was bound to, and nothing about the declaration says
|
|
204
|
+
which.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
if destination.is_files:
|
|
208
|
+
return FOLDER_KIND
|
|
209
|
+
return VIEW_KIND if target.kind == WAREHOUSE_TARGET else TABLE_KIND
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _alias_action(
|
|
213
|
+
supported,
|
|
214
|
+
*,
|
|
215
|
+
item: WeaverItemId,
|
|
216
|
+
target: BoundTarget,
|
|
217
|
+
payloads: dict[str, bytes],
|
|
218
|
+
) -> BuildAction:
|
|
219
|
+
"""One action for all of this item's aliases.
|
|
220
|
+
|
|
221
|
+
One rather than one-per-alias, because materialising an alias is not
|
|
222
|
+
instantaneous and the cost is per *wait*, not per create. A Lakehouse alias
|
|
223
|
+
becomes usable some seconds after the shortcut exists (measured at 6–31s), so
|
|
224
|
+
N actions run serially pay N waits while one action that creates everything and
|
|
225
|
+
then waits pays roughly one. A Warehouse alias is a script, and the executor
|
|
226
|
+
already runs a multi-statement script as one unit.
|
|
227
|
+
|
|
228
|
+
The action reports each alias it made in its details, so the manifest loses no
|
|
229
|
+
traceability by grouping them.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
item_slug = _slug(item)
|
|
233
|
+
if target.kind == WAREHOUSE_TARGET:
|
|
234
|
+
content = (
|
|
235
|
+
json.dumps(
|
|
236
|
+
[
|
|
237
|
+
_view_statement(alias, source_target)
|
|
238
|
+
for alias, source_target in supported
|
|
239
|
+
],
|
|
240
|
+
indent=2,
|
|
241
|
+
ensure_ascii=False,
|
|
242
|
+
)
|
|
243
|
+
+ "\n"
|
|
244
|
+
).encode("utf-8")
|
|
245
|
+
filename = f"aliases-{item_slug}.tsql-batch.json"
|
|
246
|
+
executor = "tsql_batch"
|
|
247
|
+
else:
|
|
248
|
+
content = _shortcut_payload(supported, target=target)
|
|
249
|
+
filename = f"aliases-{item_slug}.alias.json"
|
|
250
|
+
executor = "alias"
|
|
251
|
+
payloads[filename] = content
|
|
252
|
+
return BuildAction(
|
|
253
|
+
id=f"aliases-{item_slug}",
|
|
254
|
+
kind=CREATE_ALIAS,
|
|
255
|
+
# No single resource: this action stands for every alias the item
|
|
256
|
+
# consumes, and the payload names them.
|
|
257
|
+
resource_node_id=None,
|
|
258
|
+
executor=executor,
|
|
259
|
+
payload=filename,
|
|
260
|
+
payload_sha256=sha256_hex(content),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _view_statement(alias, source_target: BoundTarget) -> str:
|
|
265
|
+
"""A Warehouse alias, as the one statement that makes it exist.
|
|
266
|
+
|
|
267
|
+
The source is named by its bound item's three-part spelling, which is how a
|
|
268
|
+
Fabric Warehouse reaches another item in the same workspace, and is frozen
|
|
269
|
+
here for the same reason an authored three-part reference is: it is the
|
|
270
|
+
semantic decision, not transport.
|
|
271
|
+
|
|
272
|
+
``CREATE OR ALTER`` rather than a drop and a create. An alias holds no data —
|
|
273
|
+
it is a pointer — so replacing one is not a destructive transition needing
|
|
274
|
+
proof of prior state, and a build that could not run twice over its own
|
|
275
|
+
aliases would not be re-runnable at all. It is also the only *single-statement*
|
|
276
|
+
way to say that, and T-SQL requires ``CREATE VIEW`` to be the first statement
|
|
277
|
+
in its batch: ``drop …; create view …;`` in one batch is rejected outright,
|
|
278
|
+
which is exactly how this was found.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
schema = _tsql_ident(alias.destination.object_id.schema)
|
|
282
|
+
name = _tsql_ident(alias.destination.object_id.object)
|
|
283
|
+
source = ".".join(
|
|
284
|
+
_tsql_ident(part)
|
|
285
|
+
for part in (
|
|
286
|
+
source_target.name,
|
|
287
|
+
alias.source.object_id.schema,
|
|
288
|
+
alias.source.object_id.object,
|
|
289
|
+
)
|
|
290
|
+
)
|
|
291
|
+
return f"create or alter view {schema}.{name} as select * from {source};"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _shortcut_payload(supported, *, target: BoundTarget) -> bytes:
|
|
295
|
+
"""This item's Lakehouse aliases, as the frozen pairs of addresses they stand for.
|
|
296
|
+
|
|
297
|
+
Each is named by target id rather than by resolved path: the installer already
|
|
298
|
+
resolves every target the plan declares through its own environment, so a
|
|
299
|
+
source is addressed exactly as the destination is, and a bundle carries no
|
|
300
|
+
path from the machine that wrote it.
|
|
301
|
+
"""
|
|
302
|
+
|
|
303
|
+
mapping = {
|
|
304
|
+
"aliases": [
|
|
305
|
+
{
|
|
306
|
+
"alias": str(alias.destination),
|
|
307
|
+
"source": str(alias.source),
|
|
308
|
+
"source_target_id": source_target.id,
|
|
309
|
+
"area": FILES_AREA if alias.destination.is_files else TABLES_AREA,
|
|
310
|
+
"schema": alias.destination.object_id.schema,
|
|
311
|
+
"object": alias.destination.object_id.object,
|
|
312
|
+
"source_area": FILES_AREA if alias.source.is_files else TABLES_AREA,
|
|
313
|
+
"source_schema": alias.source.object_id.schema,
|
|
314
|
+
"source_object": alias.source.object_id.object,
|
|
315
|
+
}
|
|
316
|
+
for alias, source_target in supported
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
return (json.dumps(mapping, indent=2, sort_keys=True, ensure_ascii=False) + "\n").encode(
|
|
320
|
+
"utf-8"
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _tsql_ident(name: str) -> str:
|
|
325
|
+
return "[" + name.replace("]", "]]") + "]"
|