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,417 @@
1
+ """T-SQL load generation — one independently runnable procedure per table.
2
+
3
+ What this produces is not the procedure. It is a script that *installs* the
4
+ procedure, and the difference is the whole design.
5
+
6
+ .. code-block:: text
7
+
8
+ payload an installer script, frozen at build
9
+ reads sys.columns of the target table
10
+ assembles the procedure text
11
+ exec sp_executesql
12
+
13
+ The column lists cannot be written at generation time. A Warehouse table may
14
+ infer its shape from its query, so the generator does not always know its
15
+ columns — and even when it does, the *physical* table is what the procedure must
16
+ name. Reading ``sys.columns`` at install settles both, and it excludes the
17
+ identity column for free: the engine generates that column, so ``is_identity =
18
+ 0`` keeps it out of every insert list without this module having to know which
19
+ column it was. It is the same two-phase shape
20
+ :mod:`weaver.declaration.tsql_ddl` uses to build the table, for the same reason.
21
+
22
+ The installed procedure is independently runnable, which is the point of a
23
+ primitive::
24
+
25
+ exec [_].[Load Sales.Customer] @fault_tolerant = 1;
26
+
27
+ It reads no repository, consults no catalogue and calls nothing else Weaver
28
+ owns. It returns one row of :data:`weaver.runtime.load_result.RESULT_COLUMNS`.
29
+
30
+ **The intermediate tables are real.** ``Sales.Customer_Staging``,
31
+ ``_Upsert`` and ``_Reject`` are ordinary tables in the object's own schema, as
32
+ they were in the reference implementation. Real tables are what make a failed
33
+ load inspectable: when rows are rejected the evidence is still there afterwards,
34
+ addressable by anyone with a query tool. They are dropped at the start of every
35
+ run and again at the end, but only when the run was clean — the whole point of
36
+ keeping a reject table is that a run which rejected rows leaves it behind.
37
+
38
+ **No history.** The reference carried a ``_Current``/``_History`` pair behind a
39
+ view. Weaver builds only the authored table, so a load updates it directly and a
40
+ row absent from a non-incremental source is deleted outright. The audit columns
41
+ still record insert and update times; the delete sentinel stays live because
42
+ there is nowhere for a deleted row to go.
43
+
44
+ Ported from ``weaver_runtime.dbrep.sql.etl``, with the history layout removed and
45
+ the ``fault_tolerant`` contract and structured result added.
46
+ """
47
+
48
+ from __future__ import annotations
49
+
50
+ from .metadata import (
51
+ AUDIT_COLUMNS,
52
+ AUDIT_LIVE_DELETE_DATETIME,
53
+ SesDocument,
54
+ )
55
+ from .sql_shaping import render_sql_template, split_trailing_query
56
+ from ..runtime.load_contract import (
57
+ REASON_BLANK_PK,
58
+ REASON_DUPLICATE_PK,
59
+ REJECTION_REASON,
60
+ LoadContract,
61
+ )
62
+
63
+ #: The column a staged row's duplicate rank lands in. Named to be unmistakably
64
+ #: Weaver's: it sits beside the author's own columns in a real table, so a name
65
+ #: an author might have chosen would be a collision waiting to happen.
66
+ RANK_COLUMN = "__weaver_pk_row_number"
67
+
68
+ #: The suffixes of the three intermediate tables, in the object's own schema.
69
+ STAGING_SUFFIX = "_Staging"
70
+ UPSERT_SUFFIX = "_Upsert"
71
+ REJECT_SUFFIX = "_Reject"
72
+
73
+ #: What a run reports when it refused to start, and when it went ahead anyway.
74
+ #: Both say rejects occurred; they differ in what happened to the target, which
75
+ #: is the only thing ``fault_tolerant`` changes.
76
+ INTOLERANT_MESSAGE = (
77
+ "rows were rejected and fault_tolerant = 0, so the target was not modified"
78
+ )
79
+ TOLERATED_MESSAGE = "rows were rejected and excluded from the load"
80
+
81
+ #: Banners marking where the author's own code sits in the generated procedure.
82
+ #: A generated artefact is read by people — usually when something has gone
83
+ #: wrong — and the first question is always "which of this did I write?".
84
+ PREPROCESSING_BANNER = "/*-- Pre-processing --*/"
85
+ TRANSFORMATION_BANNER = "/*---- Data transformation ----*/"
86
+ END_TRANSFORMATION_BANNER = "/*---- End data transformation ----*/"
87
+ POSTPROCESSING_BANNER = "/*-- Post-processing --*/"
88
+
89
+
90
+ def generate_tsql_load_script(
91
+ document: SesDocument, body: str, *, procedure_name: str
92
+ ) -> str:
93
+ """The installer script for one Warehouse table's load procedure.
94
+
95
+ ``body`` is the table's own query — the same text its build materialises to
96
+ settle its shape. A load runs it for real.
97
+ """
98
+
99
+ contract = LoadContract.from_document(document)
100
+ names = _table_names(document, procedure_name)
101
+ staging_sql = _staging_sql(names, body, contract)
102
+
103
+ if contract.primary_key:
104
+ load_body = _primary_key_body(names, contract)
105
+ else:
106
+ load_body = _full_replace_body(names)
107
+
108
+ procedure = render_sql_template(
109
+ "load/load_procedure",
110
+ load_procedure=names["procedure"],
111
+ live_delete_datetime=AUDIT_LIVE_DELETE_DATETIME,
112
+ preprocessing_banner=_indent(PREPROCESSING_BANNER, 4),
113
+ postprocessing_banner=_indent(POSTPROCESSING_BANNER, 4),
114
+ start_artifact_cleanup=_indent(_cleanup(names, contract), 4),
115
+ staging_sql=_indent(staging_sql, 4),
116
+ staging_table=names["staging"],
117
+ target_table=names["target"],
118
+ load_body=_indent(load_body, 4),
119
+ end_artifact_cleanup=_indent(_end_cleanup(names, contract), 4),
120
+ ).rstrip()
121
+
122
+ return render_sql_template(
123
+ "load/install_load_procedure",
124
+ column_metadata_sql=_column_metadata_sql(names, contract),
125
+ procedure_template_sql_literal=_sql_literal(procedure),
126
+ )
127
+
128
+
129
+ # --- the pieces of the procedure ---------------------------------------------
130
+
131
+
132
+ def _staging_sql(names: dict, body: str, contract: LoadContract) -> str:
133
+ """Materialise the object's query, ranking duplicate keys as it goes.
134
+
135
+ One pass, because the rank is what identifies a duplicate and computing it
136
+ later would mean reading the staged rows twice. With no key there is nothing
137
+ to rank and the query is staged as it stands.
138
+
139
+ Only the *last standalone query* is staged. A body may set a temporary view
140
+ up first and select from it, and wrapping the whole body in a subquery would
141
+ put a ``create`` inside a ``from`` — so the preamble runs as it was written,
142
+ and the query it leads to is what fills staging.
143
+ """
144
+
145
+ preamble, query = split_trailing_query(body)
146
+ lead = f"{preamble};\n\n" if preamble else ""
147
+ if not contract.primary_key:
148
+ staged = f"create table {names['staging']} as\n{query};"
149
+ else:
150
+ partition = ", ".join(f"s.{_quote(column)}" for column in contract.primary_key)
151
+ staged = (
152
+ f"create table {names['staging']} as\n"
153
+ f"select\n"
154
+ f" s.*\n"
155
+ f" , row_number() over (partition by {partition} order by (select null)) "
156
+ f"as {_quote(RANK_COLUMN)}\n"
157
+ f"from (\n{_indent(query, 4)}\n) as s;"
158
+ )
159
+ return f"{TRANSFORMATION_BANNER}\n{lead}{staged}\n{END_TRANSFORMATION_BANNER}"
160
+
161
+
162
+ def _primary_key_body(names: dict, contract: LoadContract) -> str:
163
+ blank = _blank_key_predicate(contract.primary_key)
164
+ return render_sql_template(
165
+ "load/primary_key_body",
166
+ reject_table=names["reject"],
167
+ upsert_table=names["upsert"],
168
+ staging_table=names["staging"],
169
+ target_table=names["target"],
170
+ rank_column=RANK_COLUMN,
171
+ staging_blank_predicate=blank,
172
+ staging_target_join=_join("s", "t", contract.primary_key),
173
+ target_upsert_join=_join("c", "u", contract.primary_key),
174
+ target_missing_predicate=f"t.{_quote(contract.primary_key[0])} is null",
175
+ missing_reconciliation=_missing_reconciliation(names, contract),
176
+ prospective_deletes=_prospective_deletes(names, contract),
177
+ delete_threshold=contract.delete_threshold,
178
+ update_threshold=contract.update_threshold,
179
+ stability_rows=contract.stability_rows,
180
+ rejection_reason=REJECTION_REASON,
181
+ blank_reason=REASON_BLANK_PK,
182
+ duplicate_reason=REASON_DUPLICATE_PK,
183
+ intolerant_message=_escape_literal(INTOLERANT_MESSAGE),
184
+ tolerated_message=_escape_literal(TOLERATED_MESSAGE),
185
+ ).rstrip()
186
+
187
+
188
+ def _full_replace_body(names: dict) -> str:
189
+ return render_sql_template(
190
+ "load/full_replace_body",
191
+ target_table=names["target"],
192
+ staging_table=names["staging"],
193
+ ).rstrip()
194
+
195
+
196
+ def _prospective_deletes(names: dict, contract: LoadContract) -> str:
197
+ """Count what the load is about to delete, before it deletes any of it.
198
+
199
+ A number obtained by deleting would be a report rather than a check, and the
200
+ whole point of the guard is to decide *not* to.
201
+ """
202
+
203
+ if not contract.deletes_absent_rows:
204
+ return (
205
+ "-- Incremental: nothing is deleted, so there is nothing to count."
206
+ )
207
+ join = _join("s", "c", contract.primary_key)
208
+ return (
209
+ f"select @weaver_prospective_deletes = count(*)\n"
210
+ f"from {names['target']} as c\n"
211
+ f"where not exists (\n"
212
+ f" select 1 from {names['staging']} as s where {join}\n"
213
+ f");"
214
+ )
215
+
216
+
217
+ def _missing_reconciliation(names: dict, contract: LoadContract) -> str:
218
+ """Delete target rows the source stopped producing — a physical delete.
219
+
220
+ Only for a keyed, non-incremental load. An incremental source shows a window
221
+ rather than the whole truth, so absence from it is not a retirement and
222
+ deleting on it would destroy rows the source never claimed to describe.
223
+ """
224
+
225
+ if not contract.deletes_absent_rows:
226
+ return (
227
+ "-- Incremental: absence from the source is not a retirement, so\n"
228
+ "-- nothing is deleted."
229
+ )
230
+ join = _join("s", "c", contract.primary_key)
231
+ return (
232
+ f"delete c\n"
233
+ f"from {names['target']} as c\n"
234
+ f"where not exists (\n"
235
+ f" select 1 from {names['staging']} as s where {join}\n"
236
+ f");"
237
+ )
238
+
239
+
240
+ def _cleanup(names: dict, contract: LoadContract) -> str:
241
+ """Drop whatever a previous run left behind, newest dependency first.
242
+
243
+ Only the tables this procedure actually makes. An unkeyed load has no reject
244
+ or upsert table — with no key nothing can be matched, so there is nothing to
245
+ reject and no upsert set — and dropping them anyway would leave a reader
246
+ hunting for the statement that creates them.
247
+ """
248
+
249
+ keys = ("reject", "upsert", "staging") if contract.primary_key else ("staging",)
250
+ return "\n".join(
251
+ f"if object_id({_sql_literal(names[key])}, N'U') is not null "
252
+ f"drop table {names[key]};"
253
+ for key in keys
254
+ )
255
+
256
+
257
+ def _end_cleanup(names: dict, contract: LoadContract) -> str:
258
+ """Clear the intermediate tables, unless they are the evidence of a problem.
259
+
260
+ A run that rejected nothing leaves nothing to look at, so its artefacts go.
261
+ A run that rejected rows keeps all three: the reject table names what was
262
+ refused, and staging and upsert are what make the rejection explicable.
263
+ """
264
+
265
+ cleanup = _cleanup(names, contract)
266
+ if not contract.primary_key:
267
+ return cleanup
268
+ return (
269
+ "if @weaver_rows_rejected = 0\n"
270
+ "begin\n"
271
+ f"{_indent(cleanup, 4)}\n"
272
+ "end;"
273
+ )
274
+
275
+
276
+ # --- the installer's column metadata -----------------------------------------
277
+
278
+
279
+ def _column_metadata_sql(names: dict, contract: LoadContract) -> str:
280
+ """Read the target's loadable columns, and what an update sets.
281
+
282
+ The audit columns are excluded because the load supplies them itself, and
283
+ identity is excluded because the engine does.
284
+ """
285
+
286
+ audit = ", ".join(_sql_literal(name) for name in AUDIT_COLUMNS)
287
+ source_column_filter = (
288
+ f"c.name not in ({audit})\n and c.is_identity = 0"
289
+ )
290
+ return render_sql_template(
291
+ "load/column_metadata",
292
+ target_table_literal=_sql_literal(names["target"]),
293
+ source_column_filter=source_column_filter,
294
+ update_select=_update_select(names, contract, source_column_filter),
295
+ )
296
+
297
+
298
+ def _update_select(names: dict, contract: LoadContract, source_column_filter: str) -> str:
299
+ """Build the UPDATE SET list: every loadable column except the key.
300
+
301
+ The key is excluded because it is what matched the rows — setting a column
302
+ to the value it was joined on is work that cannot change anything. The audit
303
+ columns are appended unconditionally, since an updated row's update time and
304
+ live sentinel are Weaver's to state.
305
+ """
306
+
307
+ if not contract.primary_key:
308
+ return "set @weaver_update_set_columns = N'';"
309
+ key_values = ", ".join(
310
+ _sql_literal(column.lower()) for column in contract.primary_key
311
+ )
312
+ return (
313
+ ";with update_columns as (\n"
314
+ " select\n"
315
+ " c.name\n"
316
+ " , c.column_id\n"
317
+ " , row_number() over (order by c.column_id) as row_ordinal\n"
318
+ " from sys.columns as c\n"
319
+ f" where c.[object_id] = object_id({_sql_literal(names['target'])})\n"
320
+ f" and {source_column_filter}\n"
321
+ f" and lower(c.name) not in ({key_values})\n"
322
+ ")\n"
323
+ "select\n"
324
+ " @weaver_update_set_columns =\n"
325
+ " coalesce(\n"
326
+ " string_agg(\n"
327
+ " case\n"
328
+ " when row_ordinal = 1 then N'c.' + quotename(name) + N' = u.' + quotename(name)\n"
329
+ " else char(10) + N' , c.' + quotename(name) + N' = u.' + quotename(name)\n"
330
+ " end,\n"
331
+ " N''\n"
332
+ " ) within group (order by column_id)\n"
333
+ " + char(10) + N' , ',\n"
334
+ " N''\n"
335
+ " )\n"
336
+ " + N'c.[Row update datetime] = @weaver_load_datetime'\n"
337
+ " + char(10) + N' , c.[Row delete datetime] = @weaver_live_datetime'\n"
338
+ "from update_columns;"
339
+ )
340
+
341
+
342
+ # --- names and text ----------------------------------------------------------
343
+
344
+
345
+ def _table_names(document: SesDocument, procedure_name: str) -> dict:
346
+ """The five names one load deals in, quoted once here and reused.
347
+
348
+ The intermediate tables sit in the object's own schema beside it, which is
349
+ the reference's arrangement: they belong to the object being loaded, not to
350
+ the generated ``_`` schema, which holds procedures.
351
+ """
352
+
353
+ schema = document.object_id.schema
354
+ obj = document.object_id.object
355
+ qualified = f"{_quote(schema)}."
356
+ return {
357
+ "target": f"{qualified}{_quote(obj)}",
358
+ "staging": f"{qualified}{_quote(obj + STAGING_SUFFIX)}",
359
+ "upsert": f"{qualified}{_quote(obj + UPSERT_SUFFIX)}",
360
+ "reject": f"{qualified}{_quote(obj + REJECT_SUFFIX)}",
361
+ "procedure": procedure_name,
362
+ }
363
+
364
+
365
+ def _join(left: str, right: str, columns: tuple[str, ...]) -> str:
366
+ return "\n and ".join(
367
+ f"{left}.{_quote(column)} = {right}.{_quote(column)}" for column in columns
368
+ )
369
+
370
+
371
+ def _blank_key_predicate(columns: tuple[str, ...]) -> str:
372
+ """A key column that is null, empty or only spaces is not a key.
373
+
374
+ Blank is rejected alongside null deliberately: a key that is whitespace
375
+ matches nothing a human would call a match, and letting it through would
376
+ create a row nobody can find again.
377
+ """
378
+
379
+ predicates = [
380
+ f"nullif(trim(cast(s.{_quote(column)} as varchar(max))), '') is null"
381
+ for column in columns
382
+ ]
383
+ if len(predicates) == 1:
384
+ return predicates[0]
385
+ return "(" + "\n or ".join(predicates) + ")"
386
+
387
+
388
+ def _quote(name: str) -> str:
389
+ return "[" + name.replace("]", "]]") + "]"
390
+
391
+
392
+ def _sql_literal(text: str) -> str:
393
+ return "N'" + text.replace("'", "''") + "'"
394
+
395
+
396
+ def _escape_literal(text: str) -> str:
397
+ """Text going *inside* an already-quoted literal in the procedure template.
398
+
399
+ The procedure is itself embedded in a string literal by the installer, so a
400
+ quote here is doubled twice over. Escaping once at each layer is what keeps
401
+ the two levels straight.
402
+ """
403
+
404
+ return text.replace("'", "''")
405
+
406
+
407
+ def _indent(text: str, spaces: int) -> str:
408
+ pad = " " * spaces
409
+ return "\n".join(pad + line if line.strip() else line for line in text.splitlines())
410
+
411
+
412
+ __all__ = [
413
+ "INTOLERANT_MESSAGE",
414
+ "RANK_COLUMN",
415
+ "TOLERATED_MESSAGE",
416
+ "generate_tsql_load_script",
417
+ ]
@@ -0,0 +1,93 @@
1
+ fallback_type: varchar(max)
2
+ preserve_nullability: true
3
+ default_varchar_length: 8000
4
+ default_varbinary_length: 8000
5
+ max_length_token: max
6
+ identity_type: bigint identity not null
7
+
8
+ mappings:
9
+ bigint:
10
+ target: bigint
11
+ binary:
12
+ target: varbinary
13
+ length: source
14
+ bit:
15
+ target: bit
16
+ char:
17
+ target: varchar
18
+ length: source
19
+ date:
20
+ target: date
21
+ datetime:
22
+ target: datetime2
23
+ scale: 6
24
+ datetime2:
25
+ target: datetime2
26
+ scale: min_source_6
27
+ datetimeoffset:
28
+ target: datetime2
29
+ scale: min_source_6
30
+ decimal:
31
+ target: decimal
32
+ precision: source
33
+ scale: source
34
+ float:
35
+ target: float
36
+ image:
37
+ target: varbinary
38
+ length: max
39
+ int:
40
+ target: int
41
+ money:
42
+ target: decimal
43
+ precision: 19
44
+ scale: 4
45
+ nchar:
46
+ target: varchar
47
+ length: source
48
+ ntext:
49
+ target: varchar
50
+ length: max
51
+ numeric:
52
+ target: decimal
53
+ precision: source
54
+ scale: source
55
+ nvarchar:
56
+ target: varchar
57
+ length: source
58
+ real:
59
+ target: real
60
+ rowversion:
61
+ target: varbinary
62
+ length: 8
63
+ smalldatetime:
64
+ target: datetime2
65
+ scale: 0
66
+ smallint:
67
+ target: smallint
68
+ smallmoney:
69
+ target: decimal
70
+ precision: 10
71
+ scale: 4
72
+ text:
73
+ target: varchar
74
+ length: max
75
+ time:
76
+ target: time
77
+ scale: min_source_6
78
+ timestamp:
79
+ target: varbinary
80
+ length: 8
81
+ tinyint:
82
+ target: smallint
83
+ uniqueidentifier:
84
+ target: uniqueidentifier
85
+ varbinary:
86
+ target: varbinary
87
+ length: source
88
+ varchar:
89
+ target: varchar
90
+ length: source
91
+ xml:
92
+ target: varchar
93
+ length: max