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.
- adbc_replicate-0.1.0/.dockerignore +9 -0
- adbc_replicate-0.1.0/.github/workflows/ci.yml +218 -0
- adbc_replicate-0.1.0/.github/workflows/release.yml +147 -0
- adbc_replicate-0.1.0/.gitignore +15 -0
- adbc_replicate-0.1.0/CHANGELOG.md +41 -0
- adbc_replicate-0.1.0/CLAUDE.md +68 -0
- adbc_replicate-0.1.0/Dockerfile +27 -0
- adbc_replicate-0.1.0/LICENSE +202 -0
- adbc_replicate-0.1.0/PKG-INFO +650 -0
- adbc_replicate-0.1.0/README.md +594 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/Chart.yaml +14 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/templates/NOTES.txt +10 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/templates/_helpers.tpl +25 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/templates/configmap.yaml +26 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/templates/cronjob.yaml +104 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/templates/secret.yaml +13 -0
- adbc_replicate-0.1.0/charts/adbc-replicate/values.yaml +86 -0
- adbc_replicate-0.1.0/docker-compose.yml +37 -0
- adbc_replicate-0.1.0/pyproject.toml +93 -0
- adbc_replicate-0.1.0/src/adbc_replicate/__init__.py +3 -0
- adbc_replicate-0.1.0/src/adbc_replicate/arrowutil.py +112 -0
- adbc_replicate-0.1.0/src/adbc_replicate/cli.py +302 -0
- adbc_replicate-0.1.0/src/adbc_replicate/config.py +348 -0
- adbc_replicate-0.1.0/src/adbc_replicate/engine.py +497 -0
- adbc_replicate-0.1.0/src/adbc_replicate/events.py +57 -0
- adbc_replicate-0.1.0/src/adbc_replicate/matching.py +69 -0
- adbc_replicate-0.1.0/src/adbc_replicate/models.py +90 -0
- adbc_replicate-0.1.0/src/adbc_replicate/planner.py +183 -0
- adbc_replicate-0.1.0/src/adbc_replicate/sources/__init__.py +24 -0
- adbc_replicate-0.1.0/src/adbc_replicate/sources/base.py +175 -0
- adbc_replicate-0.1.0/src/adbc_replicate/sources/duckdb.py +93 -0
- adbc_replicate-0.1.0/src/adbc_replicate/sources/oracle.py +180 -0
- adbc_replicate-0.1.0/src/adbc_replicate/sources/postgresql.py +154 -0
- adbc_replicate-0.1.0/src/adbc_replicate/sources/snowflake.py +138 -0
- adbc_replicate-0.1.0/src/adbc_replicate/state.py +196 -0
- adbc_replicate-0.1.0/src/adbc_replicate/targets/__init__.py +5 -0
- adbc_replicate-0.1.0/src/adbc_replicate/targets/duckdb.py +139 -0
- adbc_replicate-0.1.0/src/adbc_replicate/views.py +162 -0
- adbc_replicate-0.1.0/tests/integration/conftest.py +227 -0
- adbc_replicate-0.1.0/tests/integration/test_oracle_to_gizmosql.py +207 -0
- adbc_replicate-0.1.0/tests/integration/test_postgres_to_gizmosql.py +132 -0
- adbc_replicate-0.1.0/tests/integration/test_snowflake_to_gizmosql.py +110 -0
- adbc_replicate-0.1.0/tests/unit/test_config.py +101 -0
- adbc_replicate-0.1.0/tests/unit/test_engine_duckdb.py +255 -0
- adbc_replicate-0.1.0/tests/unit/test_matching_and_arrow.py +72 -0
- adbc_replicate-0.1.0/tests/unit/test_readme.py +39 -0
- adbc_replicate-0.1.0/tests/unit/test_views.py +87 -0
|
@@ -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,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"]
|