diff --git a/.github/workflows/python_packages.yml b/.github/workflows/python_packages.yml index ade22051..7a74f449 100644 --- a/.github/workflows/python_packages.yml +++ b/.github/workflows/python_packages.yml @@ -11,7 +11,6 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 CIBW_ARCHS: x86_64 CIBW_BEFORE_ALL_LINUX: dnf makecache && dnf install --assumeyes sqlite-devel - CIBW_BEFORE_BUILD: pip install setuptools scikit-build wheel cmake CIBW_TEST_COMMAND: python -c "import pygeodiff; pygeodiff.GeoDiff().version()" steps: @@ -36,7 +35,6 @@ jobs: env: CIBW_BUILD: "*musllinux*" CIBW_BEFORE_ALL_LINUX: apk add sqlite-dev - CIBW_BEFORE_BUILD: pip install setuptools scikit-build wheel cmake CIBW_TEST_COMMAND: python -c "import pygeodiff; pygeodiff.GeoDiff().version()" steps: @@ -59,11 +57,12 @@ jobs: name: Build wheels on Windows runs-on: windows-2025 env: - CMAKE_GENERATOR: "Visual Studio 17 2022" + CMAKE_GENERATOR: "Visual Studio 18 2026" SQLite3_ROOT: "C:/vcpkg/installed/x64-windows" CIBW_SKIP: "*-win32" CIBW_TEST_COMMAND: python -c "import pygeodiff; pygeodiff.GeoDiff().version()" CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: "delvewheel repair --add-path C:/vcpkg/installed/x64-windows/bin --no-mangle-all -v -w {dest_dir} {wheel}" + MSBUILDDISABLENODEREUSE: "1" steps: - uses: actions/checkout@v3 @@ -77,7 +76,7 @@ jobs: run: | C:/vcpkg/vcpkg install sqlite3[rtree,fts3,json1] --triplet x64-windows C:/vcpkg/vcpkg integrate install - pip install setuptools scikit-build wheel cmake delvewheel + pip install delvewheel dir "C:/vcpkg/installed/x64-windows/bin" - name: Build wheels @@ -88,41 +87,6 @@ jobs: path: ./wheelhouse/*.whl name: dist-windows - build_windows_32_wheels: - name: Build 32bit wheels on Windows - runs-on: windows-2025 - env: - CMAKE_GENERATOR: "Visual Studio 17 2022" - CMAKE_GENERATOR_PLATFORM: "Win32" - SQLite3_ROOT: "C:/vcpkg/installed/x86-windows" - CIBW_SKIP: pp* *-win_amd64 - CIBW_TEST_COMMAND: python -c "import pygeodiff; pygeodiff.GeoDiff().version()" - CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: "delvewheel repair --add-path C:/vcpkg/installed/x86-windows/bin --no-mangle-all -v -w {dest_dir} {wheel}" - - steps: - - uses: actions/checkout@v3 - - - uses: actions/setup-python@v3 - name: Install Python - with: - python-version: '3.9' - architecture: x86 - - - name: Install Deps - run: | - C:/vcpkg/vcpkg install sqlite3[rtree,fts3,json1] --triplet x86-windows - C:/vcpkg/vcpkg integrate install - pip install setuptools scikit-build wheel cmake delvewheel - dir "C:/vcpkg/installed/x86-windows/bin" - - - name: Build wheels - uses: pypa/cibuildwheel@v3.3.1 - - - uses: actions/upload-artifact@v4 - with: - path: ./wheelhouse/*.whl - name: dist-windows_32 - build_macos_arm64_wheels: name: Build wheels on macos-15 (arm64) runs-on: macos-15 @@ -146,10 +110,6 @@ jobs: with: python-version: '3.9' - - name: Install Deps - run: | - pip install setuptools scikit-build wheel cmake - - name: Build wheels uses: pypa/cibuildwheel@v3.3.1 @@ -181,10 +141,6 @@ jobs: with: python-version: '3.9' - - name: Install Deps - run: | - pip install setuptools scikit-build wheel cmake - - name: Build wheels uses: pypa/cibuildwheel@v3.3.1 @@ -207,10 +163,10 @@ jobs: - name: Install deps run: | pip install --upgrade pip - pip install setuptools twine scikit-build wheel cmake + pip install build twine - name: Build sdist - run: python setup.py sdist + run: python -m build --sdist - uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/win_tests.yml b/.github/workflows/win_tests.yml index f7587a64..37aa6219 100644 --- a/.github/workflows/win_tests.yml +++ b/.github/workflows/win_tests.yml @@ -11,7 +11,7 @@ jobs: runs-on: windows-2025 steps: - name: Checkout Geodiff - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: install deps run: | @@ -19,11 +19,6 @@ jobs: C:/vcpkg/vcpkg integrate install dir "C:/vcpkg/installed/x64-windows/bin" - - name: set compiler environment - shell: cmd - run: | - CALL "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" -arch=x86 - - name: build geodiff shell: pwsh run: | @@ -37,7 +32,7 @@ jobs: cd $env:GITHUB_WORKSPACE mkdir build cd build - exec { cmake -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DENABLE_TESTS=ON -DWITH_POSTGRESQL=FALSE ../geodiff } + exec { cmake -G "Visual Studio 18 2026" -DCMAKE_TOOLCHAIN_FILE=C:/vcpkg/scripts/buildsystems/vcpkg.cmake -DENABLE_TESTS=ON -DWITH_POSTGRESQL=FALSE ../geodiff } exec { cmake --build . --config Debug } - name: Run tests diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 618fca33..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,11 +0,0 @@ -# Things to include in the built package (besides the packages defined in setup.py) -include LICENSE -include pyproject.toml -include geodiff/CMakeLists.txt -include geodiff/src/*.cpp -include geodiff/src/*.hpp -include geodiff/src/*.h -include geodiff/src/3rdparty/* -include geodiff/src/drivers/* -include geodiff/cmake_templates/* -include pygeodiff/*.py diff --git a/geodiff/CMakeLists.txt b/geodiff/CMakeLists.txt index 4ae76d28..5ec5382a 100644 --- a/geodiff/CMakeLists.txt +++ b/geodiff/CMakeLists.txt @@ -1,7 +1,7 @@ # GEODIFF (MIT License) # Copyright (C) 2019 Peter Petrik -CMAKE_MINIMUM_REQUIRED(VERSION 3.10) +CMAKE_MINIMUM_REQUIRED(VERSION 3.15) PROJECT(geodiffproject) SET(CMAKE_CXX_VISIBILITY_PRESET hidden) SET(CMAKE_VISIBILITY_INLINES_HIDDEN 1) @@ -26,7 +26,11 @@ ENDIF() IF(SKBUILD) MESSAGE(STATUS "The geodiff is built using scikit-build for pygeodiff Python package") - FIND_PACKAGE(PythonExtensions REQUIRED) + # scikit-build-core provides the project version as SKBUILD_PROJECT_VERSION; + # allow an explicit -DPYGEODIFFVERSION override for standalone invocations. + IF(NOT PYGEODIFFVERSION) + SET(PYGEODIFFVERSION "${SKBUILD_PROJECT_VERSION}") + ENDIF() SET(GEODIFF_NAME "pygeodiff-${PYGEODIFFVERSION}-python${GEODIFF_NAME_SUFFIX}") IF (CMAKE_GENERATOR_PLATFORM STREQUAL "Win32") SET(GEODIFF_NAME "${GEODIFF_NAME}-win32") diff --git a/geodiff/src/geodiff-cli.cpp b/geodiff/src/geodiff-cli.cpp index 76b04f4d..04da32fd 100644 --- a/geodiff/src/geodiff-cli.cpp +++ b/geodiff/src/geodiff-cli.cpp @@ -67,7 +67,7 @@ static bool isOption( const std::string &str ) return str.size() > 0 && str[0] == '-'; } -static bool parseDriverOption( const std::vector &args, size_t &i, const std::string &cmdName, std::string &driverName, std::string &driverOptions, std::string &tablesToSkip ) +static bool parseDriverOption( const std::vector &args, size_t &i, const std::string &cmdName, std::string &driverName, std::string &driverOptions, std::string &tablesToSkip, std::string &tablesToInclude ) { for ( ; i < args.size(); ++i ) { @@ -91,12 +91,23 @@ static bool parseDriverOption( const std::vector &args, size_t &i, if ( i + 1 >= args.size() ) { std::cout << "Error: missing arguments for skip-tables option" << std::endl; - return 1; + return false; } tablesToSkip = args[i + 1]; i += 1; continue; } + else if ( args[i] == "--include-tables" ) + { + if ( i + 1 >= args.size() ) + { + std::cout << "Error: missing arguments for include-tables option" << std::endl; + return false; + } + tablesToInclude = args[i + 1]; + i += 1; + continue; + } else { std::cout << "Error: unknown option '" << args[i] << "' for '" << cmdName << "' command." << std::endl; @@ -130,7 +141,7 @@ static int handleCmdDiff( GEODIFF_ContextH context, const std::vector= args.size() ) + { + std::cout << "Error: missing arguments for include-tables option" << std::endl; + return 1; + } + tablesToInclude = args[i + 1]; + i += 1; + continue; + } else { std::cout << "Error: unknown option '" << args[i] << "' for 'diff' command." << std::endl; @@ -194,10 +216,17 @@ static int handleCmdDiff( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); // parse required arguments if ( !parseRequiredArgument( db1, args, i, "DB_1", "diff" ) ) @@ -307,10 +336,10 @@ static int handleCmdApply( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); int ret = GEODIFF_applyChangesetEx( context, driverName.data(), driverOptions.data(), db.data(), changeset.data() ); if ( ret != GEODIFF_SUCCESS ) @@ -342,10 +373,10 @@ static int handleCmdRebaseDiff( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); int ret = GEODIFF_createRebasedChangesetEx( context, @@ -386,10 +419,10 @@ static int handleCmdRebaseDb( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); int ret = GEODIFF_rebaseEx( context, driverName.data(), driverOptions.data(), dbBase.data(), dbOur.data(), @@ -581,7 +615,7 @@ static int handleCmdCopy( GEODIFF_ContextH context, const std::vector= args.size() ) + { + std::cout << "Error: missing arguments for include-tables option" << std::endl; + return 1; + } + tablesToInclude = args[i + 1]; + i += 1; + continue; } else { @@ -634,14 +680,16 @@ static int handleCmdCopy( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); if ( driver1Name == "sqlite" && driver2Name == "sqlite" ) { - if ( !tablesToSkip.empty() ) + if ( !tablesToSkip.empty() || !tablesToInclude.empty() ) { - std::cout << "Source and destination drivers are \"sqlite\". Option \"--skip-tables\" will be ignored." << std::endl; + std::cout << "Source and destination drivers are \"sqlite\". Table filter options will be ignored." << std::endl; return 1; } @@ -675,10 +723,10 @@ static int handleCmdSchema( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); std::string json; TmpFile tmpJson; @@ -735,10 +785,10 @@ static int handleCmdDump( GEODIFF_ContextH context, const std::vector( context ); - std::vector tables = parseIgnoredTables( tablesToSkip ); - ctx->setTablesToSkip( tables ); + if ( !tablesToSkip.empty() ) + ctx->setTablesToSkip( parseIgnoredTables( tablesToSkip ) ); + else if ( !tablesToInclude.empty() ) + ctx->setTablesToInclude( parseIgnoredTables( tablesToInclude ) ); int ret = GEODIFF_dumpData( context, driverName.data(), driverOptions.data(), db.data(), chOutput.data() ); if ( ret != GEODIFF_SUCCESS ) @@ -841,7 +893,10 @@ Create and apply changesets (diffs):\n\ creation of changesets across datasets in two different drivers.\n\ --skip-tables TABLES\n\ Ignore specified tables when creating a changeset. Tables are defined as\n\ - a semicolon separated list of names.\n\ + a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when creating a changeset. Tables are defined\n\ + as a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ geodiff apply [OPTIONS...] DB CH_INPUT\n\ \n\ @@ -854,7 +909,10 @@ Create and apply changesets (diffs):\n\ database. Driver-specific options are provided in CONN_OPTIONS.\n\ --skip-tables TABLES\n\ Ignore specified tables when applying a changeset. Tables are defined as\n\ - a semicolon separated list of names.\n\ + a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when applying a changeset. Tables are defined\n\ + as a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ Rebasing:\n\ \n\ @@ -875,7 +933,10 @@ Rebasing:\n\ databases. Driver-specific options are provided in CONN_OPTIONS.\n\ --skip-tables TABLES\n\ Ignore specified tables when creating a rebased changeset. Tables are\n\ - defined as a semicolon separated list of names.\n\ + defined as a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when creating a rebased changeset. Tables are\n\ + defined as a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ geodiff rebase-db [OPTIONS...] DB_BASE DB_OUR CH_BASE_THEIR CONFLICT\n\ \n\ @@ -892,7 +953,10 @@ Rebasing:\n\ databases. Driver-specific options are provided in CONN_OPTIONS.\n\ --skip-tables TABLES\n\ Ignore specified tables when rebasing. Tables are defined as\n\ - a semicolon separated list of names.\n\ + a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when rebasing. Tables are defined as\n\ + a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ Utilities:\n\ \n\ @@ -934,7 +998,10 @@ Utilities:\n\ creation of changesets across datasets in two different drivers.\n\ --skip-tables TABLES\n\ Ignore specified tables when copying the database. Tables are defined\n\ - as a semicolon separated list of names.\n\ + as a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when copying the database. Tables are defined\n\ + as a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ geodiff schema [OPTIONS...] DB [SCHEMA_JSON]\n\ \n\ @@ -948,7 +1015,10 @@ Utilities:\n\ database. Driver-specific options are provided in CONN_OPTIONS.\n\ --skip-tables TABLES\n\ Ignore specified tables when writing a schema. Tables are defined\n\ - as a semicolon separated list of names.\n\ + as a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when writing a schema. Tables are defined\n\ + as a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ geodiff dump [OPTIONS...] DB CH_OUTPUT\n\ \n\ @@ -960,7 +1030,10 @@ Utilities:\n\ database. Driver-specific options are provided in CONN_OPTIONS.\n\ --skip-tables TABLES\n\ Ignore specified tables when dumping the database content. Tables\n\ - are defined a semicolon separated list of names.\n\ + are defined as a semicolon separated list of names. Cannot be used with --include-tables.\n\ + --include-tables TABLES\n\ + Only include specified tables when dumping the database content. Tables\n\ + are defined as a semicolon separated list of names. Cannot be used with --skip-tables.\n\ \n\ geodiff drivers\n\ \n\ diff --git a/geodiff/src/geodiff.cpp b/geodiff/src/geodiff.cpp index ffab38dd..3981e2c3 100644 --- a/geodiff/src/geodiff.cpp +++ b/geodiff/src/geodiff.cpp @@ -54,7 +54,7 @@ static int handleException( Context *context, const GeoDiffException &exc ) // use scripts/update_version.py to update the version here and in other places at once const char *GEODIFF_version() { - return "2.2.0"; + return "2.3.0"; } int GEODIFF_driverCount( GEODIFF_ContextH /*contextHandle*/ ) @@ -158,7 +158,45 @@ int GEODIFF_CX_setTablesToSkip( GEODIFF_ContextH contextHandle, int tablesCount, tables.push_back( tableName ); } - context->setTablesToSkip( tables ); + try + { + context->setTablesToSkip( tables ); + } + catch ( const GeoDiffException &exc ) + { + return handleException( context, exc ); + } + return GEODIFF_SUCCESS; +} + +int GEODIFF_CX_setTablesToInclude( GEODIFF_ContextH contextHandle, int tablesCount, const char **tablesToInclude ) +{ + Context *context = static_cast( contextHandle ); + if ( !context ) + { + return GEODIFF_ERROR; + } + + if ( tablesCount > 0 && !tablesToInclude ) + { + setAndLogError( context, "NULL arguments to GEODIFF_CX_setTablesToInclude" ); + return GEODIFF_ERROR; + } + + std::vector tables; + for ( int i = 0; i < tablesCount; ++i ) + { + tables.push_back( tablesToInclude[i] ); + } + + try + { + context->setTablesToInclude( tables ); + } + catch ( const GeoDiffException &exc ) + { + return handleException( context, exc ); + } return GEODIFF_SUCCESS; } diff --git a/geodiff/src/geodiff.h b/geodiff/src/geodiff.h index 7373ea39..885c2bd8 100644 --- a/geodiff/src/geodiff.h +++ b/geodiff/src/geodiff.h @@ -109,10 +109,24 @@ GEODIFF_EXPORT int GEODIFF_CX_setMaximumLoggerLevel( GEODIFF_ContextH contextHan * rebase, get database schema, dump database contents, copy database between different * drivers. * + * Cannot be used together with GEODIFF_CX_setTablesToInclude on the same context. + * * If empty list is passed, list will be reset. */ GEODIFF_EXPORT int GEODIFF_CX_setTablesToSkip( GEODIFF_ContextH contextHandle, int tablesCount, const char **tablesToSkip ); +/** + * Set list of tables to include in geodiff operations. Once defined, only these tables + * will be included in the following operations: create changeset, apply changeset, + * rebase, get database schema, dump database contents, copy database between different + * drivers. All other tables will be ignored. + * + * Cannot be used together with GEODIFF_CX_setTablesToSkip on the same context. + * + * If empty list is passed, list will be reset. + */ +GEODIFF_EXPORT int GEODIFF_CX_setTablesToInclude( GEODIFF_ContextH contextHandle, int tablesCount, const char **tablesToInclude ); + /** * Return null-terminated message of last error that occurred using this context. * Consider the pointer invalid after any call to the GeoDiff API. diff --git a/geodiff/src/geodiffcontext.cpp b/geodiff/src/geodiffcontext.cpp index fc821814..81af8803 100644 --- a/geodiff/src/geodiffcontext.cpp +++ b/geodiff/src/geodiffcontext.cpp @@ -25,17 +25,51 @@ const Logger &Context::logger() const void Context::setTablesToSkip( const std::vector &tablesToSkip ) { - mTablesToSkip = tablesToSkip; + if ( mTablesFilterMode == TablesFilterMode::IncludedTables ) + throw GeoDiffException( "Cannot set tables to skip when tables to include are already set" ); + + if ( tablesToSkip.empty() ) + { + mTablesFilterMode = TablesFilterMode::None; + mTablesToSkip.clear(); + } + else + { + mTablesFilterMode = TablesFilterMode::SkippedTables; + mTablesToSkip = tablesToSkip; + } +} + +void Context::setTablesToInclude( const std::vector &tablesToInclude ) +{ + if ( mTablesFilterMode == TablesFilterMode::SkippedTables ) + throw GeoDiffException( "Cannot set tables to include when tables to skip are already set" ); + + if ( tablesToInclude.empty() ) + { + mTablesFilterMode = TablesFilterMode::None; + mTablesToInclude.clear(); + } + else + { + mTablesFilterMode = TablesFilterMode::IncludedTables; + mTablesToInclude = tablesToInclude; + } } bool Context::isTableSkipped( const std::string &tableName ) const { - if ( mTablesToSkip.empty() ) + if ( mTablesFilterMode == TablesFilterMode::IncludedTables ) { - return false; + return std::none_of( mTablesToInclude.begin(), mTablesToInclude.end(), std::bind( std::equal_to(), std::placeholders::_1, tableName ) ); } - return std::any_of( mTablesToSkip.begin(), mTablesToSkip.end(), std::bind( std::equal_to< std::string >(), std::placeholders::_1, tableName ) ); + if ( mTablesFilterMode == TablesFilterMode::SkippedTables ) + { + return std::any_of( mTablesToSkip.begin(), mTablesToSkip.end(), std::bind( std::equal_to(), std::placeholders::_1, tableName ) ); + } + + return false; } void Context::setLastError( const std::string &message ) @@ -47,3 +81,8 @@ const std::string &Context::lastError() const { return mLastError; } + +TablesFilterMode Context::tableFilterMode() const +{ + return mTablesFilterMode; +} diff --git a/geodiff/src/geodiffcontext.hpp b/geodiff/src/geodiffcontext.hpp index 9c805cb5..42213d23 100644 --- a/geodiff/src/geodiffcontext.hpp +++ b/geodiff/src/geodiffcontext.hpp @@ -12,6 +12,13 @@ #include "geodiff.h" #include "geodifflogger.hpp" +enum class TablesFilterMode +{ + None, + IncludedTables, + SkippedTables +}; + class Context { public: @@ -21,14 +28,18 @@ class Context const Logger &logger() const; void setTablesToSkip( const std::vector &tablesToSkip ); + void setTablesToInclude( const std::vector &tablesToInclude ); bool isTableSkipped( const std::string &tableName ) const; void setLastError( const std::string &message ); const std::string &lastError() const; + TablesFilterMode tableFilterMode() const; private: Logger mLogger; std::vector mTablesToSkip; + std::vector mTablesToInclude; std::string mLastError; + TablesFilterMode mTablesFilterMode = TablesFilterMode::None; }; diff --git a/geodiff/tests/CMakeLists.txt b/geodiff/tests/CMakeLists.txt index 537aa734..0b02e9ba 100644 --- a/geodiff/tests/CMakeLists.txt +++ b/geodiff/tests/CMakeLists.txt @@ -70,6 +70,7 @@ ENDMACRO (ADD_GEODIFF_TEST) SET(TESTS test_c_api.cpp test_changeset_reader.cpp + test_context.cpp test_changeset_utils.cpp test_concurrent_commits.cpp test_driver_sqlite.cpp diff --git a/geodiff/tests/test_c_api.cpp b/geodiff/tests/test_c_api.cpp index 09556472..069c9d11 100644 --- a/geodiff/tests/test_c_api.cpp +++ b/geodiff/tests/test_c_api.cpp @@ -25,6 +25,8 @@ TEST( CAPITest, invalid_calls ) ASSERT_EQ( GEODIFF_ERROR, GEODIFF_CX_setMaximumLoggerLevel( invalidContext, GEODIFF_LoggerLevel::LevelWarning ) ); ASSERT_EQ( GEODIFF_ERROR, GEODIFF_CX_setTablesToSkip( invalidContext, 0, nullptr ) ); ASSERT_EQ( GEODIFF_ERROR, GEODIFF_CX_setTablesToSkip( context, 1, nullptr ) ); + ASSERT_EQ( GEODIFF_ERROR, GEODIFF_CX_setTablesToInclude( invalidContext, 0, nullptr ) ); + ASSERT_EQ( GEODIFF_ERROR, GEODIFF_CX_setTablesToInclude( context, 1, nullptr ) ); ASSERT_EQ( GEODIFF_ERROR, GEODIFF_createChangesetEx( invalidContext, "sqlite", nullptr, nullptr, nullptr, nullptr ) ); ASSERT_EQ( GEODIFF_ERROR, GEODIFF_createChangesetEx( context, "sqlite", nullptr, nullptr, nullptr, nullptr ) ); diff --git a/geodiff/tests/test_context.cpp b/geodiff/tests/test_context.cpp new file mode 100644 index 00000000..375002de --- /dev/null +++ b/geodiff/tests/test_context.cpp @@ -0,0 +1,125 @@ +/* + GEODIFF - MIT License + Copyright (C) 2026 Jan Caha +*/ + +#include "gtest/gtest.h" +#include "geodiffcontext.hpp" +#include "geodiffutils.hpp" + +TEST( ContextTest, defaultFilterModeIsNothing ) +{ + Context ctx; + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::None ); +} + +TEST( ContextTest, setTablesToSkip ) +{ + Context ctx; + ctx.setTablesToSkip( { "lines", "polygons" } ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::SkippedTables ); +} + +TEST( ContextTest, setTablesToInclude ) +{ + Context ctx; + ctx.setTablesToInclude( { "points" } ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::IncludedTables ); +} + +TEST( ContextTest, cannotSetIncludeAfterSkip ) +{ + Context ctx; + ctx.setTablesToSkip( { "lines" } ); + EXPECT_THROW( ctx.setTablesToInclude( { "points" } ), GeoDiffException ); +} + +TEST( ContextTest, cannotSetSkipAfterInclude ) +{ + Context ctx; + ctx.setTablesToInclude( { "points" } ); + EXPECT_THROW( ctx.setTablesToSkip( { "lines" } ), GeoDiffException ); +} + +TEST( ContextTest, skipModeSkipsListedTable ) +{ + Context ctx; + ctx.setTablesToSkip( { "lines" } ); + EXPECT_TRUE( ctx.isTableSkipped( "lines" ) ); + EXPECT_FALSE( ctx.isTableSkipped( "points" ) ); +} + +TEST( ContextTest, includeModeSkipsUnlistedTable ) +{ + Context ctx; + ctx.setTablesToInclude( { "points" } ); + EXPECT_FALSE( ctx.isTableSkipped( "points" ) ); + EXPECT_TRUE( ctx.isTableSkipped( "lines" ) ); +} + +TEST( ContextTest, nothingModeSkipsNothing ) +{ + Context ctx; + EXPECT_FALSE( ctx.isTableSkipped( "points" ) ); + EXPECT_FALSE( ctx.isTableSkipped( "lines" ) ); +} + +TEST( ContextTest, clearSkipByEmptyList ) +{ + Context ctx; + ctx.setTablesToSkip( { "lines" } ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::SkippedTables ); + ctx.setTablesToSkip( {} ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::None ); +} + +TEST( ContextTest, clearIncludeByEmptyList ) +{ + Context ctx; + ctx.setTablesToInclude( { "points" } ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::IncludedTables ); + ctx.setTablesToInclude( {} ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::None ); +} + +TEST( ContextTest, canSetIncludeAfterClearingSkip ) +{ + Context ctx; + ctx.setTablesToSkip( { "lines" } ); + ctx.setTablesToSkip( {} ); + EXPECT_NO_THROW( ctx.setTablesToInclude( { "points" } ) ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::IncludedTables ); +} + +TEST( ContextTest, canSetSkipAfterClearingInclude ) +{ + Context ctx; + ctx.setTablesToInclude( { "points" } ); + ctx.setTablesToInclude( {} ); + EXPECT_NO_THROW( ctx.setTablesToSkip( { "lines" } ) ); + EXPECT_EQ( ctx.tableFilterMode(), TablesFilterMode::SkippedTables ); +} + +TEST( ContextTest, clearSkipResetsIsTableSkipped ) +{ + Context ctx; + ctx.setTablesToSkip( { "lines" } ); + EXPECT_TRUE( ctx.isTableSkipped( "lines" ) ); + ctx.setTablesToSkip( {} ); + EXPECT_FALSE( ctx.isTableSkipped( "lines" ) ); +} + +TEST( ContextTest, clearIncludeResetsIsTableSkipped ) +{ + Context ctx; + ctx.setTablesToInclude( { "points" } ); + EXPECT_TRUE( ctx.isTableSkipped( "lines" ) ); + ctx.setTablesToInclude( {} ); + EXPECT_FALSE( ctx.isTableSkipped( "lines" ) ); +} + +int main( int argc, char **argv ) +{ + testing::InitGoogleTest( &argc, argv ); + return RUN_ALL_TESTS(); +} diff --git a/pygeodiff/__about__.py b/pygeodiff/__about__.py index ebbe9a81..a78226bd 100644 --- a/pygeodiff/__about__.py +++ b/pygeodiff/__about__.py @@ -2,7 +2,7 @@ __description__ = "Diff tool for geo-spatial data" __url__ = "https://github.com/MerginMaps/geodiff" # use scripts/update_version.py to update the version here and in other places at once -__version__ = "2.2.0" +__version__ = "2.3.0" __author__ = "Lutra Consulting Ltd." __author_email__ = "info@merginmaps.com" __maintainer__ = "Lutra Consulting Ltd." diff --git a/pygeodiff/geodifflib.py b/pygeodiff/geodifflib.py index bf9af7be..68fd7137 100644 --- a/pygeodiff/geodifflib.py +++ b/pygeodiff/geodifflib.py @@ -248,9 +248,20 @@ def set_tables_to_skip(self, context, tables): for i in range(len(tables)): arr[i] = tables[i].encode("utf-8") - self.lib.GEODIFF_CX_setTablesToSkip( - ctypes.c_void_p(context), ctypes.c_int(len(tables)), arr - ) + func = self.lib.GEODIFF_CX_setTablesToSkip + func.restype = ctypes.c_int + rc = func(ctypes.c_void_p(context), ctypes.c_int(len(tables)), arr) + self._parse_return_code(context, rc, "set_tables_to_skip") + + def set_tables_to_include(self, context, tables): + arr = (ctypes.c_char_p * len(tables))() + for i in range(len(tables)): + arr[i] = tables[i].encode("utf-8") + + func = self.lib.GEODIFF_CX_setTablesToInclude + func.restype = ctypes.c_int + rc = func(ctypes.c_void_p(context), ctypes.c_int(len(tables)), arr) + self._parse_return_code(context, rc, "set_tables_to_include") def version(self): func = self.lib.GEODIFF_version diff --git a/pygeodiff/main.py b/pygeodiff/main.py index 687a60c5..16bee442 100644 --- a/pygeodiff/main.py +++ b/pygeodiff/main.py @@ -81,10 +81,24 @@ def set_tables_to_skip(self, tables): database between different drivers. If empty list is passed, skip tables list will be reset. + Cannot be used together with set_tables_to_include on the same context. """ self._lazy_load() return self.clib.set_tables_to_skip(self.context, tables) + def set_tables_to_include(self, tables): + """ + Set list of tables to include in geodiff operations. Once defined, only + these tables will be included in the following operations: create changeset, + apply changeset, rebase, get database schema, dump database contents, copy + database between different drivers. All other tables will be ignored. + + If empty list is passed, include tables list will be reset. + Cannot be used together with set_tables_to_skip on the same context. + """ + self._lazy_load() + return self.clib.set_tables_to_include(self.context, tables) + LevelError = 1 LevelWarning = 2 LevelInfo = 3 diff --git a/pygeodiff/tests/test_skip_tables.py b/pygeodiff/tests/test_skip_tables.py index 53fe750a..a9046d0f 100644 --- a/pygeodiff/tests/test_skip_tables.py +++ b/pygeodiff/tests/test_skip_tables.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - :copyright: (c) 2022 Alexander Bruy - :license: MIT, see LICENSE for more details. +:copyright: (c) 2022 Alexander Bruy +:license: MIT, see LICENSE for more details. """ import os @@ -13,6 +13,7 @@ geodiff_test_dir, tmpdir, ) +from pygeodiff import GeoDiffLibError class UnitTestsPythonSingleCommit(GeoDiffTests): @@ -99,3 +100,97 @@ def test_skip_apply(self): check_nchanges(self.geodiff, changeset2, 0) self.geodiff.set_tables_to_skip([]) + + def test_include_create(self): + base = geodiff_test_dir() + "/" + "skip_tables" + "/" + "base.gpkg" + modified = geodiff_test_dir() + "/" + "skip_tables" + "/" + "modified_all.gpkg" + changeset = ( + tmpdir() + "/py" + "test_include_create" + "/" + "changeset_points.bin" + ) + changeset2 = ( + tmpdir() + "/py" + "test_include_create" + "/" + "changeset_points2.bin" + ) + changeset_inv = ( + tmpdir() + "/py" + "test_include_create" + "/" + "changeset_inv.bin" + ) + patched = tmpdir() + "/py" + "test_include_create" + "/" + "patched_points.gpkg" + + create_dir("test_include_create") + + # include only points table when creating changeset (lines changes are ignored) + self.geodiff.set_tables_to_include(["points"]) + + # create changeset + self.geodiff.create_changeset(base, modified, changeset) + check_nchanges(self.geodiff, changeset, 4) + + # apply changeset + shutil.copyfile(base, patched) + self.geodiff.apply_changeset(patched, changeset) + + # check that now it is same file (for included tables) + self.geodiff.create_changeset(patched, modified, changeset2) + check_nchanges(self.geodiff, changeset2, 0) + + # check we can create inverted changeset + os.remove(changeset2) + self.geodiff.invert_changeset(changeset, changeset_inv) + self.geodiff.apply_changeset(patched, changeset_inv) + self.geodiff.create_changeset_dr( + "sqlite", "", patched, "sqlite", "", base, changeset2 + ) + check_nchanges(self.geodiff, changeset2, 0) + + self.geodiff.set_tables_to_include([]) + + def test_include_apply(self): + base = geodiff_test_dir() + "/" + "skip_tables" + "/" + "base.gpkg" + modified = geodiff_test_dir() + "/" + "skip_tables" + "/" + "modified_all.gpkg" + changeset = ( + tmpdir() + "/py" + "test_include_apply" + "/" + "changeset_points.bin" + ) + changeset2 = ( + tmpdir() + "/py" + "test_include_apply" + "/" + "changeset_points2.bin" + ) + changeset_inv = ( + tmpdir() + "/py" + "test_include_apply" + "/" + "changeset_inv.bin" + ) + patched = tmpdir() + "/py" + "test_include_apply" + "/" + "patched_points.gpkg" + + create_dir("test_include_apply") + + # create changeset without filter (captures all 6 changes) + self.geodiff.create_changeset(base, modified, changeset) + check_nchanges(self.geodiff, changeset, 6) + + # include only points table when applying changeset + self.geodiff.set_tables_to_include(["points"]) + + # apply changeset + shutil.copyfile(base, patched) + self.geodiff.apply_changeset(patched, changeset) + + # check that now it is same file (for included tables) + self.geodiff.create_changeset(patched, modified, changeset2) + check_nchanges(self.geodiff, changeset2, 0) + + # check we can create inverted changeset + os.remove(changeset2) + self.geodiff.invert_changeset(changeset, changeset_inv) + self.geodiff.apply_changeset(patched, changeset_inv) + self.geodiff.create_changeset_dr( + "sqlite", "", patched, "sqlite", "", base, changeset2 + ) + check_nchanges(self.geodiff, changeset2, 0) + + self.geodiff.set_tables_to_include([]) + + def test_skip_and_include_raises_error(self): + with self.assertRaises(GeoDiffLibError): + self.geodiff.set_tables_to_skip(["lines"]) + self.geodiff.set_tables_to_include(["points"]) + + def test_include_and_skip_raises_error(self): + with self.assertRaises(GeoDiffLibError): + self.geodiff.set_tables_to_include(["points"]) + self.geodiff.set_tables_to_skip(["lines"]) diff --git a/pyproject.toml b/pyproject.toml index 4546686d..7111e95b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,36 @@ [build-system] -requires = ["setuptools", "wheel", "scikit-build", "cmake"] +requires = ["scikit-build-core>=0.10"] +build-backend = "scikit_build_core.build" + +[project] +name = "pygeodiff" +version = "2.3.0" +description = "Python wrapper around GeoDiff library" +readme = "README.md" +authors = [{ name = "Lutra Consulting Ltd.", email = "info@merginmaps.com" }] +license = { text = "MIT" } +requires-python = ">=3.7" +keywords = ["diff", "gis", "geo", "geopackage", "merge"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", +] + +[project.urls] +Homepage = "https://github.com/MerginMaps/geodiff" + +[tool.scikit-build] +# The geodiff C++ library (CMakeLists.txt) lives in the geodiff/ subdirectory. +cmake.source-dir = "geodiff" +# Pure-Python package shipped alongside the compiled library. +wheel.packages = ["pygeodiff"] +# The old setup.py used packages=["pygeodiff"], which did not ship the tests. +wheel.exclude = ["pygeodiff/tests/**"] +# CMakeLists.txt declares 3.15; let scikit-build-core fetch a matching CMake. +cmake.version = ">=3.15" + +[tool.scikit-build.cmake.define] +ENABLE_TESTS = "OFF" +ENABLE_COVERAGE = "OFF" +BUILD_TOOLS = "OFF" +PEDANTIC = "OFF" diff --git a/scripts/update_version.py b/scripts/update_version.py index e2fca5e3..bf0f0c3a 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -28,6 +28,6 @@ def replace_in_file(filepath, regex, sub): print("patching " + about_file) replace_in_file(about_file, "__version__\s=\s\".*", "__version__ = \"" + ver + "\"") -setup_file = os.path.join(dir_path, os.pardir, "setup.py") -print("patching " + setup_file) -replace_in_file(setup_file, "VERSION\s=\s\".*", "VERSION = \"" + ver + "\"") +pyproject_file = os.path.join(dir_path, os.pardir, "pyproject.toml") +print("patching " + pyproject_file) +replace_in_file(pyproject_file, r'^version = ".*"', 'version = "' + ver + '"') diff --git a/setup.py b/setup.py deleted file mode 100755 index 6c7d9a2d..00000000 --- a/setup.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- - -from skbuild import setup -import platform - -# use scripts/update_version.py to update the version here and in other places at once -VERSION = "2.2.0" - -cmake_args = [ - '-DENABLE_TESTS:BOOL=OFF', - '-DENABLE_COVERAGE:BOOL=OFF', - '-DBUILD_TOOLS:BOOL=OFF', - '-DPEDANTIC:BOOL=OFF', - '-DPYGEODIFFVERSION='+str(VERSION) -] - -arch = platform.architecture()[0] # 64bit or 32bit -if ('Windows' in platform.system()) and ("32" in arch): - cmake_args.append('-AWin32') - -setup( - name="pygeodiff", - version=VERSION, - author="Lutra Consulting Ltd.", - author_email="info@merginmaps.com", - description="Python wrapper around GeoDiff library", - long_description="Python wrapper around GeoDiff library", - url="https://github.com/MerginMaps/geodiff", - packages=["pygeodiff"], - include_package_data=False, - keywords=["diff", "gis", "geo", "geopackage", "merge"], - scripts=[], - entry_points={"console_scripts": ["pygeodiff=pygeodiff.main:main"]}, - zip_safe=False, - cmake_args=cmake_args, - cmake_source_dir="geodiff", - cmake_with_sdist=False, - test_suite="tests.test_project", - python_requires=">=3.7", - license="License :: OSI Approved :: MIT License", - classifiers=[ - "Programming Language :: Python :: 3", - ], -)