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.
Files changed (127) hide show
  1. weaver/__init__.py +59 -0
  2. weaver/build_bundle/__init__.py +109 -0
  3. weaver/build_bundle/aliases.py +325 -0
  4. weaver/build_bundle/bundle.py +359 -0
  5. weaver/build_bundle/catalogue_actions.py +275 -0
  6. weaver/build_bundle/changes.py +186 -0
  7. weaver/build_bundle/endpoints.py +83 -0
  8. weaver/build_bundle/executors/__init__.py +69 -0
  9. weaver/build_bundle/executors/alias.py +202 -0
  10. weaver/build_bundle/executors/base.py +132 -0
  11. weaver/build_bundle/executors/folder.py +71 -0
  12. weaver/build_bundle/executors/load_file.py +205 -0
  13. weaver/build_bundle/executors/spark_case.py +26 -0
  14. weaver/build_bundle/executors/spark_schema.py +60 -0
  15. weaver/build_bundle/executors/spark_sql.py +59 -0
  16. weaver/build_bundle/executors/spark_sql_batch.py +57 -0
  17. weaver/build_bundle/executors/spark_table.py +213 -0
  18. weaver/build_bundle/executors/sql_endpoint_refresh.py +34 -0
  19. weaver/build_bundle/executors/tsql.py +81 -0
  20. weaver/build_bundle/incremental.py +288 -0
  21. weaver/build_bundle/installer.py +384 -0
  22. weaver/build_bundle/models.py +288 -0
  23. weaver/build_bundle/payloads.py +34 -0
  24. weaver/build_bundle/physical.py +625 -0
  25. weaver/build_bundle/planner.py +389 -0
  26. weaver/build_bundle/prune.py +620 -0
  27. weaver/build_bundle/report.py +108 -0
  28. weaver/build_bundle/stages.py +196 -0
  29. weaver/build_bundle/targets.py +272 -0
  30. weaver/build_bundle/workflow.py +585 -0
  31. weaver/catalogue/__init__.py +73 -0
  32. weaver/catalogue/builtin.py +238 -0
  33. weaver/catalogue/claims.py +121 -0
  34. weaver/catalogue/projection.py +437 -0
  35. weaver/catalogue/reader.py +152 -0
  36. weaver/catalogue/reconcile.py +231 -0
  37. weaver/catalogue/render.py +410 -0
  38. weaver/catalogue/state.py +660 -0
  39. weaver/catalogue/tables.py +648 -0
  40. weaver/config.py +178 -0
  41. weaver/declaration/__init__.py +171 -0
  42. weaver/declaration/columns.py +223 -0
  43. weaver/declaration/ddl.py +266 -0
  44. weaver/declaration/dependencies.py +544 -0
  45. weaver/declaration/graph.py +240 -0
  46. weaver/declaration/item_dependencies.py +292 -0
  47. weaver/declaration/load.py +191 -0
  48. weaver/declaration/metadata.py +1405 -0
  49. weaver/declaration/model.py +448 -0
  50. weaver/declaration/references.py +294 -0
  51. weaver/declaration/repository.py +959 -0
  52. weaver/declaration/schemas.py +135 -0
  53. weaver/declaration/source.py +674 -0
  54. weaver/declaration/spark_load.py +759 -0
  55. weaver/declaration/sql_shaping.py +591 -0
  56. weaver/declaration/templates/ddl/declared_create_table.sql +64 -0
  57. weaver/declaration/templates/ddl/infer_create_table.sql +97 -0
  58. weaver/declaration/templates/ddl/metadata_column_validation.sql +30 -0
  59. weaver/declaration/templates/load/column_metadata.sql +40 -0
  60. weaver/declaration/templates/load/full_replace_body.sql +21 -0
  61. weaver/declaration/templates/load/install_load_procedure.sql +27 -0
  62. weaver/declaration/templates/load/load_procedure.sql +48 -0
  63. weaver/declaration/templates/load/primary_key_body.sql +113 -0
  64. weaver/declaration/tsql_ddl.py +468 -0
  65. weaver/declaration/tsql_load.py +417 -0
  66. weaver/declaration/warehouse_type_mapping.yml +93 -0
  67. weaver/diagnostics.py +247 -0
  68. weaver/errors.py +61 -0
  69. weaver/etl.py +469 -0
  70. weaver/fabric/__init__.py +107 -0
  71. weaver/fabric/auth.py +137 -0
  72. weaver/fabric/capacity.py +143 -0
  73. weaver/fabric/client.py +147 -0
  74. weaver/fabric/environment.py +460 -0
  75. weaver/fabric/livy.py +478 -0
  76. weaver/fabric/notebooks.py +201 -0
  77. weaver/fabric/onelake.py +263 -0
  78. weaver/fabric/resolution.py +344 -0
  79. weaver/fabric/resources.py +245 -0
  80. weaver/fabric/session.py +148 -0
  81. weaver/fabric/shortcuts.py +120 -0
  82. weaver/fabric/sql.py +118 -0
  83. weaver/fabric/store.py +198 -0
  84. weaver/initialise.py +209 -0
  85. weaver/lakehouse.py +386 -0
  86. weaver/load.py +474 -0
  87. weaver/load_execution.py +483 -0
  88. weaver/load_plan.py +912 -0
  89. weaver/load_report.py +330 -0
  90. weaver/load_resolution.py +386 -0
  91. weaver/locations.py +164 -0
  92. weaver/objects.py +392 -0
  93. weaver/operations.py +757 -0
  94. weaver/physical_wipe.py +369 -0
  95. weaver/push.py +76 -0
  96. weaver/resolution.py +292 -0
  97. weaver/runtime/__init__.py +30 -0
  98. weaver/runtime/folder_load.py +402 -0
  99. weaver/runtime/load_contract.py +245 -0
  100. weaver/runtime/load_result.py +104 -0
  101. weaver/runtime/spark_load.py +152 -0
  102. weaver/runtime/table_load.py +497 -0
  103. weaver/spark/__init__.py +49 -0
  104. weaver/spark/catalogue.py +245 -0
  105. weaver/spark/destination.py +195 -0
  106. weaver/spark/session.py +84 -0
  107. weaver/spark/tokens.py +138 -0
  108. weaver/sql/__init__.py +40 -0
  109. weaver/sql/authentication.py +38 -0
  110. weaver/sql/connection.py +90 -0
  111. weaver/sql/errors.py +25 -0
  112. weaver/sql/execution.py +123 -0
  113. weaver/sql/pool.py +174 -0
  114. weaver/sql/wipe.py +156 -0
  115. weaver/store.py +209 -0
  116. weaver/targets.py +257 -0
  117. weaver/task_logging.py +215 -0
  118. weaver/unbind.py +74 -0
  119. weaver/workspaces.py +175 -0
  120. weaver_cli/__init__.py +12 -0
  121. weaver_cli/__main__.py +7 -0
  122. weaver_cli/main.py +626 -0
  123. weaverstack-0.1.1.dist-info/METADATA +113 -0
  124. weaverstack-0.1.1.dist-info/RECORD +127 -0
  125. weaverstack-0.1.1.dist-info/WHEEL +4 -0
  126. weaverstack-0.1.1.dist-info/entry_points.txt +2 -0
  127. 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("'", "''")