Skip to content

Sapfire-Interactive/sap_db

Repository files navigation

sap_db

A lightweight SQLite3 wrapper library for C++20.

Features

  • 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)

Building

Standalone

cmake -B build
cmake --build build
ctest --test-dir build --output-on-failure

As a Submodule

add_subdirectory(libs/sap_db)
target_link_libraries(your_target PRIVATE sap::db)

Dependencies

  • sap_core: Core types and utilities (fetched automatically if not found)
  • SQLite3: Database engine (uses system library or fetches amalgamation)

Usage

Opening a Database

#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();

Creating Tables

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'))
    )
)");

Inserting Data

// 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();

Querying Data

// 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();

Transactions

// 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();

Safe Value Access

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";
    }
}

Security Note

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 + "'");

License

MIT

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors