Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .cursor/rules/project-structure.mdc
Original file line number Diff line number Diff line change
@@ -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 .`
12 changes: 6 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

commenting out cause rn it doesn't even start on gh action IDK why

# os: windows-2019
# platform: x64
# arch: x64
# cxx_standard: "-DSQLITE_ORM_ENABLE_CXX_17=ON"
# triplet: x64-windows

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

Expand Down
3 changes: 1 addition & 2 deletions dev/statement_serializer.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<decltype(element)>;
Expand Down
30 changes: 23 additions & 7 deletions dev/storage.h
Original file line number Diff line number Diff line change
Expand Up @@ -1115,8 +1115,19 @@ namespace sqlite_orm::internal {
}

template<class T, class... S>
sync_schema_result schema_status(const trigger_t<T, S...>&, sqlite3*, bool, bool*) {
return sync_schema_result::already_in_sync;
sync_schema_result schema_status(const trigger_t<T, S...>& 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<db_objects_type> 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<class... Cols>
Expand Down Expand Up @@ -1250,11 +1261,16 @@ namespace sqlite_orm::internal {
}

template<class... Cols>
sync_schema_result sync_dbo(const trigger_t<Cols...>& trigger, sqlite3* db, bool) {
const auto res = sync_schema_result::already_in_sync; // TODO Change accordingly
const serializer_context<db_objects_type> 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<Cols...>& 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<db_objects_type> context{this->db_objects};
const auto sql = serialize(trigger, context);
this->executor.perform_void_exec(db, sql.c_str());
}
return res;
}

Expand Down
40 changes: 29 additions & 11 deletions dev/storage_base.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string*>(userData) = argv[0];
}
return 0;
},
&result);
return result;
}

static int collate_callback(void* argument, int leftLength, const void* lhs, int rightLength, const void* rhs) {
Expand Down
73 changes: 53 additions & 20 deletions include/sqlite_orm/sqlite_orm.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string*>(userData) = argv[0];
}
return 0;
},
&result);
return result;
}

static int collate_callback(void* argument, int leftLength, const void* lhs, int rightLength, const void* rhs) {
Expand Down Expand Up @@ -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<decltype(element)>;
Expand Down Expand Up @@ -26108,8 +26125,19 @@ namespace sqlite_orm::internal {
}

template<class T, class... S>
sync_schema_result schema_status(const trigger_t<T, S...>&, sqlite3*, bool, bool*) {
return sync_schema_result::already_in_sync;
sync_schema_result schema_status(const trigger_t<T, S...>& 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<db_objects_type> 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<class... Cols>
Expand Down Expand Up @@ -26243,11 +26271,16 @@ namespace sqlite_orm::internal {
}

template<class... Cols>
sync_schema_result sync_dbo(const trigger_t<Cols...>& trigger, sqlite3* db, bool) {
const auto res = sync_schema_result::already_in_sync; // TODO Change accordingly
const serializer_context<db_objects_type> 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<Cols...>& 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<db_objects_type> context{this->db_objects};
const auto sql = serialize(trigger, context);
this->executor.perform_void_exec(db, sql.c_str());
}
return res;
}

Expand Down
5 changes: 2 additions & 3 deletions tests/statement_serializer_tests/schema/trigger.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
72 changes: 72 additions & 0 deletions tests/trigger_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Lead>()
.begin(select(case_<int>()
.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<Lead>()
.begin(select(case_<int>()
.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;
Expand Down
Loading