sql2deltalake 0.1.0rc1__cp310-abi3-win_amd64.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 (44) hide show
  1. sql2deltalake/__init__.py +43 -0
  2. sql2deltalake/_core.pyd +4 -0
  3. sql2deltalake/_core.pyi +72 -0
  4. sql2deltalake/_native.py +327 -0
  5. sql2deltalake/consistency.py +56 -0
  6. sql2deltalake/db_to_delta.py +1763 -0
  7. sql2deltalake/delta_logger.py +161 -0
  8. sql2deltalake/destination/__init__.py +1 -0
  9. sql2deltalake/destination/azure.py +89 -0
  10. sql2deltalake/destination/azure_utils.py +76 -0
  11. sql2deltalake/destination/databricks.py +86 -0
  12. sql2deltalake/destination/destination.py +64 -0
  13. sql2deltalake/destination/file_system.py +66 -0
  14. sql2deltalake/load_infos.py +83 -0
  15. sql2deltalake/load_lock.py +72 -0
  16. sql2deltalake/load_result.py +40 -0
  17. sql2deltalake/logging.py +21 -0
  18. sql2deltalake/metadata.py +238 -0
  19. sql2deltalake/query.py +55 -0
  20. sql2deltalake/read_utils/delta_rs.py +180 -0
  21. sql2deltalake/read_utils/duckdb_utils.py +14 -0
  22. sql2deltalake/reader/__init__.py +1 -0
  23. sql2deltalake/reader/adbc_reader.py +128 -0
  24. sql2deltalake/reader/arrowsql_reader.py +249 -0
  25. sql2deltalake/reader/local_reader.py +268 -0
  26. sql2deltalake/reader/reader.py +131 -0
  27. sql2deltalake/reader/spark_reader.py +465 -0
  28. sql2deltalake/spark/__init__.py +50 -0
  29. sql2deltalake/spark/_driver.py +1012 -0
  30. sql2deltalake/spark/_options.py +178 -0
  31. sql2deltalake/spark/_plan.py +223 -0
  32. sql2deltalake/spark/_source.py +85 -0
  33. sql2deltalake/spark/_source_load.py +393 -0
  34. sql2deltalake/sql_glot_utils.py +56 -0
  35. sql2deltalake/sql_schema.py +10 -0
  36. sql2deltalake/utils.py +32 -0
  37. sql2deltalake/write_init.py +341 -0
  38. sql2deltalake/write_utils/__init__.py +0 -0
  39. sql2deltalake/write_utils/restore_pk.py +247 -0
  40. sql2deltalake-0.1.0rc1.dist-info/METADATA +288 -0
  41. sql2deltalake-0.1.0rc1.dist-info/RECORD +44 -0
  42. sql2deltalake-0.1.0rc1.dist-info/WHEEL +4 -0
  43. sql2deltalake-0.1.0rc1.dist-info/licenses/LICENSE +21 -0
  44. sql2deltalake-0.1.0rc1.dist-info/sboms/sql2deltalake-py.cyclonedx.json +11568 -0
