adbc-replicate 0.1.0__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 (47) hide show
  1. adbc_replicate-0.1.0/.dockerignore +9 -0
  2. adbc_replicate-0.1.0/.github/workflows/ci.yml +218 -0
  3. adbc_replicate-0.1.0/.github/workflows/release.yml +147 -0
  4. adbc_replicate-0.1.0/.gitignore +15 -0
  5. adbc_replicate-0.1.0/CHANGELOG.md +41 -0
  6. adbc_replicate-0.1.0/CLAUDE.md +68 -0
  7. adbc_replicate-0.1.0/Dockerfile +27 -0
  8. adbc_replicate-0.1.0/LICENSE +202 -0
  9. adbc_replicate-0.1.0/PKG-INFO +650 -0
  10. adbc_replicate-0.1.0/README.md +594 -0
  11. adbc_replicate-0.1.0/charts/adbc-replicate/Chart.yaml +14 -0
  12. adbc_replicate-0.1.0/charts/adbc-replicate/templates/NOTES.txt +10 -0
  13. adbc_replicate-0.1.0/charts/adbc-replicate/templates/_helpers.tpl +25 -0
  14. adbc_replicate-0.1.0/charts/adbc-replicate/templates/configmap.yaml +26 -0
  15. adbc_replicate-0.1.0/charts/adbc-replicate/templates/cronjob.yaml +104 -0
  16. adbc_replicate-0.1.0/charts/adbc-replicate/templates/secret.yaml +13 -0
  17. adbc_replicate-0.1.0/charts/adbc-replicate/values.yaml +86 -0
  18. adbc_replicate-0.1.0/docker-compose.yml +37 -0
  19. adbc_replicate-0.1.0/pyproject.toml +93 -0
  20. adbc_replicate-0.1.0/src/adbc_replicate/__init__.py +3 -0
  21. adbc_replicate-0.1.0/src/adbc_replicate/arrowutil.py +112 -0
  22. adbc_replicate-0.1.0/src/adbc_replicate/cli.py +302 -0
  23. adbc_replicate-0.1.0/src/adbc_replicate/config.py +348 -0
  24. adbc_replicate-0.1.0/src/adbc_replicate/engine.py +497 -0
  25. adbc_replicate-0.1.0/src/adbc_replicate/events.py +57 -0
  26. adbc_replicate-0.1.0/src/adbc_replicate/matching.py +69 -0
  27. adbc_replicate-0.1.0/src/adbc_replicate/models.py +90 -0
  28. adbc_replicate-0.1.0/src/adbc_replicate/planner.py +183 -0
  29. adbc_replicate-0.1.0/src/adbc_replicate/sources/__init__.py +24 -0
  30. adbc_replicate-0.1.0/src/adbc_replicate/sources/base.py +175 -0
  31. adbc_replicate-0.1.0/src/adbc_replicate/sources/duckdb.py +93 -0
  32. adbc_replicate-0.1.0/src/adbc_replicate/sources/oracle.py +180 -0
  33. adbc_replicate-0.1.0/src/adbc_replicate/sources/postgresql.py +154 -0
  34. adbc_replicate-0.1.0/src/adbc_replicate/sources/snowflake.py +138 -0
  35. adbc_replicate-0.1.0/src/adbc_replicate/state.py +196 -0
  36. adbc_replicate-0.1.0/src/adbc_replicate/targets/__init__.py +5 -0
  37. adbc_replicate-0.1.0/src/adbc_replicate/targets/duckdb.py +139 -0
  38. adbc_replicate-0.1.0/src/adbc_replicate/views.py +162 -0
  39. adbc_replicate-0.1.0/tests/integration/conftest.py +227 -0
  40. adbc_replicate-0.1.0/tests/integration/test_oracle_to_gizmosql.py +207 -0
  41. adbc_replicate-0.1.0/tests/integration/test_postgres_to_gizmosql.py +132 -0
  42. adbc_replicate-0.1.0/tests/integration/test_snowflake_to_gizmosql.py +110 -0
  43. adbc_replicate-0.1.0/tests/unit/test_config.py +101 -0
  44. adbc_replicate-0.1.0/tests/unit/test_engine_duckdb.py +255 -0
  45. adbc_replicate-0.1.0/tests/unit/test_matching_and_arrow.py +72 -0
  46. adbc_replicate-0.1.0/tests/unit/test_readme.py +39 -0
  47. adbc_replicate-0.1.0/tests/unit/test_views.py +87 -0
