From edc2ff013316892777a314229b6d0238c158143e Mon Sep 17 00:00:00 2001 From: Hans Harmannij Date: Tue, 20 May 2025 11:42:14 +0200 Subject: [PATCH 1/2] Drop triggers before creating them (issue #1429) When creating triggers, they are not first dropped if they exist, and then recreated. This way, if a trigger is changed, these changes will also be applied when running sync_schema. --- dev/statement_serializer.h | 3 +- include/sqlite_orm/sqlite_orm.h | 3 +- .../schema/trigger.cpp | 3 +- tests/trigger_tests.cpp | 36 +++++++++++++++++++ 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/dev/statement_serializer.h b/dev/statement_serializer.h index b84c31fe1..5d2259f28 100644 --- a/dev/statement_serializer.h +++ b/dev/statement_serializer.h @@ -2191,9 +2191,10 @@ namespace sqlite_orm { SQLITE_ORM_STATIC_CALLOP std::string operator()(const statement_type& statement, const Ctx& context) SQLITE_ORM_OR_CONST_CALLOP { std::stringstream ss; + ss << "DROP TRIGGER IF EXISTS " << streaming_identifier(statement.name) << ";"; ss << "CREATE "; - ss << "TRIGGER IF NOT EXISTS " << streaming_identifier(statement.name) << " " + ss << "TRIGGER " << streaming_identifier(statement.name) << " " << serialize(statement.base, context); ss << " BEGIN "; iterate_tuple(statement.elements, [&ss, &context](auto& element) { diff --git a/include/sqlite_orm/sqlite_orm.h b/include/sqlite_orm/sqlite_orm.h index e02c56a1b..21a65c974 100644 --- a/include/sqlite_orm/sqlite_orm.h +++ b/include/sqlite_orm/sqlite_orm.h @@ -22146,9 +22146,10 @@ namespace sqlite_orm { SQLITE_ORM_STATIC_CALLOP std::string operator()(const statement_type& statement, const Ctx& context) SQLITE_ORM_OR_CONST_CALLOP { std::stringstream ss; + ss << "DROP TRIGGER IF EXISTS " << streaming_identifier(statement.name) << ";"; ss << "CREATE "; - ss << "TRIGGER IF NOT EXISTS " << streaming_identifier(statement.name) << " " + ss << "TRIGGER " << streaming_identifier(statement.name) << " " << serialize(statement.base, context); ss << " BEGIN "; iterate_tuple(statement.elements, [&ss, &context](auto& element) { diff --git a/tests/statement_serializer_tests/schema/trigger.cpp b/tests/statement_serializer_tests/schema/trigger.cpp index 99d669801..a4a2d7a6d 100644 --- a/tests/statement_serializer_tests/schema/trigger.cpp +++ b/tests/statement_serializer_tests/schema/trigger.cpp @@ -36,7 +36,8 @@ TEST_CASE("statement_serializer trigger") { .end()); value = serialize(expression, context); expected = - R"(CREATE TRIGGER IF NOT EXISTS "validate_email_before_insert_leads" BEFORE INSERT ON "leads" BEGIN SELECT )" + R"(DROP TRIGGER IF EXISTS "validate_email_before_insert_leads";)" + 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 f930f4307..f368ec77f 100644 --- a/tests/trigger_tests.cpp +++ b/tests/trigger_tests.cpp @@ -104,6 +104,42 @@ TEST_CASE("triggers_basics") { } } +TEST_CASE("issue1429") { + auto storagePath = "issue1429.sqlite"; + struct X { + int test = 0; + }; + + { + auto storage = make_storage( + storagePath, + make_trigger("table_insert_InsertTest", after().insert().on().begin(update_all(set(c(&X::test) = 5))).end()), + make_table("x", + make_column("test", &X::test))); + auto simulated = storage.sync_schema_simulate(); + auto ssr = storage.sync_schema(); + REQUIRE(ssr == simulated); + + storage.insert(X{1}); + auto records = storage.get_all(); + REQUIRE(records[0].test == 5); + } + { + auto storage = make_storage( + storagePath, + make_trigger("table_insert_InsertTest", after().insert().on().begin(update_all(set(c(&X::test) = 6))).end()), + make_table("x", + make_column("test", &X::test))); + auto simulated = storage.sync_schema_simulate(); + auto ssr = storage.sync_schema(); + REQUIRE(ssr == simulated); + + storage.insert(X{1}); + auto records = storage.get_all(); + REQUIRE(records[0].test == 6); + } +} + TEST_CASE("issue1280") { struct X { int test = 0; From b4322a602ead9b6ac764a28be6230842a9ee3156 Mon Sep 17 00:00:00 2001 From: Hans Harmannij Date: Tue, 20 May 2025 13:22:25 +0200 Subject: [PATCH 2/2] Allow retrieval of all existing triggers (issue #1429) --- dev/storage_base.h | 51 +++++++++++++++++++++------------ include/sqlite_orm/sqlite_orm.h | 51 +++++++++++++++++++++------------ tests/trigger_tests.cpp | 29 +++++++++++++++++++ 3 files changed, 95 insertions(+), 36 deletions(-) diff --git a/dev/storage_base.h b/dev/storage_base.h index 4f0aaabec..c9e39eec8 100644 --- a/dev/storage_base.h +++ b/dev/storage_base.h @@ -278,25 +278,16 @@ namespace sqlite_orm { * @return Returns list of tables in database. */ std::vector table_names() { - using data_t = std::vector; + return this->object_names("table"); + } - auto connection = this->get_connection(); - data_t tableNames; - this->executor.perform_exec( - connection.get(), - "SELECT name FROM sqlite_master WHERE type='table'", - [](void* data, int argc, char** argv, char** /*columnName*/) -> int { - auto& tableNames_ = *(data_t*)data; - for (int i = 0; i < argc; ++i) { - if (argv[i]) { - tableNames_.emplace_back(argv[i]); - } - } - return 0; - }, - &tableNames); - tableNames.shrink_to_fit(); - return tableNames; + /** + * Returns existing permanent trigger names in database. Doesn't check storage itself - works only with + * actual database. + * @return Returns list of triggers in database. + */ + std::vector trigger_names() { + return this->object_names("trigger"); } /** @@ -764,6 +755,30 @@ namespace sqlite_orm { return res; } + std::vector object_names(const std::string& type) { + using data_t = std::vector; + + auto connection = this->get_connection(); + data_t objectNames; + std::stringstream ss; + ss << "SELECT name FROM sqlite_master WHERE type=" << quote_string_literal(type); + this->executor.perform_exec( + connection.get(), + ss.str(), + [](void* data, int argc, char** argv, char** /*columnName*/) -> int { + auto& objectNames_ = *(data_t*)data; + for (int i = 0; i < argc; ++i) { + if (argv[i]) { + objectNames_.emplace_back(argv[i]); + } + } + return 0; + }, + &objectNames); + objectNames.shrink_to_fit(); + return objectNames; + } + #if SQLITE_VERSION_NUMBER >= 3006019 void foreign_keys(sqlite3* db, bool value) { std::string sql; diff --git a/include/sqlite_orm/sqlite_orm.h b/include/sqlite_orm/sqlite_orm.h index 21a65c974..fcbad9bc1 100644 --- a/include/sqlite_orm/sqlite_orm.h +++ b/include/sqlite_orm/sqlite_orm.h @@ -18208,25 +18208,16 @@ namespace sqlite_orm { * @return Returns list of tables in database. */ std::vector table_names() { - using data_t = std::vector; + return this->object_names("table"); + } - auto connection = this->get_connection(); - data_t tableNames; - this->executor.perform_exec( - connection.get(), - "SELECT name FROM sqlite_master WHERE type='table'", - [](void* data, int argc, char** argv, char** /*columnName*/) -> int { - auto& tableNames_ = *(data_t*)data; - for (int i = 0; i < argc; ++i) { - if (argv[i]) { - tableNames_.emplace_back(argv[i]); - } - } - return 0; - }, - &tableNames); - tableNames.shrink_to_fit(); - return tableNames; + /** + * Returns existing permanent trigger names in database. Doesn't check storage itself - works only with + * actual database. + * @return Returns list of triggers in database. + */ + std::vector trigger_names() { + return this->object_names("trigger"); } /** @@ -18694,6 +18685,30 @@ namespace sqlite_orm { return res; } + std::vector object_names(const std::string& type) { + using data_t = std::vector; + + auto connection = this->get_connection(); + data_t objectNames; + std::stringstream ss; + ss << "SELECT name FROM sqlite_master WHERE type=" << quote_string_literal(type); + this->executor.perform_exec( + connection.get(), + ss.str(), + [](void* data, int argc, char** argv, char** /*columnName*/) -> int { + auto& objectNames_ = *(data_t*)data; + for (int i = 0; i < argc; ++i) { + if (argv[i]) { + objectNames_.emplace_back(argv[i]); + } + } + return 0; + }, + &objectNames); + objectNames.shrink_to_fit(); + return objectNames; + } + #if SQLITE_VERSION_NUMBER >= 3006019 void foreign_keys(sqlite3* db, bool value) { std::string sql; diff --git a/tests/trigger_tests.cpp b/tests/trigger_tests.cpp index f368ec77f..d9e88a621 100644 --- a/tests/trigger_tests.cpp +++ b/tests/trigger_tests.cpp @@ -104,6 +104,35 @@ TEST_CASE("triggers_basics") { } } +TEST_CASE("trigger_names") { + auto storagePath = "trigger_names.sqlite"; + struct X { + int test = 0; + }; + + { + auto storage = make_storage( + storagePath, + make_trigger("trigger1", after().insert().on().begin(update_all(set(c(&X::test) = 1))).end()), + make_trigger("trigger2", after().insert().on().begin(update_all(set(c(&X::test) = 2))).end()), + make_table("x", + make_column("test", &X::test))); + storage.sync_schema(); + } + { + auto storage = make_storage( + storagePath, + make_trigger("trigger2", after().insert().on().begin(update_all(set(c(&X::test) = 2))).end()), + make_trigger("trigger3", after().insert().on().begin(update_all(set(c(&X::test) = 3))).end()), + make_table("x", + make_column("test", &X::test))); + storage.sync_schema(); + + auto trigger_names=storage.trigger_names(); + REQUIRE_THAT(trigger_names, Catch::Matchers::UnorderedEquals(std::vector{"trigger1", "trigger2", "trigger3"})); + } +} + TEST_CASE("issue1429") { auto storagePath = "issue1429.sqlite"; struct X {