Skip to content

Commit e18ea01

Browse files
authored
Merge pull request #1480 from fnc12/feature/trigger-sync
added triggers sync
2 parents 62d49e8 + 3f40f79 commit e18ea01

8 files changed

Lines changed: 197 additions & 49 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
description: Project structure rules for sqlite_orm library development
3+
alwaysApply: true
4+
---
5+
6+
# Project Structure
7+
8+
- Library source code lives exclusively in the `dev/` folder — only edit library code there.
9+
- Tests in `tests/` and examples in `examples/` are also editable.
10+
- The file `include/sqlite_orm/sqlite_orm.h` is an auto-generated amalgamation and MUST NOT be edited manually.
11+
- To regenerate it, run: `python3 third_party/amalgamate/amalgamate.py -c third_party/amalgamate/config.json -s .`

.github/workflows/ci.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ jobs:
5353
cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_20=ON"
5454
triplet: x86-windows
5555

56-
- name: "VS 2019, x64, C++17"
57-
os: windows-2019
58-
platform: x64
59-
arch: x64
60-
cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_17=ON"
61-
triplet: x64-windows
56+
# - name: "VS 2019, x64, C++17"
57+
# os: windows-2019
58+
# platform: x64
59+
# arch: x64
60+
# cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_17=ON"
61+
# triplet: x64-windows
6262

6363
name: Windows - ${{ matrix.name }}
6464

