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,468 @@
|
|
|
1
|
+
"""T-SQL create generation — one self-contained script per Warehouse object.
|
|
2
|
+
|
|
3
|
+
A Warehouse table's build is a single T-SQL script that finishes itself on the
|
|
4
|
+
server (how-does-build-work §2): it materialises the object's query in shape-only
|
|
5
|
+
form into a temp table, reads that temp table's column metadata back, validates
|
|
6
|
+
the authored metadata against it, and creates the one physical table — all in one
|
|
7
|
+
execution, so the installer makes no round-trip. This is the Warehouse
|
|
8
|
+
counterpart of the ``spark_table`` executor; the script *is* the frozen payload.
|
|
9
|
+
|
|
10
|
+
Two paths, sharing the shape-only materialisation and metadata validation:
|
|
11
|
+
|
|
12
|
+
- **inferred** — no declared schema: the physical column types are computed on
|
|
13
|
+
the server from the temp table's own types, through a Fabric-safe type map;
|
|
14
|
+
- **declared** — a declared schema: the table is created from the declaration
|
|
15
|
+
and the query is materialised only to validate case-exact column-set
|
|
16
|
+
equivalence.
|
|
17
|
+
|
|
18
|
+
Only the authored main table is built. There is no generated view, no
|
|
19
|
+
``_Current`` and no ``_History`` — history is a later, automatic load-time
|
|
20
|
+
consequence. Ported from the ``weaver_runtime.dbrep.sql`` reference generator,
|
|
21
|
+
with that older physical layout removed.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import hashlib
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
import re
|
|
29
|
+
|
|
30
|
+
import yaml
|
|
31
|
+
|
|
32
|
+
from .columns import metadata_column_references
|
|
33
|
+
from .metadata import SesDocument
|
|
34
|
+
from .sql_shaping import (
|
|
35
|
+
insert_select_into,
|
|
36
|
+
insert_where_one_eq_zero,
|
|
37
|
+
render_sql_template,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
TYPE_MAPPING_PATH = Path(__file__).resolve().parent / "warehouse_type_mapping.yml"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# --- entry points -----------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def generate_tsql_table_script(document: SesDocument, body: str) -> str:
|
|
47
|
+
"""A self-contained T-SQL script that builds ``document``'s main table."""
|
|
48
|
+
|
|
49
|
+
mapping = _load_type_mapping()
|
|
50
|
+
temp_table = _weaver_temp_table_name("#weaver_shape", document.qualified)
|
|
51
|
+
guarded = insert_where_one_eq_zero(body)
|
|
52
|
+
shape_sql = _ensure_terminated(insert_select_into(guarded, temp_table))
|
|
53
|
+
|
|
54
|
+
if document.has_declared_schema:
|
|
55
|
+
create_sql = _render_declared_create(document, temp_table)
|
|
56
|
+
else:
|
|
57
|
+
create_sql = _render_inferred_create(document, temp_table, mapping)
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
"/* weaver generated table build script. */\n"
|
|
61
|
+
"set nocount on;\n\n"
|
|
62
|
+
f"if object_id('tempdb..{temp_table}') is not null drop table {temp_table};\n\n"
|
|
63
|
+
f"{shape_sql}\n\n"
|
|
64
|
+
f"{create_sql}\n"
|
|
65
|
+
f"\ndrop table {temp_table};\n"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def generate_tsql_view_script(document: SesDocument, body: str) -> str:
|
|
70
|
+
"""A strict ``CREATE VIEW`` over the validated query body."""
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
f"create view {_quote_multipart(document.qualified)} as\n"
|
|
74
|
+
f"{_normalise_view_body(body)}\n"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# --- inferred path ----------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _render_inferred_create(
|
|
82
|
+
document: SesDocument, temp_table: str, mapping: dict
|
|
83
|
+
) -> str:
|
|
84
|
+
target = _quote_multipart(document.qualified)
|
|
85
|
+
temp_literal = _sql_literal(f"tempdb..{temp_table}")
|
|
86
|
+
identity = document.identity_column
|
|
87
|
+
return render_sql_template(
|
|
88
|
+
"ddl/infer_create_table",
|
|
89
|
+
temp_object_literal=temp_literal,
|
|
90
|
+
metadata_validation_sql=_render_metadata_validation(document, temp_literal),
|
|
91
|
+
identity_guard_sql=_render_identity_guard(identity, temp_literal),
|
|
92
|
+
identity_column_sql=_render_identity_union(identity),
|
|
93
|
+
first_ordinal="0" if identity else "1",
|
|
94
|
+
primary_key_columns_cte=_render_primary_key_cte(document.primary_key),
|
|
95
|
+
not_null_columns_cte=_render_name_only_cte(document.not_null),
|
|
96
|
+
type_case=_render_type_case(mapping),
|
|
97
|
+
target_table=target,
|
|
98
|
+
target_table_literal=_sql_literal(target),
|
|
99
|
+
pk_constraint=_pk_constraint_name(document.qualified),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _render_identity_union(column) -> str:
|
|
104
|
+
"""A leading ``all_columns`` entry (ordinal 0) for the identity column."""
|
|
105
|
+
|
|
106
|
+
if column is None:
|
|
107
|
+
return ""
|
|
108
|
+
definition = _column_definition(column)
|
|
109
|
+
# This is the leading SELECT of the all_columns CTE, so it must name both
|
|
110
|
+
# columns — a CTE takes its column names from its first SELECT, and an
|
|
111
|
+
# unnamed literal there is a T-SQL error ("No column name was specified").
|
|
112
|
+
return (
|
|
113
|
+
f" select 0 as column_ordinal, {_sql_literal(definition)} as column_definition\n"
|
|
114
|
+
" union all\n\n"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _render_identity_guard(column, temp_literal: str) -> str:
|
|
119
|
+
"""Refuse an inferred query that already produces the identity column's name.
|
|
120
|
+
|
|
121
|
+
The identity column is Weaver's own; if the query also produces it the create
|
|
122
|
+
would carry two same-named columns. Checked case-insensitively, like Spark.
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
if column is None:
|
|
126
|
+
return ""
|
|
127
|
+
return (
|
|
128
|
+
"if exists (\n"
|
|
129
|
+
" select 1 from tempdb.sys.columns\n"
|
|
130
|
+
f" where [object_id] = object_id({temp_literal})\n"
|
|
131
|
+
f" and lower(name) = lower({_sql_literal(column.name)})\n"
|
|
132
|
+
")\n"
|
|
133
|
+
"begin\n"
|
|
134
|
+
f" throw 51006, {_sql_literal(f'Identity {column.name} collides with a query column')}, 1;\n"
|
|
135
|
+
"end;\n"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# --- declared path ----------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _render_declared_create(document: SesDocument, temp_table: str) -> str:
|
|
143
|
+
target = _quote_multipart(document.qualified)
|
|
144
|
+
temp_literal = _sql_literal(f"tempdb..{temp_table}")
|
|
145
|
+
return render_sql_template(
|
|
146
|
+
"ddl/declared_create_table",
|
|
147
|
+
temp_object_literal=temp_literal,
|
|
148
|
+
metadata_validation_sql=_render_metadata_validation(document, temp_literal),
|
|
149
|
+
declared_columns_cte=_render_declared_columns_cte(document),
|
|
150
|
+
declared_column_definitions=_render_declared_definitions(document),
|
|
151
|
+
target_table=target,
|
|
152
|
+
target_table_literal=_sql_literal(target),
|
|
153
|
+
pk_alter_sql=_render_declared_pk(document, target),
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _render_declared_definitions(document: SesDocument) -> str:
|
|
158
|
+
"""Static column definitions: identity, declared business, then audit columns."""
|
|
159
|
+
|
|
160
|
+
lines = [_column_definition(column) for column in document.effective_schema]
|
|
161
|
+
return _leading_comma_list(lines, first_indent=" ", comma_indent=" ")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _render_declared_columns_cte(document: SesDocument) -> str:
|
|
165
|
+
values = _leading_comma_list(
|
|
166
|
+
[f"({_sql_literal(column.name)})" for column in document.schema],
|
|
167
|
+
first_indent=" ",
|
|
168
|
+
comma_indent=" ",
|
|
169
|
+
)
|
|
170
|
+
return (
|
|
171
|
+
" select column_name\n"
|
|
172
|
+
" from (values\n"
|
|
173
|
+
f"{values}\n"
|
|
174
|
+
" ) as declared(column_name)"
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _render_declared_pk(document: SesDocument, target: str) -> str:
|
|
179
|
+
if not document.primary_key:
|
|
180
|
+
return ""
|
|
181
|
+
columns = ", ".join(_quote_part(name) for name in document.primary_key)
|
|
182
|
+
constraint = _pk_constraint_name(document.qualified)
|
|
183
|
+
return (
|
|
184
|
+
f"\nalter table {target} add constraint {constraint} "
|
|
185
|
+
f"primary key nonclustered ({columns}) not enforced;\n"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --- shared rendering -------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _column_definition(column) -> str:
|
|
193
|
+
"""One column's physical definition, identity included.
|
|
194
|
+
|
|
195
|
+
The identity column is the only one whose type is not simply what it
|
|
196
|
+
declares: the Warehouse generates its values, so the definition carries
|
|
197
|
+
``identity`` and a load never names the column in an insert. Nullability
|
|
198
|
+
still comes from the column, as it does for every other definition here.
|
|
199
|
+
|
|
200
|
+
Bare ``identity``, with no seed and increment: Fabric does not let either be
|
|
201
|
+
chosen and refuses the parenthesised form even where it would spell Fabric's
|
|
202
|
+
own behaviour. So the values a load sees are the engine's to decide, and
|
|
203
|
+
nothing may assume they start at one or rise by one.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
identity = " identity" if column.is_identity else ""
|
|
207
|
+
return (
|
|
208
|
+
f"{_quote_part(column.name)} {column.type}{identity}"
|
|
209
|
+
f"{_nullability(column.not_null)}"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _render_metadata_validation(document: SesDocument, temp_literal: str) -> str:
|
|
214
|
+
references = metadata_column_references(document)
|
|
215
|
+
if not references:
|
|
216
|
+
return ""
|
|
217
|
+
return render_sql_template(
|
|
218
|
+
"ddl/metadata_column_validation",
|
|
219
|
+
temp_object_literal=temp_literal,
|
|
220
|
+
metadata_columns_cte=_render_metadata_columns_cte(references),
|
|
221
|
+
identity_available_sql=_render_identity_available(document.identity),
|
|
222
|
+
).rstrip()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _render_identity_available(identity: str | None) -> str:
|
|
226
|
+
"""Make the identity column an *available* column for the metadata check.
|
|
227
|
+
|
|
228
|
+
The identity is Weaver's own column, not one the query produces, so the
|
|
229
|
+
primary key may name it — but the query-shape temp table does not contain it.
|
|
230
|
+
Union it into the ``described`` set so a primary key on the surrogate resolves
|
|
231
|
+
(mirrors the Python validator's available-set in ``weaver.ses.columns``).
|
|
232
|
+
"""
|
|
233
|
+
|
|
234
|
+
if identity is None:
|
|
235
|
+
return ""
|
|
236
|
+
return f"\n\n union all\n\n select {_sql_literal(identity)} as column_name"
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _render_metadata_columns_cte(references: tuple[tuple[str, str], ...]) -> str:
|
|
240
|
+
lines = []
|
|
241
|
+
for index, (kind, column) in enumerate(references):
|
|
242
|
+
prefix = " select" if index == 0 else " union all\n\n select"
|
|
243
|
+
lines.append(
|
|
244
|
+
f"{prefix}\n"
|
|
245
|
+
f" {_sql_literal(kind)} as metadata_kind\n"
|
|
246
|
+
f" , {_sql_literal(column)} as column_name"
|
|
247
|
+
)
|
|
248
|
+
return "\n".join(lines)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _render_primary_key_cte(primary_key: tuple[str, ...]) -> str:
|
|
252
|
+
if not primary_key:
|
|
253
|
+
return (
|
|
254
|
+
" select\n"
|
|
255
|
+
" convert(int, null) as column_ordinal\n"
|
|
256
|
+
" , convert(nvarchar(128), null) as column_name\n"
|
|
257
|
+
" where 1 = 0"
|
|
258
|
+
)
|
|
259
|
+
values = _leading_comma_list(
|
|
260
|
+
[
|
|
261
|
+
f"({index}, {_sql_literal(name)})"
|
|
262
|
+
for index, name in enumerate(primary_key, start=1)
|
|
263
|
+
],
|
|
264
|
+
first_indent=" ",
|
|
265
|
+
comma_indent=" ",
|
|
266
|
+
)
|
|
267
|
+
return (
|
|
268
|
+
" select column_ordinal, column_name\n"
|
|
269
|
+
" from (values\n"
|
|
270
|
+
f"{values}\n"
|
|
271
|
+
" ) as pk(column_ordinal, column_name)"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _render_name_only_cte(names: tuple[str, ...]) -> str:
|
|
276
|
+
"""A single-column ``(column_name)`` CTE, empty when there are no names."""
|
|
277
|
+
|
|
278
|
+
if not names:
|
|
279
|
+
return (
|
|
280
|
+
" select convert(nvarchar(128), null) as column_name\n"
|
|
281
|
+
" where 1 = 0"
|
|
282
|
+
)
|
|
283
|
+
values = _leading_comma_list(
|
|
284
|
+
[f"({_sql_literal(name)})" for name in names],
|
|
285
|
+
first_indent=" ",
|
|
286
|
+
comma_indent=" ",
|
|
287
|
+
)
|
|
288
|
+
return (
|
|
289
|
+
" select column_name\n"
|
|
290
|
+
" from (values\n"
|
|
291
|
+
f"{values}\n"
|
|
292
|
+
" ) as names(column_name)"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
# --- type mapping (ported) --------------------------------------------------
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _load_type_mapping() -> dict:
|
|
300
|
+
with TYPE_MAPPING_PATH.open("r", encoding="utf-8") as mapping_file:
|
|
301
|
+
loaded = yaml.safe_load(mapping_file) or {}
|
|
302
|
+
if "mappings" not in loaded:
|
|
303
|
+
raise ValueError("warehouse type mapping must define a mappings block")
|
|
304
|
+
return loaded
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _render_type_case(mapping: dict) -> str:
|
|
308
|
+
fallback = mapping.get("fallback_type", "varchar(max)")
|
|
309
|
+
mappings = mapping.get("mappings", {})
|
|
310
|
+
lines = ["case bt.base_type"]
|
|
311
|
+
for source_type in sorted(mappings):
|
|
312
|
+
expression = _render_target_type_expression(source_type, mappings[source_type])
|
|
313
|
+
lines.append(f" when '{source_type.lower()}' then {expression}")
|
|
314
|
+
lines.append(f" else N'{_escape_literal(fallback)}'")
|
|
315
|
+
lines.append(" end")
|
|
316
|
+
return "\n ".join(lines)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _render_target_type_expression(source_type: str, mapping: dict) -> str:
|
|
320
|
+
target = mapping["target"]
|
|
321
|
+
if "precision" in mapping and "scale" in mapping:
|
|
322
|
+
precision = _numeric_part_expression(mapping["precision"], "precision")
|
|
323
|
+
scale = _numeric_part_expression(mapping["scale"], "scale")
|
|
324
|
+
return f"N'{target}(' + {precision} + N',' + {scale} + N')'"
|
|
325
|
+
if "scale" in mapping:
|
|
326
|
+
return f"N'{target}(' + {_scale_expression(mapping['scale'])} + N')'"
|
|
327
|
+
if "length" in mapping:
|
|
328
|
+
return f"N'{target}(' + {_length_expression(source_type, mapping['length'])} + N')'"
|
|
329
|
+
return f"N'{target}'"
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _numeric_part_expression(value, column_name: str) -> str:
|
|
333
|
+
if value == "source":
|
|
334
|
+
default_value = "38" if column_name == "precision" else "0"
|
|
335
|
+
return (
|
|
336
|
+
f"convert(nvarchar(20), "
|
|
337
|
+
f"coalesce(nullif(convert(int, d.{column_name}), 0), {default_value}))"
|
|
338
|
+
)
|
|
339
|
+
return f"N'{value}'"
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _scale_expression(value) -> str:
|
|
343
|
+
if value == "min_source_6":
|
|
344
|
+
return (
|
|
345
|
+
"convert(nvarchar(20), "
|
|
346
|
+
"case "
|
|
347
|
+
"when d.scale is null then 6 "
|
|
348
|
+
"when convert(int, d.scale) > 6 then 6 "
|
|
349
|
+
"when convert(int, d.scale) < 0 then 0 "
|
|
350
|
+
"else convert(int, d.scale) "
|
|
351
|
+
"end)"
|
|
352
|
+
)
|
|
353
|
+
return f"N'{value}'"
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _length_expression(source_type: str, value) -> str:
|
|
357
|
+
if value == "max":
|
|
358
|
+
return "N'max'"
|
|
359
|
+
if value == "source":
|
|
360
|
+
divisor = "2" if source_type.lower() in {"nchar", "nvarchar"} else "1"
|
|
361
|
+
source_length = f"convert(int, d.max_length) / {divisor}"
|
|
362
|
+
return (
|
|
363
|
+
"case "
|
|
364
|
+
"when d.max_length = -1 then N'max' "
|
|
365
|
+
"when d.max_length is null or d.max_length = 0 then N'1' "
|
|
366
|
+
f"else convert(nvarchar(20), case when {source_length} < 1 then 1 else {source_length} end) "
|
|
367
|
+
"end"
|
|
368
|
+
)
|
|
369
|
+
return f"N'{value}'"
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# --- identifiers and literals -----------------------------------------------
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def _normalise_view_body(body: str) -> str:
|
|
376
|
+
text = body.strip()
|
|
377
|
+
if text.endswith(";"):
|
|
378
|
+
text = text[:-1].rstrip()
|
|
379
|
+
if text[:1] == ";" and text[1:].lstrip().upper().startswith("WITH"):
|
|
380
|
+
return text[1:].lstrip()
|
|
381
|
+
return text
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _ensure_terminated(sql_text: str) -> str:
|
|
385
|
+
stripped = sql_text.rstrip()
|
|
386
|
+
return stripped if stripped.endswith(";") else f"{stripped};"
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _nullability(not_null: bool) -> str:
|
|
390
|
+
return " not null" if not_null else " null"
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _pk_constraint_name(qualified: str) -> str:
|
|
394
|
+
object_name = _unquote_part(_split_identifier(qualified)[-1])
|
|
395
|
+
return _quote_part(f"PK_{object_name}")
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _weaver_temp_table_name(prefix: str, qualified: str) -> str:
|
|
399
|
+
normalised_prefix = prefix if prefix.startswith("#") else f"#{prefix}"
|
|
400
|
+
safe = re.sub(r"[^A-Za-z0-9_]", "_", qualified)
|
|
401
|
+
candidate = f"{normalised_prefix}_{safe}"
|
|
402
|
+
if len(candidate) > 111:
|
|
403
|
+
digest = hashlib.sha1(qualified.encode("utf-8")).hexdigest()[:12]
|
|
404
|
+
candidate = f"{candidate[:98]}_{digest}"
|
|
405
|
+
return candidate
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _leading_comma_list(
|
|
409
|
+
items: list[str], *, first_indent: str = " ", comma_indent: str = " "
|
|
410
|
+
) -> str:
|
|
411
|
+
if not items:
|
|
412
|
+
return ""
|
|
413
|
+
lines = [f"{first_indent}{items[0]}"]
|
|
414
|
+
lines.extend(f"{comma_indent}, {item}" for item in items[1:])
|
|
415
|
+
return "\n".join(lines)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _quote_multipart(identifier: str) -> str:
|
|
419
|
+
parts = _split_identifier(identifier)
|
|
420
|
+
if not parts:
|
|
421
|
+
raise ValueError("identifier must not be empty")
|
|
422
|
+
return ".".join(_quote_part(part) for part in parts)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _split_identifier(identifier: str) -> list[str]:
|
|
426
|
+
parts: list[str] = []
|
|
427
|
+
current: list[str] = []
|
|
428
|
+
in_brackets = False
|
|
429
|
+
for character in identifier.strip():
|
|
430
|
+
if character == "[" and not in_brackets:
|
|
431
|
+
in_brackets = True
|
|
432
|
+
current.append(character)
|
|
433
|
+
continue
|
|
434
|
+
if character == "]" and in_brackets:
|
|
435
|
+
in_brackets = False
|
|
436
|
+
current.append(character)
|
|
437
|
+
continue
|
|
438
|
+
if character == "." and not in_brackets:
|
|
439
|
+
part = "".join(current).strip()
|
|
440
|
+
if part:
|
|
441
|
+
parts.append(part)
|
|
442
|
+
current = []
|
|
443
|
+
continue
|
|
444
|
+
current.append(character)
|
|
445
|
+
part = "".join(current).strip()
|
|
446
|
+
if part:
|
|
447
|
+
parts.append(part)
|
|
448
|
+
return parts
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _quote_part(part: str) -> str:
|
|
452
|
+
stripped = _unquote_part(part)
|
|
453
|
+
return f"[{stripped.replace(']', ']]')}]"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def _unquote_part(part: str) -> str:
|
|
457
|
+
stripped = part.strip()
|
|
458
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
459
|
+
return stripped[1:-1].replace("]]", "]")
|
|
460
|
+
return stripped
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _sql_literal(value: str) -> str:
|
|
464
|
+
return f"N'{_escape_literal(value)}'"
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _escape_literal(value: str) -> str:
|
|
468
|
+
return value.replace("'", "''")
|