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.
- odbc2deltalake2-0.1.0a0/.github/workflows/python-test.yml +69 -0
- odbc2deltalake2-0.1.0a0/.github/workflows/workflow.yml +32 -0
- odbc2deltalake2-0.1.0a0/.gitignore +172 -0
- odbc2deltalake2-0.1.0a0/.python-version +1 -0
- odbc2deltalake2-0.1.0a0/LICENSE +21 -0
- odbc2deltalake2-0.1.0a0/PKG-INFO +35 -0
- odbc2deltalake2-0.1.0a0/docs/superpowers/specs/2026-09-09-odbc2deltalake2-design.md +138 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/__init__.py +25 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/consistency.py +56 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/db_to_delta.py +1327 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/delta_logger.py +162 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/__init__.py +1 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/azure.py +79 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/azure_utils.py +76 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/databricks.py +72 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/destination.py +53 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/destination/file_system.py +55 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/load_infos.py +70 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/load_result.py +40 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/logging.py +21 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/metadata.py +205 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/odbc_utils.py +53 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/query.py +55 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/read_utils/delta_rs.py +167 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/__init__.py +1 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/adbc_reader.py +388 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/odbc_reader.py +369 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/reader.py +123 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/reader/spark_reader.py +356 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/sql_glot_utils.py +56 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/sql_schema.py +10 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/utils.py +32 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/write_init.py +303 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/write_utils/__init__.py +0 -0
- odbc2deltalake2-0.1.0a0/odbc2deltalake2/write_utils/restore_pk.py +228 -0
- odbc2deltalake2-0.1.0a0/pyproject.toml +91 -0
- odbc2deltalake2-0.1.0a0/test_server/__init__.py +112 -0
- odbc2deltalake2-0.1.0a0/test_server/mssql.env +4 -0
- odbc2deltalake2-0.1.0a0/test_server/postgres.env +3 -0
- odbc2deltalake2-0.1.0a0/test_spark_import.py +13 -0
- odbc2deltalake2-0.1.0a0/tester.py +4 -0
- odbc2deltalake2-0.1.0a0/tests/__init__.py +0 -0
- odbc2deltalake2-0.1.0a0/tests/conftest.py +236 -0
- odbc2deltalake2-0.1.0a0/tests/jar/mssql-jdbc-12.6.1.jre11.jar +0 -0
- odbc2deltalake2-0.1.0a0/tests/jar/postgresql-42.7.7.jar +0 -0
- odbc2deltalake2-0.1.0a0/tests/sqls/init_mssql.sql +194 -0
- odbc2deltalake2-0.1.0a0/tests/sqls/init_postgres.sql +175 -0
- odbc2deltalake2-0.1.0a0/tests/test_01_first_full.py +132 -0
- odbc2deltalake2-0.1.0a0/tests/test_02_full_load.py +106 -0
- odbc2deltalake2-0.1.0a0/tests/test_03_delta.py +220 -0
- odbc2deltalake2-0.1.0a0/tests/test_04_strange_delta.py +229 -0
- odbc2deltalake2-0.1.0a0/tests/test_05_conversion.py +125 -0
- odbc2deltalake2-0.1.0a0/tests/test_06_append_inserts.py +80 -0
- odbc2deltalake2-0.1.0a0/tests/test_07_simple_delta.py +77 -0
- odbc2deltalake2-0.1.0a0/tests/test_08_simple_delta_check.py +112 -0
- odbc2deltalake2-0.1.0a0/tests/test_09_query.py +127 -0
- odbc2deltalake2-0.1.0a0/tests/test_10_consistency_fix.py +85 -0
- odbc2deltalake2-0.1.0a0/tests/test_11_schema_drift.py +102 -0
- odbc2deltalake2-0.1.0a0/tests/test_12_insert_while_load.py +87 -0
- odbc2deltalake2-0.1.0a0/tests/test_dbutils.py +29 -0
- odbc2deltalake2-0.1.0a0/tests/utils.py +156 -0
- 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
|