A lightweight SQLite3 wrapper library for C++20.
- Modern C++20 API with RAII resource management
- Type-safe query results with
result<T>error handling - Prepared statements with parameter binding (prevents SQL injection)
- Support for all SQLite types: INTEGER, REAL, TEXT, BLOB, NULL
- Named and positional parameter binding
- RAII transaction support with automatic rollback
- Full-text search support (FTS5)
cmake -B build
cmake --build build
ctest --test-dir build --output-on-failureadd_subdirectory(libs/sap_db)
target_link_libraries(your_target PRIVATE sap::db)- sap_core: Core types and utilities (fetched automatically if not found)
- SQLite3: Database engine (uses system library or fetches amalgamation)
#include <sap/db/database.hpp>
// Open or create a file-based database
auto result = sap::db::Database::open("path/to/database.db");
if (!result) {
std::cerr << "Error: " << result.error() << "\n";
return 1;
}
auto& db = result.value();
// Or use in-memory database (for testing)
auto mem_db = sap::db::Database::open_memory();db.execute(R"(
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE,
created_at INTEGER DEFAULT (strftime('%s', 'now'))
)
)");// Using prepared statement with positional parameters
auto stmt = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
stmt->bind(1, "Alice");
stmt->bind(2, "alice@example.com");
auto result = stmt->execute();
// Using named parameters
auto stmt2 = db.prepare("INSERT INTO users (name, email) VALUES (:name, :email)");
stmt2->bind("name", "Bob");
stmt2->bind("email", "bob@example.com");
stmt2->execute();// Fetch all rows
auto rows = db.query("SELECT * FROM users");
for (const auto& row : rows.value()) {
std::cout << row.get<i64>("id") << ": "
<< row.get<std::string>("name") << "\n";
}
// Fetch one row
auto row = db.query_one("SELECT * FROM users WHERE id = 1");
if (row.value()) {
std::cout << row.value()->get<std::string>("name") << "\n";
}
// With parameters
auto stmt = db.prepare("SELECT * FROM users WHERE name LIKE ?");
stmt->bind(1, "%ali%");
auto results = stmt->fetch_all();// RAII transaction (auto-rollback if not committed)
{
auto txn = db.transaction();
db.execute("INSERT INTO users (name) VALUES ('User1')");
db.execute("INSERT INTO users (name) VALUES ('User2')");
txn.commit(); // Or let destructor rollback
}
// Manual transaction
db.begin_transaction();
db.execute("...");
db.commit(); // or db.rollback();auto row = db.query_one("SELECT * FROM users WHERE id = 1");
if (row.value()) {
// Direct access (asserts on wrong type)
auto name = row.value()->get<std::string>("name");
// Safe access (returns nullopt on wrong type or missing)
auto age = row.value()->try_get<i64>("age");
if (age) {
std::cout << "Age: " << *age << "\n";
}
// Check for NULL
if (row.value()->is_null("optional_field")) {
std::cout << "Field is NULL\n";
}
}Always use parameter binding (? or :name) for user-provided data:
// SAFE: User input is escaped
stmt->bind(1, user_provided_string);
// UNSAFE: SQL injection vulnerability!
db.execute("SELECT * FROM users WHERE name = '" + user_input + "'");MIT