dev/statement_serializer.h

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,8 +2582,7 @@ namespace sqlite_orm::internal {
25822582
std::stringstream ss;
25832583
ss << "CREATE ";
25842584

2585-
ss << "TRIGGER IF NOT EXISTS " << streaming_identifier(statement.name) << " "
2586-
<< serialize(statement.base, context);
2585+
ss << "TRIGGER " << streaming_identifier(statement.name) << " " << serialize(statement.base, context);
25872586
ss << " BEGIN ";
25882587
iterate_tuple(statement.elements, [&ss, &context](auto& element) {
25892588
using element_type = polyfill::remove_cvref_t<decltype(element)>;

dev/storage.h

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,8 +1115,19 @@ namespace sqlite_orm::internal {
11151115
}
11161116

11171117
template<class T, class... S>
1118-
sync_schema_result schema_status(const trigger_t<T, S...>&, sqlite3*, bool, bool*) {
1119-
return sync_schema_result::already_in_sync;
1118+
sync_schema_result schema_status(const trigger_t<T, S...>& trigger, sqlite3* db, bool, bool*) {
1119+
auto dbTriggerSql = this->retrieve_object_sql(db, "trigger", trigger.name);
1120+
if (dbTriggerSql.empty()) {
1121+
return sync_schema_result::new_table_created;
1122+
}
1123+
1124+
const serializer_context<db_objects_type> context{this->db_objects};
1125+
auto storageSql = serialize(trigger, context);
1126+
1127+
if (dbTriggerSql == storageSql) {
1128+
return sync_schema_result::already_in_sync;
1129+
}
1130+
return sync_schema_result::dropped_and_recreated;
11201131
}
11211132

11221133
template<class... Cols>
@@ -1250,11 +1261,16 @@ namespace sqlite_orm::internal {
12501261
}
12511262

12521263
template<class... Cols>
1253-
sync_schema_result sync_dbo(const trigger_t<Cols...>& trigger, sqlite3* db, bool) {
1254-
const auto res = sync_schema_result::already_in_sync; // TODO Change accordingly
1255-
const serializer_context<db_objects_type> context{this->db_objects};
1256-
const auto sql = serialize(trigger, context);
1257-
this->executor.perform_void_exec(db, sql.c_str());
1264+
sync_schema_result sync_dbo(const trigger_t<Cols...>& trigger, sqlite3* db, bool preserve) {
1265+
auto res = this->schema_status(trigger, db, preserve, nullptr);
1266+
if (res != sync_schema_result::already_in_sync) {
1267+
if (res == sync_schema_result::dropped_and_recreated) {
1268+
this->drop_trigger_internal(trigger.name, true, db);
1269+
}
1270+
const serializer_context<db_objects_type> context{this->db_objects};
1271+
const auto sql = serialize(trigger, context);
1272+
this->executor.perform_void_exec(db, sql.c_str());
1273+
}
12581274
return res;
12591275
}
12601276

dev/storage_base.h

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,18 +1064,36 @@ namespace sqlite_orm::internal {
10641064
}
10651065

10661066
void drop_trigger_internal(const std::string& triggerName, bool ifExists) {
1067-
std::string sql;
1068-
{
1069-
std::stringstream ss;
1070-
ss << "DROP TRIGGER";
1071-
if (ifExists) {
1072-
ss << " IF EXISTS";
1073-
}
1074-
ss << ' ' << quote_identifier(triggerName) << std::flush;
1075-
sql = ss.str();
1076-
}
10771067
auto connection = this->get_connection();
1078-
this->executor.perform_void_exec(connection.get(), sql.c_str());
1068+
this->drop_trigger_internal(triggerName, ifExists, connection.get());
1069+
}
1070+
1071+
void drop_trigger_internal(const std::string& triggerName, bool ifExists, sqlite3* db) {
1072+
std::stringstream ss;
1073+
ss << "DROP TRIGGER";
1074+
if (ifExists) {
1075+
ss << " IF EXISTS";
1076+
}
1077+
ss << ' ' << quote_identifier(triggerName) << std::flush;
1078+
this->executor.perform_void_exec(db, ss.str().c_str());
1079+
}
1080+
1081+
std::string retrieve_object_sql(sqlite3* db, const std::string& type, const std::string& name) const {
1082+
std::string result;
1083+
std::stringstream ss;
1084+
ss << "SELECT sql FROM sqlite_master WHERE type = " << quote_string_literal(type)
1085+
<< " AND name = " << quote_string_literal(name);
1086+
this->executor.perform_exec(
1087+
db,
1088+
ss.str(),
1089+
[](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*columnName*/) -> int {
1090+
if (argv[0]) {
1091+
*static_cast<std::string*>(userData) = argv[0];
1092+
}
1093+
return 0;
1094+
},
1095+
&result);
1096+
return result;
10791097
}
10801098

10811099
static int collate_callback(void* argument, int leftLength, const void* lhs, int rightLength, const void* rhs) {

include/sqlite_orm/sqlite_orm.h

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20514,18 +20514,36 @@ namespace sqlite_orm::internal {
2051420514
}
2051520515

2051620516
void drop_trigger_internal(const std::string& triggerName, bool ifExists) {
20517-
std::string sql;
20518-
{
20519-
std::stringstream ss;
20520-
ss << "DROP TRIGGER";
20521-
if (ifExists) {
20522-
ss << " IF EXISTS";
20523-
}
20524-
ss << ' ' << quote_identifier(triggerName) << std::flush;
20525-
sql = ss.str();
20526-
}
2052720517
auto connection = this->get_connection();
20528-
this->executor.perform_void_exec(connection.get(), sql.c_str());
20518+
this->drop_trigger_internal(triggerName, ifExists, connection.get());
20519+
}
20520+
20521+
void drop_trigger_internal(const std::string& triggerName, bool ifExists, sqlite3* db) {
20522+
std::stringstream ss;
20523+
ss << "DROP TRIGGER";
20524+
if (ifExists) {
20525+
ss << " IF EXISTS";
20526+
}
20527+
ss << ' ' << quote_identifier(triggerName) << std::flush;
20528+
this->executor.perform_void_exec(db, ss.str().c_str());
20529+
}
20530+
20531+
std::string retrieve_object_sql(sqlite3* db, const std::string& type, const std::string& name) const {
20532+
std::string result;
20533+
std::stringstream ss;
20534+
ss << "SELECT sql FROM sqlite_master WHERE type = " << quote_string_literal(type)
20535+
<< " AND name = " << quote_string_literal(name);
20536+
this->executor.perform_exec(
20537+
db,
20538+
ss.str(),
20539+
[](void* userData, int /*argc*/, orm_gsl::zstring* argv, orm_gsl::zstring* /*columnName*/) -> int {
20540+
if (argv[0]) {
20541+
*static_cast<std::string*>(userData) = argv[0];
20542+
}
20543+
return 0;
20544+
},
20545+
&result);
20546+
return result;
2052920547
}
2053020548

2053120549
static int collate_callback(void* argument, int leftLength, const void* lhs, int rightLength, const void* rhs) {
@@ -24224,8 +24242,7 @@ namespace sqlite_orm::internal {
2422424242
std::stringstream ss;
2422524243
ss << "CREATE ";
2422624244

24227-
ss << "TRIGGER IF NOT EXISTS " << streaming_identifier(statement.name) << " "
24228-
<< serialize(statement.base, context);
24245+
ss << "TRIGGER " << streaming_identifier(statement.name) << " " << serialize(statement.base, context);
2422924246
ss << " BEGIN ";
2423024247
iterate_tuple(statement.elements, [&ss, &context](auto& element) {
2423124248
using element_type = polyfill::remove_cvref_t<decltype(element)>;
@@ -26108,8 +26125,19 @@ namespace sqlite_orm::internal {
2610826125
}
2610926126

2611026127
template<class T, class... S>
26111-
sync_schema_result schema_status(const trigger_t<T, S...>&, sqlite3*, bool, bool*) {
26112-
return sync_schema_result::already_in_sync;
26128+
sync_schema_result schema_status(const trigger_t<T, S...>& trigger, sqlite3* db, bool, bool*) {
26129+
auto dbTriggerSql = this->retrieve_object_sql(db, "trigger", trigger.name);
26130+
if (dbTriggerSql.empty()) {
26131+
return sync_schema_result::new_table_created;
26132+
}
26133+
26134+
const serializer_context<db_objects_type> context{this->db_objects};
26135+
auto storageSql = serialize(trigger, context);
26136+
26137+
if (dbTriggerSql == storageSql) {
26138+
return sync_schema_result::already_in_sync;
26139+
}
26140+
return sync_schema_result::dropped_and_recreated;
2611326141
}
2611426142

2611526143
template<class... Cols>
@@ -26243,11 +26271,16 @@ namespace sqlite_orm::internal {
2624326271
}
2624426272

2624526273
template<class... Cols>
26246-
sync_schema_result sync_dbo(const trigger_t<Cols...>& trigger, sqlite3* db, bool) {
26247-
const auto res = sync_schema_result::already_in_sync; // TODO Change accordingly
26248-
const serializer_context<db_objects_type> context{this->db_objects};
26249-
const auto sql = serialize(trigger, context);
26250-
this->executor.perform_void_exec(db, sql.c_str());
26274+
sync_schema_result sync_dbo(const trigger_t<Cols...>& trigger, sqlite3* db, bool preserve) {
26275+
auto res = this->schema_status(trigger, db, preserve, nullptr);
26276+
if (res != sync_schema_result::already_in_sync) {
26277+
if (res == sync_schema_result::dropped_and_recreated) {
26278+
this->drop_trigger_internal(trigger.name, true, db);
26279+
}
26280+
const serializer_context<db_objects_type> context{this->db_objects};
26281+
const auto sql = serialize(trigger, context);
26282+
this->executor.perform_void_exec(db, sql.c_str());
26283+
}
2625126284
return res;
2625226285
}
2625326286

tests/statement_serializer_tests/schema/trigger.cpp

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,8 @@ TEST_CASE("statement_serializer trigger") {
3535
.end()))
3636
.end());
3737
value = serialize(expression, context);
38-
expected =
39-
R"(CREATE TRIGGER IF NOT EXISTS "validate_email_before_insert_leads" BEFORE INSERT ON "leads" BEGIN SELECT )"
40-
R"(CASE WHEN NOT NEW."email" LIKE '%_@__%.__%' THEN RAISE(ABORT, 'Invalid email address') END; END)";
38+
expected = R"(CREATE TRIGGER "validate_email_before_insert_leads" BEFORE INSERT ON "leads" BEGIN SELECT )"
39+
R"(CASE WHEN NOT NEW."email" LIKE '%_@__%.__%' THEN RAISE(ABORT, 'Invalid email address') END; END)";
4140
}
4241
REQUIRE(value == expected);
4342
}

tests/trigger_tests.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,78 @@ TEST_CASE("trigger_names") {
132132
}
133133
}
134134

135+
TEST_CASE("issue1429") {
136+
struct Lead {
137+
int id = 0;
138+
std::string name;
139+
std::string email;
140+
};
141+
142+
auto storagePath = "issue1429.sqlite";
143+
std::remove(storagePath);
144+
145+
// first: create storage with trigger checking "name" column
146+
{
147+
auto storage = make_storage(storagePath,
148+
make_trigger("validate_email_before_insert_leads",
149+
before()
150+
.insert()
151+
.on<Lead>()
152+
.begin(select(case_<int>()
153+
.when(not like(new_(&Lead::name), "%_@__%.__%"),
154+
then(raise_abort("Invalid email address")))
155+
.end()))
156+
.end()),
157+
make_table("leads",
158+
make_column("id", &Lead::id, primary_key()),
159+
make_column("name", &Lead::name),
160+
make_column("email", &Lead::email)));
161+
auto syncResult = storage.sync_schema();
162+
REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::new_table_created);
163+
164+
// second sync should report already_in_sync
165+
syncResult = storage.sync_schema();
166+
REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::already_in_sync);
167+
}
168+
// second: create storage with trigger checking "email" column instead
169+
{
170+
auto storage = make_storage(storagePath,
171+
make_trigger("validate_email_before_insert_leads",
172+
before()
173+
.insert()
174+
.on<Lead>()
175+
.begin(select(case_<int>()
176+
.when(not like(new_(&Lead::email), "%_@__%.__%"),
177+
then(raise_abort("Invalid email address")))
178+
.end()))
179+
.end()),
180+
make_table("leads",
181+
make_column("id", &Lead::id, primary_key()),
182+
make_column("name", &Lead::name),
183+
make_column("email", &Lead::email)));
184+
185+
// simulate should detect the change
186+
auto simulateResult = storage.sync_schema_simulate();
187+
REQUIRE(simulateResult.at("validate_email_before_insert_leads") == sync_schema_result::dropped_and_recreated);
188+
189+
// sync should update the trigger
190+
auto syncResult = storage.sync_schema();
191+
REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::dropped_and_recreated);
192+
193+
// verify trigger was updated: inserting a row with invalid email should fail
194+
REQUIRE_THROWS(storage.insert(Lead{0, "John", "not_an_email"}));
195+
196+
// valid email should succeed
197+
REQUIRE_NOTHROW(storage.insert(Lead{0, "John", "john@example.com"}));
198+
199+
// after update, second sync should be already_in_sync
200+
syncResult = storage.sync_schema();
201+
REQUIRE(syncResult.at("validate_email_before_insert_leads") == sync_schema_result::already_in_sync);
202+
}
203+
204+
std::remove(storagePath);
205+
}
206+
135207
TEST_CASE("issue1280") {
136208
struct X {
137209
int test = 0;

0 commit comments

Comments
 (0)