diff --git a/docs/superpowers/plans/2026-05-19-quack-transactions-prepare.md b/docs/superpowers/plans/2026-05-19-quack-transactions-prepare.md new file mode 100644 index 0000000..32dce3a --- /dev/null +++ b/docs/superpowers/plans/2026-05-19-quack-transactions-prepare.md @@ -0,0 +1,72 @@ + + +# Quack Transactions And Prepare-Only Statements + +**Goal:** Implement ADBC transaction callbacks and parameterless statement prepare support for the Quack driver, while keeping bind parameters unsupported. + +## Summary + +- Implement ADBC connection transactions over the existing attached Quack catalog/session. +- Implement parameterless `AdbcStatementPrepare` for SQL statements while keeping bind parameters unsupported. +- Adjust bound-stream state handling so SQL and bound streams can coexist until execution, where unsupported parameter binding fails explicitly. + +## Key Changes + +- In `src/adbc_driver_quack.cc`, add transaction state to `ConnectionState`: autocommit enabled by default plus active remote transaction tracking. +- Add `DriverConnectionCommit` and `DriverConnectionRollback`, export `AdbcConnectionCommit` and `AdbcConnectionRollback`, and wire both callbacks in `InitDriver`. +- Extend `DriverConnectionSetOption`/`GetOption` for `ADBC_CONNECTION_OPTION_AUTOCOMMIT`, returning `"true"`/`"false"` and rejecting commit/rollback while autocommit is enabled. +- When autocommit is disabled, send remote `BEGIN TRANSACTION`; after commit or rollback, send `COMMIT`/`ROLLBACK` and begin a fresh remote transaction so repeated commit/rollback calls while autocommit is disabled remain valid. +- Implement `DriverStatementPrepare` without binding: require initialized statement, initialized connection, and non-empty `state->sql`; set a `prepared` flag. +- Change bound-stream behavior: + - `DriverStatementBindStream` stores the stream but does not clear `state->sql`. + - Setting `ADBC_INGEST_OPTION_TARGET_TABLE` clears `state->sql`, since that statement is now bulk ingest. + - `DriverStatementExecuteQuery` with `has_bound_stream && !state->sql.empty()` returns `ADBC_STATUS_NOT_IMPLEMENTED` for parameterized SQL execution. + - `DriverStatementExecuteQuery` with `has_bound_stream && state->sql.empty()` keeps the current bulk-ingest path. +- Keep `DriverStatementBind` returning `ADBC_STATUS_NOT_IMPLEMENTED`, and do not implement parameter schema. + +## Validation Changes + +- Update `validation/tests/quack.py`: + - `connection_transactions=True` + - `statement_prepare=True` + - leave `statement_bind=False` + - leave `statement_get_parameter_schema=False` +- Do not add `validation/queries/type/bind/*.txtcase` overrides unless a validation run shows bind tests are still collected despite `statement_bind=False`. + +## Tests + +- Update C++ symbol tests to expect exported/populated `ConnectionCommit` and `ConnectionRollback`. +- Replace the existing C++ "prepare is unsupported" assertion with prepare state tests: + - uninitialized statement returns `ADBC_STATUS_INVALID_STATE`; + - initialized statement with no SQL returns `ADBC_STATUS_INVALID_STATE`; + - initialized SQL statement can be prepared; + - bind remains `ADBC_STATUS_NOT_IMPLEMENTED`. +- Add C++ state-routing tests for bound streams: + - `BindStream` after `SetSqlQuery` preserves SQL and execution fails with `ADBC_STATUS_NOT_IMPLEMENTED`; + - setting `ADBC_INGEST_OPTION_TARGET_TABLE` clears SQL so the statement uses the bulk-ingest path. +- Run: + - `./ci/scripts/build.sh test linux amd64` + - `./ci/scripts/test.sh linux amd64` + - `pixi run validate -k "transaction_toggle or prepare"` + - `pixi run validate --collect-only` to confirm bind cases remain skipped or marked unsupported + - `pre-commit run --all-files` outside the sandbox. + +## Assumptions + +- Prepared statements mean parameterless `prepare()` support only for now. +- No Quack protocol changes are included. +- Bind support remains blocked until Quack exposes a typed remote parameter mechanism. diff --git a/pixi.lock b/pixi.lock index e281bb2..f80dbd9 100644 --- a/pixi.lock +++ b/pixi.lock @@ -18,7 +18,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.1-h0c1763c_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda @@ -34,7 +34,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl - pypi: https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - - pypi: git+https://github.com/adbc-drivers/dev#4800538aea19ea883231b3b1ded59c6832ef6bdc + - pypi: git+https://github.com/adbc-drivers/dev#427e35d084e11dca1f58a5635cd168927aba749f - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/cc/b1/1464f468d2e5813f5808de95df9d3113a645a5bfa2ffcaecbc542ddae272/duckdb-1.5.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl @@ -52,7 +52,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/e5/8d/35f6096c42caefb715ca29e991279b493275c0051a3c83081099644d3f4a/pygit2-1.19.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a1/5c/8b56b08db91e569d0a4fbfa3e492ed2026081bdd7e892f63ba1c88a2f548/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl - pypi: https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl @@ -60,7 +60,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: git+https://github.com/adbc-drivers/validation#b96f6bd7f7f3e5a2450abcb0bbdd34f6a36b7a41 + - pypi: git+https://github.com/adbc-drivers/validation#ea7eb76aa2ebe8fb71dfd12eeb6132d7cc04c9e2 - pypi: https://files.pythonhosted.org/packages/b9/4a/f4e3620f9542b092f31c3de6810e137cc1c01c33a5a25cc3f90a138b98a3/whenever-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda @@ -74,7 +74,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.3-he30d5cf_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.53.1-h022381a_0.conda - - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.1-h1022ec0_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.2-hdc9db2a_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.2-h546c87b_0.conda @@ -90,7 +90,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl - pypi: https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - - pypi: git+https://github.com/adbc-drivers/dev#4800538aea19ea883231b3b1ded59c6832ef6bdc + - pypi: git+https://github.com/adbc-drivers/dev#427e35d084e11dca1f58a5635cd168927aba749f - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/28/dc/ad45ac3c0b6c4687dc649e8f6cf01af1c8b0443932a39b2abb4ebcb3babd/duckdb-1.5.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl @@ -108,7 +108,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/2f/64/d674b3f854cecf53bccbc21a095734759cd3599624578ed3c78602eb22a3/pygit2-1.19.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/75/08/e365ee305367559f57ba6179d836ecc3d31c7d3fdff2a40ebf6c32823a1f/ruamel_yaml_clib-0.2.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl - pypi: https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl @@ -116,7 +116,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: git+https://github.com/adbc-drivers/validation#b96f6bd7f7f3e5a2450abcb0bbdd34f6a36b7a41 + - pypi: git+https://github.com/adbc-drivers/validation#ea7eb76aa2ebe8fb71dfd12eeb6132d7cc04c9e2 - pypi: https://files.pythonhosted.org/packages/75/6f/a2ca2635fae996e030ab30b1ce0e1b54dbe19bbee5b303314eb5e53053bd/whenever-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl osx-arm64: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda @@ -141,7 +141,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl - - pypi: git+https://github.com/adbc-drivers/dev#4800538aea19ea883231b3b1ded59c6832ef6bdc + - pypi: git+https://github.com/adbc-drivers/dev#427e35d084e11dca1f58a5635cd168927aba749f - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/09/70/ce750854d37bb5a45cccbb2c3cb04df4af56aea8fc30a2499bb643b4a9c0/duckdb-1.5.2-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl @@ -159,7 +159,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/a7/86/4bb6f196b13bd7ed825f4e931fb7152a36d01e8de24c8de44425702ad18c/pygit2-1.19.2-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/69/f2/c4cec0a30f1955510fde498aac451d2e52b24afdbcb00204d3a951b772c3/ruamel_yaml_clib-0.2.15-cp314-cp314-macosx_11_0_arm64.whl - pypi: https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl @@ -167,7 +167,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: git+https://github.com/adbc-drivers/validation#b96f6bd7f7f3e5a2450abcb0bbdd34f6a36b7a41 + - pypi: git+https://github.com/adbc-drivers/validation#ea7eb76aa2ebe8fb71dfd12eeb6132d7cc04c9e2 - pypi: https://files.pythonhosted.org/packages/f5/eb/2b3ef4807ceffcd7a447386350f180e8a130e87a642dd3f74c159eaa2573/whenever-0.10.0-cp314-cp314-macosx_11_0_arm64.whl win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda @@ -184,9 +184,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_36.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_36.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda - pypi: https://files.pythonhosted.org/packages/33/64/5247eb91f9902e7111bf7a75c1af4da7a1818e31a26a754d97d0f0df7dcb/adbc_driver_manager-1.11.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl @@ -195,7 +195,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl - - pypi: git+https://github.com/adbc-drivers/dev#4800538aea19ea883231b3b1ded59c6832ef6bdc + - pypi: git+https://github.com/adbc-drivers/dev#427e35d084e11dca1f58a5635cd168927aba749f - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/ce/32/6673607e024722473fa7aafdd29c0e3dd231dd528f6cd8b5797fbeeb229d/duckdb-1.5.2-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl @@ -213,7 +213,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/22/00/24df5ac51a316e36a07bbf9e4c91fade523b9e80a84d5c9e7acd10b22248/pygit2-1.19.2-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl - - pypi: https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/d2/67/be582a7370fdc9e6846c5be4888a530dcadd055eef5b932e0e85c33c7d73/ruamel_yaml_clib-0.2.15-cp314-cp314-win_amd64.whl - pypi: https://files.pythonhosted.org/packages/88/4e/80705091aaf9c95e125d243f0aa871bc9f3670b4c9d963e6bad3b3dce8ff/sqlglot-30.8.0-py3-none-any.whl @@ -223,7 +223,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl - - pypi: git+https://github.com/adbc-drivers/validation#b96f6bd7f7f3e5a2450abcb0bbdd34f6a36b7a41 + - pypi: git+https://github.com/adbc-drivers/validation#ea7eb76aa2ebe8fb71dfd12eeb6132d7cc04c9e2 - pypi: https://files.pythonhosted.org/packages/c5/b2/8ad678802988ac7ad4937372cf8562f5c267d8ce605194a9409d4c1b9d82/whenever-0.10.0-cp314-cp314-win_amd64.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda @@ -309,7 +309,7 @@ packages: - pyarrow>=14.0.1 ; extra == 'test' - pytest>=9 ; extra == 'test' requires_python: '>=3.10' -- pypi: git+https://github.com/adbc-drivers/dev#4800538aea19ea883231b3b1ded59c6832ef6bdc +- pypi: git+https://github.com/adbc-drivers/dev#427e35d084e11dca1f58a5635cd168927aba749f name: adbc-drivers-dev version: '0.1' requires_dist: @@ -322,7 +322,7 @@ packages: - requests - ruamel-yaml>=0.18.11,<0.19 - tomlkit>=0.13.2,<0.14 -- pypi: git+https://github.com/adbc-drivers/validation#b96f6bd7f7f3e5a2450abcb0bbdd34f6a36b7a41 +- pypi: git+https://github.com/adbc-drivers/validation#ea7eb76aa2ebe8fb71dfd12eeb6132d7cc04c9e2 name: adbc-drivers-validation version: '0.1' requires_dist: @@ -842,27 +842,25 @@ packages: purls: [] size: 1304178 timestamp: 1777986510497 -- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda - sha256: bc1b08c92626c91500fd9f26f2c797f3eb153b627d53e9c13cd167f1e12b2829 - md5: 38ffe67b78c9d4de527be8315e5ada2c +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42.1-h5347b49_0.conda + sha256: 3f0edf1280e2f6684a986f821eaa3e123d2694a00b31b96ca0d4a4c12c129231 + md5: 7d0a66598195ef00b6efc55aefc7453b depends: - __glibc >=2.17,<3.0.a0 - libgcc >=14 license: BSD-3-Clause - license_family: BSD purls: [] - size: 40297 - timestamp: 1775052476770 -- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42-h1022ec0_0.conda - sha256: 7d427edf58c702c337bf62bc90f355b7fc374a65fd9f70ea7a490f13bb76b1b9 - md5: a0b5de740d01c390bdbb46d7503c9fab + size: 40163 + timestamp: 1779118517630 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.42.1-h1022ec0_0.conda + sha256: 1628839b062e98b2192857d4da8496ac9ac6b0dbb77aa040c34efc9192c440ee + md5: 0f42f9fedd2a32d798de95a7f65c456f depends: - libgcc >=14 license: BSD-3-Clause - license_family: BSD purls: [] - size: 43567 - timestamp: 1775052485727 + size: 43453 + timestamp: 1779118526838 - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 md5: d87ff7921124eccd67248aa483c23fec @@ -1307,10 +1305,10 @@ packages: purls: [] size: 313930 timestamp: 1765813902568 -- pypi: https://files.pythonhosted.org/packages/15/5a/4a949d170476de3c04ac036b5466422fbcbf348a917d8042eedf2cac7d1b/requests-2.34.1-py3-none-any.whl +- pypi: https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl name: requests - version: 2.34.1 - sha256: bf38a3ff993960d3dd819c08862c40b3c703306eb7c744fcd9f4ddbb95b548f0 + version: 2.34.2 + sha256: 2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 requires_dist: - charset-normalizer>=2,<4 - idna>=2.5,<4 @@ -1485,43 +1483,43 @@ packages: - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' requires_python: '>=3.10' -- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_36.conda - sha256: 7c86d8ed3ac473c3e4dde0dd05aeb1f3189a26ad66c0e250f6cf4018e73358f2 - md5: 3466ff4a8753003eeb173f508d3d5a49 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.5-h1b7c187_36.conda + sha256: dbcbad366e38979ac8ca9efb0ec48e5fedf9ce76f9485120c131cab7315c681c + md5: 10eac3d81ceea1be614f1d90045c7e9b depends: - - vc14_runtime >=14.44.35208 + - vc14_runtime >=14.51.36231 track_features: - vc14 license: BSD-3-Clause license_family: BSD purls: [] - size: 19989 - timestamp: 1778688080106 -- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_36.conda - sha256: 902984f2282859a76d764d80d74f873df7c7749117cfac15c5106e086fb2b772 - md5: 65f5c81f2796961fcfd808eee8e73596 + size: 20260 + timestamp: 1779061872533 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.51.36231-h1b9f54f_36.conda + sha256: e6f48954124c4f9419e50b3de7cb4b88f3a6078bf3616e997ea60144d499aa30 + md5: df9d8c15f117f28087b4aa6efa529a56 depends: - ucrt >=10.0.20348.0 - - vcomp14 14.44.35208 h818238b_36 + - vcomp14 14.51.36231 h1b9f54f_36 constrains: - - vs2015_runtime 14.44.35208.* *_36 + - vs2015_runtime 14.51.36231.* *_36 license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary purls: [] - size: 683790 - timestamp: 1778688078434 -- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_36.conda - sha256: 0cd5b905ab2b5e9fcb170fe8801b64917effef8e3a73ffd9b2cc4c3ee387f09c - md5: 4aa1884260877bd57d16070d20271e2d + size: 739707 + timestamp: 1779061867466 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.51.36231-h1b9f54f_36.conda + sha256: bba3bcaf805eefd0aa14beb3d08a34a81d5d36e6890bd6ce33fcb968429a3bc7 + md5: d929e2c56341be7ae1bd9a77a9b535c2 depends: - ucrt >=10.0.20348.0 constrains: - - vs2015_runtime 14.44.35208.* *_36 + - vs2015_runtime 14.51.36231.* *_36 license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary purls: [] - size: 115995 - timestamp: 1778688058077 + size: 124124 + timestamp: 1779061850036 - pypi: https://files.pythonhosted.org/packages/75/6f/a2ca2635fae996e030ab30b1ce0e1b54dbe19bbee5b303314eb5e53053bd/whenever-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl name: whenever version: 0.10.0 diff --git a/src/adbc_driver_quack.cc b/src/adbc_driver_quack.cc index a514310..12e9fba 100644 --- a/src/adbc_driver_quack.cc +++ b/src/adbc_driver_quack.cc @@ -45,6 +45,8 @@ AdbcStatusCode DriverDatabaseSetOption(AdbcDatabase* database, char const* key, AdbcStatusCode DriverDatabaseRelease(AdbcDatabase* database, AdbcError* error); AdbcStatusCode DriverConnectionInit(AdbcConnection* connection, AdbcDatabase* database, AdbcError* error); +AdbcStatusCode DriverConnectionCommit(AdbcConnection* connection, + AdbcError* error); AdbcStatusCode DriverConnectionGetInfo(AdbcConnection* connection, uint32_t const* info_codes, size_t info_codes_length, @@ -60,6 +62,8 @@ AdbcStatusCode DriverConnectionNew(AdbcConnection* connection, AdbcError* error); AdbcStatusCode DriverConnectionRelease(AdbcConnection* connection, AdbcError* error); +AdbcStatusCode DriverConnectionRollback(AdbcConnection* connection, + AdbcError* error); AdbcStatusCode DriverConnectionSetOption(AdbcConnection* connection, char const* key, char const* value, AdbcError* error); @@ -98,6 +102,8 @@ struct ConnectionState { duckdb_connection connection = nullptr; uint64_t next_ingest_id = 0; bool initialized = false; + bool autocommit = true; + bool transaction_active = false; }; struct StatementState { @@ -110,6 +116,7 @@ struct StatementState { bool ingest_temporary = false; ArrowArrayStream bound_stream = {}; bool has_bound_stream = false; + bool prepared = false; }; struct DriverState { @@ -640,6 +647,7 @@ void CloseConnectionState(ConnectionState* state) { duckdb_close(&state->database); } state->initialized = false; + state->transaction_active = false; } AdbcStatusCode RunDuckDbQuery(ConnectionState* state, std::string const& sql, @@ -890,7 +898,7 @@ AdbcStatusCode ExecuteBulkIngest(StatementState* state, int64_t* rows_affected, RunDuckDbQuery(state->connection, "DROP TABLE IF EXISTS " + quoted_data, nullptr); - if (status == ADBC_STATUS_OK) { + if (status == ADBC_STATUS_OK && state->connection->autocommit) { RunDuckDbQuery(state->connection, "COMMIT", nullptr); } if (status == ADBC_STATUS_OK && rows_affected != nullptr) { @@ -1023,12 +1031,14 @@ AdbcStatusCode InitDriver(int version, void* raw_driver, AdbcError* error) { driver->DatabaseSetOption = DriverDatabaseSetOption; driver->DatabaseRelease = DriverDatabaseRelease; + driver->ConnectionCommit = DriverConnectionCommit; driver->ConnectionInit = DriverConnectionInit; driver->ConnectionGetInfo = DriverConnectionGetInfo; driver->ConnectionGetOption = DriverConnectionGetOption; driver->ConnectionGetObjects = DriverConnectionGetObjects; driver->ConnectionNew = DriverConnectionNew; driver->ConnectionRelease = DriverConnectionRelease; + driver->ConnectionRollback = DriverConnectionRollback; driver->ConnectionSetOption = DriverConnectionSetOption; driver->StatementBind = DriverStatementBind; @@ -1166,6 +1176,51 @@ AdbcStatusCode DriverConnectionRelease(AdbcConnection* connection, return Ok(error); } +static AdbcStatusCode BeginRemoteTransaction(ConnectionState* state, + AdbcError* error) { + AdbcStatusCode const status = + RunRemoteQuery(state, "BEGIN TRANSACTION", error); + if (status == ADBC_STATUS_OK) { + state->transaction_active = true; + } + return status; +} + +static AdbcStatusCode FinishRemoteTransaction(ConnectionState* state, + char const* sql, + AdbcError* error) { + if (state->autocommit) { + return InvalidState(error, + "connection is in autocommit mode; cannot finish a " + "manual transaction"); + } + + AdbcStatusCode status = RunRemoteQuery(state, sql, error); + if (status != ADBC_STATUS_OK) { + return status; + } + state->transaction_active = false; + return BeginRemoteTransaction(state, error); +} + +AdbcStatusCode DriverConnectionCommit(AdbcConnection* connection, + AdbcError* error) { + ConnectionState* state = GetConnection(connection); + if (state == nullptr || !state->initialized) { + return InvalidState(error, "connection is not initialized"); + } + return FinishRemoteTransaction(state, "COMMIT", error); +} + +AdbcStatusCode DriverConnectionRollback(AdbcConnection* connection, + AdbcError* error) { + ConnectionState* state = GetConnection(connection); + if (state == nullptr || !state->initialized) { + return InvalidState(error, "connection is not initialized"); + } + return FinishRemoteTransaction(state, "ROLLBACK", error); +} + AdbcStatusCode DriverConnectionGetInfo(AdbcConnection* connection, uint32_t const* info_codes, size_t info_codes_length, @@ -1215,6 +1270,9 @@ AdbcStatusCode DriverConnectionGetOption(AdbcConnection* connection, if (status != ADBC_STATUS_OK) { return status; } + } else if (std::strcmp(key, ADBC_CONNECTION_OPTION_AUTOCOMMIT) == 0) { + option_value = state->autocommit ? ADBC_OPTION_VALUE_ENABLED + : ADBC_OPTION_VALUE_DISABLED; } else { return NotFound(error, std::string{"connection option not found: "} + key); } @@ -1272,6 +1330,30 @@ AdbcStatusCode DriverConnectionSetOption(AdbcConnection* connection, std::string const sql = "USE " + QuoteIdentifier(value); return RunRemoteQueryAllowNotFound(state, sql, error); } + if (std::strcmp(key, ADBC_CONNECTION_OPTION_AUTOCOMMIT) == 0) { + if (std::strcmp(value, ADBC_OPTION_VALUE_ENABLED) == 0) { + if (!state->autocommit && state->transaction_active) { + AdbcStatusCode const status = RunRemoteQuery(state, "COMMIT", error); + if (status != ADBC_STATUS_OK) { + return status; + } + state->transaction_active = false; + } + state->autocommit = true; + return Ok(error); + } + if (std::strcmp(value, ADBC_OPTION_VALUE_DISABLED) == 0) { + if (state->autocommit) { + AdbcStatusCode const status = BeginRemoteTransaction(state, error); + if (status != ADBC_STATUS_OK) { + return status; + } + } + state->autocommit = false; + return Ok(error); + } + return InvalidArgument(error, "invalid autocommit option value"); + } return NotImplemented(error, "unsupported connection option"); } @@ -1300,6 +1382,7 @@ AdbcStatusCode DriverStatementSetSqlQuery(AdbcStatement* statement, } ReleaseBoundStream(state); state->sql = query; + state->prepared = false; return Ok(error); } @@ -1315,6 +1398,10 @@ AdbcStatusCode DriverStatementExecuteQuery(AdbcStatement* statement, return InvalidState(error, "connection is not initialized"); } if (state->has_bound_stream) { + if (!state->sql.empty()) { + return NotImplemented(error, + "parameterized SQL execution is not implemented"); + } if (out != nullptr) { return InvalidArgument(error, "bulk ingest does not produce a result stream"); @@ -1344,8 +1431,20 @@ AdbcStatusCode DriverStatementExecuteQuery(AdbcStatement* statement, return status; } -AdbcStatusCode DriverStatementPrepare(AdbcStatement*, AdbcError* error) { - return NotImplemented(error, "parameterized statements are not implemented"); +AdbcStatusCode DriverStatementPrepare(AdbcStatement* statement, + AdbcError* error) { + StatementState* state = GetStatement(statement); + if (state == nullptr) { + return InvalidState(error, "statement is not initialized"); + } + if (state->connection == nullptr || !state->connection->initialized) { + return InvalidState(error, "connection is not initialized"); + } + if (state->sql.empty()) { + return InvalidState(error, "SQL query is not set"); + } + state->prepared = true; + return Ok(error); } AdbcStatusCode DriverStatementBind(AdbcStatement*, ArrowArray*, ArrowSchema*, @@ -1364,7 +1463,6 @@ AdbcStatusCode DriverStatementBindStream(AdbcStatement* statement, return InvalidArgument(error, "Arrow stream must not be null"); } ReleaseBoundStream(state); - state->sql.clear(); state->bound_stream = *stream; state->has_bound_stream = true; stream->release = nullptr; @@ -1396,6 +1494,8 @@ AdbcStatusCode DriverStatementSetOption(AdbcStatement* statement, } if (std::strcmp(key, ADBC_INGEST_OPTION_TARGET_TABLE) == 0) { state->ingest_target_table = value; + state->sql.clear(); + state->prepared = false; return Ok(error); } if (std::strcmp(key, ADBC_INGEST_OPTION_TARGET_CATALOG) == 0) { @@ -1462,6 +1562,16 @@ ADBC_EXPORT AdbcStatusCode AdbcConnectionRelease(AdbcConnection* connection, return DriverConnectionRelease(connection, error); } +ADBC_EXPORT AdbcStatusCode AdbcConnectionCommit(AdbcConnection* connection, + AdbcError* error) { + return DriverConnectionCommit(connection, error); +} + +ADBC_EXPORT AdbcStatusCode AdbcConnectionRollback(AdbcConnection* connection, + AdbcError* error) { + return DriverConnectionRollback(connection, error); +} + ADBC_EXPORT AdbcStatusCode AdbcConnectionSetOption(AdbcConnection* connection, char const* key, char const* value, @@ -1469,6 +1579,13 @@ ADBC_EXPORT AdbcStatusCode AdbcConnectionSetOption(AdbcConnection* connection, return DriverConnectionSetOption(connection, key, value, error); } +ADBC_EXPORT AdbcStatusCode AdbcConnectionGetOption(AdbcConnection* connection, + char const* key, char* value, + size_t* length, + AdbcError* error) { + return DriverConnectionGetOption(connection, key, value, length, error); +} + ADBC_EXPORT AdbcStatusCode AdbcConnectionGetInfo(AdbcConnection* connection, uint32_t const* info_codes, size_t info_codes_length, diff --git a/tests/adbc_driver_test.cc b/tests/adbc_driver_test.cc index 7f9d4d0..69af868 100644 --- a/tests/adbc_driver_test.cc +++ b/tests/adbc_driver_test.cc @@ -15,7 +15,9 @@ #include #include +#include #include +#include extern "C" AdbcStatusCode AdbcDriverInit(int version, void* driver, AdbcError* error); @@ -29,18 +31,154 @@ void ExpectErrorMessage(AdbcError* error, char const* message) { error->release(error); } +void ReleaseTestStream(ArrowArrayStream* stream) { stream->release = nullptr; } + +bool GetQuackUri(std::string* uri) { + char const* value = std::getenv("QUACK_URI"); + if (value == nullptr || value[0] == '\0') { + return false; + } + *uri = value; + return true; +} + +struct TestConnection { + AdbcDatabase database = {}; + AdbcConnection connection = {}; + + explicit TestConnection(std::string const& uri) { + AdbcError error = ADBC_ERROR_INIT; + EXPECT_EQ(AdbcDatabaseNew(&database, &error), ADBC_STATUS_OK); + EXPECT_EQ(AdbcDatabaseSetOption(&database, "uri", uri.c_str(), &error), + ADBC_STATUS_OK); + EXPECT_EQ(AdbcDatabaseInit(&database, &error), ADBC_STATUS_OK); + EXPECT_EQ(AdbcConnectionNew(&connection, &error), ADBC_STATUS_OK); + EXPECT_EQ(AdbcConnectionInit(&connection, &database, &error), + ADBC_STATUS_OK); + } + + ~TestConnection() { + EXPECT_EQ(AdbcConnectionRelease(&connection, nullptr), ADBC_STATUS_OK); + EXPECT_EQ(AdbcDatabaseRelease(&database, nullptr), ADBC_STATUS_OK); + } +}; + +class AdbcDriverLiveTest : public ::testing::Test { + protected: + void SetUp() override { + if (!GetQuackUri(&quack_uri_)) { + GTEST_SKIP() << "QUACK_URI is required for live Quack server tests"; + } + } + + std::string quack_uri_; +}; + } // namespace -TEST(AdbcDriverTest, UnsupportedStatementApisReturnNotImplemented) { +TEST(AdbcDriverTest, PrepareRejectsUninitializedStatement) { AdbcStatement statement = {}; AdbcError error = ADBC_ERROR_INIT; EXPECT_EQ(AdbcStatementPrepare(&statement, &error), - ADBC_STATUS_NOT_IMPLEMENTED); + ADBC_STATUS_INVALID_STATE); +} + +TEST_F(AdbcDriverLiveTest, PrepareRequiresSqlQuery) { + TestConnection connection(quack_uri_); + AdbcStatement statement = {}; + AdbcError error = ADBC_ERROR_INIT; + + ASSERT_EQ(AdbcStatementNew(&connection.connection, &statement, &error), + ADBC_STATUS_OK); + EXPECT_EQ(AdbcStatementPrepare(&statement, &error), + ADBC_STATUS_INVALID_STATE); + + ASSERT_EQ(AdbcStatementRelease(&statement, nullptr), ADBC_STATUS_OK); +} + +TEST_F(AdbcDriverLiveTest, PrepareAcceptsSqlQuery) { + TestConnection connection(quack_uri_); + AdbcStatement statement = {}; + AdbcError error = ADBC_ERROR_INIT; + + ASSERT_EQ(AdbcStatementNew(&connection.connection, &statement, &error), + ADBC_STATUS_OK); + ASSERT_EQ(AdbcStatementSetSqlQuery(&statement, "SELECT 1", &error), + ADBC_STATUS_OK); + EXPECT_EQ(AdbcStatementPrepare(&statement, &error), ADBC_STATUS_OK); + + ASSERT_EQ(AdbcStatementRelease(&statement, nullptr), ADBC_STATUS_OK); +} + +TEST(AdbcDriverTest, BindRemainsNotImplemented) { + AdbcStatement statement = {}; + AdbcError error = ADBC_ERROR_INIT; + EXPECT_EQ(AdbcStatementBind(&statement, nullptr, nullptr, &error), ADBC_STATUS_NOT_IMPLEMENTED); } +TEST_F(AdbcDriverLiveTest, BindStreamPreservesSqlUntilExecute) { + TestConnection connection(quack_uri_); + AdbcStatement statement = {}; + ArrowArrayStream stream = {}; + stream.release = ReleaseTestStream; + AdbcError error = ADBC_ERROR_INIT; + + ASSERT_EQ(AdbcStatementNew(&connection.connection, &statement, &error), + ADBC_STATUS_OK); + ASSERT_EQ(AdbcStatementSetSqlQuery(&statement, "SELECT ?", &error), + ADBC_STATUS_OK); + ASSERT_EQ(AdbcStatementBindStream(&statement, &stream, &error), + ADBC_STATUS_OK); + EXPECT_EQ(AdbcStatementExecuteQuery(&statement, nullptr, nullptr, &error), + ADBC_STATUS_NOT_IMPLEMENTED); + + ASSERT_EQ(AdbcStatementRelease(&statement, nullptr), ADBC_STATUS_OK); +} + +TEST_F(AdbcDriverLiveTest, IngestTargetTableClearsSqlQuery) { + TestConnection connection(quack_uri_); + AdbcStatement statement = {}; + AdbcError error = ADBC_ERROR_INIT; + + ASSERT_EQ(AdbcStatementNew(&connection.connection, &statement, &error), + ADBC_STATUS_OK); + ASSERT_EQ(AdbcStatementSetSqlQuery(&statement, "SELECT 1", &error), + ADBC_STATUS_OK); + ASSERT_EQ(AdbcStatementSetOption(&statement, ADBC_INGEST_OPTION_TARGET_TABLE, + "target", &error), + ADBC_STATUS_OK); + EXPECT_EQ(AdbcStatementPrepare(&statement, &error), + ADBC_STATUS_INVALID_STATE); + + ASSERT_EQ(AdbcStatementRelease(&statement, nullptr), ADBC_STATUS_OK); +} + +TEST_F(AdbcDriverLiveTest, AutocommitDefaultsToTrue) { + TestConnection connection(quack_uri_); + char value[8] = {}; + size_t length = sizeof(value); + AdbcError error = ADBC_ERROR_INIT; + + EXPECT_EQ(AdbcConnectionGetOption(&connection.connection, + ADBC_CONNECTION_OPTION_AUTOCOMMIT, value, + &length, &error), + ADBC_STATUS_OK); + EXPECT_STREQ(value, ADBC_OPTION_VALUE_ENABLED); +} + +TEST_F(AdbcDriverLiveTest, CommitRollbackRejectAutocommit) { + TestConnection connection(quack_uri_); + AdbcError error = ADBC_ERROR_INIT; + + EXPECT_EQ(AdbcConnectionCommit(&connection.connection, &error), + ADBC_STATUS_INVALID_STATE); + EXPECT_EQ(AdbcConnectionRollback(&connection.connection, &error), + ADBC_STATUS_INVALID_STATE); +} + TEST(AdbcDriverTest, DriverExposesGetObjects) { AdbcDriver driver = {}; ASSERT_EQ(AdbcDriverInit(ADBC_VERSION_1_1_0, &driver, nullptr), @@ -73,10 +211,9 @@ TEST(AdbcDriverTest, ErrorMessagesIncludeQuackPrefix) { AdbcStatement statement = {}; AdbcError error = ADBC_ERROR_INIT; - EXPECT_EQ(AdbcStatementPrepare(&statement, &error), + EXPECT_EQ(AdbcStatementBind(&statement, nullptr, nullptr, &error), ADBC_STATUS_NOT_IMPLEMENTED); - ExpectErrorMessage(&error, - "[quack] parameterized statements are not implemented"); + ExpectErrorMessage(&error, "[quack] parameter binding is not implemented"); } TEST(AdbcDriverTest, HelperErrorMessagesIncludeQuackPrefix) { @@ -104,7 +241,7 @@ TEST(AdbcDriverTest, PropagatedErrorMessagesIncludeQuackPrefix) { ASSERT_EQ(AdbcDatabaseRelease(&database, nullptr), ADBC_STATUS_OK); } -TEST(AdbcDriverTest, DatabaseInitHandlesManagerExtendedError) { +TEST_F(AdbcDriverLiveTest, DatabaseInitHandlesManagerExtendedError) { AdbcDriver driver = {}; ASSERT_EQ(AdbcDriverInit(ADBC_VERSION_1_1_0, &driver, nullptr), ADBC_STATUS_OK); @@ -117,9 +254,7 @@ TEST(AdbcDriverTest, DatabaseInitHandlesManagerExtendedError) { ASSERT_EQ(AdbcDatabaseNew(&database, &error), ADBC_STATUS_OK); EXPECT_EQ(database.private_driver, &driver); - ASSERT_EQ(AdbcDatabaseSetOption(&database, "uri", - "quack://localhost:9494/?token=quack-secret", - &error), + ASSERT_EQ(AdbcDatabaseSetOption(&database, "uri", quack_uri_.c_str(), &error), ADBC_STATUS_OK); EXPECT_EQ(AdbcDatabaseInit(&database, &error), ADBC_STATUS_OK); EXPECT_EQ(database.private_driver, &driver); diff --git a/tests/exported_symbols_test.cc b/tests/exported_symbols_test.cc index ac65e0f..471abfb 100644 --- a/tests/exported_symbols_test.cc +++ b/tests/exported_symbols_test.cc @@ -62,15 +62,27 @@ TEST(ExportedSymbolsTest, ExportsRequiredAdbcEntryPoints) { ASSERT_NE(library, nullptr); char const* symbols[] = { - "AdbcDatabaseNew", "AdbcDatabaseSetOption", - "AdbcDatabaseInit", "AdbcDatabaseRelease", - "AdbcConnectionNew", "AdbcConnectionInit", - "AdbcConnectionGetInfo", "AdbcConnectionSetOption", - "AdbcConnectionRelease", "AdbcStatementNew", - "AdbcStatementSetSqlQuery", "AdbcStatementExecuteQuery", - "AdbcStatementPrepare", "AdbcStatementSetOption", - "AdbcStatementBind", "AdbcStatementBindStream", - "AdbcStatementRelease", "AdbcDriverInit", + "AdbcDatabaseNew", + "AdbcDatabaseSetOption", + "AdbcDatabaseInit", + "AdbcDatabaseRelease", + "AdbcConnectionNew", + "AdbcConnectionInit", + "AdbcConnectionGetInfo", + "AdbcConnectionGetOption", + "AdbcConnectionSetOption", + "AdbcConnectionCommit", + "AdbcConnectionRollback", + "AdbcConnectionRelease", + "AdbcStatementNew", + "AdbcStatementSetSqlQuery", + "AdbcStatementExecuteQuery", + "AdbcStatementPrepare", + "AdbcStatementSetOption", + "AdbcStatementBind", + "AdbcStatementBindStream", + "AdbcStatementRelease", + "AdbcDriverInit", "AdbcDriverQuackInit", }; @@ -105,9 +117,11 @@ TEST(ExportedSymbolsTest, DriverInitFunctionsPopulateDriverTable) { EXPECT_NE(driver.DatabaseRelease, nullptr) << init_symbol; EXPECT_NE(driver.ConnectionNew, nullptr) << init_symbol; EXPECT_NE(driver.ConnectionInit, nullptr) << init_symbol; + EXPECT_NE(driver.ConnectionCommit, nullptr) << init_symbol; EXPECT_NE(driver.ConnectionGetInfo, nullptr) << init_symbol; EXPECT_NE(driver.ConnectionSetOption, nullptr) << init_symbol; EXPECT_NE(driver.ConnectionRelease, nullptr) << init_symbol; + EXPECT_NE(driver.ConnectionRollback, nullptr) << init_symbol; EXPECT_NE(driver.StatementNew, nullptr) << init_symbol; EXPECT_NE(driver.StatementSetSqlQuery, nullptr) << init_symbol; EXPECT_NE(driver.StatementSetOption, nullptr) << init_symbol; diff --git a/validation/.env.linux b/validation/.env.linux new file mode 100644 index 0000000..31c1781 --- /dev/null +++ b/validation/.env.linux @@ -0,0 +1,15 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +export QUACK_URI="quack://localhost:9494/?token=quack-secret" diff --git a/validation/tests/quack.py b/validation/tests/quack.py index 7268df6..ec595ca 100644 --- a/validation/tests/quack.py +++ b/validation/tests/quack.py @@ -13,7 +13,6 @@ # limitations under the License. import functools -import os from pathlib import Path from adbc_drivers_validation import model, quirks @@ -30,7 +29,7 @@ class QuackQuirks(model.DriverQuirks): connection_get_table_schema=False, connection_set_current_catalog=True, connection_set_current_schema=True, - connection_transactions=False, + connection_transactions=True, current_catalog="quack-validation", current_schema="main", get_objects=True, @@ -38,7 +37,7 @@ class QuackQuirks(model.DriverQuirks): statement_bulk_ingest=True, statement_execute_schema=False, statement_get_parameter_schema=False, - statement_prepare=False, + statement_prepare=True, statement_rows_affected=False, statement_rows_affected_ddl=False, secondary_catalog="quack-validation", @@ -47,9 +46,7 @@ class QuackQuirks(model.DriverQuirks): ) setup = model.DriverSetup( database={ - "uri": os.environ.get( - "QUACK_URI", "quack://localhost:9494/?token=quack-secret" - ) + "uri": model.FromEnv("QUACK_URI"), }, connection={}, statement={}, diff --git a/validation/tests/test_ingest.py b/validation/tests/test_ingest.py index 5ee4734..cfa922b 100644 --- a/validation/tests/test_ingest.py +++ b/validation/tests/test_ingest.py @@ -12,7 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +import adbc_driver_manager.dbapi import adbc_drivers_validation.tests.ingest as ingest_tests +import pyarrow +import pytest +from adbc_drivers_validation.utils import execute_query_without_prepare from .quack import get_quirks @@ -23,4 +27,32 @@ def pytest_generate_tests(metafunc) -> None: class TestIngest(ingest_tests.TestIngest): - pass + @pytest.mark.requires_features(["statement_bulk_ingest", "connection_transactions"]) + def test_bulk_ingest_rolls_back_with_manual_transaction( + self, driver, driver_path: str, db_kwargs: dict + ) -> None: + table_name = "test_bulk_ingest_manual_rollback" + data = pyarrow.table({"idx": [1], "value": ["a"]}) + + with adbc_driver_manager.dbapi.connect( + driver=driver_path, db_kwargs=db_kwargs, autocommit=True + ) as cleanup_conn: + with cleanup_conn.cursor() as cursor: + driver.try_drop_table(cursor, table_name=table_name) + + with adbc_driver_manager.dbapi.connect( + driver=driver_path, db_kwargs=db_kwargs, autocommit=False + ) as conn: + with conn.cursor() as cursor: + cursor.adbc_ingest(table_name, data, mode="create") + + conn.rollback() + + with conn.cursor() as cursor: + with pytest.raises(adbc_driver_manager.dbapi.Error) as excinfo: + execute_query_without_prepare( + cursor, + f"SELECT * FROM {driver.quote_identifier(table_name)}", + ) + + assert driver.is_table_not_found(table_name, excinfo.value)