sqllocks-shape-fabric 0.9.1__tar.gz
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.
- sqllocks_shape_fabric-0.9.1/LICENSE +21 -0
- sqllocks_shape_fabric-0.9.1/PKG-INFO +244 -0
- sqllocks_shape_fabric-0.9.1/README.md +211 -0
- sqllocks_shape_fabric-0.9.1/pyproject.toml +73 -0
- sqllocks_shape_fabric-0.9.1/setup.cfg +4 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/__init__.py +84 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/_auth.py +103 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/_storage.py +124 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/_tsql.py +544 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/answer_check.py +331 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/auth.py +381 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/commands.py +1097 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/drift_report.py +644 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/errors.py +110 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/eventhouse.py +286 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/eventhouse_writer.py +177 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/eventstream.py +98 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/eventstream_writer.py +89 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/fabric_api.py +279 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/kerberos.py +184 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/keyvault.py +113 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/known_answer.py +1082 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/kusto.py +439 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/lakehouse.py +179 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/livecheck.py +131 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/notebook.py +211 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/onelake.py +233 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/recording.py +487 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/scenarios.py +662 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/semantic_model.py +319 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/semantic_profile.py +221 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/semantic_source.py +410 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/setup_env.py +52 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/sinks.py +595 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/source.py +67 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/sqldb.py +653 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/synapse.py +262 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/targets.py +120 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/testing.py +1131 -0
- sqllocks_shape_fabric-0.9.1/src/shape_fabric/warehouse.py +271 -0
- sqllocks_shape_fabric-0.9.1/src/sqllocks_shape_fabric.egg-info/PKG-INFO +244 -0
- sqllocks_shape_fabric-0.9.1/src/sqllocks_shape_fabric.egg-info/SOURCES.txt +108 -0
- sqllocks_shape_fabric-0.9.1/src/sqllocks_shape_fabric.egg-info/dependency_links.txt +1 -0
- sqllocks_shape_fabric-0.9.1/src/sqllocks_shape_fabric.egg-info/entry_points.txt +24 -0
- sqllocks_shape_fabric-0.9.1/src/sqllocks_shape_fabric.egg-info/requires.txt +16 -0
- sqllocks_shape_fabric-0.9.1/src/sqllocks_shape_fabric.egg-info/top_level.txt +1 -0
- sqllocks_shape_fabric-0.9.1/tests/test_auth.py +570 -0
- sqllocks_shape_fabric-0.9.1/tests/test_auth_cli.py +446 -0
- sqllocks_shape_fabric-0.9.1/tests/test_auth_tokens.py +49 -0
- sqllocks_shape_fabric-0.9.1/tests/test_check_answers.py +491 -0
- sqllocks_shape_fabric-0.9.1/tests/test_command_conformance.py +83 -0
- sqllocks_shape_fabric-0.9.1/tests/test_dax_formats_compat.py +157 -0
- sqllocks_shape_fabric-0.9.1/tests/test_demo_targets.py +291 -0
- sqllocks_shape_fabric-0.9.1/tests/test_emulator.py +152 -0
- sqllocks_shape_fabric-0.9.1/tests/test_eventhouse.py +308 -0
- sqllocks_shape_fabric-0.9.1/tests/test_eventhouse_mapping.py +123 -0
- sqllocks_shape_fabric-0.9.1/tests/test_eventhouse_schema_cache.py +89 -0
- sqllocks_shape_fabric-0.9.1/tests/test_eventhouse_writer.py +303 -0
- sqllocks_shape_fabric-0.9.1/tests/test_eventstream.py +51 -0
- sqllocks_shape_fabric-0.9.1/tests/test_eventstream_writer.py +66 -0
- sqllocks_shape_fabric-0.9.1/tests/test_export_model.py +309 -0
- sqllocks_shape_fabric-0.9.1/tests/test_fabric_api_edges.py +166 -0
- sqllocks_shape_fabric-0.9.1/tests/test_fabric_commands.py +328 -0
- sqllocks_shape_fabric-0.9.1/tests/test_kerberos.py +518 -0
- sqllocks_shape_fabric-0.9.1/tests/test_keyvault_names.py +20 -0
- sqllocks_shape_fabric-0.9.1/tests/test_known_answer.py +720 -0
- sqllocks_shape_fabric-0.9.1/tests/test_kusto_emulator.py +109 -0
- sqllocks_shape_fabric-0.9.1/tests/test_kusto_types.py +26 -0
- sqllocks_shape_fabric-0.9.1/tests/test_lakehouse.py +145 -0
- sqllocks_shape_fabric-0.9.1/tests/test_landing_zone_inputs.py +28 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live.py +75 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live_commands.py +170 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live_git_sync.py +378 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live_scale_sinks.py +213 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live_semantic_model.py +43 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live_synapse.py +76 -0
- sqllocks_shape_fabric-0.9.1/tests/test_live_writers.py +187 -0
- sqllocks_shape_fabric-0.9.1/tests/test_livecheck.py +167 -0
- sqllocks_shape_fabric-0.9.1/tests/test_notebook_inputs.py +45 -0
- sqllocks_shape_fabric-0.9.1/tests/test_onelake.py +101 -0
- sqllocks_shape_fabric-0.9.1/tests/test_onelake_segments.py +88 -0
- sqllocks_shape_fabric-0.9.1/tests/test_optional_extras.py +112 -0
- sqllocks_shape_fabric-0.9.1/tests/test_portability.py +38 -0
- sqllocks_shape_fabric-0.9.1/tests/test_profile_model.py +390 -0
- sqllocks_shape_fabric-0.9.1/tests/test_publish.py +545 -0
- sqllocks_shape_fabric-0.9.1/tests/test_publish_report.py +756 -0
- sqllocks_shape_fabric-0.9.1/tests/test_recorded.py +83 -0
- sqllocks_shape_fabric-0.9.1/tests/test_recording_edges.py +103 -0
- sqllocks_shape_fabric-0.9.1/tests/test_scale_sinks.py +162 -0
- sqllocks_shape_fabric-0.9.1/tests/test_security_review.py +150 -0
- sqllocks_shape_fabric-0.9.1/tests/test_seed_sqlserver_emulator.py +96 -0
- sqllocks_shape_fabric-0.9.1/tests/test_semantic_source.py +471 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sink_uris.py +33 -0
- sqllocks_shape_fabric-0.9.1/tests/test_skeleton.py +9 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sql_emulator.py +121 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sql_write_path.py +651 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sql_write_path_cli.py +345 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sqldb.py +428 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sqlserver_sink.py +291 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sqlserver_sink_emulator.py +301 -0
- sqllocks_shape_fabric-0.9.1/tests/test_sqlserver_sink_uri.py +28 -0
- sqllocks_shape_fabric-0.9.1/tests/test_storage_edges.py +49 -0
- sqllocks_shape_fabric-0.9.1/tests/test_synapse.py +698 -0
- sqllocks_shape_fabric-0.9.1/tests/test_tsql_connection_strings.py +35 -0
- sqllocks_shape_fabric-0.9.1/tests/test_tsql_create_table.py +45 -0
- sqllocks_shape_fabric-0.9.1/tests/test_tsql_types.py +43 -0
- sqllocks_shape_fabric-0.9.1/tests/test_uncovered_paths.py +119 -0
- sqllocks_shape_fabric-0.9.1/tests/test_warehouse.py +230 -0
- sqllocks_shape_fabric-0.9.1/tests/test_write_mode_atomicity.py +79 -0
- sqllocks_shape_fabric-0.9.1/tests/test_write_tables_order.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 SQLLocks (Jonathan Stewart)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqllocks-shape-fabric
|
|
3
|
+
Version: 0.9.1
|
|
4
|
+
Summary: Shape plugin: Microsoft Fabric, Synapse and Azure Data Factory pipeline integration
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/sqllocks/shape
|
|
7
|
+
Project-URL: Repository, https://github.com/sqllocks/shape
|
|
8
|
+
Project-URL: Issues, https://github.com/sqllocks/shape/issues
|
|
9
|
+
Project-URL: Documentation, https://github.com/sqllocks/shape/tree/main/plugins/shape-fabric
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Topic :: Database
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: sqllocks-shape==0.9.1
|
|
22
|
+
Provides-Extra: eventhubs
|
|
23
|
+
Requires-Dist: sqllocks-shape-eventhubs==0.9.1; extra == "eventhubs"
|
|
24
|
+
Provides-Extra: sqlserver
|
|
25
|
+
Requires-Dist: sqllocks-shape-sqlserver==0.9.1; extra == "sqlserver"
|
|
26
|
+
Provides-Extra: entra
|
|
27
|
+
Requires-Dist: azure-identity>=1.16.1; extra == "entra"
|
|
28
|
+
Provides-Extra: onelake
|
|
29
|
+
Requires-Dist: sqllocks-shape[azure]==0.9.1; extra == "onelake"
|
|
30
|
+
Provides-Extra: semantic-link
|
|
31
|
+
Requires-Dist: semantic-link-sempy>=0.8; extra == "semantic-link"
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# sqllocks-shape-fabric
|
|
35
|
+
|
|
36
|
+
Shape plugin: Microsoft Fabric. It holds the two event emitters of `shape emit` (`shape.emitters`),
|
|
37
|
+
the `sqlserver`, `warehouse` and `synapse` sinks (`shape.sinks`), the writers behind the scale router's Fabric
|
|
38
|
+
sinks, `--auth` and credential references ([fabric-auth](../../docs/plugins/fabric-auth.md)), and
|
|
39
|
+
the commands `shape fabric publish|notebook|deploy-notebook|setup|export-model|known-answer|
|
|
40
|
+
check-answers|publish-report` with their top-level aliases ([fabric-commands](../../docs/plugins/fabric-commands.md)).
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install sqllocks-shape-fabric # Lakehouse, Eventhouse, notebooks, commands
|
|
44
|
+
pip install 'sqllocks-shape-fabric[sqlserver,eventhubs]' # + SQL targets and the Eventstream
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`[sqlserver]` brings the SQL Server plugin (and `pyodbc`) for the `sqlserver`, `warehouse` and
|
|
48
|
+
`synapse` sinks and the SQL database writer; `[eventhubs]` brings the Event Hubs plugin for the
|
|
49
|
+
`eventstream` emitter and writer. Without one, that part says which extra to install. The plugins
|
|
50
|
+
are not on PyPI yet: install from the release's wheels with `--find-links` (docs/INSTALL.md).
|
|
51
|
+
|
|
52
|
+
## `eventstream://`: a Fabric Eventstream
|
|
53
|
+
|
|
54
|
+
Add a **custom endpoint** source to an Eventstream, copy its connection string (it ends in
|
|
55
|
+
`EntityPath=...`) and:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
export SHAPE_EVENTSTREAM_CONNECTION_STRING='Endpoint=sb://...;EntityPath=es_...'
|
|
59
|
+
shape emit retail --realtime --rate 2000 --sink eventstream://my-eventstream
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The custom endpoint speaks the Event Hubs protocol, so this is the Event Hubs emitter (idempotency
|
|
63
|
+
key `<table>/<seq>` in the property `shape_key`, at-least-once, throttling waited for) with the
|
|
64
|
+
Eventstream's connection string. Sign-in with Microsoft Entra is not available for a custom
|
|
65
|
+
endpoint.
|
|
66
|
+
|
|
67
|
+
## `eventhouse://`: a Fabric Eventhouse (KQL database)
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
export SHAPE_EVENTHOUSE_TOKEN=$(az account get-access-token --resource https://<query-uri-host> --query accessToken -o tsv)
|
|
71
|
+
shape emit retail --sink eventhouse://<query-uri-host>/<database>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Events are sent by Kusto **streaming ingestion**, a JSON-lines request per batch (under the 4 MB
|
|
75
|
+
limit). Each Shape table goes to the KQL table of the same name, created from its schema
|
|
76
|
+
(`.create-merge table`, plus a JSON ingestion mapping); put a table name last in the URI to send
|
|
77
|
+
everything to one table. The idempotency key is the pair of columns `_shape_table`,
|
|
78
|
+
`_shape_seq`. Streaming ingestion does not deduplicate, so a batch the runtime repeats after a
|
|
79
|
+
failure is in the table twice: read it with
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from shape_fabric.eventhouse import dedupe_query
|
|
83
|
+
|
|
84
|
+
dedupe_query("order_line") # ['order_line'] | summarize take_any(*) by _shape_table, _shape_seq
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Sign-in: `token` option, `SHAPE_EVENTHOUSE_TOKEN`, or Microsoft Entra through `azure-identity`
|
|
88
|
+
(`pip install 'sqllocks-shape-fabric[entra]'`). A 429/503 is waited for (`Retry-After` honoured);
|
|
89
|
+
400 (a malformed event, or streaming ingestion not enabled) and 401/403 stop the run.
|
|
90
|
+
|
|
91
|
+
## Writers (Python API)
|
|
92
|
+
|
|
93
|
+
Batch writers take Arrow `RecordBatch` iterators, one table per `write_table` call (or a mapping to
|
|
94
|
+
`write_tables`), and a `credential` (any object with `get_token(scope)`, or a function
|
|
95
|
+
`scope -> token`; `None` means the writer's default). A failure raises `WriteError`; its `.result`
|
|
96
|
+
lists the tables completed before it. Nothing is reported written unless the destination accepted it.
|
|
97
|
+
|
|
98
|
+
| writer | destination | notes |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| `LakehouseWriter(folder)` | Parquet/CSV/JSONL files in a local folder or OneLake (`abfss://`, `onelake://<ws>/<lakehouse>/Files/..`) | streamed; a file is complete or absent; landing-zone, manifest and done-flag helpers |
|
|
101
|
+
| `SqlDatabaseWriter(connection_string)` | Fabric SQL database, Azure SQL, SQL Server | parameterised `INSERT`s, `batch_size` rows per trip, one transaction per table |
|
|
102
|
+
| `WarehouseWriter(connection_string, staging_path)` | Fabric Warehouse | Parquet staged in OneLake, `COPY INTO`, rows loaded must equal rows staged, staging always removed |
|
|
103
|
+
| `SynapseWriter(connection_string, staging_path)` | Synapse dedicated SQL pool | Parquet staged in ADLS Gen2, `COPY INTO` (managed or signed-in identity), rows loaded must equal rows staged, staging always removed; `distribution` and `index` table options |
|
|
104
|
+
| `EventhouseWriter(uri)` | KQL table | the emitter's Kusto transport (retries, sign-in), a table's own columns |
|
|
105
|
+
| `EventstreamWriter(uri)` | Eventstream | the emitter's transport, flat events with the `_shape_table`/`_shape_seq` key |
|
|
106
|
+
|
|
107
|
+
`write_mode` (SQL, Warehouse, Eventhouse): `create` (default: an existing table is an error), `append`,
|
|
108
|
+
`truncate`, `replace`. In a SQL database and a Warehouse, `replace` drops the old table. The `TRUNCATE` or
|
|
109
|
+
`DROP TABLE` commits together with the new rows, so a failed write keeps the old rows. Names are quoted and checked, values are parameters; the
|
|
110
|
+
one literal that cannot be a parameter (the `COPY INTO` location) is validated against a strict
|
|
111
|
+
character set. `shape_fabric.sinks` has `Sink`-protocol adapters; `shape_fabric.onelake` builds
|
|
112
|
+
OneLake paths.
|
|
113
|
+
|
|
114
|
+
In Eventhouse, `replace` clears the rows and rebuilds the column schema in the existing table,
|
|
115
|
+
retaining its identity and permissions. Streaming ingestion is enabled as for the other write
|
|
116
|
+
modes. It removes old columns and supports changed
|
|
117
|
+
column types. The management steps and streaming writes are not atomic: a failed replacement
|
|
118
|
+
can leave the table empty with an intermediate schema, or with only the rows already accepted.
|
|
119
|
+
`management_timeout` defaults to 600 seconds to allow streamed rows to be sealed before clearing;
|
|
120
|
+
ingestion and queries retain the 100-second `timeout` default.
|
|
121
|
+
|
|
122
|
+
Profile a lakehouse straight from OneLake: `shape profile onelake://<workspace>/<lakehouse>/Tables/<table>`
|
|
123
|
+
(Delta) or `.../Files/<path>`; authentication and `adlfs` handling are core's `abfss://` source.
|
|
124
|
+
|
|
125
|
+
## `mssql://`: a live SQL Server, Azure SQL or Fabric SQL sink
|
|
126
|
+
|
|
127
|
+
`shape-fabric` registers three `shape.sinks` entries: `sqlserver` (URI schemes `mssql` and
|
|
128
|
+
`sqlserver`, bulk insert into a live database), `warehouse` (`warehouse://`, `COPY INTO`) and
|
|
129
|
+
`synapse` (`synapse://<workspace>.sql.azuresynapse.net/<pool>`, `COPY INTO` a dedicated SQL pool;
|
|
130
|
+
see [fabric-writers](../../docs/plugins/fabric-writers.md#synapse-a-synapse-dedicated-sql-pool)). The
|
|
131
|
+
`sql` sink of core is different: it writes a script file and never connects.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from shape.plugins.host import PluginHost
|
|
135
|
+
|
|
136
|
+
sink = PluginHost().get("shape.sinks", "sqlserver")
|
|
137
|
+
sink.write(
|
|
138
|
+
"mssql://myserver.database.windows.net/shop?schema=dbo&write_mode=append",
|
|
139
|
+
"customer",
|
|
140
|
+
batches, # batches: an iterable of Arrow RecordBatch
|
|
141
|
+
credential=my_credential, # get_token(scope) or scope -> token (Entra)
|
|
142
|
+
commit_rows=50_000, # optional: commit every 50,000 rows
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| URI part or option | meaning |
|
|
147
|
+
|---|---|
|
|
148
|
+
| `mssql://[user[:password]@]host[:port]/database` | the server and database (`sqlserver://` is the same) |
|
|
149
|
+
| `?schema=`, `schema_name=` | SQL schema (default `dbo`; created when missing) |
|
|
150
|
+
| `?write_mode=`, `write_mode=` | `create` (default; an existing table is an error), `append`, `truncate`, `replace`; the `TRUNCATE` or `DROP TABLE` commits together with the new rows, so a failed write keeps the old rows |
|
|
151
|
+
| `?batch_size=`, `batch_size=` | rows per round trip (default 5,000) |
|
|
152
|
+
| `?commit_rows=`, `commit_rows=` | commit every N rows while the batches are consumed, so readers see rows as they arrive; the default is one transaction per call (a failure rolls everything back). With `commit_rows` a failure rolls back only the open chunk and keeps what was committed |
|
|
153
|
+
| `credential=` | Microsoft Entra sign-in; without it the login is `user` and `password` |
|
|
154
|
+
| `connection_string=`, `connection=` | an ODBC/ADO.NET connection string, or an open DB-API connection (never closed by the sink), instead of the URI's host and database |
|
|
155
|
+
| `driver`, `encrypt`, `trust_server_certificate`, `timeout` | connection settings (query or option) |
|
|
156
|
+
|
|
157
|
+
Each call writes one table and is safe to call again for each micro-batch of a stream with
|
|
158
|
+
`write_mode=append`. The batches are consumed one at a time; nothing is held back until the end.
|
|
159
|
+
A password is accepted as an option or in the URI's user information, never in the query string; no
|
|
160
|
+
password, token or connection string appears in an exception message or a log record, and none
|
|
161
|
+
should go on a command line (use an environment variable and pass it as an option, or sign in
|
|
162
|
+
with `--auth` and `--connection-string` on `shape generate --to mssql://...`; see the next section).
|
|
163
|
+
|
|
164
|
+
## Sign-in and credential references
|
|
165
|
+
|
|
166
|
+
`--auth cli|msi|spn|sql|device-code|fabric`, `--tenant-id`, `--client-id`, `--client-secret REF`,
|
|
167
|
+
`--sql-user`, `--sql-password REF` and `--connection-string` on `shape generate --scale-mode`, `shape emit`,
|
|
168
|
+
`shape stream`, `shape profile` and `shape jobs`; in Python `shape_fabric.auth` (`AuthSettings`,
|
|
169
|
+
`build_credential`, `writer_options`) builds the `credential` every writer takes. Secrets are `env://`,
|
|
170
|
+
`file://` (refused when others can read the file) or `kv://VAULT/SECRET` (`shape_fabric.keyvault`, Azure Key
|
|
171
|
+
Vault over HTTPS) references, never command-line values. See `docs/plugins/fabric-auth.md`.
|
|
172
|
+
|
|
173
|
+
## Tests
|
|
174
|
+
|
|
175
|
+
Contract tests replay recorded interactions (`tests/fixtures/*.json`, made by `python -m
|
|
176
|
+
shape_fabric.scenarios record <dir>`; secrets are scrubbed, including quoted, braced and JSON
|
|
177
|
+
values, a tape that still holds one is refused when it is written and when it is replayed, and a
|
|
178
|
+
test fails if a tape holds one).
|
|
179
|
+
Emulator tests: `test_sql_emulator.py` and `test_sqlserver_sink_emulator.py` (SQL Server), `test_kusto_emulator.py`. Live tests
|
|
180
|
+
(`test_live_writers.py`, nightly `fabric-live` job) need the `FABRIC_*` secrets listed in the file.
|
|
181
|
+
|
|
182
|
+
Contract tests run on every PR against in-memory fakes (`shape_fabric.testing`; the emitter
|
|
183
|
+
contract is `shape.streaming.emit.contract`). `pytest -m emulator plugins/shape-fabric/tests` runs
|
|
184
|
+
the end-to-end tests against the Event Hubs emulator (as an Eventstream stand-in) and the Kusto
|
|
185
|
+
emulator (`docker compose -f ci/emulators/docker-compose.yml up -d --wait azurite eventhubs && docker
|
|
186
|
+
compose -f ci/emulators/docker-compose.yml up -d kusto`), nightly in CI. `pytest -m live
|
|
187
|
+
plugins/shape-fabric/tests` needs the owner's `FABRIC_*` secrets (`FABRIC_EVENTSTREAM_CONNECTION_STRING`;
|
|
188
|
+
`FABRIC_EVENTHOUSE_URI`, `FABRIC_EVENTHOUSE_TOKEN`); the nightly job runs each only where its secret
|
|
189
|
+
exists.
|
|
190
|
+
|
|
191
|
+
Its version always equals core's (`sqllocks-shape`), and it is released together with core.
|
|
192
|
+
How plugins are written: `docs/plugins/authoring.md` in the repository.
|
|
193
|
+
|
|
194
|
+
## Which Fabric surfaces this plugin calls
|
|
195
|
+
|
|
196
|
+
[`docs/FABRIC_PLATFORM.md`](../../docs/FABRIC_PLATFORM.md) lists every Fabric and OneLake API, item type and Spark runtime that Shape calls, with its release stage, and describes the live Git sync check (`tests/test_live_git_sync.py`). A test keeps preview APIs out of the code.
|
|
197
|
+
|
|
198
|
+
## `synapse://`: a Synapse dedicated SQL pool
|
|
199
|
+
|
|
200
|
+
`synapse` (`shape.sinks`, `shape_fabric.SynapseSink`; the writer is `shape_fabric.SynapseWriter`)
|
|
201
|
+
writes a table into a **dedicated SQL pool** of an Azure Synapse workspace. It follows the Warehouse
|
|
202
|
+
writer: it prepares the table with the same T-SQL helpers (same `write_mode` values, the safe `create`
|
|
203
|
+
default, `schema_name`, `columns`, `primary_key`), stages the rows as Parquet files of at most
|
|
204
|
+
`chunk_rows` rows (default 1,000,000) under `<staging_path>/staging/<run>/<table>/`, runs **one**
|
|
205
|
+
`COPY INTO ... WITH (FILE_TYPE = 'PARQUET')` over that folder, checks that the number of rows it
|
|
206
|
+
loaded equals the number staged (otherwise the write fails and a table this call created is dropped),
|
|
207
|
+
and deletes the staged files, also when a step fails.
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
shape generate retail --to synapse://myws.sql.azuresynapse.net/pool1 \
|
|
211
|
+
--auth cli --sink-config synapse.staging_path=abfss://stage@myacct.dfs.core.windows.net/shape
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
| URI part or option | meaning |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `synapse://<workspace>.sql.azuresynapse.net/<pool>` | the workspace's SQL endpoint and the dedicated pool. No user part, port or query: a serverless endpoint (`-ondemand`) is refused, and so is a password anywhere in the URI |
|
|
217
|
+
| `staging_path` | **required**: an ADLS Gen2 folder, `abfss://<container>@<account>.dfs.core.windows.net/<folder>`, that the pool can read. `COPY INTO` reads it as `https://<account>.dfs.core.windows.net/<container>/<folder>/...` |
|
|
218
|
+
| `copy_identity` | who `COPY INTO` reads the storage as: `managed_identity` (default; the workspace's managed identity, `CREDENTIAL = (IDENTITY = 'Managed Identity')`) or `signed_in` (the Microsoft Entra identity of the connection, no `CREDENTIAL` clause; refused with a SQL login, which has no such identity) |
|
|
219
|
+
| `distribution` | `ROUND_ROBIN` (default), `REPLICATE` or `HASH(column)`; the column must be one of the table's and is quoted |
|
|
220
|
+
| `index` | `CLUSTERED COLUMNSTORE INDEX` (default) or `HEAP` |
|
|
221
|
+
| `write_mode`, `schema_name`, `chunk_rows`, `columns`, `primary_key`, `schema` | as the Warehouse writer (`schema` creates an empty table from no batches) |
|
|
222
|
+
| `credential`, `connection_string`, `connection` | sign-in, below |
|
|
223
|
+
|
|
224
|
+
`distribution` and `index` apply to a table this call creates; a table that already exists
|
|
225
|
+
(`append`, `truncate`) keeps its own. Synapse applies its own limits to the hash column's type; a
|
|
226
|
+
refusal from the pool fails the write and a table this call created is dropped. A primary key is
|
|
227
|
+
declared `NONCLUSTERED ... NOT ENFORCED` (the pool does not enforce keys). Column types are the
|
|
228
|
+
Warehouse's (`VARCHAR(8000)`, `DATETIME2(6)`, `BIT`, ...; no `(N)VARCHAR(MAX)`, which a clustered
|
|
229
|
+
columnstore index cannot hold), and timestamps are staged in microseconds in UTC.
|
|
230
|
+
|
|
231
|
+
**Sign-in** is the plugin's `--auth` modes ([fabric-auth](../../docs/plugins/fabric-auth.md)): `cli`, `msi`, `spn`,
|
|
232
|
+
`device-code` and `fabric` send a Microsoft Entra token for the SQL connection and for the storage;
|
|
233
|
+
`--auth sql` adds a SQL login to `--connection-string synapse://<workspace>.sql.azuresynapse.net/<pool>`
|
|
234
|
+
(the password a reference, never a command-line value). From Python give `credential=` or a
|
|
235
|
+
`connection_string` with the login. No password, token or connection string appears in an exception
|
|
236
|
+
message or a log record.
|
|
237
|
+
|
|
238
|
+
**What a failure leaves behind.** The load is one `COPY INTO`, committed once the row count has been
|
|
239
|
+
checked. A failure rolls back and drops a table this call created (`replace` has already dropped the
|
|
240
|
+
old table and `truncate` has already emptied it); the staged files are removed in every case (a
|
|
241
|
+
removal that itself fails is a `RuntimeWarning`).
|
|
242
|
+
|
|
243
|
+
Out of scope: serverless SQL pools, Synapse Spark pools and Synapse pipelines (those are
|
|
244
|
+
`integrations/synapse`).
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# sqllocks-shape-fabric
|
|
2
|
+
|
|
3
|
+
Shape plugin: Microsoft Fabric. It holds the two event emitters of `shape emit` (`shape.emitters`),
|
|
4
|
+
the `sqlserver`, `warehouse` and `synapse` sinks (`shape.sinks`), the writers behind the scale router's Fabric
|
|
5
|
+
sinks, `--auth` and credential references ([fabric-auth](../../docs/plugins/fabric-auth.md)), and
|
|
6
|
+
the commands `shape fabric publish|notebook|deploy-notebook|setup|export-model|known-answer|
|
|
7
|
+
check-answers|publish-report` with their top-level aliases ([fabric-commands](../../docs/plugins/fabric-commands.md)).
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
pip install sqllocks-shape-fabric # Lakehouse, Eventhouse, notebooks, commands
|
|
11
|
+
pip install 'sqllocks-shape-fabric[sqlserver,eventhubs]' # + SQL targets and the Eventstream
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`[sqlserver]` brings the SQL Server plugin (and `pyodbc`) for the `sqlserver`, `warehouse` and
|
|
15
|
+
`synapse` sinks and the SQL database writer; `[eventhubs]` brings the Event Hubs plugin for the
|
|
16
|
+
`eventstream` emitter and writer. Without one, that part says which extra to install. The plugins
|
|
17
|
+
are not on PyPI yet: install from the release's wheels with `--find-links` (docs/INSTALL.md).
|
|
18
|
+
|
|
19
|
+
## `eventstream://`: a Fabric Eventstream
|
|
20
|
+
|
|
21
|
+
Add a **custom endpoint** source to an Eventstream, copy its connection string (it ends in
|
|
22
|
+
`EntityPath=...`) and:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
export SHAPE_EVENTSTREAM_CONNECTION_STRING='Endpoint=sb://...;EntityPath=es_...'
|
|
26
|
+
shape emit retail --realtime --rate 2000 --sink eventstream://my-eventstream
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The custom endpoint speaks the Event Hubs protocol, so this is the Event Hubs emitter (idempotency
|
|
30
|
+
key `<table>/<seq>` in the property `shape_key`, at-least-once, throttling waited for) with the
|
|
31
|
+
Eventstream's connection string. Sign-in with Microsoft Entra is not available for a custom
|
|
32
|
+
endpoint.
|
|
33
|
+
|
|
34
|
+
## `eventhouse://`: a Fabric Eventhouse (KQL database)
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
export SHAPE_EVENTHOUSE_TOKEN=$(az account get-access-token --resource https://<query-uri-host> --query accessToken -o tsv)
|
|
38
|
+
shape emit retail --sink eventhouse://<query-uri-host>/<database>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Events are sent by Kusto **streaming ingestion**, a JSON-lines request per batch (under the 4 MB
|
|
42
|
+
limit). Each Shape table goes to the KQL table of the same name, created from its schema
|
|
43
|
+
(`.create-merge table`, plus a JSON ingestion mapping); put a table name last in the URI to send
|
|
44
|
+
everything to one table. The idempotency key is the pair of columns `_shape_table`,
|
|
45
|
+
`_shape_seq`. Streaming ingestion does not deduplicate, so a batch the runtime repeats after a
|
|
46
|
+
failure is in the table twice: read it with
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from shape_fabric.eventhouse import dedupe_query
|
|
50
|
+
|
|
51
|
+
dedupe_query("order_line") # ['order_line'] | summarize take_any(*) by _shape_table, _shape_seq
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Sign-in: `token` option, `SHAPE_EVENTHOUSE_TOKEN`, or Microsoft Entra through `azure-identity`
|
|
55
|
+
(`pip install 'sqllocks-shape-fabric[entra]'`). A 429/503 is waited for (`Retry-After` honoured);
|
|
56
|
+
400 (a malformed event, or streaming ingestion not enabled) and 401/403 stop the run.
|
|
57
|
+
|
|
58
|
+
## Writers (Python API)
|
|
59
|
+
|
|
60
|
+
Batch writers take Arrow `RecordBatch` iterators, one table per `write_table` call (or a mapping to
|
|
61
|
+
`write_tables`), and a `credential` (any object with `get_token(scope)`, or a function
|
|
62
|
+
`scope -> token`; `None` means the writer's default). A failure raises `WriteError`; its `.result`
|
|
63
|
+
lists the tables completed before it. Nothing is reported written unless the destination accepted it.
|
|
64
|
+
|
|
65
|
+
| writer | destination | notes |
|
|
66
|
+
|---|---|---|
|
|
67
|
+
| `LakehouseWriter(folder)` | Parquet/CSV/JSONL files in a local folder or OneLake (`abfss://`, `onelake://<ws>/<lakehouse>/Files/..`) | streamed; a file is complete or absent; landing-zone, manifest and done-flag helpers |
|
|
68
|
+
| `SqlDatabaseWriter(connection_string)` | Fabric SQL database, Azure SQL, SQL Server | parameterised `INSERT`s, `batch_size` rows per trip, one transaction per table |
|
|
69
|
+
| `WarehouseWriter(connection_string, staging_path)` | Fabric Warehouse | Parquet staged in OneLake, `COPY INTO`, rows loaded must equal rows staged, staging always removed |
|
|
70
|
+
| `SynapseWriter(connection_string, staging_path)` | Synapse dedicated SQL pool | Parquet staged in ADLS Gen2, `COPY INTO` (managed or signed-in identity), rows loaded must equal rows staged, staging always removed; `distribution` and `index` table options |
|
|
71
|
+
| `EventhouseWriter(uri)` | KQL table | the emitter's Kusto transport (retries, sign-in), a table's own columns |
|
|
72
|
+
| `EventstreamWriter(uri)` | Eventstream | the emitter's transport, flat events with the `_shape_table`/`_shape_seq` key |
|
|
73
|
+
|
|
74
|
+
`write_mode` (SQL, Warehouse, Eventhouse): `create` (default: an existing table is an error), `append`,
|
|
75
|
+
`truncate`, `replace`. In a SQL database and a Warehouse, `replace` drops the old table. The `TRUNCATE` or
|
|
76
|
+
`DROP TABLE` commits together with the new rows, so a failed write keeps the old rows. Names are quoted and checked, values are parameters; the
|
|
77
|
+
one literal that cannot be a parameter (the `COPY INTO` location) is validated against a strict
|
|
78
|
+
character set. `shape_fabric.sinks` has `Sink`-protocol adapters; `shape_fabric.onelake` builds
|
|
79
|
+
OneLake paths.
|
|
80
|
+
|
|
81
|
+
In Eventhouse, `replace` clears the rows and rebuilds the column schema in the existing table,
|
|
82
|
+
retaining its identity and permissions. Streaming ingestion is enabled as for the other write
|
|
83
|
+
modes. It removes old columns and supports changed
|
|
84
|
+
column types. The management steps and streaming writes are not atomic: a failed replacement
|
|
85
|
+
can leave the table empty with an intermediate schema, or with only the rows already accepted.
|
|
86
|
+
`management_timeout` defaults to 600 seconds to allow streamed rows to be sealed before clearing;
|
|
87
|
+
ingestion and queries retain the 100-second `timeout` default.
|
|
88
|
+
|
|
89
|
+
Profile a lakehouse straight from OneLake: `shape profile onelake://<workspace>/<lakehouse>/Tables/<table>`
|
|
90
|
+
(Delta) or `.../Files/<path>`; authentication and `adlfs` handling are core's `abfss://` source.
|
|
91
|
+
|
|
92
|
+
## `mssql://`: a live SQL Server, Azure SQL or Fabric SQL sink
|
|
93
|
+
|
|
94
|
+
`shape-fabric` registers three `shape.sinks` entries: `sqlserver` (URI schemes `mssql` and
|
|
95
|
+
`sqlserver`, bulk insert into a live database), `warehouse` (`warehouse://`, `COPY INTO`) and
|
|
96
|
+
`synapse` (`synapse://<workspace>.sql.azuresynapse.net/<pool>`, `COPY INTO` a dedicated SQL pool;
|
|
97
|
+
see [fabric-writers](../../docs/plugins/fabric-writers.md#synapse-a-synapse-dedicated-sql-pool)). The
|
|
98
|
+
`sql` sink of core is different: it writes a script file and never connects.
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from shape.plugins.host import PluginHost
|
|
102
|
+
|
|
103
|
+
sink = PluginHost().get("shape.sinks", "sqlserver")
|
|
104
|
+
sink.write(
|
|
105
|
+
"mssql://myserver.database.windows.net/shop?schema=dbo&write_mode=append",
|
|
106
|
+
"customer",
|
|
107
|
+
batches, # batches: an iterable of Arrow RecordBatch
|
|
108
|
+
credential=my_credential, # get_token(scope) or scope -> token (Entra)
|
|
109
|
+
commit_rows=50_000, # optional: commit every 50,000 rows
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| URI part or option | meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `mssql://[user[:password]@]host[:port]/database` | the server and database (`sqlserver://` is the same) |
|
|
116
|
+
| `?schema=`, `schema_name=` | SQL schema (default `dbo`; created when missing) |
|
|
117
|
+
| `?write_mode=`, `write_mode=` | `create` (default; an existing table is an error), `append`, `truncate`, `replace`; the `TRUNCATE` or `DROP TABLE` commits together with the new rows, so a failed write keeps the old rows |
|
|
118
|
+
| `?batch_size=`, `batch_size=` | rows per round trip (default 5,000) |
|
|
119
|
+
| `?commit_rows=`, `commit_rows=` | commit every N rows while the batches are consumed, so readers see rows as they arrive; the default is one transaction per call (a failure rolls everything back). With `commit_rows` a failure rolls back only the open chunk and keeps what was committed |
|
|
120
|
+
| `credential=` | Microsoft Entra sign-in; without it the login is `user` and `password` |
|
|
121
|
+
| `connection_string=`, `connection=` | an ODBC/ADO.NET connection string, or an open DB-API connection (never closed by the sink), instead of the URI's host and database |
|
|
122
|
+
| `driver`, `encrypt`, `trust_server_certificate`, `timeout` | connection settings (query or option) |
|
|
123
|
+
|
|
124
|
+
Each call writes one table and is safe to call again for each micro-batch of a stream with
|
|
125
|
+
`write_mode=append`. The batches are consumed one at a time; nothing is held back until the end.
|
|
126
|
+
A password is accepted as an option or in the URI's user information, never in the query string; no
|
|
127
|
+
password, token or connection string appears in an exception message or a log record, and none
|
|
128
|
+
should go on a command line (use an environment variable and pass it as an option, or sign in
|
|
129
|
+
with `--auth` and `--connection-string` on `shape generate --to mssql://...`; see the next section).
|
|
130
|
+
|
|
131
|
+
## Sign-in and credential references
|
|
132
|
+
|
|
133
|
+
`--auth cli|msi|spn|sql|device-code|fabric`, `--tenant-id`, `--client-id`, `--client-secret REF`,
|
|
134
|
+
`--sql-user`, `--sql-password REF` and `--connection-string` on `shape generate --scale-mode`, `shape emit`,
|
|
135
|
+
`shape stream`, `shape profile` and `shape jobs`; in Python `shape_fabric.auth` (`AuthSettings`,
|
|
136
|
+
`build_credential`, `writer_options`) builds the `credential` every writer takes. Secrets are `env://`,
|
|
137
|
+
`file://` (refused when others can read the file) or `kv://VAULT/SECRET` (`shape_fabric.keyvault`, Azure Key
|
|
138
|
+
Vault over HTTPS) references, never command-line values. See `docs/plugins/fabric-auth.md`.
|
|
139
|
+
|
|
140
|
+
## Tests
|
|
141
|
+
|
|
142
|
+
Contract tests replay recorded interactions (`tests/fixtures/*.json`, made by `python -m
|
|
143
|
+
shape_fabric.scenarios record <dir>`; secrets are scrubbed, including quoted, braced and JSON
|
|
144
|
+
values, a tape that still holds one is refused when it is written and when it is replayed, and a
|
|
145
|
+
test fails if a tape holds one).
|
|
146
|
+
Emulator tests: `test_sql_emulator.py` and `test_sqlserver_sink_emulator.py` (SQL Server), `test_kusto_emulator.py`. Live tests
|
|
147
|
+
(`test_live_writers.py`, nightly `fabric-live` job) need the `FABRIC_*` secrets listed in the file.
|
|
148
|
+
|
|
149
|
+
Contract tests run on every PR against in-memory fakes (`shape_fabric.testing`; the emitter
|
|
150
|
+
contract is `shape.streaming.emit.contract`). `pytest -m emulator plugins/shape-fabric/tests` runs
|
|
151
|
+
the end-to-end tests against the Event Hubs emulator (as an Eventstream stand-in) and the Kusto
|
|
152
|
+
emulator (`docker compose -f ci/emulators/docker-compose.yml up -d --wait azurite eventhubs && docker
|
|
153
|
+
compose -f ci/emulators/docker-compose.yml up -d kusto`), nightly in CI. `pytest -m live
|
|
154
|
+
plugins/shape-fabric/tests` needs the owner's `FABRIC_*` secrets (`FABRIC_EVENTSTREAM_CONNECTION_STRING`;
|
|
155
|
+
`FABRIC_EVENTHOUSE_URI`, `FABRIC_EVENTHOUSE_TOKEN`); the nightly job runs each only where its secret
|
|
156
|
+
exists.
|
|
157
|
+
|
|
158
|
+
Its version always equals core's (`sqllocks-shape`), and it is released together with core.
|
|
159
|
+
How plugins are written: `docs/plugins/authoring.md` in the repository.
|
|
160
|
+
|
|
161
|
+
## Which Fabric surfaces this plugin calls
|
|
162
|
+
|
|
163
|
+
[`docs/FABRIC_PLATFORM.md`](../../docs/FABRIC_PLATFORM.md) lists every Fabric and OneLake API, item type and Spark runtime that Shape calls, with its release stage, and describes the live Git sync check (`tests/test_live_git_sync.py`). A test keeps preview APIs out of the code.
|
|
164
|
+
|
|
165
|
+
## `synapse://`: a Synapse dedicated SQL pool
|
|
166
|
+
|
|
167
|
+
`synapse` (`shape.sinks`, `shape_fabric.SynapseSink`; the writer is `shape_fabric.SynapseWriter`)
|
|
168
|
+
writes a table into a **dedicated SQL pool** of an Azure Synapse workspace. It follows the Warehouse
|
|
169
|
+
writer: it prepares the table with the same T-SQL helpers (same `write_mode` values, the safe `create`
|
|
170
|
+
default, `schema_name`, `columns`, `primary_key`), stages the rows as Parquet files of at most
|
|
171
|
+
`chunk_rows` rows (default 1,000,000) under `<staging_path>/staging/<run>/<table>/`, runs **one**
|
|
172
|
+
`COPY INTO ... WITH (FILE_TYPE = 'PARQUET')` over that folder, checks that the number of rows it
|
|
173
|
+
loaded equals the number staged (otherwise the write fails and a table this call created is dropped),
|
|
174
|
+
and deletes the staged files, also when a step fails.
|
|
175
|
+
|
|
176
|
+
```
|
|
177
|
+
shape generate retail --to synapse://myws.sql.azuresynapse.net/pool1 \
|
|
178
|
+
--auth cli --sink-config synapse.staging_path=abfss://stage@myacct.dfs.core.windows.net/shape
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
| URI part or option | meaning |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `synapse://<workspace>.sql.azuresynapse.net/<pool>` | the workspace's SQL endpoint and the dedicated pool. No user part, port or query: a serverless endpoint (`-ondemand`) is refused, and so is a password anywhere in the URI |
|
|
184
|
+
| `staging_path` | **required**: an ADLS Gen2 folder, `abfss://<container>@<account>.dfs.core.windows.net/<folder>`, that the pool can read. `COPY INTO` reads it as `https://<account>.dfs.core.windows.net/<container>/<folder>/...` |
|
|
185
|
+
| `copy_identity` | who `COPY INTO` reads the storage as: `managed_identity` (default; the workspace's managed identity, `CREDENTIAL = (IDENTITY = 'Managed Identity')`) or `signed_in` (the Microsoft Entra identity of the connection, no `CREDENTIAL` clause; refused with a SQL login, which has no such identity) |
|
|
186
|
+
| `distribution` | `ROUND_ROBIN` (default), `REPLICATE` or `HASH(column)`; the column must be one of the table's and is quoted |
|
|
187
|
+
| `index` | `CLUSTERED COLUMNSTORE INDEX` (default) or `HEAP` |
|
|
188
|
+
| `write_mode`, `schema_name`, `chunk_rows`, `columns`, `primary_key`, `schema` | as the Warehouse writer (`schema` creates an empty table from no batches) |
|
|
189
|
+
| `credential`, `connection_string`, `connection` | sign-in, below |
|
|
190
|
+
|
|
191
|
+
`distribution` and `index` apply to a table this call creates; a table that already exists
|
|
192
|
+
(`append`, `truncate`) keeps its own. Synapse applies its own limits to the hash column's type; a
|
|
193
|
+
refusal from the pool fails the write and a table this call created is dropped. A primary key is
|
|
194
|
+
declared `NONCLUSTERED ... NOT ENFORCED` (the pool does not enforce keys). Column types are the
|
|
195
|
+
Warehouse's (`VARCHAR(8000)`, `DATETIME2(6)`, `BIT`, ...; no `(N)VARCHAR(MAX)`, which a clustered
|
|
196
|
+
columnstore index cannot hold), and timestamps are staged in microseconds in UTC.
|
|
197
|
+
|
|
198
|
+
**Sign-in** is the plugin's `--auth` modes ([fabric-auth](../../docs/plugins/fabric-auth.md)): `cli`, `msi`, `spn`,
|
|
199
|
+
`device-code` and `fabric` send a Microsoft Entra token for the SQL connection and for the storage;
|
|
200
|
+
`--auth sql` adds a SQL login to `--connection-string synapse://<workspace>.sql.azuresynapse.net/<pool>`
|
|
201
|
+
(the password a reference, never a command-line value). From Python give `credential=` or a
|
|
202
|
+
`connection_string` with the login. No password, token or connection string appears in an exception
|
|
203
|
+
message or a log record.
|
|
204
|
+
|
|
205
|
+
**What a failure leaves behind.** The load is one `COPY INTO`, committed once the row count has been
|
|
206
|
+
checked. A failure rolls back and drops a table this call created (`replace` has already dropped the
|
|
207
|
+
old table and `truncate` has already emptied it); the staged files are removed in every case (a
|
|
208
|
+
removal that itself fails is a `RuntimeWarning`).
|
|
209
|
+
|
|
210
|
+
Out of scope: serverless SQL pools, Synapse Spark pools and Synapse pipelines (those are
|
|
211
|
+
`integrations/synapse`).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqllocks-shape-fabric"
|
|
7
|
+
# Lockstep with core (T-09): scripts/check_plugin_skeletons.py fails when this drifts.
|
|
8
|
+
version = "0.9.1"
|
|
9
|
+
description = "Shape plugin: Microsoft Fabric, Synapse and Azure Data Factory pipeline integration"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
license-files = ["LICENSE"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Database",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["sqllocks-shape==0.9.1"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/sqllocks/shape"
|
|
28
|
+
Repository = "https://github.com/sqllocks/shape"
|
|
29
|
+
Issues = "https://github.com/sqllocks/shape/issues"
|
|
30
|
+
Documentation = "https://github.com/sqllocks/shape/tree/main/plugins/shape-fabric"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
# The Eventstream emitter and writer: the Event Hubs plugin (azure-eventhub).
|
|
34
|
+
eventhubs = ["sqllocks-shape-eventhubs==0.9.1"]
|
|
35
|
+
# The SQL database, Warehouse, Synapse and SQL Server targets: the SQL Server plugin (pyodbc,
|
|
36
|
+
# which needs unixODBC and the Microsoft ODBC driver on Linux).
|
|
37
|
+
sqlserver = ["sqllocks-shape-sqlserver==0.9.1"]
|
|
38
|
+
# Microsoft Entra sign-in to an Eventhouse instead of a bearer token.
|
|
39
|
+
entra = ["azure-identity>=1.16.1"]
|
|
40
|
+
# Reading and writing OneLake / ADLS Gen2 paths (abfss://, onelake://): adlfs and azure-identity.
|
|
41
|
+
onelake = ["sqllocks-shape[azure]==0.9.1"]
|
|
42
|
+
# Reading a semantic model (semantic-model://, shape profile-model): sempy runs inside a Fabric notebook.
|
|
43
|
+
semantic-link = ["semantic-link-sempy>=0.8"]
|
|
44
|
+
|
|
45
|
+
# Entry points (shape.* groups, plugin API v1) are added here by the work package that
|
|
46
|
+
# implements each part of this plugin; see docs/plugins/authoring.md.
|
|
47
|
+
[project.entry-points."shape.emitters"]
|
|
48
|
+
eventstream = "shape_fabric:EventstreamEmitter"
|
|
49
|
+
eventhouse = "shape_fabric:EventhouseEmitter"
|
|
50
|
+
|
|
51
|
+
[project.entry-points."shape.commands"]
|
|
52
|
+
fabric = "shape_fabric.commands:FabricCommand"
|
|
53
|
+
publish = "shape_fabric.commands:PublishCommand"
|
|
54
|
+
notebook = "shape_fabric.commands:NotebookCommand"
|
|
55
|
+
deploy-notebook = "shape_fabric.commands:DeployNotebookCommand"
|
|
56
|
+
setup-fabric = "shape_fabric.commands:SetupFabricCommand"
|
|
57
|
+
export-model = "shape_fabric.commands:ExportModelCommand"
|
|
58
|
+
profile-model = "shape_fabric.commands:ProfileModelCommand"
|
|
59
|
+
known-answer = "shape_fabric.commands:KnownAnswerCommand"
|
|
60
|
+
check-answers = "shape_fabric.commands:CheckAnswersCommand"
|
|
61
|
+
publish-report = "shape_fabric.commands:PublishReportCommand"
|
|
62
|
+
|
|
63
|
+
[project.entry-points."shape.sinks"]
|
|
64
|
+
sqlserver = "shape_fabric:SqlServerSink"
|
|
65
|
+
warehouse = "shape_fabric:WarehouseSink"
|
|
66
|
+
synapse = "shape_fabric:SynapseSink"
|
|
67
|
+
|
|
68
|
+
[project.entry-points."shape.sources"]
|
|
69
|
+
onelake = "shape_fabric:LakehouseSource"
|
|
70
|
+
semantic-model = "shape_fabric.semantic_source:SemanticModelSource"
|
|
71
|
+
|
|
72
|
+
[tool.setuptools.packages.find]
|
|
73
|
+
where = ["src"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Shape plugin for Fabric, Synapse and Azure Data Factory pipeline integration.
|
|
2
|
+
|
|
3
|
+
* ``shape.emitters``: ``eventstream`` sends events to a Fabric Eventstream custom endpoint
|
|
4
|
+
(``eventstream://<name>``) and ``eventhouse`` to an Eventhouse (KQL database) by streaming
|
|
5
|
+
ingestion (``eventhouse://<query-uri host>/<database>``), for ``shape emit``.
|
|
6
|
+
* ``shape.sources``: ``onelake`` reads a lakehouse Delta table or files by
|
|
7
|
+
``onelake://<workspace>/<lakehouse>/Tables|Files/...`` (profiling, ``shape profile``), and
|
|
8
|
+
``semantic-model`` reads a table of a Power BI / Fabric semantic model through ``sempy`` by
|
|
9
|
+
``semantic-model://<workspace>/<model>/<table>`` (:mod:`shape_fabric.semantic_source`).
|
|
10
|
+
* ``shape.commands``: ``shape profile-model WORKSPACE/MODEL`` profiles every table of a semantic
|
|
11
|
+
model with its declared relationships (:mod:`shape_fabric.semantic_profile`).
|
|
12
|
+
* ``shape.sinks``: ``sqlserver`` writes to a live SQL Server, Azure SQL or Fabric SQL database
|
|
13
|
+
(``mssql://`` and ``sqlserver://`` URIs, bulk insert) and ``warehouse`` to a Fabric Warehouse
|
|
14
|
+
(``warehouse://``, Parquet staged in OneLake then ``COPY INTO``), ``synapse`` to a Synapse
|
|
15
|
+
dedicated SQL pool (``synapse://``, Parquet staged in ADLS Gen2 then ``COPY INTO``).
|
|
16
|
+
* Writers (a Python API; the scale router's sinks are built on it): ``LakehouseWriter`` (files
|
|
17
|
+
in OneLake or a folder), ``SqlDatabaseWriter`` (Fabric SQL database, Azure SQL, SQL Server),
|
|
18
|
+
``WarehouseWriter`` (Parquet staged in OneLake, then ``COPY INTO``), ``SynapseWriter``
|
|
19
|
+
(the same into a dedicated SQL pool), ``EventhouseWriter`` and
|
|
20
|
+
``EventstreamWriter``; ``Sink`` adapters in :mod:`shape_fabric.sinks`.
|
|
21
|
+
* :mod:`shape_fabric.targets`: what ``shape demo`` needs of a destination besides writing to it:
|
|
22
|
+
drop a table, remove files, and check that a target answers.
|
|
23
|
+
|
|
24
|
+
Other entry points are added by the work packages that implement them. Imports here are lazy:
|
|
25
|
+
no Azure or ODBC library is loaded until a writer needs it.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import importlib
|
|
31
|
+
from typing import TYPE_CHECKING, Any
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from .errors import AuthError as AuthError
|
|
35
|
+
from .errors import WriteError as WriteError
|
|
36
|
+
from .errors import WriteResult as WriteResult
|
|
37
|
+
from .eventhouse import EventhouseEmitter as EventhouseEmitter
|
|
38
|
+
from .eventhouse_writer import EventhouseWriter as EventhouseWriter
|
|
39
|
+
from .eventstream import EventstreamEmitter as EventstreamEmitter
|
|
40
|
+
from .eventstream_writer import EventstreamWriter as EventstreamWriter
|
|
41
|
+
from .lakehouse import LakehouseWriter as LakehouseWriter
|
|
42
|
+
from .sinks import SqlServerSink as SqlServerSink
|
|
43
|
+
from .sinks import SynapseSink as SynapseSink
|
|
44
|
+
from .sinks import WarehouseSink as WarehouseSink
|
|
45
|
+
from .source import LakehouseSource as LakehouseSource
|
|
46
|
+
from .sqldb import SqlDatabaseWriter as SqlDatabaseWriter
|
|
47
|
+
from .synapse import SynapseWriter as SynapseWriter
|
|
48
|
+
from .warehouse import WarehouseWriter as WarehouseWriter
|
|
49
|
+
|
|
50
|
+
SHAPE_API = "1.0"
|
|
51
|
+
|
|
52
|
+
_EXPORTS = {
|
|
53
|
+
"AuthError": "errors",
|
|
54
|
+
"EventhouseEmitter": "eventhouse",
|
|
55
|
+
"EventhouseWriter": "eventhouse_writer",
|
|
56
|
+
"EventstreamEmitter": "eventstream",
|
|
57
|
+
"EventstreamWriter": "eventstream_writer",
|
|
58
|
+
"LakehouseSource": "source",
|
|
59
|
+
"LakehouseWriter": "lakehouse",
|
|
60
|
+
"SqlDatabaseWriter": "sqldb",
|
|
61
|
+
"SqlServerSink": "sinks",
|
|
62
|
+
"SynapseSink": "sinks",
|
|
63
|
+
"SynapseWriter": "synapse",
|
|
64
|
+
"WarehouseSink": "sinks",
|
|
65
|
+
"WarehouseWriter": "warehouse",
|
|
66
|
+
"WriteError": "errors",
|
|
67
|
+
"WriteResult": "errors",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
__all__ = ["SHAPE_API", *_EXPORTS]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def __getattr__(name: str) -> Any:
|
|
74
|
+
try:
|
|
75
|
+
module = _EXPORTS[name]
|
|
76
|
+
except KeyError:
|
|
77
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
78
|
+
value = getattr(importlib.import_module(f".{module}", __name__), name)
|
|
79
|
+
globals()[name] = value
|
|
80
|
+
return value
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def __dir__() -> list[str]:
|
|
84
|
+
return sorted({*globals(), *_EXPORTS})
|