odbc2deltalake2 0.1.0a0__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.
Files changed (62) hide show
  1. odbc2deltalake2-0.1.0a0/.github/workflows/python-test.yml +69 -0
  2. odbc2deltalake2-0.1.0a0/.github/workflows/workflow.yml +32 -0
  3. odbc2deltalake2-0.1.0a0/.gitignore +172 -0
  4. odbc2deltalake2-0.1.0a0/.python-version +1 -0
  5. odbc2deltalake2-0.1.0a0/LICENSE +21 -0
  6. odbc2deltalake2-0.1.0a0/PKG-INFO +35 -0
  7. odbc2deltalake2-0.1.0a0/docs/superpowers/specs/2026-09-09-odbc2deltalake2-design.md +138 -0
  8. odbc2deltalake2-0.1.0a0/odbc2deltalake2/__init__.py +25 -0
  9. odbc2deltalake2-0.1.0a0/odbc2deltalake2/consistency.py +56 -0
  10. odbc2deltalake2-0.1.0a0/odbc2deltalake2/db_to_delta.py +1327 -0
  11. odbc2deltalake2-0.1.0a0/odbc2deltalake2/delta_logger.py +162 -0
  12. odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/__init__.py +1 -0
  13. odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/azure.py +79 -0
  14. odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/azure_utils.py +76 -0
  15. odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/databricks.py +72 -0
  16. odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/destination.py +53 -0
  17. odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/file_system.py +55 -0
  18. odbc2deltalake2-0.1.0a0/odbc2deltalake2/load_infos.py +70 -0
  19. odbc2deltalake2-0.1.0a0/odbc2deltalake2/load_result.py +40 -0
  20. odbc2deltalake2-0.1.0a0/odbc2deltalake2/logging.py +21 -0
  21. odbc2deltalake2-0.1.0a0/odbc2deltalake2/metadata.py +205 -0
  22. odbc2deltalake2-0.1.0a0/odbc2deltalake2/odbc_utils.py +53 -0
  23. odbc2deltalake2-0.1.0a0/odbc2deltalake2/query.py +55 -0
  24. odbc2deltalake2-0.1.0a0/odbc2deltalake2/read_utils/delta_rs.py +167 -0
  25. odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/__init__.py +1 -0
  26. odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/adbc_reader.py +388 -0
  27. odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/odbc_reader.py +369 -0
  28. odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/reader.py +123 -0
  29. odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/spark_reader.py +356 -0
  30. odbc2deltalake2-0.1.0a0/odbc2deltalake2/sql_glot_utils.py +56 -0
  31. odbc2deltalake2-0.1.0a0/odbc2deltalake2/sql_schema.py +10 -0
  32. odbc2deltalake2-0.1.0a0/odbc2deltalake2/utils.py +32 -0
  33. odbc2deltalake2-0.1.0a0/odbc2deltalake2/write_init.py +303 -0
  34. odbc2deltalake2-0.1.0a0/odbc2deltalake2/write_utils/__init__.py +0 -0
  35. odbc2deltalake2-0.1.0a0/odbc2deltalake2/write_utils/restore_pk.py +228 -0
  36. odbc2deltalake2-0.1.0a0/pyproject.toml +91 -0
  37. odbc2deltalake2-0.1.0a0/test_server/__init__.py +112 -0
  38. odbc2deltalake2-0.1.0a0/test_server/mssql.env +4 -0
  39. odbc2deltalake2-0.1.0a0/test_server/postgres.env +3 -0
  40. odbc2deltalake2-0.1.0a0/test_spark_import.py +13 -0
  41. odbc2deltalake2-0.1.0a0/tester.py +4 -0
  42. odbc2deltalake2-0.1.0a0/tests/__init__.py +0 -0
  43. odbc2deltalake2-0.1.0a0/tests/conftest.py +236 -0
  44. odbc2deltalake2-0.1.0a0/tests/jar/mssql-jdbc-12.6.1.jre11.jar +0 -0
  45. odbc2deltalake2-0.1.0a0/tests/jar/postgresql-42.7.7.jar +0 -0
  46. odbc2deltalake2-0.1.0a0/tests/sqls/init_mssql.sql +194 -0
  47. odbc2deltalake2-0.1.0a0/tests/sqls/init_postgres.sql +175 -0
  48. odbc2deltalake2-0.1.0a0/tests/test_01_first_full.py +132 -0
  49. odbc2deltalake2-0.1.0a0/tests/test_02_full_load.py +106 -0
  50. odbc2deltalake2-0.1.0a0/tests/test_03_delta.py +220 -0
  51. odbc2deltalake2-0.1.0a0/tests/test_04_strange_delta.py +229 -0
  52. odbc2deltalake2-0.1.0a0/tests/test_05_conversion.py +125 -0
  53. odbc2deltalake2-0.1.0a0/tests/test_06_append_inserts.py +80 -0
  54. odbc2deltalake2-0.1.0a0/tests/test_07_simple_delta.py +77 -0
  55. odbc2deltalake2-0.1.0a0/tests/test_08_simple_delta_check.py +112 -0
  56. odbc2deltalake2-0.1.0a0/tests/test_09_query.py +127 -0
  57. odbc2deltalake2-0.1.0a0/tests/test_10_consistency_fix.py +85 -0
  58. odbc2deltalake2-0.1.0a0/tests/test_11_schema_drift.py +102 -0
  59. odbc2deltalake2-0.1.0a0/tests/test_12_insert_while_load.py +87 -0
  60. odbc2deltalake2-0.1.0a0/tests/test_dbutils.py +29 -0
  61. odbc2deltalake2-0.1.0a0/tests/utils.py +156 -0
  62. odbc2deltalake2-0.1.0a0/uv.lock +4768 -0
