From 42f8caab9ee82f2f9b512f8a8a00660c07d7d561 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:42:44 +0000 Subject: [PATCH 1/4] Initial plan From 461e771575c880bb6f859293f2b654af928dd077 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:46:44 +0000 Subject: [PATCH 2/4] Add support for prepared statement flags - Define PREPARE_* constants in Database.h/cpp - Add prepFlags parameter to Statement constructors - Update Statement implementation to use sqlite3_prepare_v3 - Add comprehensive tests for prepare flags Co-authored-by: SRombauts <868490+SRombauts@users.noreply.github.com> --- include/SQLiteCpp/Database.h | 9 +++++++++ include/SQLiteCpp/Statement.h | 9 ++++++--- src/Database.cpp | 6 ++++++ src/Statement.cpp | 20 ++++++++++++++++--- tests/Statement_test.cpp | 36 +++++++++++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/include/SQLiteCpp/Database.h b/include/SQLiteCpp/Database.h index e92e2898..ac00cc25 100644 --- a/include/SQLiteCpp/Database.h +++ b/include/SQLiteCpp/Database.h @@ -106,6 +106,15 @@ SQLITECPP_API extern const int OPEN_PRIVATECACHE; // SQLITE_OPEN_PRIVATECACHE /// Database filename is not allowed to be a symbolic link (Note: only since SQlite 3.31.0 from 2020-01-22) SQLITECPP_API extern const int OPEN_NOFOLLOW; // SQLITE_OPEN_NOFOLLOW +/// Prepared statement will be retained and reused many times (hint to the query planner) +SQLITECPP_API extern const int PREPARE_PERSISTENT; // SQLITE_PREPARE_PERSISTENT +/// Normalize the prepared statement (no-op, kept for compatibility) +SQLITECPP_API extern const int PREPARE_NORMALIZE; // SQLITE_PREPARE_NORMALIZE +/// Return error if statement uses any virtual tables +SQLITECPP_API extern const int PREPARE_NO_VTAB; // SQLITE_PREPARE_NO_VTAB +/// Prevents SQL compiler errors from being sent to the error log (useful for testing SQL syntax) +SQLITECPP_API extern const int PREPARE_DONT_LOG; // SQLITE_PREPARE_DONT_LOG + SQLITECPP_API extern const int OK; ///< SQLITE_OK (used by check() bellow) diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 3b9b0a70..8c2efcb2 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -57,21 +57,23 @@ class SQLITECPP_API Statement * * @param[in] aDatabase the SQLite Database Connection * @param[in] apQuery an UTF-8 encoded query string + * @param[in] aPrepFlags Optional prepare flags (e.g., SQLite::PREPARE_PERSISTENT) * * Exception is thrown in case of error, then the Statement object is NOT constructed. */ - Statement(const Database& aDatabase, const char* apQuery); + Statement(const Database& aDatabase, const char* apQuery, const unsigned int aPrepFlags = 0); /** * @brief Compile and register the SQL query for the provided SQLite Database Connection * * @param[in] aDatabase the SQLite Database Connection * @param[in] aQuery an UTF-8 encoded query string + * @param[in] aPrepFlags Optional prepare flags (e.g., SQLite::PREPARE_PERSISTENT) * * Exception is thrown in case of error, then the Statement object is NOT constructed. */ - Statement(const Database& aDatabase, const std::string& aQuery) : - Statement(aDatabase, aQuery.c_str()) + Statement(const Database& aDatabase, const std::string& aQuery, const unsigned int aPrepFlags = 0) : + Statement(aDatabase, aQuery.c_str(), aPrepFlags) {} // Statement is non-copyable @@ -717,6 +719,7 @@ class SQLITECPP_API Statement int mColumnCount = 0; //!< Number of columns in the result of the prepared statement bool mbHasRow = false; //!< true when a row has been fetched with executeStep() bool mbDone = false; //!< true when the last executeStep() had no more row to fetch + unsigned int mPrepFlags = 0; //!< Prepared statement flags (SQLITE_PREPARE_*) /// Map of columns index by name (mutable so getColumnIndex can be const) mutable std::map mColumnNames; diff --git a/src/Database.cpp b/src/Database.cpp index 30f71158..c5d73633 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -43,6 +43,12 @@ const int OPEN_NOFOLLOW = SQLITE_OPEN_NOFOLLOW; const int OPEN_NOFOLLOW = 0; #endif +// Prepared statement flags (available since SQLite 3.20.0) +const int PREPARE_PERSISTENT = SQLITE_PREPARE_PERSISTENT; +const int PREPARE_NORMALIZE = SQLITE_PREPARE_NORMALIZE; +const int PREPARE_NO_VTAB = SQLITE_PREPARE_NO_VTAB; +const int PREPARE_DONT_LOG = SQLITE_PREPARE_DONT_LOG; + const char* const VERSION = SQLITE_VERSION; const int VERSION_NUMBER = SQLITE_VERSION_NUMBER; diff --git a/src/Statement.cpp b/src/Statement.cpp index e4a264f4..b9ebf1b1 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -31,11 +31,13 @@ namespace SQLite { -Statement::Statement(const Database& aDatabase, const char* apQuery) : +Statement::Statement(const Database& aDatabase, const char* apQuery, const unsigned int aPrepFlags) : mQuery(apQuery), mpSQLite(aDatabase.getHandle()), - mpPreparedStatement(prepareStatement()) // prepare the SQL query (needs Database friendship) + mpPreparedStatement(nullptr), // Will be initialized after mPrepFlags + mPrepFlags(aPrepFlags) { + mpPreparedStatement = prepareStatement(); // prepare the SQL query (needs Database friendship) mColumnCount = sqlite3_column_count(mpPreparedStatement.get()); } @@ -46,6 +48,7 @@ Statement::Statement(Statement&& aStatement) noexcept : mColumnCount(aStatement.mColumnCount), mbHasRow(aStatement.mbHasRow), mbDone(aStatement.mbDone), + mPrepFlags(aStatement.mPrepFlags), mColumnNames(std::move(aStatement.mColumnNames)) { aStatement.mpSQLite = nullptr; @@ -360,7 +363,18 @@ std::string Statement::getExpandedSQL() const { Statement::TStatementPtr Statement::prepareStatement() { sqlite3_stmt* statement; - const int ret = sqlite3_prepare_v2(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), &statement, nullptr); + int ret; + + // Use sqlite3_prepare_v3 if prepare flags are specified, otherwise use sqlite3_prepare_v2 + if (mPrepFlags != 0) + { + ret = sqlite3_prepare_v3(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), mPrepFlags, &statement, nullptr); + } + else + { + ret = sqlite3_prepare_v2(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), &statement, nullptr); + } + if (SQLITE_OK != ret) { throw SQLite::Exception(mpSQLite, ret); diff --git a/tests/Statement_test.cpp b/tests/Statement_test.cpp index a76d06a8..5bccd9e5 100644 --- a/tests/Statement_test.cpp +++ b/tests/Statement_test.cpp @@ -1026,3 +1026,39 @@ TEST(Statement, getBindParameterCount) SQLite::Statement query3(db, "SELECT id, msg FROM test"); EXPECT_EQ(0, query3.getBindParameterCount()); } + +TEST(Statement, prepareFlags) +{ + // Create a new database + SQLite::Database db(":memory:", SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE); + EXPECT_EQ(0, db.exec("CREATE TABLE test (id INTEGER PRIMARY KEY, msg TEXT)")); + + // Test creating a statement with PREPARE_PERSISTENT flag + SQLite::Statement query1(db, "SELECT * FROM test", SQLite::PREPARE_PERSISTENT); + EXPECT_STREQ("SELECT * FROM test", query1.getQuery().c_str()); + EXPECT_EQ(2, query1.getColumnCount()); + + // Test creating a statement with PREPARE_DONT_LOG flag + // This should not throw even though we're testing SQL syntax + SQLite::Statement query2(db, "SELECT * FROM test", SQLite::PREPARE_DONT_LOG); + EXPECT_STREQ("SELECT * FROM test", query2.getQuery().c_str()); + EXPECT_EQ(2, query2.getColumnCount()); + + // Test creating a statement with combined flags + SQLite::Statement query3(db, "SELECT * FROM test", + SQLite::PREPARE_PERSISTENT | SQLite::PREPARE_DONT_LOG); + EXPECT_STREQ("SELECT * FROM test", query3.getQuery().c_str()); + EXPECT_EQ(2, query3.getColumnCount()); + + // Test that the statement works correctly + EXPECT_EQ(1, db.exec("INSERT INTO test VALUES (1, 'hello')")); + EXPECT_TRUE(query3.executeStep()); + EXPECT_EQ(1, query3.getColumn(0).getInt()); + EXPECT_STREQ("hello", query3.getColumn(1).getText()); + + // Test PREPARE_DONT_LOG with invalid SQL (should throw but not log) + // We can't easily verify it's not logged, but we can verify the exception is still thrown + EXPECT_THROW(SQLite::Statement(db, "SELECT * FROM nonexistent", SQLite::PREPARE_DONT_LOG), + SQLite::Exception); +} + From 98a8c77ffa35fce50e4f766dcfcf3ab6c4f3d97e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 21:47:33 +0000 Subject: [PATCH 3/4] Update CHANGELOG.md for prepare flags feature Co-authored-by: SRombauts <868490+SRombauts@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bdece6..f20a4e0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +Version 3.3.4 - TBD +- Added support for prepared statement flags (PREPARE_PERSISTENT, PREPARE_DONT_LOG, etc.) +- Statement constructor now accepts optional prepFlags parameter + 2012 Mar 30 - Start of a new thin C++ SQLite wrapper From 90173e87d1989222be6731894d116c74cd82c208 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 22:33:27 +0000 Subject: [PATCH 4/4] Refactor prepareStatement to address review feedback - Remove mPrepFlags member variable, pass as parameter instead - Always use sqlite3_prepare_v3 when available (SQLite >= 3.20.0) - Add version checking for PREPARE_* flags - Simplify implementation by eliminating conditional v2/v3 logic Co-authored-by: SRombauts <868490+SRombauts@users.noreply.github.com> --- include/SQLiteCpp/Statement.h | 4 ++-- src/Database.cpp | 7 +++++++ src/Statement.cpp | 24 ++++++++++-------------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/include/SQLiteCpp/Statement.h b/include/SQLiteCpp/Statement.h index 8c2efcb2..3be985c1 100644 --- a/include/SQLiteCpp/Statement.h +++ b/include/SQLiteCpp/Statement.h @@ -701,9 +701,10 @@ class SQLITECPP_API Statement /** * @brief Prepare statement object. * + * @param[in] aPrepFlags Optional prepare flags (e.g., SQLite::PREPARE_PERSISTENT) * @return Shared pointer to prepared statement object */ - TStatementPtr prepareStatement(); + TStatementPtr prepareStatement(const unsigned int aPrepFlags); /** * @brief Return a prepared statement object. @@ -719,7 +720,6 @@ class SQLITECPP_API Statement int mColumnCount = 0; //!< Number of columns in the result of the prepared statement bool mbHasRow = false; //!< true when a row has been fetched with executeStep() bool mbDone = false; //!< true when the last executeStep() had no more row to fetch - unsigned int mPrepFlags = 0; //!< Prepared statement flags (SQLITE_PREPARE_*) /// Map of columns index by name (mutable so getColumnIndex can be const) mutable std::map mColumnNames; diff --git a/src/Database.cpp b/src/Database.cpp index c5d73633..8040d8ae 100644 --- a/src/Database.cpp +++ b/src/Database.cpp @@ -44,10 +44,17 @@ const int OPEN_NOFOLLOW = 0; #endif // Prepared statement flags (available since SQLite 3.20.0) +#if SQLITE_VERSION_NUMBER >= 3020000 const int PREPARE_PERSISTENT = SQLITE_PREPARE_PERSISTENT; const int PREPARE_NORMALIZE = SQLITE_PREPARE_NORMALIZE; const int PREPARE_NO_VTAB = SQLITE_PREPARE_NO_VTAB; const int PREPARE_DONT_LOG = SQLITE_PREPARE_DONT_LOG; +#else +const int PREPARE_PERSISTENT = 0; +const int PREPARE_NORMALIZE = 0; +const int PREPARE_NO_VTAB = 0; +const int PREPARE_DONT_LOG = 0; +#endif const char* const VERSION = SQLITE_VERSION; const int VERSION_NUMBER = SQLITE_VERSION_NUMBER; diff --git a/src/Statement.cpp b/src/Statement.cpp index b9ebf1b1..073cd367 100644 --- a/src/Statement.cpp +++ b/src/Statement.cpp @@ -34,10 +34,8 @@ namespace SQLite Statement::Statement(const Database& aDatabase, const char* apQuery, const unsigned int aPrepFlags) : mQuery(apQuery), mpSQLite(aDatabase.getHandle()), - mpPreparedStatement(nullptr), // Will be initialized after mPrepFlags - mPrepFlags(aPrepFlags) + mpPreparedStatement(prepareStatement(aPrepFlags)) // prepare the SQL query (needs Database friendship) { - mpPreparedStatement = prepareStatement(); // prepare the SQL query (needs Database friendship) mColumnCount = sqlite3_column_count(mpPreparedStatement.get()); } @@ -48,7 +46,6 @@ Statement::Statement(Statement&& aStatement) noexcept : mColumnCount(aStatement.mColumnCount), mbHasRow(aStatement.mbHasRow), mbDone(aStatement.mbDone), - mPrepFlags(aStatement.mPrepFlags), mColumnNames(std::move(aStatement.mColumnNames)) { aStatement.mpSQLite = nullptr; @@ -360,20 +357,19 @@ std::string Statement::getExpandedSQL() const { // Prepare SQLite statement object and return shared pointer to this object -Statement::TStatementPtr Statement::prepareStatement() +Statement::TStatementPtr Statement::prepareStatement(const unsigned int aPrepFlags) { sqlite3_stmt* statement; int ret; - // Use sqlite3_prepare_v3 if prepare flags are specified, otherwise use sqlite3_prepare_v2 - if (mPrepFlags != 0) - { - ret = sqlite3_prepare_v3(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), mPrepFlags, &statement, nullptr); - } - else - { - ret = sqlite3_prepare_v2(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), &statement, nullptr); - } + // Use sqlite3_prepare_v3 when available (SQLite >= 3.20.0), otherwise fall back to sqlite3_prepare_v2 +#if SQLITE_VERSION_NUMBER >= 3020000 + ret = sqlite3_prepare_v3(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), aPrepFlags, &statement, nullptr); +#else + // sqlite3_prepare_v3 not available, ignore prepFlags and use sqlite3_prepare_v2 + (void)aPrepFlags; // Avoid unused parameter warning + ret = sqlite3_prepare_v2(mpSQLite, mQuery.c_str(), static_cast(mQuery.size()), &statement, nullptr); +#endif if (SQLITE_OK != ret) {