@@ -0,0 +1,9 @@
1
+ .venv
2
+ .git
3
+ .github
4
+ tests
5
+ charts
6
+ dist
7
+ **/__pycache__
8
+ *.duckdb
9
+ .env
@@ -0,0 +1,218 @@
1
+ name: ci
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+ # release.yml runs this whole workflow against the tagged commit before publishing anything.
9
+ workflow_call:
10
+
11
+ permissions:
12
+ contents: read
13
+
14
+ concurrency:
15
+ group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
16
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
17
+
18
+ jobs:
19
+ lint:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v7
23
+ - uses: actions/setup-python@v7
24
+ with:
25
+ python-version: '3.13'
26
+ - run: pip install ruff
27
+ - run: ruff check src tests
28
+
29
+ unit:
30
+ name: unit (${{ matrix.os }}, py${{ matrix.python }})
31
+ runs-on: ${{ matrix.os }}
32
+ strategy:
33
+ fail-fast: false
34
+ matrix:
35
+ os: [ubuntu-latest]
36
+ python: ['3.11', '3.12', '3.13', '3.14']
37
+ include:
38
+ - os: macos-latest
39
+ python: '3.13'
40
+ - os: windows-latest
41
+ python: '3.13'
42
+ steps:
43
+ - uses: actions/checkout@v7
44
+ - uses: actions/setup-python@v7
45
+ with:
46
+ python-version: ${{ matrix.python }}
47
+ - run: pip install -e ".[dev]"
48
+ - run: pytest tests/unit -v
49
+
50
+ lowest-dependencies:
51
+ # The oldest versions pyproject.toml allows must really work.
52
+ runs-on: ubuntu-latest
53
+ steps:
54
+ - uses: actions/checkout@v7
55
+ - uses: astral-sh/setup-uv@v7
56
+ - run: uv venv --python 3.11
57
+ - run: uv pip install --resolution lowest-direct -e ".[all]" "pytest>=8"
58
+ - run: uv pip list
59
+ - run: .venv/bin/pytest tests/unit -v
60
+
61
+ integration:
62
+ name: integration (py${{ matrix.python }})
63
+ runs-on: ubuntu-latest
64
+ strategy:
65
+ fail-fast: false
66
+ matrix:
67
+ python: ['3.11', '3.14']
68
+ services: &services
69
+ gizmosql:
70
+ image: gizmodata/gizmosql:v1.41.0-slim
71
+ env:
72
+ GIZMOSQL_USERNAME: gizmosql
73
+ GIZMOSQL_PASSWORD: gizmosql
74
+ TLS_ENABLED: '0'
75
+ DATABASE_FILENAME: /tmp/target.duckdb
76
+ ports:
77
+ - 31377:31337
78
+ oracle:
79
+ image: gvenzl/oracle-free:23-slim-faststart
80
+ env:
81
+ ORACLE_PASSWORD: oracle
82
+ APP_USER: replicate
83
+ APP_USER_PASSWORD: replicate
84
+ ports:
85
+ - 15211:1521
86
+ options: >-
87
+ --health-cmd healthcheck.sh
88
+ --health-interval 10s
89
+ --health-timeout 5s
90
+ --health-retries 30
91
+ postgres:
92
+ image: postgres:16-alpine
93
+ env:
94
+ POSTGRES_PASSWORD: postgres
95
+ POSTGRES_DB: source
96
+ ports:
97
+ - 15432:5432
98
+ options: >-
99
+ --health-cmd "pg_isready -U postgres"
100
+ --health-interval 5s
101
+ --health-timeout 5s
102
+ --health-retries 20
103
+ steps:
104
+ - uses: actions/checkout@v7
105
+ - uses: actions/setup-python@v7
106
+ with:
107
+ python-version: ${{ matrix.python }}
108
+ - run: pip install -e ".[dev]"
109
+ # An unreachable service FAILS these tests (never skips), and pytest exits 5 if nothing ran.
110
+ - run: pytest tests/integration -m integration -v
111
+
112
+ python-3-15:
113
+ # Python 3.15 is brand new: adbc-driver-manager and pyyaml build from source here, and DuckDB
114
+ # has no 3.15 wheels yet, so this job covers the Oracle / PostgreSQL → GizmoSQL path.
115
+ runs-on: ubuntu-latest
116
+ services: *services
117
+ steps:
118
+ - uses: actions/checkout@v7
119
+ - uses: actions/setup-python@v7
120
+ with:
121
+ python-version: '3.15'
122
+ allow-prereleases: true
123
+ - run: pip install -e ".[oracle,postgresql,gizmosql]" "pytest>=8"
124
+ - run: pytest tests/unit --ignore=tests/unit/test_engine_duckdb.py -v
125
+ - run: pytest tests/integration -m integration -v
126
+
127
+ snowflake:
128
+ # Needs credentials, so it doesn't run for pull requests (fork PRs get no secrets). Everywhere
129
+ # else, including the release gate, missing secrets are a failure.
130
+ if: github.event_name != 'pull_request'
131
+ runs-on: ubuntu-latest
132
+ services:
133
+ gizmosql:
134
+ image: gizmodata/gizmosql:v1.41.0-slim
135
+ env:
136
+ GIZMOSQL_USERNAME: gizmosql
137
+ GIZMOSQL_PASSWORD: gizmosql
138
+ TLS_ENABLED: '0'
139
+ DATABASE_FILENAME: /tmp/target.duckdb
140
+ ports:
141
+ - 31377:31337
142
+ env:
143
+ ADBC_REPLICATE_TEST_SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
144
+ ADBC_REPLICATE_TEST_SNOWFLAKE_PRIVATE_KEY: ${{ secrets.SNOWFLAKE_PRIVATE_KEY }}
145
+ ADBC_REPLICATE_TEST_SNOWFLAKE_PRIVATE_KEY_PASSPHRASE: ${{ secrets.SNOWFLAKE_PRIVATE_KEY_PASSPHRASE }}
146
+ steps:
147
+ - uses: actions/checkout@v7
148
+ - uses: actions/setup-python@v7
149
+ with:
150
+ python-version: '3.13'
151
+ - run: pip install -e ".[dev]"
152
+ - run: pytest tests/integration -m snowflake -v
153
+
154
+ docker:
155
+ # Build the image and run a real PostgreSQL → GizmoSQL replication with it.
156
+ runs-on: ubuntu-latest
157
+ services:
158
+ gizmosql:
159
+ image: gizmodata/gizmosql:v1.41.0-slim
160
+ env:
161
+ GIZMOSQL_USERNAME: gizmosql
162
+ GIZMOSQL_PASSWORD: gizmosql
163
+ TLS_ENABLED: '0'
164
+ DATABASE_FILENAME: /tmp/target.duckdb
165
+ ports:
166
+ - 31377:31337
167
+ postgres:
168
+ image: postgres:16-alpine
169
+ env:
170
+ POSTGRES_PASSWORD: postgres
171
+ POSTGRES_DB: source
172
+ ports:
173
+ - 15432:5432
174
+ options: >-
175
+ --health-cmd "pg_isready -U postgres"
176
+ --health-interval 5s
177
+ --health-timeout 5s
178
+ --health-retries 20
179
+ steps:
180
+ - uses: actions/checkout@v7
181
+ - run: docker build -t adbc-replicate:ci .
182
+ - run: docker run --rm adbc-replicate:ci --version
183
+ - name: Seed PostgreSQL
184
+ run: |
185
+ docker exec ${{ job.services.postgres.id }} psql -U postgres -d source -v ON_ERROR_STOP=1 -c "
186
+ CREATE SCHEMA app;
187
+ CREATE TABLE app.items AS SELECT i AS id, 'item ' || i AS name, i * 1.5::numeric(10,2) AS price
188
+ FROM generate_series(1, 1000) i;
189
+ CREATE VIEW app.cheap AS SELECT id, name FROM app.items WHERE price < 100;"
190
+ - name: Replicate with the image
191
+ run: |
192
+ cat > /tmp/replicate.yaml <<'EOF'
193
+ version: 1
194
+ source: {type: postgresql, uri: "postgresql://127.0.0.1:15432/source", username: postgres, password: "${PGPASSWORD}"}
195
+ target: {type: gizmosql, uri: "gizmosql://127.0.0.1:31377?transport=tcp", username: gizmosql, password: "${GZPASSWORD}"}
196
+ objects: [{db_schemas: [app]}]
197
+ EOF
198
+ for _ in 1 2 3 4 5 6; do
199
+ docker run --rm --network host -v /tmp/replicate.yaml:/config/replicate.yaml:ro \
200
+ -e PGPASSWORD=postgres -e GZPASSWORD=gizmosql adbc-replicate:ci test-connection --config /config/replicate.yaml && break
201
+ sleep 5
202
+ done
203
+ docker run --rm --network host -v /tmp/replicate.yaml:/config/replicate.yaml:ro \
204
+ -e PGPASSWORD=postgres -e GZPASSWORD=gizmosql adbc-replicate:ci run --config /config/replicate.yaml
205
+
206
+ helm:
207
+ runs-on: ubuntu-latest
208
+ steps:
209
+ - uses: actions/checkout@v7
210
+ - uses: azure/setup-helm@v5
211
+ - run: helm lint charts/adbc-replicate --set config.version=1
212
+ - run: |
213
+ helm template demo charts/adbc-replicate \
214
+ --set config.version=1 --set existingSecret=demo-env \
215
+ --set-string 'viewOverrides.HR\.V_X\.sql=SELECT 1' > /tmp/rendered.yaml
216
+ grep -q 'kind: CronJob' /tmp/rendered.yaml
217
+ grep -q 'concurrencyPolicy: Forbid' /tmp/rendered.yaml
218
+ grep -q 'karpenter.sh/do-not-disrupt' /tmp/rendered.yaml
@@ -0,0 +1,147 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ['v*']
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ # The full suite (unit, lowest deps, integration against real GizmoSQL/Oracle/PostgreSQL,
12
+ # Python 3.15, Snowflake, Docker, Helm) on the tagged commit itself. Nothing publishes without it.
13
+ tests:
14
+ uses: ./.github/workflows/ci.yml
15
+ secrets: inherit
16
+ permissions:
17
+ contents: read
18
+
19
+ build:
20
+ needs: tests
21
+ runs-on: ubuntu-latest
22
+ outputs:
23
+ version: ${{ steps.version.outputs.version }}
24
+ steps:
25
+ - uses: actions/checkout@v7
26
+ - uses: actions/setup-python@v7
27
+ with:
28
+ python-version: '3.13'
29
+ - id: version
30
+ name: Check the tag matches the package, chart and CHANGELOG
31
+ run: |
32
+ VERSION="${GITHUB_REF_NAME#v}"
33
+ [ -n "$VERSION" ] || { echo "empty version"; exit 1; }
34
+ PKG=$(python -c "import re,pathlib; print(re.search(r'__version__ = \"(.+)\"', pathlib.Path('src/adbc_replicate/__init__.py').read_text()).group(1))")
35
+ CHART=$(awk '/^version:/ {print $2}' charts/adbc-replicate/Chart.yaml)
36
+ APP=$(awk '/^appVersion:/ {gsub(/"/, "", $2); print $2}' charts/adbc-replicate/Chart.yaml)
37
+ echo "tag=$VERSION package=$PKG chart=$CHART appVersion=$APP"
38
+ [ "$VERSION" = "$PKG" ] && [ "$VERSION" = "$CHART" ] && [ "$VERSION" = "$APP" ] || { echo "version mismatch"; exit 1; }
39
+ grep -q "^## \[$VERSION\]" CHANGELOG.md || { echo "CHANGELOG.md has no [$VERSION] section"; exit 1; }
40
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
41
+ - run: pip install build
42
+ - run: python -m build
43
+ - uses: azure/setup-helm@v5
44
+ - run: helm package charts/adbc-replicate --destination dist-chart
45
+ - uses: actions/upload-artifact@v7
46
+ with:
47
+ name: dist
48
+ path: dist/
49
+ - uses: actions/upload-artifact@v7
50
+ with:
51
+ name: chart
52
+ path: dist-chart/
53
+
54
+ pypi:
55
+ needs: build
56
+ runs-on: ubuntu-latest
57
+ environment:
58
+ name: pypi
59
+ url: https://pypi.org/p/adbc-replicate
60
+ permissions:
61
+ id-token: write
62
+ steps:
63
+ - uses: actions/download-artifact@v8
64
+ with:
65
+ name: dist
66
+ path: dist
67
+ - uses: pypa/gh-action-pypi-publish@release/v1
68
+ with:
69
+ packages-dir: dist
70
+ - name: Confirm PyPI serves the new version
71
+ run: |
72
+ for _ in $(seq 1 30); do
73
+ curl -fsS "https://pypi.org/pypi/adbc-replicate/${{ needs.build.outputs.version }}/json" >/dev/null && exit 0
74
+ sleep 10
75
+ done
76
+ echo "PyPI doesn't list adbc-replicate ${{ needs.build.outputs.version }}"; exit 1
77
+
78
+ docker:
79
+ needs: build
80
+ runs-on: ubuntu-latest
81
+ permissions:
82
+ contents: read
83
+ packages: write
84
+ steps:
85
+ - uses: actions/checkout@v7
86
+ - uses: docker/setup-qemu-action@v4
87
+ - uses: docker/setup-buildx-action@v4
88
+ - uses: docker/login-action@v4
89
+ with:
90
+ registry: ghcr.io
91
+ username: ${{ github.actor }}
92
+ password: ${{ secrets.GITHUB_TOKEN }}
93
+ - uses: docker/build-push-action@v7
94
+ with:
95
+ context: .
96
+ platforms: linux/amd64,linux/arm64
97
+ push: true
98
+ tags: |
99
+ ghcr.io/gizmodata/adbc-replicate:${{ needs.build.outputs.version }}
100
+ ghcr.io/gizmodata/adbc-replicate:latest
101
+ - name: Confirm the image is published for both architectures
102
+ run: |
103
+ docker buildx imagetools inspect ghcr.io/gizmodata/adbc-replicate:${{ needs.build.outputs.version }} | tee /tmp/inspect.txt
104
+ grep -q 'linux/amd64' /tmp/inspect.txt && grep -q 'linux/arm64' /tmp/inspect.txt
105
+
106
+ helm:
107
+ needs: build
108
+ runs-on: ubuntu-latest
109
+ permissions:
110
+ contents: read
111
+ packages: write
112
+ steps:
113
+ - uses: actions/download-artifact@v8
114
+ with:
115
+ name: chart
116
+ path: dist-chart
117
+ - uses: azure/setup-helm@v5
118
+ - run: echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin
119
+ - run: helm push dist-chart/adbc-replicate-${{ needs.build.outputs.version }}.tgz oci://ghcr.io/gizmodata/charts
120
+ - run: helm show chart oci://ghcr.io/gizmodata/charts/adbc-replicate --version ${{ needs.build.outputs.version }}
121
+
122
+ github-release:
123
+ needs: [build, pypi, docker, helm]
124
+ runs-on: ubuntu-latest
125
+ permissions:
126
+ contents: write
127
+ steps:
128
+ - uses: actions/checkout@v7
129
+ - uses: actions/download-artifact@v8
130
+ with:
131
+ path: artifacts
132
+ - name: Extract release notes from CHANGELOG.md
133
+ run: |
134
+ VERSION="${{ needs.build.outputs.version }}"
135
+ awk -v ver="${VERSION}" '
136
+ $0 ~ "^## \\[" ver "\\]" { found=1; next }
137
+ found && /^## \[/ { exit }
138
+ found { print }
139
+ ' CHANGELOG.md > release_notes.md
140
+ test -s release_notes.md
141
+ cat release_notes.md
142
+ - uses: softprops/action-gh-release@v3
143
+ with:
144
+ body_path: release_notes.md
145
+ files: |
146
+ artifacts/dist/*
147
+ artifacts/chart/*.tgz
@@ -0,0 +1,15 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ *.duckdb
10
+ *.duckdb.wal
11
+ .env
12
+ .env.*
13
+ !.env.example
14
+ *.tgz
15
+ .idea/
@@ -0,0 +1,41 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-14
11
+
12
+ ### Added
13
+
14
+ - First release: config-driven ADBC-to-ADBC replication into GizmoSQL (or DuckDB).
15
+ - Sources: Oracle (adbc-driver-oracle), PostgreSQL, Snowflake and DuckDB, each a small
16
+ plugin for discovery, consistency, change detection, view dialect and type fixes.
17
+ - A versioned YAML/JSON config (JSON Schema via `adbc-replicate schema`) with `${VAR}`
18
+ environment references, resolved after YAML parsing; unset variables are an error.
19
+ - Selection by case-insensitive glob or `/regex/` on db_schemas, tables and views; excludes;
20
+ per-rule target db_schema templates; views materialized as tables; a copied table
21
+ replaces a same-named view, and references to it inside views are re-pointed.
22
+ - Column exclude / allow-list and masks (`null`, salted SHA-256 `hash`, `redact`, source-SQL
23
+ `expression`) evaluated inside the source database. Exclude/include/mask patterns that
24
+ match nothing fail the run (`fail_on_unmatched_column_rules`).
25
+ - Consistency modes: `serializable` (one transaction) and `snapshot` (parallel workers at
26
+ one point in time: Oracle `AS OF SCN`, PostgreSQL exported snapshots, Snowflake time travel).
27
+ Snapshot mode is refused at plan time on Snowflake shared (imported) databases, which have
28
+ no time travel.
29
+ - Change detection: Snowflake `LAST_ALTERED` for base tables, otherwise a row count plus a
30
+ content fingerprint computed in the source; column-rule changes force a re-copy.
31
+ - View translation with sqlglot: Oracle `(+)` joins to ANSI joins, `FROM DUAL`, `ROWNUM`
32
+ limits, Oracle view column lists, dependency-ordered creation, overrides directory and
33
+ `on_failure: materialize`.
34
+ - Lossless numerics: unconstrained Oracle `NUMBER` and PostgreSQL `numeric` become
35
+ `DECIMAL(38,10)` (rounded to 10 fractional digits; integer overflow fails loudly).
36
+ - Streaming with batch coalescing (65,536 rows / 12 MiB), parallel table copies, atomic
37
+ per-table swaps, `--row-limit` trial runs, `--tables` filters, `plan` and `test-connection`.
38
+ - Sync state, run log and per-object results stored in the target (`_adbc_replicate`).
39
+ - Emoji text output and `--log-format json`; exit codes 0/1/2/3.
40
+ - Container image (`ghcr.io/gizmodata/adbc-replicate`) and a Helm chart running a CronJob.
41
+ - Python 3.11 – 3.15.
@@ -0,0 +1,68 @@
1
+ # CLAUDE.md
2
+
3
+ Project notes for Claude and other contributors.
4
+
5
+ ## What this is
6
+
7
+ `adbc-replicate`: config-driven ADBC → ADBC replication into GizmoSQL (or DuckDB). Python;
8
+ the data path is Arrow C streams between drivers (`fetch_record_batch` → `adbc_ingest`), so
9
+ Python never touches rows. It generalizes per-customer jobs like
10
+ the Snowflake → GizmoSQL loader behind the Vaden Automotive case study (its lessons are
11
+ what the defaults here encode: 12 MiB batch coalescing, 32 MiB gRPC cap, one catalog snapshot
12
+ instead of per-object queries, `abandoned` runs). Planned later: a GizmoData Cloud wizard
13
+ that writes this config, so keep the config schema, `plan --format json` and the event
14
+ stream stable and UI-friendly.
15
+
16
+ ## Layout
17
+
18
+ ```
19
+ src/adbc_replicate/config.py pydantic models = the config contract (+ JSON Schema)
20
+ src/adbc_replicate/planner.py rules + source catalog -> Plan (tables, views, name_map)
21
+ src/adbc_replicate/engine.py run: consistency, parallel copies, change detection, views
22
+ src/adbc_replicate/sources/*.py one plugin per database (base.py is the interface)
23
+ src/adbc_replicate/targets/ DuckDB-family targets (GizmoSQL, DuckDB)
24
+ src/adbc_replicate/views.py sqlglot translation (Oracle rewrites, name re-pointing)
25
+ src/adbc_replicate/state.py sync state / run log tables in the target
26
+ tests/unit no servers; DuckDB -> DuckDB engine tests, README YAML check
27
+ tests/integration real GizmoSQL + Oracle + PostgreSQL (docker-compose.yml);
28
+ Snowflake tests are marker `snowflake` (need credentials)
29
+ charts/adbc-replicate Helm CronJob
30
+ ```
31
+
32
+ ## Conventions
33
+
34
+ - ADBC vocabulary everywhere (catalog, db_schema, table) — never Oracle-specific terms.
35
+ - Keyword arguments at call sites (also for pyarrow/ADBC calls that accept them).
36
+ - Python floor 3.11; CI matrix 3.11–3.14 + a 3.15 job (DuckDB has no 3.15 wheels yet, so the
37
+ `all` extra skips it there). The lowest-dependencies CI job keeps pyproject floors honest.
38
+ - Tests read only `ADBC_REPLICATE_TEST_*` variables: developer shells export real
39
+ `GIZMOSQL_*` credentials. Docker addresses are `127.0.0.1`, never `localhost`.
40
+ - Integration tests FAIL (never skip) when a server is unreachable.
41
+ - Every YAML block in README.md is validated by tests/unit/test_readme.py; quick-start
42
+ output in the README was captured from a real run — re-capture it if output changes.
43
+ - Releases: bump `src/adbc_replicate/__init__.py`, `charts/adbc-replicate/Chart.yaml`
44
+ (version + appVersion) and CHANGELOG together; tag `vX.Y.Z`. release.yml re-runs ci.yml on
45
+ the tag and checks all three versions match before publishing to PyPI, GHCR (image + chart)
46
+ and creating the GitHub Release.
47
+
48
+ ## Gotchas found the hard way
49
+
50
+ - Oracle `AS OF SCN` fails with ORA-01466 for ~2 s after a table is created/altered/truncated.
51
+ `TIMESTAMP_TO_SCN` is ~3 s coarse; an exact SCN needs `EXECUTE ON SYS.DBMS_FLASHBACK`
52
+ (only SYS can grant it in 23ai). The integration fixture waits until flashback reads work.
53
+ - Oracle unconstrained `NUMBER` reads as float64 by default; we CAST to NUMBER(38,10).
54
+ Oracle computed columns (CASE, functions) lose precision metadata — but
55
+ `CASE WHEN 1 = 0 THEN col END` keeps it, which is how the `null` mask stays typed.
56
+ - PostgreSQL's driver returns `numeric` as text (arrow.opaque); we round in SQL then strictly
57
+ cast to decimal128 client-side. `pg_get_viewdef` expands `SELECT *` into explicit columns.
58
+ - sqlglot silently drops Oracle `(+)` when generating; always run `eliminate_join_marks` first.
59
+ sqlglot renamed `Select.args["from"]` to `"from_"` — use the `_FROM` constant in views.py.
60
+ - DuckDB can't resolve `"x"."t"` when a catalog and a db_schema are both named `x`; target
61
+ DDL and translated views qualify with the catalog exactly in that case.
62
+ - DuckDB's ADBC driver supports ingest mode `replace` only from 1.5.x (floor: 1.5.6).
63
+ - The GizmoSQL driver's wheels need glibc >= 2.34 (Docker base: bookworm or newer).
64
+ - Snowflake shared (imported) databases have no time travel: `AT(TIMESTAMP => …)` fails with
65
+ 000707 on every table (found on SNOWFLAKE_SAMPLE_DATA), so the plan refuses snapshot mode
66
+ there (`INFORMATION_SCHEMA.DATABASES.TYPE = 'IMPORTED DATABASE'`).
67
+ - The Snowflake CI user (`ADBC_REPLICATE_CI`) has only PUBLIC: it can read the sample share and
68
+ use SNOWFLAKE_LEARNING_WH, nothing else. Snowflake tests must stay read-only.
@@ -0,0 +1,27 @@
1
+ # adbc-replicate container image: the CLI plus every source/target driver.
2
+ # Debian bookworm (glibc 2.36): the GizmoSQL driver's wheels need glibc >= 2.34.
3
+ FROM python:3.13-slim-bookworm
4
+
5
+ LABEL org.opencontainers.image.title="adbc-replicate" \
6
+ org.opencontainers.image.description="Config-driven ADBC-to-ADBC replication into GizmoSQL" \
7
+ org.opencontainers.image.source="https://github.com/gizmodata/adbc-replicate" \
8
+ org.opencontainers.image.vendor="GizmoData LLC" \
9
+ org.opencontainers.image.licenses="Apache-2.0"
10
+
11
+ ENV PYTHONDONTWRITEBYTECODE=1 \
12
+ PYTHONUNBUFFERED=1 \
13
+ PIP_NO_CACHE_DIR=1 \
14
+ PIP_DISABLE_PIP_VERSION_CHECK=1
15
+
16
+ WORKDIR /opt/adbc-replicate
17
+ COPY pyproject.toml README.md LICENSE ./
18
+ COPY src ./src
19
+ RUN pip install ".[all]" && adbc-replicate --version
20
+
21
+ # Run as an unprivileged user; configs are mounted read-only (e.g. /config/replicate.yaml).
22
+ RUN useradd --uid 10001 --create-home --shell /usr/sbin/nologin replicate
23
+ USER 10001
24
+ WORKDIR /home/replicate
25
+
26
+ ENTRYPOINT ["adbc-replicate"]
27
+ CMD ["--help"]