diff --git a/.github/workflows/cpp.yml b/.github/workflows/cpp.yml index 4b47db3873..df811c9a67 100644 --- a/.github/workflows/cpp.yml +++ b/.github/workflows/cpp.yml @@ -195,6 +195,20 @@ jobs: ctest --output-on-failure --no-tests=error if %errorlevel% neq 0 then exit /b %errorlevel% cd ..\.. + - name: Build/Test SQLite Driver + shell: cmd /C call {0} + run: | + mkdir build\driver_sqlite + cd build\driver_sqlite + cmake -GNinja ..\..\c\driver\sqlite -DADBC_BUILD_TESTS=ON -DCMAKE_BUILD_TYPE=Debug -DCMAKE_PREFIX_PATH=%CONDA_PREFIX% -DADBC_BUILD_SHARED=ON -DADBC_BUILD_STATIC=OFF + if %errorlevel% neq 0 then exit /b %errorlevel% + cmake --build . + dir + if %errorlevel% neq 0 then exit /b %errorlevel% + set PATH=%PATH%;${{ github.workspace }}\build\driver_sqlite + ctest --output-on-failure --no-tests=error + if %errorlevel% neq 0 then exit /b %errorlevel% + cd ..\.. - name: Build/Test Python Driver Manager shell: cmd /C call {0} run: | @@ -203,3 +217,11 @@ jobs: set PATH=%PATH%;${{ github.workspace }}\build\driver_sqlite python -m pytest -vv cd ..\.. + - name: Build/Test Python Driver SQLite + shell: cmd /C call {0} + run: | + cd python\adbc_driver_sqlite + set ADBC_SQLITE_LIBRARY=${{ github.workspace }}\build\driver_sqlite\adbc_driver_sqlite.dll + pip install -e . + python -m pytest -vv + cd ..\.. diff --git a/c/driver/sqlite/CMakeLists.txt b/c/driver/sqlite/CMakeLists.txt index 1a40a3fa81..a80fd0dc9a 100644 --- a/c/driver/sqlite/CMakeLists.txt +++ b/c/driver/sqlite/CMakeLists.txt @@ -25,9 +25,16 @@ project(adbc_driver_sqlite VERSION "${ADBC_BASE_VERSION}" LANGUAGES CXX) include(CTest) -find_package(PkgConfig) -find_package(SQLite3 REQUIRED) +if(WIN32) + # XXX: for now, assume vcpkg + find_package(unofficial-sqlite3 CONFIG REQUIRED) + set(SQLite3_LINK_LIBRARIES unofficial::sqlite3::sqlite3) + set(SQLite3_INCLUDE_DIRS) +else() + find_package(SQLite3 REQUIRED) + set(SQLite3_LINK_LIBRARIES SQLite::SQLite3) +endif() add_arrow_lib(adbc_driver_sqlite SOURCES @@ -39,10 +46,10 @@ add_arrow_lib(adbc_driver_sqlite SHARED_LINK_FLAGS ${ADBC_LINK_FLAGS} SHARED_LINK_LIBS - SQLite::SQLite3 + ${SQLite3_LINK_LIBRARIES} nanoarrow STATIC_LINK_LIBS - SQLite::SQLite3 + ${SQLite3_LINK_LIBRARIES} nanoarrow ${LIBPQ_STATIC_LIBRARIES}) include_directories(SYSTEM ${REPOSITORY_ROOT}) diff --git a/c/driver/sqlite/sqlite.c b/c/driver/sqlite/sqlite.c index 233e5c1d26..82eac6c318 100644 --- a/c/driver/sqlite/sqlite.c +++ b/c/driver/sqlite/sqlite.c @@ -1783,6 +1783,7 @@ AdbcStatusCode AdbcStatementExecutePartitions(struct AdbcStatement* statement, } // NOLINT(whitespace/indent) // due to https://github.com/cpplint/cpplint/pull/189 +ADBC_EXPORT AdbcStatusCode AdbcDriverInit(int version, void* driver, struct AdbcError* error) { return SqliteDriverInit(version, driver, error); } diff --git a/c/driver/sqlite/utils.h b/c/driver/sqlite/utils.h index 5d65d88962..3fb3b1e021 100644 --- a/c/driver/sqlite/utils.h +++ b/c/driver/sqlite/utils.h @@ -21,9 +21,16 @@ #include +#if defined(__GNUC__) +#define SET_ERROR_ATTRIBUTE __attribute__((format(printf, 2, 3))) +#else +#define SET_ERROR_ATTRIBUTE +#endif + /// Set error details using a format string. -void SetError(struct AdbcError* error, const char* format, ...) - __attribute__((format(printf, 2, 3))); +void SetError(struct AdbcError* error, const char* format, ...) SET_ERROR_ATTRIBUTE; + +#undef SET_ERROR_ATTRIBUTE /// Wrap a single batch as a stream. AdbcStatusCode BatchToArrayStream(struct ArrowArray* values, struct ArrowSchema* schema, diff --git a/c/driver_manager/adbc_driver_manager.cc b/c/driver_manager/adbc_driver_manager.cc index 80b0358327..236073baba 100644 --- a/c/driver_manager/adbc_driver_manager.cc +++ b/c/driver_manager/adbc_driver_manager.cc @@ -665,7 +665,9 @@ AdbcStatusCode AdbcLoadDriver(const char* driver_name, const char* entrypoint, void* load_handle = GetProcAddress(handle, entrypoint); init_func = reinterpret_cast(load_handle); if (!init_func) { - std::string message = "GetProcAddress() failed: "; + std::string message = "GetProcAddress("; + message += entrypoint; + message += ") failed: "; GetWinError(&message); if (!FreeLibrary(handle)) { message += "\nFreeLibrary() failed: "; @@ -722,7 +724,9 @@ AdbcStatusCode AdbcLoadDriver(const char* driver_name, const char* entrypoint, void* load_handle = dlsym(handle, entrypoint); if (!load_handle) { - std::string message = "dlsym() failed: "; + std::string message = "dlsym("; + message += entrypoint; + message += ") failed: "; message += dlerror(); SetError(error, message); return ADBC_STATUS_INTERNAL; diff --git a/ci/scripts/python_build.sh b/ci/scripts/python_build.sh index 6d39a533a3..135f6438ec 100755 --- a/ci/scripts/python_build.sh +++ b/ci/scripts/python_build.sh @@ -21,6 +21,7 @@ set -e : ${BUILD_ALL:=1} : ${BUILD_DRIVER_MANAGER:=${BUILD_ALL}} : ${BUILD_DRIVER_POSTGRES:=${BUILD_ALL}} +: ${BUILD_DRIVER_SQLITE:=${BUILD_ALL}} if [[ $(uname) = "Darwin" ]]; then ADBC_LIBRARY_SUFFIX="dylib" @@ -33,10 +34,13 @@ build_subproject() { local -r install_dir="${2}" local -r subproject="${3}" - if [[ "${subproject}" -eq "adbc_driver_postgres" ]]; then + if [[ "${subproject}" = "adbc_driver_postgres" ]]; then export ADBC_POSTGRES_LIBRARY="${install_dir}/lib/libadbc_driver_postgres.${ADBC_LIBRARY_SUFFIX}" + elif [[ "${subproject}" = "adbc_driver_sqlite" ]]; then + export ADBC_SQLITE_LIBRARY="${install_dir}/lib/libadbc_driver_sqlite.${ADBC_LIBRARY_SUFFIX}" fi + echo foo $subproject $ADBC_SQLITE_LIBRARY python -m pip install -e "${source_dir}/python/${subproject}" } @@ -56,6 +60,10 @@ main() { if [[ "${BUILD_DRIVER_POSTGRES}" -gt 0 ]]; then build_subproject "${source_dir}" "${install_dir}" adbc_driver_postgres fi + + if [[ "${BUILD_DRIVER_SQLITE}" -gt 0 ]]; then + build_subproject "${source_dir}" "${install_dir}" adbc_driver_sqlite + fi } main "$@" diff --git a/ci/scripts/python_sdist_build.sh b/ci/scripts/python_sdist_build.sh index ea3337584b..535331c108 100755 --- a/ci/scripts/python_sdist_build.sh +++ b/ci/scripts/python_sdist_build.sh @@ -20,6 +20,9 @@ set -ex source_dir=${1} +script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +source "${script_dir}/python_util.sh" echo "=== (${PYTHON_VERSION}) Building ADBC sdists ===" @@ -30,7 +33,7 @@ pip install --upgrade pip setuptools # For drivers, which bundle shared libraries, defer that to install time export _ADBC_IS_SDIST=1 -for component in adbc_driver_manager adbc_driver_postgres; do +for component in ${COMPONENTS}; do pushd ${source_dir}/python/$component echo "=== (${PYTHON_VERSION}) Building $component sdist ===" diff --git a/ci/scripts/python_test.sh b/ci/scripts/python_test.sh index 17c56c1f2f..c858420c03 100755 --- a/ci/scripts/python_test.sh +++ b/ci/scripts/python_test.sh @@ -21,6 +21,7 @@ set -e : ${BUILD_ALL:=1} : ${BUILD_DRIVER_MANAGER:=${BUILD_ALL}} : ${BUILD_DRIVER_POSTGRES:=${BUILD_ALL}} +: ${BUILD_DRIVER_SQLITE:=${BUILD_ALL}} test_subproject() { local -r source_dir=${1} @@ -53,6 +54,10 @@ main() { if [[ "${BUILD_DRIVER_POSTGRES}" -gt 0 ]]; then test_subproject "${source_dir}" "${install_dir}" adbc_driver_postgres fi + + if [[ "${BUILD_DRIVER_SQLITE}" -gt 0 ]]; then + test_subproject "${source_dir}" "${install_dir}" adbc_driver_sqlite + fi } main "$@" diff --git a/ci/scripts/python_util.sh b/ci/scripts/python_util.sh index fb06d6cf38..bd2a01bd48 100644 --- a/ci/scripts/python_util.sh +++ b/ci/scripts/python_util.sh @@ -17,7 +17,7 @@ # specific language governing permissions and limitations # under the License. -COMPONENTS="adbc_driver_manager adbc_driver_postgres" +COMPONENTS="adbc_driver_manager adbc_driver_postgres adbc_driver_sqlite" function build_drivers { local -r source_dir="$1" @@ -30,21 +30,29 @@ function build_drivers { # Enable manifest mode : ${VCPKG_FEATURE_FLAGS:=manifests} # Add our custom triplets - : ${VCPKG_OVERLAY_TRIPLETS:="${source_dir}/ci/vcpkg/triplets/"} + export VCPKG_OVERLAY_TRIPLETS="${source_dir}/ci/vcpkg/triplets/" if [[ $(uname) == "Linux" ]]; then export ADBC_POSTGRES_LIBRARY=${build_dir}/lib/libadbc_driver_postgres.so + export ADBC_SQLITE_LIBRARY=${build_dir}/lib/libadbc_driver_sqlite.so export VCPKG_DEFAULT_TRIPLET="x64-linux-static-release" + + # XXX: Patch the portfile + sed -i "s|include/postgresql/server/pg_config.h|include/server/pg_config.h|" \ + "${VCPKG_ROOT}/ports/libpq/portfile.cmake" else # macOS export ADBC_POSTGRES_LIBRARY=${build_dir}/lib/libadbc_driver_postgres.dylib + export ADBC_SQLITE_LIBRARY=${build_dir}/lib/libadbc_driver_sqlite.dylib export VCPKG_DEFAULT_TRIPLET="x64-osx-static-release" - fi - echo ${VCPKG_DEFAULT_TRIPLET} - - mkdir -p ${build_dir} - pushd ${build_dir} + # XXX: Patch the portfile + sed -i '.bak' "s|include/postgresql/server/pg_config.h|include/server/pg_config.h|" \ + "${VCPKG_ROOT}/ports/libpq/portfile.cmake" + fi + echo "=== Building driver/postgres ===" + mkdir -p ${build_dir}/driver/postgres + pushd ${build_dir}/driver/postgres cmake \ -G ${CMAKE_GENERATOR} \ -DADBC_BUILD_SHARED=ON \ @@ -53,23 +61,44 @@ function build_drivers { -DCMAKE_INSTALL_PREFIX=${build_dir} \ -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} \ + -DVCPKG_OVERLAY_TRIPLETS="${VCPKG_OVERLAY_TRIPLETS}" \ + -DVCPKG_TARGET_TRIPLET="${VCPKG_DEFAULT_TRIPLET}" \ ${source_dir}/c/driver/postgres cmake --build . --target install -j popd + + echo "=== Building driver/sqlite ===" + mkdir -p ${build_dir}/driver/sqlite + pushd ${build_dir}/driver/sqlite + cmake \ + -G ${CMAKE_GENERATOR} \ + -DADBC_BUILD_SHARED=ON \ + -DADBC_BUILD_STATIC=OFF \ + -DCMAKE_INSTALL_LIBDIR=lib \ + -DCMAKE_INSTALL_PREFIX=${build_dir} \ + -DCMAKE_TOOLCHAIN_FILE=${VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake \ + -DCMAKE_UNITY_BUILD=${CMAKE_UNITY_BUILD} \ + -DVCPKG_OVERLAY_TRIPLETS="${VCPKG_OVERLAY_TRIPLETS}" \ + -DVCPKG_TARGET_TRIPLET="${VCPKG_DEFAULT_TRIPLET}" \ + ${source_dir}/c/driver/sqlite + cmake --build . --target install -j + popd } function test_packages { - python -c " -import adbc_driver_manager -import adbc_driver_manager.dbapi -import adbc_driver_postgres -import adbc_driver_postgres.dbapi + for component in ${COMPONENTS}; do + echo "=== Testing $component ===" + + python -c " +import $component +import $component.dbapi " - # Will only run some smoke tests - # --import-mode required, else tries to import from the source dir instead of installed package - echo "=== Testing adbc_driver_manager ===" - python -m pytest -vvx --import-mode append -k "not sqlite" ${source_dir}/python/adbc_driver_manager/tests - echo "=== Testing adbc_driver_postgres ===" - python -m pytest -vvx --import-mode append ${source_dir}/python/adbc_driver_postgres/tests + # --import-mode required, else tries to import from the source dir instead of installed package + if [[ "${component}" = "adbc_driver_manager" ]]; then + python -m pytest -vvx --import-mode append -k "not sqlite" ${source_dir}/python/$component/tests + else + python -m pytest -vvx --import-mode append ${source_dir}/python/$component/tests + fi + done } diff --git a/ci/scripts/python_wheel_unix_build.sh b/ci/scripts/python_wheel_unix_build.sh index 58c4e6199e..616462c817 100755 --- a/ci/scripts/python_wheel_unix_build.sh +++ b/ci/scripts/python_wheel_unix_build.sh @@ -62,18 +62,19 @@ function check_wheels { fi } -echo "=== (${PYTHON_VERSION}) Building ADBC libpq driver ===" -# Sets ADBC_POSTGRES_LIBRARY +echo "=== (${PYTHON_VERSION}) Building C/C++ driver components ===" +# Sets ADBC_POSTGRES_LIBRARY, ADBC_SQLITE_LIBRARY build_drivers "${source_dir}" "${build_dir}" # Check that we don't expose any unwanted symbols check_visibility $ADBC_POSTGRES_LIBRARY +check_visibility $ADBC_SQLITE_LIBRARY # https://github.com/pypa/pip/issues/7555 # Get the latest pip so we have in-tree-build by default -pip install --upgrade pip +pip install --upgrade pip auditwheel -for component in adbc_driver_manager adbc_driver_postgres; do +for component in $COMPONENTS; do pushd ${source_dir}/python/$component echo "=== (${PYTHON_VERSION}) Clean build artifacts===" diff --git a/ci/scripts/python_wheel_windows_build.bat b/ci/scripts/python_wheel_windows_build.bat index 654dbfdbed..3dcea73811 100644 --- a/ci/scripts/python_wheel_windows_build.bat +++ b/ci/scripts/python_wheel_windows_build.bat @@ -30,8 +30,10 @@ set VCPKG_TARGET_TRIPLET=x64-windows-static IF NOT DEFINED VCPKG_ROOT (echo "Must set VCPKG_ROOT" && exit /B 1) -mkdir %build_dir% -pushd %build_dir% +%VCPKG_ROOT%\vcpkg install --triplet=%VCPKG_TARGET_TRIPLET% libpq sqlite3 + +mkdir %build_dir%\postgres +pushd %build_dir%\postgres cmake ^ -G "%CMAKE_GENERATOR%" ^ @@ -50,9 +52,29 @@ set ADBC_POSTGRES_LIBRARY=%build_dir%\bin\adbc_driver_postgres.dll popd +mkdir %build_dir%\sqlite +pushd %build_dir%\sqlite + +cmake ^ + -G "%CMAKE_GENERATOR%" ^ + -DADBC_BUILD_SHARED=ON ^ + -DADBC_BUILD_STATIC=OFF ^ + -DCMAKE_BUILD_TYPE=%CMAKE_BUILD_TYPE% ^ + -DCMAKE_INSTALL_PREFIX=%build_dir% ^ + -DCMAKE_TOOLCHAIN_FILE=%VCPKG_ROOT%/scripts/buildsystems/vcpkg.cmake ^ + -DCMAKE_UNITY_BUILD=%CMAKE_UNITY_BUILD% ^ + -DVCPKG_TARGET_TRIPLET=%VCPKG_TARGET_TRIPLET% ^ + %source_dir%\c\driver\sqlite || exit /B 1 +cmake --build . --config %CMAKE_BUILD_TYPE% --target install -j || exit /B 1 + +@REM XXX: CMake installs it to bin instead of lib for some reason +set ADBC_SQLITE_LIBRARY=%build_dir%\bin\adbc_driver_sqlite.dll + +popd + python -m pip install --upgrade pip delvewheel -FOR %%c IN (adbc_driver_manager adbc_driver_postgres) DO ( +FOR %%c IN (adbc_driver_manager adbc_driver_postgres adbc_driver_sqlite) DO ( pushd %source_dir%\python\%%c echo "=== (%PYTHON_VERSION%) Building %%c wheel ===" diff --git a/ci/scripts/python_wheel_windows_test.bat b/ci/scripts/python_wheel_windows_test.bat index 7273f35a8a..19305ca549 100644 --- a/ci/scripts/python_wheel_windows_test.bat +++ b/ci/scripts/python_wheel_windows_test.bat @@ -21,7 +21,7 @@ set source_dir=%1 echo "=== (%PYTHON_VERSION%) Installing wheels ===" -FOR %%c IN (adbc_driver_manager adbc_driver_postgres) DO ( +FOR %%c IN (adbc_driver_manager adbc_driver_postgres adbc_driver_sqlite) DO ( FOR %%w IN (%source_dir%\python\%%c\dist\*.whl) DO ( pip install --force-reinstall %%w || exit /B 1 ) @@ -31,7 +31,7 @@ pip install pytest pyarrow pandas echo "=== (%PYTHON_VERSION%) Testing wheels ===" -FOR %%c IN (adbc_driver_manager adbc_driver_postgres) DO ( +FOR %%c IN (adbc_driver_manager adbc_driver_postgres adbc_driver_sqlite) DO ( echo "=== Testing %%c ===" python -c "import %%c" || exit /B 1 python -c "import %%c.dbapi" || exit /B 1 diff --git a/docs/source/cpp/driver/index.rst b/docs/source/cpp/driver/index.rst index 7a05c79b2b..df901a7c07 100644 --- a/docs/source/cpp/driver/index.rst +++ b/docs/source/cpp/driver/index.rst @@ -27,3 +27,4 @@ protocols/databases. More may be available from third parties. flight_sql postgres + sqlite diff --git a/docs/source/cpp/driver/postgres.rst b/docs/source/cpp/driver/postgres.rst index 6a8f0b4b96..eb2ee965d7 100644 --- a/docs/source/cpp/driver/postgres.rst +++ b/docs/source/cpp/driver/postgres.rst @@ -19,8 +19,8 @@ libpq-based Driver ================== -The Flight SQL Driver provides access to any database that supports -the Postgres wire format. +The Postgres driver provides access to any database that supports the +Postgres wire format. Installation ============ diff --git a/docs/source/cpp/driver/sqlite.rst b/docs/source/cpp/driver/sqlite.rst new file mode 100644 index 0000000000..19788fa340 --- /dev/null +++ b/docs/source/cpp/driver/sqlite.rst @@ -0,0 +1,63 @@ +.. Licensed to the Apache Software Foundation (ASF) under one +.. or more contributor license agreements. See the NOTICE file +.. distributed with this work for additional information +.. regarding copyright ownership. The ASF licenses this file +.. to you 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. + +============= +SQLite Driver +============= + +The SQLite driver provides access to any database that supports the +Postgres wire format. + +Installation +============ + +The SQLite driver is shipped as a standalone library. See +:ref:`Installation `. + +Usage +===== + +To connect to a database, supply the "uri" parameter when constructing +the :cpp:class:`AdbcDatabase`. This should be a filename or `URI +filename `_. +If omitted, it will default to an in-memory database, but one that is +shared across all connections. + +.. tab-set:: + + .. tab-item:: C++ + :sync: cpp + + .. code-block:: cpp + + #include "adbc.h" + + // Ignoring error handling + struct AdbcDatabase database; + AdbcDatabaseNew(&database, nullptr); + AdbcDatabaseSetOption(&database, "uri", "file:mydb.db", nullptr); + AdbcDatabaseInit(&database, nullptr); + + .. tab-item:: Python + :sync: python + + .. code-block:: python + + import adbc_driver_sqlite.dbapi + + with adbc_driver_sqlite.dbapi.connect() as conn: + pass diff --git a/docs/source/cpp/install.rst b/docs/source/cpp/install.rst index ee2a57bc39..0fad01a459 100644 --- a/docs/source/cpp/install.rst +++ b/docs/source/cpp/install.rst @@ -81,6 +81,28 @@ Flight SQL Driver libpq-based Driver ================== +.. tab-set:: + + .. tab-item:: C++ + :sync: cpp + + .. note:: Under construction + + .. tab-item:: Python (pip) + :sync: python + + .. note:: Under construction + + .. tab-item:: Python (conda-forge) + :sync: python-conda-forge + + .. note:: Under construction + +.. _cpp-install-sqlite: + +SQLite Driver +============= + .. tab-set:: .. tab-item:: C++ diff --git a/go/adbc/drivermgr/wrapper_sqlite_test.go b/go/adbc/drivermgr/wrapper_sqlite_test.go index ba3b277c9b..8ec3af0776 100644 --- a/go/adbc/drivermgr/wrapper_sqlite_test.go +++ b/go/adbc/drivermgr/wrapper_sqlite_test.go @@ -216,7 +216,7 @@ func TestDriverMgrCustomInitFunc(t *testing.T) { var exp *adbc.Error assert.ErrorAs(t, err, &exp) assert.Equal(t, adbc.StatusInternal, exp.Code) - assert.Contains(t, exp.Msg, "dlsym() failed") + assert.Contains(t, exp.Msg, "dlsym(ThisSymbolDoesNotExist) failed") switch runtime.GOOS { case "darwin": assert.Contains(t, exp.Msg, "ThisSymbolDoesNotExist): symbol not found") diff --git a/python/adbc_driver_postgres/README.md b/python/adbc_driver_postgres/README.md index b5dcf40051..af697da8af 100644 --- a/python/adbc_driver_postgres/README.md +++ b/python/adbc_driver_postgres/README.md @@ -30,10 +30,8 @@ manager](../adbc_driver_manager/README.md) to provide a [DBAPI 2.0/PEP Dependencies: a build of the libpq driver. -Set the environment variable `ADBC_POSTGRES_LIBRARY` to the directory -containing `libadbc_driver_postgres.so` before running `pip install`. - -(This library does not yet support Windows/MacOS.) +Set the environment variable `ADBC_POSTGRES_LIBRARY` to the path to +`libadbc_driver_postgres.{dll,dylib,so}` before running `pip install`. See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on the general build process. diff --git a/python/adbc_driver_sqlite/.gitignore b/python/adbc_driver_sqlite/.gitignore new file mode 100644 index 0000000000..126488c6c5 --- /dev/null +++ b/python/adbc_driver_sqlite/.gitignore @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +adbc_driver_postgres/*.c +adbc_driver_postgres/*.cpp +build/ diff --git a/python/adbc_driver_sqlite/MANIFEST.in b/python/adbc_driver_sqlite/MANIFEST.in new file mode 100644 index 0000000000..f9de8e7b85 --- /dev/null +++ b/python/adbc_driver_sqlite/MANIFEST.in @@ -0,0 +1,20 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# setuptools manifest + +include adbc_driver_sqlite/libadbc_driver_sqlite.so diff --git a/python/adbc_driver_sqlite/README.md b/python/adbc_driver_sqlite/README.md new file mode 100644 index 0000000000..88b0528fbd --- /dev/null +++ b/python/adbc_driver_sqlite/README.md @@ -0,0 +1,48 @@ + + +# ADBC SQLite Driver for Python + +This package contains bindings for the [SQLite +driver](../../c/driver/sqlite/README.md), using the [driver +manager](../adbc_driver_manager/README.md) to provide a [DBAPI 2.0/PEP +249-compatible][dbapi] interface on top. + +[dbapi]: https://peps.python.org/pep-0249/ + +## Building + +Dependencies: a build of the SQLite driver. + +Set the environment variable `ADBC_SQLITE_LIBRARY` to the path to +`libadbc_driver_sqlite.{dll,dylib,so}` before running `pip install`. + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on the +general build process. + +## Testing + +To run the tests, use pytest: + +```shell +$ pytest -vvx +``` + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on the +general test process. diff --git a/python/adbc_driver_sqlite/adbc_driver_sqlite/__init__.py b/python/adbc_driver_sqlite/adbc_driver_sqlite/__init__.py new file mode 100644 index 0000000000..c01ea88124 --- /dev/null +++ b/python/adbc_driver_sqlite/adbc_driver_sqlite/__init__.py @@ -0,0 +1,35 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +import importlib.resources +import typing + +import adbc_driver_manager + +from ._version import __version__ + +__all__ = ["connect", "__version__"] + + +def connect(uri: typing.Optional[str] = None) -> adbc_driver_manager.AdbcDatabase: + """Create a low level ADBC connection to SQLite.""" + with importlib.resources.path( + __package__, "libadbc_driver_sqlite.so" + ) as entrypoint: + if uri is None: + return adbc_driver_manager.AdbcDatabase(driver=str(entrypoint)) + return adbc_driver_manager.AdbcDatabase(driver=str(entrypoint), uri=uri) diff --git a/python/adbc_driver_sqlite/adbc_driver_sqlite/_static_version.py b/python/adbc_driver_sqlite/adbc_driver_sqlite/_static_version.py new file mode 100644 index 0000000000..7ca903c79b --- /dev/null +++ b/python/adbc_driver_sqlite/adbc_driver_sqlite/_static_version.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# Generated by miniver (CC0). + +# This file is part of 'miniver': https://github.com/jbweston/miniver +# +# This file will be overwritten by setup.py when a source or binary +# distribution is made. The magic value "__use_git__" is interpreted by +# _version.py. + +version = "__use_git__" + +# These values are only set if the distribution was created with 'git archive' +refnames = "$Format:%D$" +git_hash = "$Format:%h$" diff --git a/python/adbc_driver_sqlite/adbc_driver_sqlite/_version.py b/python/adbc_driver_sqlite/adbc_driver_sqlite/_version.py new file mode 100644 index 0000000000..09bfd6f1bd --- /dev/null +++ b/python/adbc_driver_sqlite/adbc_driver_sqlite/_version.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +# Generated by miniver (CC0). + +import os + +# This file is part of 'miniver': https://github.com/jbweston/miniver +# +from collections import namedtuple + +Version = namedtuple("Version", ("release", "dev", "labels")) + +# No public API +__all__ = [] + +package_root = os.path.dirname(os.path.realpath(__file__)) +package_name = os.path.basename(package_root) + +STATIC_VERSION_FILE = "_static_version.py" + + +def get_version(version_file=STATIC_VERSION_FILE): + version_info = get_static_version_info(version_file) + version = version_info["version"] + if version == "__use_git__": + version = get_version_from_git() + if not version: + version = get_version_from_git_archive(version_info) + if not version: + version = Version("unknown", None, None) + return pep440_format(version) + else: + return version + + +def get_static_version_info(version_file=STATIC_VERSION_FILE): + version_info = {} + with open(os.path.join(package_root, version_file), "rb") as f: + exec(f.read(), {}, version_info) + return version_info + + +def version_is_from_git(version_file=STATIC_VERSION_FILE): + return get_static_version_info(version_file)["version"] == "__use_git__" + + +def pep440_format(version_info): + release, dev, labels = version_info + + version_parts = [release] + if dev: + if release.endswith("-dev") or release.endswith(".dev"): + version_parts.append(dev) + else: # prefer PEP440 over strict adhesion to semver + version_parts.append(".dev{}".format(dev)) + + if labels: + version_parts.append("+") + version_parts.append(".".join(labels)) + + return "".join(version_parts) + + +def get_version_from_git(): + import subprocess + + # git describe --first-parent does not take into account tags from branches + # that were merged-in. The '--long' flag gets us the 'dev' version and + # git hash, '--always' returns the git hash even if there are no tags. + for opts in [["--first-parent"], []]: + try: + p = subprocess.Popen( + ["git", "describe", "--long", "--always"] + opts, + cwd=package_root, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + except OSError: + return + if p.wait() == 0: + break + else: + return + + description = ( + p.communicate()[0] + .decode() + .strip("v") # Tags can have a leading 'v', but the version should not + .rstrip("\n") + .rsplit("-", 3) # Split the latest tag, commits since tag, and hash + ) + + try: + _, release, dev, git = description + except ValueError: # No tags, only the git hash + # prepend 'g' to match with format returned by 'git describe' + git = "g{}".format(*description) + # XXX: assume version if not given + release = "0.0.0" + dev = None + + labels = [] + if dev == "0": + dev = None + else: + labels.append(git) + + try: + p = subprocess.Popen(["git", "diff", "--quiet"], cwd=package_root) + except OSError: + labels.append("confused") # This should never happen. + else: + if p.wait() == 1: + labels.append("dirty") + + return Version(release, dev, labels) + + +# TODO: change this logic when there is a git pretty-format +# that gives the same output as 'git describe'. +# Currently we can only tell the tag the current commit is +# pointing to, or its hash (with no version info) +# if it is not tagged. +def get_version_from_git_archive(version_info): + try: + refnames = version_info["refnames"] + git_hash = version_info["git_hash"] + except KeyError: + # These fields are not present if we are running from an sdist. + # Execution should never reach here, though + return None + + if git_hash.startswith("$Format") or refnames.startswith("$Format"): + # variables not expanded during 'git archive' + return None + + VTAG = "tag: adbc-" + refs = set(r.strip() for r in refnames.split(",")) + version_tags = set(r[len(VTAG) :] for r in refs if r.startswith(VTAG)) + if version_tags: + release, *_ = sorted(version_tags) # prefer e.g. "2.0" over "2.0rc1" + return Version(release, dev=None, labels=None) + else: + return Version("unknown", dev=None, labels=["g{}".format(git_hash)]) + + +__version__ = get_version() + + +# The following section defines a 'get_cmdclass' function +# that can be used from setup.py. The '__version__' module +# global is used (but not modified). + + +def _write_version(fname): + # This could be a hard link, so try to delete it first. Is there any way + # to do this atomically together with opening? + try: + os.remove(fname) + except OSError: + pass + with open(fname, "w") as f: + f.write( + "# This file has been created by setup.py.\n" + "version = '{}'\n".format(__version__) + ) + + +def get_cmdclass(pkg_source_path): + from setuptools.command.build_py import build_py as build_py_orig + from setuptools.command.sdist import sdist as sdist_orig + + class _build_py(build_py_orig): + def run(self): + super().run() + + src_marker = "".join(["src", os.path.sep]) + + if pkg_source_path.startswith(src_marker): + path = pkg_source_path[len(src_marker) :] + else: + path = pkg_source_path + _write_version(os.path.join(self.build_lib, path, STATIC_VERSION_FILE)) + + class _sdist(sdist_orig): + def make_release_tree(self, base_dir, files): + super().make_release_tree(base_dir, files) + _write_version(os.path.join(base_dir, pkg_source_path, STATIC_VERSION_FILE)) + + return dict(sdist=_sdist, build_py=_build_py) + + +if __name__ == "__main__": + print("Version: ", get_version()) diff --git a/python/adbc_driver_sqlite/adbc_driver_sqlite/dbapi.py b/python/adbc_driver_sqlite/adbc_driver_sqlite/dbapi.py new file mode 100644 index 0000000000..e85766fa7b --- /dev/null +++ b/python/adbc_driver_sqlite/adbc_driver_sqlite/dbapi.py @@ -0,0 +1,117 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +""" +DBAPI 2.0-compatible facade for the ADBC libpq driver. +""" + +import typing + +import adbc_driver_sqlite + +import adbc_driver_manager +import adbc_driver_manager.dbapi + +__all__ = [ + "BINARY", + "DATETIME", + "NUMBER", + "ROWID", + "STRING", + "Connection", + "Cursor", + "DataError", + "DatabaseError", + "Date", + "DateFromTicks", + "Error", + "IntegrityError", + "InterfaceError", + "InternalError", + "NotSupportedError", + "OperationalError", + "ProgrammingError", + "Time", + "TimeFromTicks", + "Timestamp", + "TimestampFromTicks", + "Warning", + "apilevel", + "connect", + "paramstyle", + "threadsafety", +] + +# ---------------------------------------------------------- +# Globals + +apilevel = adbc_driver_manager.dbapi.apilevel +threadsafety = adbc_driver_manager.dbapi.threadsafety +paramstyle = "qmark" + +Warning = adbc_driver_manager.dbapi.Warning +Error = adbc_driver_manager.dbapi.Error +InterfaceError = adbc_driver_manager.dbapi.InterfaceError +DatabaseError = adbc_driver_manager.dbapi.DatabaseError +DataError = adbc_driver_manager.dbapi.DataError +OperationalError = adbc_driver_manager.dbapi.OperationalError +IntegrityError = adbc_driver_manager.dbapi.IntegrityError +InternalError = adbc_driver_manager.dbapi.InternalError +ProgrammingError = adbc_driver_manager.dbapi.ProgrammingError +NotSupportedError = adbc_driver_manager.dbapi.NotSupportedError + +# ---------------------------------------------------------- +# Types + +Date = adbc_driver_manager.dbapi.Date +Time = adbc_driver_manager.dbapi.Time +Timestamp = adbc_driver_manager.dbapi.Timestamp +DateFromTicks = adbc_driver_manager.dbapi.DateFromTicks +TimeFromTicks = adbc_driver_manager.dbapi.TimeFromTicks +TimestampFromTicks = adbc_driver_manager.dbapi.TimestampFromTicks +STRING = adbc_driver_manager.dbapi.STRING +BINARY = adbc_driver_manager.dbapi.BINARY +NUMBER = adbc_driver_manager.dbapi.NUMBER +DATETIME = adbc_driver_manager.dbapi.DATETIME +ROWID = adbc_driver_manager.dbapi.ROWID + +# ---------------------------------------------------------- +# Functions + + +def connect(uri: typing.Optional[str] = None) -> "Connection": + """Connect to SQLite via ADBC.""" + db = None + conn = None + + try: + db = adbc_driver_sqlite.connect(uri) + conn = adbc_driver_manager.AdbcConnection(db) + return adbc_driver_manager.dbapi.Connection(db, conn) + except Exception: + if conn: + conn.close() + if db: + db.close() + raise + + +# ---------------------------------------------------------- +# Classes + +Connection = adbc_driver_manager.dbapi.Connection +Cursor = adbc_driver_manager.dbapi.Cursor diff --git a/python/adbc_driver_sqlite/pyproject.toml b/python/adbc_driver_sqlite/pyproject.toml new file mode 100644 index 0000000000..ea720f53a6 --- /dev/null +++ b/python/adbc_driver_sqlite/pyproject.toml @@ -0,0 +1,39 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +[project] +name = "adbc_driver_sqlite" +description = "An ADBC driver for working with SQLite." +authors = [{name = "Apache Arrow Developers", email = "dev@arrow.apache.org"}] +license = {text = "Apache-2.0"} +requires-python = ">=3.8" +dynamic = ["version"] + +[project.optional-dependencies] +dbapi = ["pandas", "pyarrow>=8.0.0"] +test = ["pandas", "pyarrow>=8.0.0", "pytest"] + +[project.urls] +homepage = "https://arrow.apache.org" +repository = "https://github.com/apache/arrow-adbc" + +[build-system] +requires = ["setuptools >= 61.0.0"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +include-package-data = true diff --git a/python/adbc_driver_sqlite/setup.py b/python/adbc_driver_sqlite/setup.py new file mode 100644 index 0000000000..4f72cebd9a --- /dev/null +++ b/python/adbc_driver_sqlite/setup.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +import os +import shutil +from pathlib import Path + +from setuptools import setup + +source_root = Path(__file__).parent +repo_root = source_root.joinpath("../../") + +# ------------------------------------------------------------ +# Resolve Shared Library + +library = os.environ.get("ADBC_SQLITE_LIBRARY") +if not library: + if os.environ.get("_ADBC_IS_SDIST", "").strip().lower() in ("1", "true"): + print("Building sdist, not requiring ADBC_SQLITE_LIBRARY") + else: + raise ValueError("Must provide ADBC_SQLITE_LIBRARY") +else: + target = source_root.joinpath( + "./adbc_driver_sqlite/libadbc_driver_sqlite.so" + ).resolve() + shutil.copy(library, target) + +# ------------------------------------------------------------ +# Resolve Version (miniver) + + +def get_version_and_cmdclass(pkg_path): + """ + Load version.py module without importing the whole package. + + Template code from miniver. + """ + from importlib.util import module_from_spec, spec_from_file_location + + spec = spec_from_file_location("version", os.path.join(pkg_path, "_version.py")) + module = module_from_spec(spec) + spec.loader.exec_module(module) + return module.__version__, module.get_cmdclass(pkg_path) + + +version, cmdclass = get_version_and_cmdclass("adbc_driver_sqlite") + +# ------------------------------------------------------------ +# Setup + +setup( + cmdclass=cmdclass, + version=version, +) diff --git a/python/adbc_driver_sqlite/tests/__init__.py b/python/adbc_driver_sqlite/tests/__init__.py new file mode 100644 index 0000000000..13a83393a9 --- /dev/null +++ b/python/adbc_driver_sqlite/tests/__init__.py @@ -0,0 +1,16 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. diff --git a/python/adbc_driver_sqlite/tests/test_dbapi.py b/python/adbc_driver_sqlite/tests/test_dbapi.py new file mode 100644 index 0000000000..74a221de22 --- /dev/null +++ b/python/adbc_driver_sqlite/tests/test_dbapi.py @@ -0,0 +1,31 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +import pytest +from adbc_driver_sqlite import dbapi + + +@pytest.fixture +def sqlite(): + with dbapi.connect() as conn: + yield conn + + +def test_query_trivial(sqlite): + with sqlite.cursor() as cur: + cur.execute("SELECT 1") + assert cur.fetchone() == (1,) diff --git a/python/adbc_driver_sqlite/tests/test_lowlevel.py b/python/adbc_driver_sqlite/tests/test_lowlevel.py new file mode 100644 index 0000000000..051676adf9 --- /dev/null +++ b/python/adbc_driver_sqlite/tests/test_lowlevel.py @@ -0,0 +1,41 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. + +import adbc_driver_sqlite +import pyarrow +import pytest + +import adbc_driver_manager + + +@pytest.fixture +def sqlite(): + with adbc_driver_sqlite.connect() as db: + with adbc_driver_manager.AdbcConnection(db) as conn: + yield conn + + +def test_query_trivial(sqlite): + with adbc_driver_manager.AdbcStatement(sqlite) as stmt: + stmt.set_sql_query("SELECT 1") + stream, _ = stmt.execute_query() + reader = pyarrow.RecordBatchReader._import_from_c(stream.address) + assert reader.read_all() + + +def test_version(): + assert adbc_driver_sqlite.__version__