From 3f40f798b7385c2b8b60a61d561d2e729588b362 Mon Sep 17 00:00:00 2001 From: Yevgeniy Zakharov Date: Thu, 19 Mar 2026 13:52:31 +0500 Subject: [PATCH] added triggers sync --- .cursor/rules/project-structure.mdc | 11 +++ .github/workflows/ci.yaml | 12 +-- dev/statement_serializer.h | 3 +- dev/storage.h | 30 ++++++-- dev/storage_base.h | 40 +++++++--- include/sqlite_orm/sqlite_orm.h | 73 ++++++++++++++----- .../schema/trigger.cpp | 5 +- tests/trigger_tests.cpp | 72 ++++++++++++++++++ 8 files changed, 197 insertions(+), 49 deletions(-) create mode 100644 .cursor/rules/project-structure.mdc diff --git a/.cursor/rules/project-structure.mdc b/.cursor/rules/project-structure.mdc new file mode 100644 index 000000000..fa5d2ad4d --- /dev/null +++ b/.cursor/rules/project-structure.mdc @@ -0,0 +1,11 @@ +--- +description: Project structure rules for sqlite_orm library development +alwaysApply: true +--- + +# Project Structure + +- Library source code lives exclusively in the `dev/` folder — only edit library code there. +- Tests in `tests/` and examples in `examples/` are also editable. +- The file `include/sqlite_orm/sqlite_orm.h` is an auto-generated amalgamation and MUST NOT be edited manually. +- To regenerate it, run: `python3 third_party/amalgamate/amalgamate.py -c third_party/amalgamate/config.json -s .` diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e66363865..e762b7372 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -53,12 +53,12 @@ jobs: cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_20=ON" triplet: x86-windows - - name: "VS 2019, x64, C++17" - os: windows-2019 - platform: x64 - arch: x64 - cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_17=ON" - triplet: x64-windows + # - name: "VS 2019, x64, C++17" + # os: windows-2019 + # platform: x64 + # arch: x64 + # cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_17=ON" + # triplet: x64-windows name: Windows - ${{ matrix.name }} diff --git a/dev/statement_serializer.h b/dev/statement_serializer.h index 6c330fde6..fc7cc4e7a 100644 --- a/dev/statement_serializer.h +++ b/dev/statement_serializer.h @@ -2582,8 +2582,7 @@ namespace sqlite_orm::internal { std::stringstream ss; ss << "CREATE "; - ss << "TRIGGER IF NOT EXISTS " << streaming_identifier(statement.name) << " " - << serialize(statement.base, context); + ss << "TRIGGER " << streaming_identifier(statement.name) << " " << serialize(statement.base, context); ss << " BEGIN "; iterate_tuple(statement.elements, [&ss, &context](auto& element) { using element_type = polyfill::remove_cvref_t; diff --git a/dev/storage.h b/dev/storage.h index 537143621..5bbcd89d8 100644 --- a/dev/storage.h +++ b/dev/storage.h @@ -1115,8 +1115,19 @@ namespace sqlite_orm::internal { } template - sync_schema_result schema_status(const trigger_t&, sqlite3*, bool, bool*) { - return sync_schema_result::already_in_sync; + sync_schema_result schema_status(const trigger_t& trigger, sqlite3* db, bool, bool*) { + auto dbTriggerSql = this->retrieve_object_sql(db, "trigger", trigger.name); + if (dbTriggerSql.empty()) { + return sync_schema_result::new_table_created; + } + + const serializer_context context{this->db_objects}; + auto storageSql = serialize(trigger, context); + + if (dbTriggerSql == storageSql) { + return sync_schema_result::already_in_sync; + } + return sync_schema_result::dropped_and_recreated; } template @@ -1250,11 +1261,16 @@ namespace sqlite_orm::internal { } template - sync_schema_result sync_dbo(const trigger_t& trigger, sqlite3* db, bool) { - const auto res = sync_schema_result::already_in_sync; // TODO Change accordingly - const serializer_context context{this->db_objects}; - const auto sql = serialize(trigger, context); - this->executor.perform_void_exec(db, sql.c_str()); + sync_schema_result sync_dbo(const trigger_t& trigger, sqlite3* db, bool preserve) { + auto res = this->schema_status(trigger, db, preserve, nullptr); + if (res != sync_schema_result::already_in_sync) { + if (res == sync_schema_result::dropped_and_recreated) { + this->drop_trigger_internal(trigger.name, true, db); + } + const serializer_context context{this->db_objects}; + const auto sql = serialize(trigger, context); + this->executor.perform_void_exec(db, sql.c_str()); + } return res; } diff --git a/dev/storage_base.h b/dev/storage_base.h index f5bb9cc4a..b63fa27be 100644 --- a/dev/storage_base.h +++ b/dev/storage_base.h @@ -1064,18 +1064,36 @@ namespace sqlite_orm::internal { } void drop_trigger_internal(const std::string& triggerName, bool ifExists) { - std::string sql; - { - std::stringstream ss; - ss << "DROP TRIGGER"; - if (ifExists) { - ss << " IF EXISTS"; - } - ss << ' ' << quote_identifier(triggerName) << std::flush; - sql = ss.str(); - } auto connection = this->get_connection(); - this->executor.perform_void_exec(connection.get(), sql.c_str()); + this->drop_trigger_internal(triggerName, ifExists, connection.get()); + } + + void drop_trigger_internal(const std::string& triggerName, bool ifExists, sqlite3* db) { + std::stringstream ss; + ss << "DROP TRIGGER"; + if (ifExists) { + ss << " IF EXISTS"; + } + ss << ' ' << quote_identifier(triggerName) << std::flush; + this->executor.perform_void_exec(db, ss.str().c_str()); + } + + std::string retrieve_object_sql(sqlite3* db, const std::string& type, const std::string& name) const { + std::string result; + std::stringstream ss; + ss << "SELECT sql FROM sqlite_master WHERE type = " << quote_string_literal(type) + << " AND name = " << quote_string_literal(name); + this->executor.perform_exec( + db, + ss.str(), + [](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*columnName*/) -> int { + if (argv[0]) { + *static_cast(userData) = argv[0]; + } + return 0; + }, + &result); + return result; } static int collate_callback(void* argument, int leftLength, const void* lhs, int rightLength, const void* rhs) { diff --git a/include/sqlite_orm/sqlite_orm.h b/include/sqlite_orm/sqlite_orm.h index e14e856cb..261b47294 100644 --- a/include/sqlite_orm/sqlite_orm.h +++ b/include/sqlite_orm/sqlite_orm.h @@ -20514,18 +20514,36 @@ namespace sqlite_orm::internal { } void drop_trigger_internal(const std::string& triggerName, bool ifExists) { - std::string sql; - { - std::stringstream ss; - ss << "DROP TRIGGER"; - if (ifExists) { - ss << " IF EXISTS"; - } - ss << ' ' << quote_identifier(triggerName) << std::flush; - sql = ss.str(); - } auto connection = this->get_connection(); - this->executor.perform_void_exec(connection.get(), sql.c_str()); + this->drop_trigger_internal(triggerName, ifExists, connection.get()); + } + + void drop_trigger_internal(const std::string& triggerName, bool ifExists, sqlite3* db) { + std::stringstream ss; + ss << "DROP TRIGGER"; + if (ifExists) { + ss << " IF EXISTS"; + } + ss << ' ' << quote_identifier(triggerName) << std::flush; + this->executor.perform_void_exec(db, ss.str().c_str()); + } + + std::string retrieve_object_sql(sqlite3* db, const std::string& type, const std::string& name) const { + std::string result; + std::stringstream ss; + ss << "SELECT sql FROM sqlite_master WHERE type = " << quote_string_literal(type) + << " AND name = " << quote_string_literal(name); + this->executor.perform_exec( + db, + ss.str(), + [](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*columnName*/) -> int { + if (argv[0]) { + *static_cast(userData) = argv[0]; + } + return 0; + }, + &result); + return result; } static int collate_callback(void* argument, int leftLength, const void* lhs, int rightLength, const void* rhs) { @@ -24224,8 +24242,7 @@ namespace sqlite_orm::internal { std::stringstream ss; ss << "CREATE "; - ss << "TRIGGER IF NOT EXISTS " << streaming_identifier(statement.name) << " " - << serialize(statement.base, context); + ss << "TRIGGER " << streaming_identifier(statement.name) << " " << serialize(statement.base, context); ss << " BEGIN "; iterate_tuple(statement.elements, [&ss, &context](auto& element) { using element_type = polyfill::remove_cvref_t; @@ -26108,8 +26125,19 @@ namespace sqlite_orm::internal { } template - sync_schema_result schema_status(const trigger_t&, sqlite3*, bool, bool*) { - return sync_schema_result::already_in_sync; + sync_schema_result schema_status(const trigger_t& trigger, sqlite3* db, bool, bool*) { + auto dbTriggerSql = this->retrieve_object_sql(db, "trigger", trigger.name); + if (dbTriggerSql.empty()) { + return sync_schema_result::new_table_created; + } + + const serializer_context context{this->db_objects}; + auto storageSql = serialize(trigger, context); + + if (dbTriggerSql == storageSql) { + return sync_schema_result::already_in_sync; + } + return sync_schema_result::dropped_and_recreated; } template @@ -26243,11 +26271,16 @@ namespace sqlite_orm::internal { } template - sync_schema_result sync_dbo(const trigger_t& trigger, sqlite3* db, bool) { - const auto res = sync_schema_result::already_in_sync; // TODO Change accordingly - const serializer_context context{this->db_objects}; - const auto sql = serialize(trigger, context); - this->executor.perform_void_exec(db, sql.c_str()); + sync_schema_result sync_dbo(const trigger_t& trigger, sqlite3* db, bool preserve) { + auto res = this->schema_status(trigger, db, preserve, nullptr); + if (res != sync_schema_result::already_in_sync) { + if (res == sync_schema_result::dropped_and_recreated) { + this->drop_trigger_internal(trigger.name, true, db); + } + const serializer_context context{this->db_objects}; + const auto sql = serialize(trigger, context); + this->executor.perform_void_exec(db, sql.c_str()); + } return res; } diff --git a/tests/statement_serializer_tests/schema/trigger.cpp b/tests/statement_serializer_tests/schema/trigger.cpp index 99d669801..6f21bc6ed 100644 --- a/tests/statement_serializer_tests/schema/trigger.cpp +++ b/tests/statement_serializer_tests/schema/trigger.cpp @@ -35,9 +35,8 @@ TEST_CASE("statement_serializer trigger") { .end())) .end()); value = serialize(expression, context); - expected = - R"(CREATE TRIGGER IF NOT EXISTS "validate_email_before_insert_leads" BEFORE INSERT ON "leads" BEGIN SELECT )" - R"(CASE WHEN NOT NEW."email" LIKE '%_@__%.__%' THEN RAISE(ABORT, 'Invalid email address') END; END)"; + expected = R"(CREATE TRIGGER "validate_email_before_insert_leads" BEFORE INSERT ON "leads" BEGIN SELECT )" + R"(CASE WHEN NOT NEW."email" LIKE '%_@__%.__%' THEN RAISE(ABORT, 'Invalid email address') END; END)"; } REQUIRE(value == expected); } diff --git a/tests/trigger_tests.cpp b/tests/trigger_tests.cpp index bf50ac8a8..8c556b13d 100644 --- a/tests/trigger_tests.cpp +++ b/tests/trigger_tests.cpp @@ -132,6 +132,78 @@ TEST_CASE("trigger_names") { } } +TEST_CASE("issue1429") { + struct Lead { + int id = 0; + std::string name; + std::string email; + }; + + auto storagePath = "issue1429.sqlite"; + std::remove(storagePath); + + // first: create storage with trigger checking "name" column + { + auto storage = make_storage(storagePath, + make_trigger("validate_email_before_insert_leads", + before() + .insert() + .on() + .begin(select(case_() + .when(not like(new_(&Lead::name), "%_@__%.__%"), + then(raise_abort("Invalid email address"))) + .end())) + .end()), + make_table("leads", + make_column("id", &Lead::id, primary_key()), + make_column("name", &Lead::name), + make_column("email", &Lead::email))); + auto syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::new_table_created); + + // second sync should report already_in_sync + syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::already_in_sync); + } + // second: create storage with trigger checking "email" column instead + { + auto storage = make_storage(storagePath, + make_trigger("validate_email_before_insert_leads", + before() + .insert() + .on() + .begin(select(case_() + .when(not like(new_(&Lead::email), "%_@__%.__%"), + then(raise_abort("Invalid email address"))) + .end())) + .end()), + make_table("leads", + make_column("id", &Lead::id, primary_key()), + make_column("name", &Lead::name), + make_column("email", &Lead::email))); + + // simulate should detect the change + auto simulateResult = storage.sync_schema_simulate(); + REQUIRE(simulateResult.at("validate_email_before_insert_leads") == sync_schema_result::dropped_and_recreated); + + // sync should update the trigger + auto syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::dropped_and_recreated); + + // verify trigger was updated: inserting a row with invalid email should fail + REQUIRE_THROWS(storage.insert(Lead{0, "John", "not_an_email"})); + + // valid email should succeed + REQUIRE_NOTHROW(storage.insert(Lead{0, "John", "john@example.com"})); + + // after update, second sync should be already_in_sync + syncResult = storage.sync_schema(); + REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::already_in_sync); + } + + std::remove(storagePath); +} + TEST_CASE("issue1280") { struct X { int test = 0;