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
10 changes: 1 addition & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,9 @@ jobs:
run: |
choco install ninja

- name: Install dependencies (Windows)
if: matrix.os == 'windows-2025'
shell: bash
run: |
choco install ninja
curl https://www.sqlite.org/2026/sqlite-amalgamation-3510200.zip -o sqlite.zip
unzip

- name: Configure
run: |
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DVSQLITE_BUILD_EXAMPLES=ON
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DVSQLITE_BUILD_EXAMPLES=ON -DVSQLITE_BUNDLED_SQLITE=ON
shell: bash

- name: Build
Expand Down
57 changes: 56 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,50 @@ include(CTest)
option(VSQLITE_BUILD_TESTS "Build unit tests" ${BUILD_TESTING})
option(VSQLITE_ENABLE_COVERAGE "Enable code coverage instrumentation" OFF)

set(VSQLITE_BUNDLED_SQLITE_DEFAULT OFF)
if(VSQLITE_BUILD_TESTS)
set(VSQLITE_BUNDLED_SQLITE_DEFAULT ON)
endif()
option(VSQLITE_BUNDLED_SQLITE "Build and link bundled SQLite3" ${VSQLITE_BUNDLED_SQLITE_DEFAULT})

set(VSQLITEPP_SOVERSION "${PROJECT_VERSION_MAJOR}" CACHE STRING "Shared object version to use")

list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