@@ -0,0 +1,43 @@
1
+ """sql2deltalake: incremental loads from SQL Server and Postgres into append-only SCD2 Delta tables.
2
+
3
+ The Spark data source lives in ``sql2deltalake.spark``."""
4
+
5
+ from .write_init import (
6
+ make_writer,
7
+ DBDeltaPathConfigs,
8
+ WriteConfig,
9
+ DEFAULT_DATA_TYPE_MAP,
10
+ WriteConfigAndInfos,
11
+ )
12
+ from .reader.reader import DataSourceReader
13
+ from .destination.destination import Destination
14
+ from .load_lock import LoadAlreadyRunning
15
+ from pathlib import Path
16
+ from typing import Union
17
+
18
+
19
+ def write_db_to_delta(
20
+ source: Union[DataSourceReader, str],
21
+ table_or_query: tuple[str, str],
22
+ destination: Union[Destination, Path],
23
+ write_config: Union[WriteConfig, None] = None,
24
+ ):
25
+ return make_writer(
26
+ source=source,
27
+ table_or_query=table_or_query,
28
+ destination=destination,
29
+ write_config=write_config,
30
+ ).execute()
31
+
32
+
33
+ __all__ = [
34
+ "DEFAULT_DATA_TYPE_MAP",
35
+ "DataSourceReader",
36
+ "DBDeltaPathConfigs",
37
+ "Destination",
38
+ "LoadAlreadyRunning",
39
+ "WriteConfig",
40
+ "WriteConfigAndInfos",
41
+ "make_writer",
42
+ "write_db_to_delta",
43
+ ]
@@ -0,0 +1,4 @@
1
+ [diffend] Oversized file quarantined before diffing.
2
+ name: sql2deltalake/_core.pyd
3
+ size: 44678656 bytes
4
+ sha256: 1e2694007749412cd8f9bd969180ac606f7a1f8138bfc967b0715bad487d6af4
@@ -0,0 +1,72 @@
1
+ """Type stubs of the Rust extension module (crates/sql2deltalake-py)."""
2
+
3
+ from typing import Any, Mapping, Optional, Sequence, Union
4
+
5
+ __version__: str
6
+
7
+ class ArrowStream:
8
+ """An Arrow C stream (`__arrow_c_stream__`), consumable once."""
9
+
10
+ def __arrow_c_stream__(self, requested_schema: Optional[object] = None) -> object: ...
11
+ def __arrow_c_schema__(self) -> object: ...
12
+
13
+ class SqlBuilder:
14
+ def __init__(
15
+ self,
16
+ dialect_name: str,
17
+ source_ref: Union[Sequence[str], str],
18
+ type_map_spec: Mapping[str, tuple[str, bool]],
19
+ no_trim: bool,
20
+ columns: Sequence[Mapping[str, Any]],
21
+ ) -> None: ...
22
+ def full_load(self, valid_from: Optional[str] = None) -> str: ...
23
+ def pk_ts(self, columns: Sequence[int]) -> str: ...
24
+ def updates(self, criteria: Sequence[str], valid_from: Optional[str] = None) -> str: ...
25
+ def criterion(self, column: int, op: str, value: Any) -> str: ...
26
+ def source_ts_cnt(self, column: Optional[int] = None) -> str: ...
27
+ def rows_by_pk_json(
28
+ self, pks: Sequence[int], json: str, valid_from: Optional[str] = None
29
+ ) -> str: ...
30
+
31
+ def load_timestamp_literal(dialect_name: str, micros: int) -> str: ...
32
+ def compat_name(name: str) -> str: ...
33
+ def normalize(
34
+ source: object,
35
+ time_scales: Optional[Mapping[int, int]] = None,
36
+ target: Optional[object] = None,
37
+ ) -> ArrowStream: ...
38
+ def read_sql(
39
+ connection_string: str,
40
+ sql: str,
41
+ batch_size: int = 65536,
42
+ utf8_strings: bool = False,
43
+ partition_on: Optional[str] = None,
44
+ partitions: int = 1,
45
+ ) -> ArrowStream:
46
+ """Only in builds with the cargo feature `arrowsql-core` (the default)."""
47
+ ...
48
+ def pk_diff(
49
+ snapshot: object,
50
+ previous: object,
51
+ keys: Sequence[str],
52
+ delta: str,
53
+ watermark: object,
54
+ ) -> dict[str, Any]: ...
55
+ def count_new_keys(rows: object, previous: object, keys: Sequence[str]) -> int: ...
56
+ def merge_latest(
57
+ previous: Optional[object],
58
+ loaded: object,
59
+ keys: Sequence[str],
60
+ delta: str,
61
+ order: str = "__timestamp",
62
+ deleted: str = "__is_deleted",
63
+ ) -> dict[str, Any]: ...
64
+ def replay_state(
65
+ log: object,
66
+ keys: Sequence[str],
67
+ delta: str,
68
+ seq: str = "__state_seq",
69
+ deleted: str = "__is_deleted",
70
+ ) -> ArrowStream: ...
71
+ def run_load(host: object, load_spec: Mapping[str, Any]) -> dict[str, Any]: ...
72
+ def restore_last_pk(host: object, load_spec: Mapping[str, Any]) -> bool: ...
@@ -0,0 +1,327 @@
1
+ """The boundary to the Rust core (`sql2deltalake._core`): sqlglot objects and user callables are
2
+ turned into plain values here, once per load, so the core needs no Python while it runs."""
3
+
4
+ from typing import TYPE_CHECKING, Any, Mapping, Union
5
+
6
+ import sqlglot.expressions as ex
7
+
8
+ from .sql_schema import is_string_type
9
+
10
+ if TYPE_CHECKING:
11
+ from .metadata import InformationSchemaColInfo
12
+ from .write_init import WriteConfig, WriteConfigAndInfos
13
+
14
+
15
+ def column_spec(
16
+ c: "InformationSchemaColInfo", write_config: "WriteConfig"
17
+ ) -> dict[str, Any]:
18
+ dialect = write_config.dialect
19
+ return {
20
+ "name": c.column_name,
21
+ "target": write_config.get_target_name(c),
22
+ "type_key": ex.DataType(this=c.data_type.this).sql(dialect).lower(),
23
+ "type_sql": c.data_type.sql(dialect),
24
+ "orig_type": c.data_type_str or "",
25
+ "is_string": bool(is_string_type(c.data_type)),
26
+ "is_numeric": c.data_type.this
27
+ in [ex.DataType.Type.BOOLEAN, *ex.DataType.NUMERIC_TYPES],
28
+ "is_identity": bool(c.is_identity),
29
+ }
30
+
31
+
32
+ def type_map_spec(write_config: "WriteConfig") -> dict[str, tuple[str, bool]]:
33
+ from .write_init import DEFAULT_DATA_TYPE_MAP
34
+
35
+ tm: Mapping[str, Union[ex.DataType, ex.DataType.Type]] = (
36
+ DEFAULT_DATA_TYPE_MAP.get(write_config.dialect, {})
37
+ if write_config.data_type_map == "default"
38
+ else write_config.data_type_map
39
+ )
40
+ res: dict[str, tuple[str, bool]] = {}
41
+ for key, mapped in tm.items():
42
+ if not isinstance(mapped, (ex.DataType, ex.DataType.Type, str)):
43
+ continue # never looked up, e.g. a dialect-keyed map merged in by mistake
44
+ to = ex.cast(ex.column("x"), mapped).to # what sqlglot renders for CAST(x AS mapped)
45
+ res[key] = (to.sql(write_config.dialect), bool(is_string_type(mapped)))
46
+ return res
47
+
48
+
49
+ def source_spec(infos: "WriteConfigAndInfos") -> Union[list[str], str]:
50
+ """The table name parts, or the query rendered in the source dialect."""
51
+ toq = infos.table_or_query
52
+ if isinstance(toq, ex.Query):
53
+ return toq.sql(infos.write_config.dialect)
54
+ if isinstance(toq, str):
55
+ return [toq]
56
+ return list(toq)
57
+
58
+
59
+ def fail_point(name: str) -> "Union[bool, None]":
60
+ """Test hook, called by the load at a few named steps (`load_updates`, `additional_updates`,
61
+ `restore_last_pk`, `write_delta2`, `local_pk_count`). Tests patch it to inject a failure or to
62
+ change the source mid-load; returning False from `restore_last_pk` skips the restore."""
63
+ return None
64
+
65
+
66
+ class PanicError(RuntimeError):
67
+ """A Rust panic during a load (pyo3's ``PanicException``, a ``BaseException``; arrowsql
68
+ raises one when the server kills the connection), turned into an ``Exception`` once the load
69
+ restored its state, so ``except Exception`` in callers sees it."""
70
+
71
+
72
+ def is_panic(e: BaseException) -> bool:
73
+ """pyo3's ``pyo3_runtime.PanicException``, of any extension module (it cannot be imported)."""
74
+ return not isinstance(e, Exception) and type(e).__name__ == "PanicException"
75
+
76
+
77
+ def as_load_error(e: BaseException) -> BaseException:
78
+ """``e``, or a :class:`PanicError` caused by it if it is a panic."""
79
+ if is_panic(e):
80
+ err = PanicError(f"the load panicked: {e}")
81
+ err.__cause__ = e
82
+ if getattr(e, "__notes__", None): # e.g. a restore that failed too
83
+ err.__notes__ = list(e.__notes__) # type: ignore[attr-defined]
84
+ return err
85
+ return e
86
+
87
+
88
+ def enabled() -> bool:
89
+ import os
90
+
91
+ return os.getenv("SQL2DELTA_NATIVE", "1") not in ("0", "false", "False", "")
92
+
93
+
94
+ def supports(infos: "WriteConfigAndInfos") -> bool:
95
+ """The Rust load handles readers with local Delta tables; for now SQL Server through arrowsql."""
96
+ from .reader.arrowsql_reader import ArrowSQLReader
97
+
98
+ return (
99
+ enabled()
100
+ and isinstance(infos.source, ArrowSQLReader)
101
+ and infos.write_config.dialect in ("tsql", "mssql")
102
+ )
103
+
104
+
105
+ def _overrides(reader, name: str) -> bool:
106
+ """True if a subclass replaced the reader's own implementation of `name`."""
107
+ from .reader.adbc_reader import ADBCReader
108
+ from .reader.arrowsql_reader import ArrowSQLReader
109
+
110
+ impl = getattr(type(reader), name)
111
+ return all(
112
+ impl is not getattr(base, name, None) for base in (ArrowSQLReader, ADBCReader)
113
+ )
114
+
115
+
116
+ class Host:
117
+ """What the Rust load calls back into: the source, the Delta tables below the destination and
118
+ the logger. Data crosses as Arrow C streams."""
119
+
120
+ def __init__(self, infos: "WriteConfigAndInfos"):
121
+ self.infos = infos
122
+ self.reader = infos.source
123
+ self.dest = infos.destination
124
+ self.logger = infos.logger
125
+ self._copies = _overrides(self.reader, "source_write_sql_to_delta")
126
+
127
+ # source
128
+ def source_query(self, sql: str):
129
+ reader = self.reader
130
+ if hasattr(reader, "_source_arrow") and not _overrides(reader, "source_sql_to_py"):
131
+ return reader._source_arrow(sql, utf8_strings=False) # type: ignore
132
+ import pyarrow as pa
133
+
134
+ return pa.Table.from_pylist(reader.source_sql_to_py(sql))
135
+
136
+ def source_connection(self):
137
+ """(connection string, utf8_strings, partitions) if Rust reads the source itself through
138
+ arrowsql's core, None if the reads go through the reader's Python methods."""
139
+ from .reader.arrowsql_reader import ArrowSQLSource, core_reads
140
+
141
+ reader = self.reader
142
+ if (
143
+ self._copies
144
+ or not isinstance(reader, ArrowSQLSource)
145
+ or _overrides(reader, "source_sql_to_py")
146
+ or _overrides(reader, "_source_arrow")
147
+ or not core_reads(reader.connection_string)
148
+ ):
149
+ return None
150
+ return (reader.connection_string, reader.utf8_strings, reader.partitions)
151
+
152
+ def source_stream(self, sql: str, partition_on=None):
153
+ if self._copies or not hasattr(self.reader, "_source_arrow"):
154
+ return None
155
+ return self.reader._source_arrow( # type: ignore
156
+ sql,
157
+ utf8_strings=getattr(self.reader, "utf8_strings", False),
158
+ partition_on=partition_on,
159
+ )
160
+
161
+ def source_copy(self, sql: str, rel: str, mode, drift):
162
+ self.reader.source_write_sql_to_delta(
163
+ sql, self.dest / rel, mode, allow_schema_drift=drift
164
+ )
165
+
166
+ # target
167
+ def kernel_root(self):
168
+ """The target folder as a local path, whose Delta tables Rust reads itself (delta-kernel-rs)."""
169
+ from .destination.file_system import FileSystemDestination
170
+
171
+ if type(self.dest) is FileSystemDestination:
172
+ return str(self.dest.path.absolute())
173
+ return None
174
+
175
+ def _dt(self, rel: str, version=None):
176
+ from .reader.local_reader import _delta_table
177
+
178
+ dt = _delta_table(self.dest / rel)
179
+ if version is not None:
180
+ dt.load_as_version(version)
181
+ return dt
182
+
183
+ def table_exists(self, rel: str) -> bool:
184
+ return self.reader.local_delta_table_exists(self.dest / rel)
185
+
186
+ def path_exists(self, rel: str) -> bool:
187
+ return (self.dest / rel).exists()
188
+
189
+ def version(self, rel: str) -> int:
190
+ return self._dt(rel).version()
191
+
192
+ def table_id(self, rel: str) -> "Union[str, None]":
193
+ return str(self._dt(rel).metadata().id) # a UUID string (typed int by deltalake)
194
+
195
+ def read(self, rel: str, version, columns):
196
+ import pyarrow as pa
197
+
198
+ ds = self._dt(rel, version).to_pyarrow_dataset()
199
+ if columns is not None:
200
+ names = {n.lower(): n for n in ds.schema.names}
201
+ columns = [c if c in ds.schema.names else names.get(c.lower(), c) for c in columns]
202
+ return ds.scanner(columns=columns).to_reader()
203
+
204
+ def column_names(self, rel: str) -> "list[str]":
205
+ return [f.name for f in self._dt(rel).schema().fields]
206
+
207
+ def write(self, rel: str, stream, mode, drift):
208
+ import pyarrow as pa
209
+
210
+ self.reader._write_reader_to_delta( # type: ignore
211
+ pa.RecordBatchReader.from_stream(stream),
212
+ self.dest / rel,
213
+ mode,
214
+ drift,
215
+ all_nullable=True,
216
+ )
217
+
218
+ def restore(self, rel: str, version: int):
219
+ self.reader.get_local_delta_ops(self.dest / rel).restore(version)
220
+
221
+ def remove(self, rel: str, recursive: bool):
222
+ (self.dest / rel).remove(recursive)
223
+
224
+ def vacuum(self, rel: str):
225
+ self.reader.get_local_delta_ops(self.dest / rel).vacuum()
226
+
227
+ def mkdir(self, rel: str):
228
+ (self.dest / rel).mkdir()
229
+
230
+ def acquire_lock(self, rel: str) -> str:
231
+ from .load_lock import acquire
232
+
233
+ return acquire(self.dest / rel, self.logger)
234
+
235
+ def release_lock(self, rel: str, token: str):
236
+ from .load_lock import release
237
+
238
+ release(self.dest / rel, token, self.logger)
239
+
240
+ def log(self, level: str, msg: str, kwargs: dict):
241
+ getattr(self.logger, level)(msg, **kwargs)
242
+
243
+ def fail_point(self, name: str):
244
+ return fail_point(name)
245
+
246
+
247
+ def load_spec(infos: "WriteConfigAndInfos") -> dict[str, Any]:
248
+ from .db_to_delta import (
249
+ _shortcut_allowed,
250
+ _source_schema_fingerprint,
251
+ _watermark_parts,
252
+ _watermark_sql,
253
+ )
254
+
255
+ wc = infos.write_config
256
+ cols = list(infos.col_infos)
257
+
258
+ def _index(c) -> int:
259
+ for i, x in enumerate(cols):
260
+ if x is c:
261
+ return i
262
+ for i, x in enumerate(cols):
263
+ if x.column_name == c.column_name:
264
+ return i
265
+ raise ValueError(f"column {c.column_name} not in the columns of the load")
266
+
267
+ return {
268
+ "dialect": wc.dialect,
269
+ "source": source_spec(infos),
270
+ "type_map": type_map_spec(wc),
271
+ "no_trim": wc.no_trim,
272
+ "columns": [column_spec(c, wc) for c in cols],
273
+ "pks": [_index(c) for c in infos.pk_cols],
274
+ "delta_col": _index(infos.delta_col) if infos.delta_col is not None else None,
275
+ "load_mode": wc.load_mode,
276
+ "allow_schema_drift": wc.allow_schema_drift,
277
+ "no_complex_entries_load": wc.no_complex_entries_load,
278
+ "schema_fingerprint": _source_schema_fingerprint(infos),
279
+ "shortcut_allowed": _shortcut_allowed(infos),
280
+ "watermark_sql": _watermark_sql(infos),
281
+ "watermark_parts": _watermark_parts(infos),
282
+ }
283
+
284
+
285
+ def _result(res: dict):
286
+ from .load_result import (
287
+ AppendOnlyLoadResult,
288
+ DeltaLoadResult,
289
+ FullLoadResult,
290
+ NoLoadResult,
291
+ )
292
+
293
+ t = res["type"]
294
+ if t == "full":
295
+ return FullLoadResult()
296
+ if t == "none":
297
+ return NoLoadResult()
298
+ if t == "append_only":
299
+ return AppendOnlyLoadResult()
300
+ r = DeltaLoadResult()
301
+ r.starting_local_state = res["starting_local_state"]
302
+ r.starting_source_state = res["starting_source_state"]
303
+ if "end_source_state" in res:
304
+ r.end_source_state = res["end_source_state"]
305
+ r.dirty = res["dirty"]
306
+ return r
307
+
308
+
309
+ def execute(infos: "WriteConfigAndInfos"):
310
+ """Runs the load in Rust (`_core.run_load`); the GIL is released while it runs."""
311
+ from . import _core
312
+
313
+ try:
314
+ return _result(_core.run_load(Host(infos), load_spec(infos)))
315
+ except BaseException as e:
316
+ err = as_load_error(e)
317
+ if err is e:
318
+ raise
319
+ raise err from e
320
+ finally:
321
+ infos.logger.flush()
322
+
323
+
324
+ def restore_last_pk(infos: "WriteConfigAndInfos") -> bool:
325
+ from . import _core
326
+
327
+ return _core.restore_last_pk(Host(infos), load_spec(infos))
@@ -0,0 +1,56 @@
1
+ from typing import Any
2
+ from .write_init import WriteConfigAndInfos, DBDeltaPathConfigs
3
+ from .write_utils.restore_pk import create_last_pk_version_view
4
+ import sqlglot as sg
5
+ import sqlglot.expressions as ex
6
+
7
+
8
+ class InconsistentPrimaryKeyError(Exception):
9
+ """Exception raised when primary keys are inconsistent."""
10
+
11
+ def __init__(self, message, invalid_data: list[dict[str, Any]]):
12
+ self.message = message
13
+ self.invalid_data = invalid_data
14
+ super().__init__(self.message)
15
+
16
+
17
+ def check_latest_pk(infos: WriteConfigAndInfos, raise_if_not_consistent=True):
18
+ if not infos.delta_col or not infos.pk_cols:
19
+ raise ValueError("Primary keys and delta column must be defined")
20
+ postfix = "_" + str(abs(hash(str(infos.destination))))
21
+ lpk_view = "lastest_pk_" + postfix
22
+ infos.source.local_register_update_view(
23
+ infos.destination / "delta_load" / DBDeltaPathConfigs.LATEST_PK_VERSION,
24
+ lpk_view,
25
+ )
26
+ _, view_name, success = create_last_pk_version_view(
27
+ infos, view_prefix="v_" + postfix
28
+ )
29
+ d_cols = list(infos.pk_cols) + [infos.delta_col]
30
+ col_names = [infos.write_config.get_target_name(p) for p in d_cols]
31
+ assert success
32
+ assert view_name is not None
33
+ query1 = sg.except_(
34
+ sg.from_(ex.table_(lpk_view, alias="lpk"))
35
+ .select(*[ex.column(c, "lpk", quoted=True) for c in col_names])
36
+ .select(ex.convert("added in persisted data"), append=True),
37
+ sg.from_(ex.table_(view_name, alias="rs"))
38
+ .select(*[ex.column(c, "rs", quoted=True) for c in col_names])
39
+ .select(ex.convert("added in persisted data"), append=True),
40
+ )
41
+ query2 = sg.except_(
42
+ sg.from_(ex.table_(view_name, alias="rs"))
43
+ .select(*[ex.column(c, "rs", quoted=True) for c in col_names])
44
+ .select(ex.convert("missing in persisted data"), append=True),
45
+ sg.from_(ex.table_(lpk_view, alias="lpk"))
46
+ .select(*[ex.column(c, "lpk", quoted=True) for c in col_names])
47
+ .select(ex.convert("missing in persisted data"), append=True),
48
+ )
49
+ result = infos.source.local_execute_sql_to_py(
50
+ ex.union(query1, query2, distinct=False)
51
+ )
52
+ if result:
53
+ print(result)
54
+ if raise_if_not_consistent:
55
+ raise InconsistentPrimaryKeyError("Primary keys are not consistent", result)
56
+ return result