@@ -0,0 +1,69 @@
1
+ name: Python Test
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ paths-ignore: ["README.md", "docs", ".github"]
7
+ pull_request:
8
+ branches: ["main"]
9
+ paths-ignore: ["README.md", "docs", ".github"]
10
+
11
+ jobs:
12
+ test_types:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ python-version: ["3.11"]
18
+ steps:
19
+ - uses: actions/checkout@v3
20
+ - name: Set up Python ${{ matrix.python-version }}
21
+ uses: actions/setup-python@v3
22
+ with:
23
+ python-version: ${{ matrix.python-version }}
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ curl -LsSf https://astral.sh/uv/install.sh | sh
28
+ - name: Install all dependencies
29
+ run: uv sync --all-extras --all-groups
30
+
31
+ - name: ty
32
+ run: uv run ty check odbc2deltalake2
33
+ test:
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ fail-fast: false
37
+ matrix:
38
+ python-version: ["3.11"]
39
+ test_config: ["local", "spark", "azure"]
40
+ database: ["postgres", "mssql"]
41
+
42
+ steps:
43
+ - uses: actions/checkout@v3
44
+ - name: Set up Python ${{ matrix.python-version }}
45
+ uses: actions/setup-python@v3
46
+ with:
47
+ python-version: ${{ matrix.python-version }}
48
+ - uses: Yarden-zamir/install-mssql-odbc@main
49
+ with:
50
+ ODBC_VERSION: 18
51
+ if: matrix.database == 'mssql'
52
+ - name: Install dependencies
53
+ run: |
54
+ python -m pip install --upgrade pip
55
+ curl -LsSf https://astral.sh/uv/install.sh | sh
56
+ - name: Install project dependencies incl Spark
57
+ if: matrix.test_config == 'spark'
58
+ run: uv sync --group test --group dev --group spark --extra ${{matrix.database}}
59
+ - name: Test beeing able to import
60
+ if: matrix.test_config == 'spark'
61
+ run: uv run ./test_spark_import.py
62
+ - name: Install project dependencies
63
+ if: matrix.test_config != 'spark'
64
+ run: uv sync --extra local --extra local_azure --group test --group dev --extra ${{matrix.database}}
65
+ - name: Test with pytest
66
+ run: uv run -m pytest --maxfail=3 --cov=odbc2deltalake2 tests
67
+ env:
68
+ ODBCLAKE_TEST_CONFIGURATION: ${{ matrix.test_config }}
69
+ ODBCLAKE_TEST_SOURCE_SERVER: ${{matrix.database}}
@@ -0,0 +1,32 @@
1
+ # This workflow will upload a Python Package using Twine when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ name: Upload Python Package
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ jobs:
11
+ deploy:
12
+ runs-on: ubuntu-latest
13
+ environment:
14
+ name: pypi
15
+ url: https://pypi.org/p/odbc2deltalake2
16
+ permissions:
17
+ id-token: write
18
+ steps:
19
+ - uses: actions/checkout@v3
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v3
22
+ with:
23
+ python-version: "3.11"
24
+ - name: Install dependencies
25
+ run: |
26
+ python -m pip install --upgrade pip
27
+ curl -LsSf https://astral.sh/uv/install.sh | sh
28
+ uv sync --all-extras
29
+ - name: Build package
30
+ run: uv build
31
+ - name: Publish package to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,172 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/#use-with-ide
110
+ .pdm.toml
111
+
112
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113
+ __pypackages__/
114
+
115
+ # Celery stuff
116
+ celerybeat-schedule
117
+ celerybeat.pid
118
+
119
+ # SageMath parsed files
120
+ *.sage.py
121
+
122
+ # Environments
123
+ .env
124
+ .venv
125
+ env/
126
+ venv/
127
+ ENV/
128
+ env.bak/
129
+ venv.bak/
130
+
131
+ # Spyder project settings
132
+ .spyderproject
133
+ .spyproject
134
+
135
+ # Rope project settings
136
+ .ropeproject
137
+
138
+ # mkdocs documentation
139
+ /site
140
+
141
+ # mypy
142
+ .mypy_cache/
143
+ .dmypy.json
144
+ dmypy.json
145
+
146
+ # Pyre type checker
147
+ .pyre/
148
+
149
+ # pytype static type analyzer
150
+ .pytype/
151
+
152
+ # Cython debug symbols
153
+ cython_debug/
154
+
155
+ # PyCharm
156
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
159
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160
+ #.idea/
161
+
162
+ tests/_data
163
+ tests/_db
164
+ __azurite*
165
+ __blob*
166
+ debug.sql
167
+ requirements.txt
168
+ prof
169
+ repo.py
170
+
171
+ # claude / serena tooling state, not project source
172
+ .serena/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Building Material Suisse
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,35 @@
1
+ Metadata-Version: 2.5
2
+ Name: odbc2deltalake2
3
+ Version: 0.1.0a0
4
+ Summary: Faster fork of odbc2deltalake: ODBC/ADBC/Spark to SCD2 Delta tables
5
+ Author-email: Adrian Ehrsam <adrian.ehrsam@bmsuisse.ch>, Dominik Peter <dominik.peter@bmsuisse.ch>
6
+ License-File: LICENSE
7
+ Requires-Python: ~=3.9
8
+ Requires-Dist: azure-identity<2,>=1.15.0
9
+ Requires-Dist: pydantic>=1.10.0
10
+ Requires-Dist: sqlglot>=23.17.0
11
+ Provides-Extra: adbc
12
+ Requires-Dist: adbc-driver-manager>=1.7.0; extra == 'adbc'
13
+ Provides-Extra: local
14
+ Requires-Dist: arrow-odbc; extra == 'local'
15
+ Requires-Dist: arrow-odbc>=5.0.0; extra == 'local'
16
+ Requires-Dist: deltalake; extra == 'local'
17
+ Requires-Dist: deltalake2db[deltalake]>=0.9.3; extra == 'local'
18
+ Requires-Dist: deltalake>=0.17.1; extra == 'local'
19
+ Requires-Dist: duckdb; extra == 'local'
20
+ Requires-Dist: pyarrow>=16.1; extra == 'local'
21
+ Requires-Dist: pyodbc<6,>=5.1.0; extra == 'local'
22
+ Provides-Extra: local-azure
23
+ Requires-Dist: adlfs; extra == 'local-azure'
24
+ Requires-Dist: adlfs<2025,>=2024.2.0; extra == 'local-azure'
25
+ Requires-Dist: arrow-odbc>=5.0.0; extra == 'local-azure'
26
+ Requires-Dist: azure-identity; extra == 'local-azure'
27
+ Requires-Dist: azure-storage-file-datalake; extra == 'local-azure'
28
+ Requires-Dist: deltalake2db[deltalake]>=0.9.3; extra == 'local-azure'
29
+ Requires-Dist: deltalake>=0.17.1; extra == 'local-azure'
30
+ Requires-Dist: pyarrow>=16.1; extra == 'local-azure'
31
+ Requires-Dist: pyodbc<6,>=5.1.0; extra == 'local-azure'
32
+ Provides-Extra: mssql
33
+ Requires-Dist: pyodbc<6,>=5.1.0; extra == 'mssql'
34
+ Provides-Extra: postgres
35
+ Requires-Dist: adbc-driver-postgresql>=1.7.0; extra == 'postgres'
@@ -0,0 +1,138 @@
1
+ # odbc2deltalake2 — Design Spec
2
+
3
+ ## Goal
4
+
5
+ Replace [bmsuisse/odbc2deltalake](https://github.com/bmsuisse/odbc2deltalake) with a faster,
6
+ cleaner package (`odbc2deltalake2`), published to PyPI, drop-in for the production
7
+ Databricks usage in Fabricks.Runtime (`extractors/sql_server/extract_table.ipynb`,
8
+ `SparkReader` + `DatabricksDestination`, onetrade SQL Server extraction).
9
+
10
+ "Faster" is decided by measurement, not assumption. No Rust/PyO3 rewrite — the
11
+ production path is Spark/JDBC on Databricks; the heavy lifting already runs in the
12
+ JVM, not Python. A prior finding confirmed `SparkReader`'s JDBC read has zero
13
+ partitioning options (`partitionColumn`/`numPartitions`/`lowerBound`/`upperBound`),
14
+ meaning every table reads through a single-threaded JDBC connection today — the
15
+ leading suspect, to be confirmed by Phase 0.
16
+
17
+ ## Non-goals
18
+
19
+ - Rust/PyO3 core rewrite (delta writes already go through native `delta-rs`; the
20
+ Spark production path bypasses Python compute entirely).
21
+ - Touching the ODBC/ADBC (non-Databricks) reader path in 1.0 — left as-is,
22
+ correctness-only, ported and tested but not speed-tuned.
23
+ - Any refactor of `db_to_delta.py` beyond what's needed to unblock Phase 3 speed work.
24
+
25
+ ## Phases
26
+
27
+ ### Phase 0 — Baseline measurement (no code changes)
28
+
29
+ Run the *published* `odbc2deltalake` against 5 real onetrade tables on the
30
+ Databricks premium workspace: 1 tiny (<100k rows), 2 mid (1-10M), 2 large (>50M,
31
+ one routed through `linked_server_proxy`, one direct). 3 runs each, off-peak,
32
+ using the production `WriteConfig` load modes actually in use (`simple_delta`,
33
+ `force_full`, `append`).
34
+
35
+ Instrument read / diff-merge / write stage timings separately (Spark UI stage
36
+ timings + `time.perf_counter` around orchestrator stages). Record cluster config,
37
+ DBR version, row counts, output bytes.
38
+
39
+ **Exit:** results committed as CSV/Delta table + a short "where the time goes"
40
+ writeup. This decides Phase 3's priority order — if the plan below turns out
41
+ wrong once we have numbers, the plan changes, not the numbers.
42
+
43
+ ### Phase 1 — Repo bootstrap
44
+
45
+ - Copy `odbc2deltalake` source in as the starting point (MIT licensed — keep
46
+ original LICENSE + copyright notice, credit in README).
47
+ - Rename package to `odbc2deltalake2`. `uv`-managed `pyproject.toml`.
48
+ - Apply [bmsuisse coding-guidelines-python](https://github.com/bmsuisse/skills/tree/main/skills/coding-guidelines-python):
49
+ full typing, `from __future__ import annotations`, `uv run ty check` clean,
50
+ dataclasses/`TypedDict` over raw dicts, `Literal`/`Enum` over string constants,
51
+ no mutable defaults, no module-level mutable state.
52
+ - Set up [prek](https://github.com/bmsuisse/skills/tree/main/skills/prek):
53
+ ruff check+format, `check_files.py` line-limit guard, `prek install`.
54
+ - Port the existing test suite (`tests/test_01`...`test_12`, `test_dbutils.py`,
55
+ MSSQL/Postgres test-server fixtures) — **must pass green, unmodified behavior,
56
+ before anything else.**
57
+ - Publish `0.1.0a0` to PyPI immediately to prove the publish pipeline works.
58
+
59
+ **Exit:** `pip install odbc2deltalake2` on a cluster reproduces Phase 0 numbers
60
+ within noise (this is the correctness guardrail for every phase after).
61
+
62
+ ### Phase 2 — Architecture audit (time-boxed, 3 days)
63
+
64
+ Apply the aihero-style audit methodology to `db_to_delta.py` and `write_init.py`:
65
+ tag findings Strong / Worth-exploring / Speculative. Fix **only Strong findings
66
+ that block Phase 3** (e.g. reader/writer seams needed to inject partition
67
+ options or per-stage timing). Everything else goes to a backlog, not into scope.
68
+
69
+ Ponytail applies throughout: no speculative abstractions, shortest diff that
70
+ unblocks Phase 3.
71
+
72
+ **Exit:** tests still green, Phase 0 numbers unchanged (refactor caused no
73
+ regression or improvement — it's structural, not a speed change), audit doc
74
+ committed, backlog list captured for later.
75
+
76
+ ### Phase 3 — Speed work
77
+
78
+ Metrics per table: wall clock (primary), rows/sec, SQL-Server-side CPU/wait
79
+ during the run (safety check on the source), DBU cost (secondary).
80
+
81
+ Priority order, by confidence (from Phase 0 data, adjust as needed):
82
+
83
+ 1. **Strong** — Partitioned JDBC reads: auto-pick partition column (PK
84
+ int/identity/rowversion/date), one `MIN/MAX` query for bounds,
85
+ `numPartitions` = min(executor cores, configurable cap ~32) to avoid
86
+ overwhelming a shared SQL Server. Fallback to single-partition read when no
87
+ suitable column exists or bounds indicate skew.
88
+ 2. **Strong** — JDBC `fetchsize` tuning (default is tiny; test 10k-100k).
89
+ 3. **Strong** — Concurrent multi-table orchestration via `ThreadPool`, bounded
90
+ by a **global connection budget** shared across tables (not per-table
91
+ `numPartitions` × table count unbounded — that would multiply source load).
92
+ 4. **Worth-exploring** — SCD2 diff/merge optimization (avoid recomputing full
93
+ hashes, broadcast small sides, `MERGE` predicate pushdown) — only pursued if
94
+ Phase 0 shows diff/merge as a significant share of wall time.
95
+ 5. **Worth-exploring** — Delta write tuning (`optimizeWrite`, target file size,
96
+ dropping unneeded `count()` actions).
97
+ 6. **Speculative** — Lakehouse Federation as an alternate reader (spike only,
98
+ requires DBR ≥16.1 / Serverless SQL Warehouse).
99
+
100
+ Each technique: A/B run against the Phase 0 baseline on the same 5 tables,
101
+ plus a correctness check (row count + hash of SCD2 output matches baseline) —
102
+ not just green pytest, since these changes touch load-mode logic directly.
103
+
104
+ New tests required for every technique added here (partitioning column
105
+ selection, connection budget enforcement, skew fallback), on top of the ported
106
+ Phase 1 suite staying green throughout.
107
+
108
+ **Exit:** each technique's A/B result recorded; net improvement vs. baseline
109
+ established with real numbers, not estimates.
110
+
111
+ ### Phase 4 — Release
112
+
113
+ - Publish `1.0.0`. README covers migration from `odbc2deltalake`, tuning knobs,
114
+ and results from Phase 0/3.
115
+ - Switch Fabricks.Runtime's onetrade extraction to `odbc2deltalake2` behind a
116
+ flag; one week of shadow runs before full cutover.
117
+
118
+ **Exit:** production onetrade extraction running on `odbc2deltalake2`, with
119
+ Phase 0 numbers as the documented before/after.
120
+
121
+ ## Risks
122
+
123
+ - **Production SQL Server load during benchmarking/tuning:** off-peak only,
124
+ hard partition-count cap, monitor `sys.dm_exec_requests`, coordinate timing
125
+ with DBA — the `linked_server_proxy` hop doubles the load surface (both
126
+ servers take the hit).
127
+ - **Partition-column skew:** identity columns with large deleted ranges produce
128
+ empty/oversized partitions — needs a skew check and single-partition fallback.
129
+ - **Benchmark noise:** shared cluster and shared source mean median-of-3 runs,
130
+ not single-run or mean.
131
+ - **License:** original repo is MIT — no restriction on copying, keep the
132
+ notice.
133
+
134
+ ## Explicitly deferred / open
135
+
136
+ - ODBC/ADBC reader path: untouched in 1.0, ported + tested for correctness only.
137
+ - Correctness oracle beyond pytest for SCD2 output equivalence when diff/merge
138
+ logic changes in Phase 3 — needs defining before Phase 3.4 starts, not before.
@@ -0,0 +1,25 @@
1
+ from .write_init import (
2
+ make_writer,
3
+ DBDeltaPathConfigs,
4
+ WriteConfig,
5
+ DEFAULT_DATA_TYPE_MAP,
6
+ WriteConfigAndInfos,
7
+ )
8
+ from .reader.reader import DataSourceReader
9
+ from .destination.destination import Destination
10
+ from pathlib import Path
11
+ from typing import Union
12
+
13
+
14
+ def write_db_to_delta(
15
+ source: Union[DataSourceReader, str],
16
+ table_or_query: tuple[str, str],
17
+ destination: Union[Destination, Path],
18
+ write_config: Union[WriteConfig, None] = None,
19
+ ):
20
+ return make_writer(
21
+ source=source,
22
+ table_or_query=table_or_query,
23
+ destination=destination,
24
+ write_config=write_config,
25
+ ).execute()
@@ -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