find_package(SQLite3 REQUIRED)
if(VSQLITE_BUNDLED_SQLITE)
enable_language(C)
include(FetchContent)
FetchContent_Declare(
sqlite3_amalgamation
URL https://www.sqlite.org/2026/sqlite-amalgamation-3510200.zip
URL_HASH SHA3_256=9a9dd4eef7a97809bfacd84a7db5080a5c0eff7aaf1fc1aca20a6dc9a0c26f96
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(sqlite3_amalgamation)

add_library(sqlite3 STATIC "${sqlite3_amalgamation_SOURCE_DIR}/sqlite3.c")
set_target_properties(sqlite3 PROPERTIES POSITION_INDEPENDENT_CODE ON)
target_include_directories(sqlite3
PUBLIC
$<BUILD_INTERFACE:${sqlite3_amalgamation_SOURCE_DIR}>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
target_compile_definitions(sqlite3
PRIVATE
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_SESSION
SQLITE_ENABLE_SNAPSHOT
SQLITE_ENABLE_PREUPDATE_HOOK
SQLITE_THREADSAFE=1
)
add_library(SQLite::SQLite3 ALIAS sqlite3)
Comment thread
vinzenz marked this conversation as resolved.
else()
find_package(SQLite3 REQUIRED)
endif()

if(VSQLITE_ENABLE_COVERAGE)
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
Expand Down Expand Up @@ -109,6 +145,21 @@ install(TARGETS vsqlitepp
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)

if(VSQLITE_BUNDLED_SQLITE)
install(TARGETS sqlite3
EXPORT vsqliteppTargets
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(FILES
"${sqlite3_amalgamation_SOURCE_DIR}/sqlite3.h"
"${sqlite3_amalgamation_SOURCE_DIR}/sqlite3ext.h"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)
endif()

install(DIRECTORY "${PROJECT_SOURCE_DIR}/include/"
DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
FILES_MATCHING PATTERN "*.hpp"
Expand Down Expand Up @@ -147,13 +198,17 @@ install(FILES

if(VSQLITE_BUILD_TESTS)
enable_testing()
set(VSQLITE_OLD_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS OFF)
set(INSTALL_GTEST OFF)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/v1.14.0.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(googletest)
set(BUILD_SHARED_LIBS ${VSQLITE_OLD_BUILD_SHARED_LIBS})

set(VSQLITE_TEST_SOURCES
tests/test_backup.cpp
Expand Down
3 changes: 3 additions & 0 deletions tests/test_command_query.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ using namespace testhelpers;

TEST(CommandQueryTest, BindsAndRetrievesData) {
sqlite::connection conn(":memory:");
dump_sqlite_diagnostics(conn, "CommandQueryTest.BindsAndRetrievesData");
sqlite::execute(conn,
"CREATE TABLE sample(id INTEGER PRIMARY KEY, ival INTEGER, lval INTEGER, note "
"TEXT, amount REAL, data BLOB, nullable TEXT);",
Expand All @@ -38,6 +39,8 @@ TEST(CommandQueryTest, BindsAndRetrievesData) {
std::span<const unsigned char>(blob_view_data) % std::string("value");
insert();

dump_table_info(conn, "sample");

sqlite::query q(conn,
"SELECT id, ival, lval, note, amount, data, nullable FROM sample ORDER BY id;");
auto result = q.get_result();
Expand Down
92 changes: 92 additions & 0 deletions tests/test_common.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@

#include <atomic>
#include <chrono>
#include <cstdlib>
#include <filesystem>
#include <iostream>
#include <optional>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>

#include <sqlite3.h>

namespace testhelpers {

inline std::filesystem::path test_root() {
Expand Down Expand Up @@ -76,6 +80,94 @@ inline std::string quote_identifier(std::string_view identifier) {
return quoted;
}

inline bool diagnostics_enabled() {
if (std::getenv("VSQLITE_TEST_DIAGNOSTICS")) {
return true;
}
if (std::getenv("CI")) {
return true;
}
return false;
}

inline void dump_sqlite_diagnostics(sqlite::connection &con, std::string_view label) {
if (!diagnostics_enabled()) {
return;
}
std::cerr << "\n[sqlite diagnostics] " << label << '\n';
std::cerr << " sqlite3_libversion: " << sqlite3_libversion() << '\n';
std::cerr << " sqlite3_sourceid: " << sqlite3_sourceid() << '\n';

try {
sqlite::query version_q(con, "SELECT sqlite_version();");
auto version_res = version_q.get_result();
if (version_res->next_row()) {
std::cerr << " sqlite_version(): " << version_res->get<std::string>(0) << '\n';
} else {
std::cerr << " sqlite_version(): <no rows>\n";
}
} catch (std::exception const &ex) {
std::cerr << " sqlite_version(): <error> " << ex.what() << '\n';
}

try {
sqlite::query db_list(con, "PRAGMA database_list;");
auto db_res = db_list.get_result();
std::cerr << " database_list:\n";
while (db_res->next_row()) {
std::cerr << " " << db_res->get<int>(0) << " | " << db_res->get<std::string>(1)
<< " | " << db_res->get<std::string>(2) << '\n';
}
} catch (std::exception const &ex) {
std::cerr << " database_list: <error> " << ex.what() << '\n';
}

try {
sqlite::query opts(con, "PRAGMA compile_options;");
auto opt_res = opts.get_result();
std::cerr << " compile_options:\n";
while (opt_res->next_row()) {
std::cerr << " " << opt_res->get<std::string>(0) << '\n';
}
} catch (std::exception const &ex) {
std::cerr << " compile_options: <error> " << ex.what() << '\n';
}
}

inline void dump_table_info(sqlite::connection &con, std::string_view table) {
if (!diagnostics_enabled()) {
return;
}
std::cerr << " table_info(" << table << "):\n";
try {
sqlite::query schema_q(
con, "SELECT name, sql FROM sqlite_master WHERE type='table' AND name=?;");
schema_q % std::string(table);
auto schema_res = schema_q.get_result();
if (schema_res->next_row()) {
std::cerr << " schema: " << schema_res->get<std::string>(1) << '\n';
} else {
std::cerr << " schema: <not found>\n";
}
} catch (std::exception const &ex) {
std::cerr << " schema: <error> " << ex.what() << '\n';
}

try {
sqlite::query count_q(con,
"SELECT COUNT(*) FROM " + quote_identifier(std::string(table)) +
";");
auto count_res = count_q.get_result();
if (count_res->next_row()) {
std::cerr << " row_count: " << count_res->get<int>(0) << '\n';
} else {
std::cerr << " row_count: <no rows>\n";
}
} catch (std::exception const &ex) {
std::cerr << " row_count: <error> " << ex.what() << '\n';
}
}

inline std::string unique_memory_uri() {
static std::atomic<uint64_t> counter{0};
std::ostringstream oss;
Expand Down
137 changes: 97 additions & 40 deletions tests/test_snapshot.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,50 +13,107 @@ TEST(SnapshotTest, TransactionSnapshotProvidesHistoricalReads) {
if (!sqlite::snapshots_supported()) {
GTEST_SKIP() << "SQLite snapshot APIs not available in this build.";
}
sqlite::connection conn(":memory:");
sqlite::execute(conn, "CREATE TABLE docs(id INTEGER PRIMARY KEY, body TEXT);", true);

sqlite::transaction txn(conn, sqlite::transaction_type::immediate);
sqlite::command insert(conn, "INSERT INTO docs(body) VALUES (?);");
insert % std::string("a");
insert.step_once();
auto snap = txn.take_snapshot();
insert % std::string("b");
insert.step_once();
txn.commit();

sqlite::transaction read(conn, sqlite::transaction_type::deferred);
snap.open(conn);
sqlite::query q(conn, "SELECT COUNT(*) FROM docs;");
auto res = q.get_result();
ASSERT_TRUE(res->next_row());
EXPECT_EQ(res->get<int>(0), 1);
try {
TempFile db("snapshot_txn");
sqlite::connection writer(db.string());
sqlite::enable_wal(writer);
dump_sqlite_diagnostics(writer, "SnapshotTest.TransactionSnapshotProvidesHistoricalReads");
sqlite::execute(writer, "CREATE TABLE docs(id INTEGER PRIMARY KEY, body TEXT);", true);

sqlite::command insert(writer, "INSERT INTO docs(body) VALUES (?);");
{
sqlite::transaction txn(writer, sqlite::transaction_type::immediate);
insert % std::string("a");
insert.step_once();
insert.clear();
txn.commit();
}

sqlite::connection reader_snapshot(db.string());
sqlite::connection reader_open(db.string());

sqlite::snapshot snap;
{
sqlite::transaction read(reader_snapshot, sqlite::transaction_type::deferred);
sqlite::query q(reader_snapshot, "SELECT COUNT(*) FROM docs;");
auto res = q.get_result();
ASSERT_TRUE(res->next_row());
EXPECT_EQ(res->get<int>(0), 1);
snap = read.take_snapshot();
read.commit();
}

{
sqlite::transaction txn(writer, sqlite::transaction_type::immediate);
insert % std::string("b");
insert.step_once();
insert.clear();
txn.commit();
}

dump_table_info(writer, "docs");

sqlite::transaction read(reader_open, sqlite::transaction_type::deferred);
snap.open(reader_open);
sqlite::query q(reader_open, "SELECT COUNT(*) FROM docs;");
auto res = q.get_result();
ASSERT_TRUE(res->next_row());
EXPECT_EQ(res->get<int>(0), 1);
} catch (sqlite::database_exception const &ex) {
GTEST_SKIP() << ex.what();
}
}

TEST(SnapshotTest, SavepointSnapshotControlsScope) {
if (!sqlite::snapshots_supported()) {
GTEST_SKIP() << "SQLite snapshot APIs not available in this build.";
}
sqlite::connection conn(":memory:");
sqlite::execute(conn, "CREATE TABLE docs(id INTEGER PRIMARY KEY, body TEXT);", true);

sqlite::savepoint sp(conn, "sp");
sqlite::command insert(conn, "INSERT INTO docs(body) VALUES (?);");
insert % std::string("alpha");
insert.step_once();
auto snap = sp.take_snapshot();
insert % std::string("beta");
insert.step_once();
sp.rollback();

sqlite::query q(conn, "SELECT COUNT(*) FROM docs;");
auto res = q.get_result();
ASSERT_TRUE(res->next_row());
EXPECT_EQ(res->get<int>(0), 0);

snap.open(conn);
sqlite::query check(conn, "SELECT COUNT(*) FROM docs;");
auto snap_res = check.get_result();
ASSERT_TRUE(snap_res->next_row());
EXPECT_EQ(snap_res->get<int>(0), 1);
try {
TempFile db("snapshot_savepoint");
sqlite::connection writer(db.string());
sqlite::enable_wal(writer);
dump_sqlite_diagnostics(writer, "SnapshotTest.SavepointSnapshotControlsScope");
sqlite::execute(writer, "CREATE TABLE docs(id INTEGER PRIMARY KEY, body TEXT);", true);

sqlite::command insert(writer, "INSERT INTO docs(body) VALUES (?);");
{
sqlite::transaction txn(writer, sqlite::transaction_type::immediate);
insert % std::string("alpha");
insert.step_once();
insert.clear();
txn.commit();
}

sqlite::connection reader_snapshot(db.string());
sqlite::connection reader_open(db.string());

sqlite::snapshot snap;
{
sqlite::savepoint sp(reader_snapshot, "sp");
sqlite::query prime(reader_snapshot, "SELECT COUNT(*) FROM docs;");
auto prime_res = prime.get_result();
ASSERT_TRUE(prime_res->next_row());
snap = sp.take_snapshot();
sp.release();
}

{
sqlite::transaction txn(writer, sqlite::transaction_type::immediate);
insert % std::string("beta");
insert.step_once();
insert.clear();
txn.commit();
}

dump_table_info(writer, "docs");

sqlite::savepoint sp(reader_open, "sp_read");
sp.open_snapshot(snap);
sqlite::query check(reader_open, "SELECT COUNT(*) FROM docs;");
auto snap_res = check.get_result();
ASSERT_TRUE(snap_res->next_row());
EXPECT_EQ(snap_res->get<int>(0), 1);
} catch (sqlite::database_exception const &ex) {
GTEST_SKIP() << ex.what();
}
}
Loading