System SQLite vs Amalgamation
SQLite can be consumed in two ways: linking against a system-installed shared library, or compiling the single-file "amalgamation" directly into your project. Each approach has distinct trade-offs.
The system library approach uses find_package or pkg-config to locate an installed SQLite:
# CMakeLists.txt — System SQLite via find_package
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
# CMake 3.14+ provides FindSQLite3 module
find_package(SQLite3 3.36 REQUIRED)
add_executable(myapp src/main.cpp src/database.cpp)
target_link_libraries(myapp PRIVATE SQLite::SQLite3)
message(STATUS "SQLite version: ${SQLite3_VERSION}")
message(STATUS "SQLite include: ${SQLite3_INCLUDE_DIRS}")
The amalgamation approach bundles SQLite source directly — giving you full control over compile options and version:
# CMakeLists.txt — SQLite amalgamation (vendored)
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES C CXX)
# Build SQLite from amalgamation files
add_library(sqlite3 STATIC
third_party/sqlite/sqlite3.c
)
target_include_directories(sqlite3 PUBLIC
third_party/sqlite
)
# Essential compile-time options
target_compile_definitions(sqlite3 PRIVATE
SQLITE_THREADSAFE=1
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_RTREE
SQLITE_DQS=0
)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE sqlite3)
# SQLite needs pthread on Unix
if(UNIX)
find_package(Threads REQUIRED)
target_link_libraries(sqlite3 PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
endif()
FetchContent Approach
Rather than vendoring the amalgamation manually, use FetchContent to download it automatically during configuration:
# CMakeLists.txt — SQLite via FetchContent
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES C CXX)
include(FetchContent)
# Download SQLite amalgamation
# Version 3.46.0 = year 2024, release 3460000
FetchContent_Declare(sqlite3
URL https://www.sqlite.org/2024/sqlite-amalgamation-3460000.zip
URL_HASH SHA256=... # Always pin the hash for reproducibility
)
FetchContent_MakeAvailable(sqlite3)
# Create library target from fetched source
add_library(sqlite3_lib STATIC
${sqlite3_SOURCE_DIR}/sqlite3.c
)
target_include_directories(sqlite3_lib PUBLIC
${sqlite3_SOURCE_DIR}
)
target_compile_definitions(sqlite3_lib PRIVATE
SQLITE_THREADSAFE=1
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_COLUMN_METADATA
SQLITE_MAX_EXPR_DEPTH=10000
)
if(UNIX)
find_package(Threads REQUIRED)
target_link_libraries(sqlite3_lib PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
endif()
# Application
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE sqlite3_lib)
URL_HASH SHA256=... to prevent silent corruption or supply-chain attacks. The hash is listed on SQLite's download page.
Compile-Time Options
SQLite's behavior is extensively configurable via compile-time defines. These are set as target_compile_definitions on the sqlite3 library target:
# Comprehensive compile-time configuration
target_compile_definitions(sqlite3_lib PRIVATE
# Threading
SQLITE_THREADSAFE=1 # 0=single-thread, 1=serialized, 2=multi-thread
# Extensions
SQLITE_ENABLE_FTS5 # Full-text search v5
SQLITE_ENABLE_JSON1 # JSON functions
SQLITE_ENABLE_RTREE # R-Tree spatial index
SQLITE_ENABLE_GEOPOLY # Geopolygon extension
SQLITE_ENABLE_COLUMN_METADATA # Column origin metadata
SQLITE_ENABLE_STAT4 # Advanced query planner stats
SQLITE_ENABLE_UPDATE_DELETE_LIMIT # LIMIT on UPDATE/DELETE
# Performance tuning
SQLITE_DEFAULT_MEMSTATUS=0 # Disable memory tracking
SQLITE_DEFAULT_WAL_SYNCHRONOUS=1 # WAL mode: NORMAL sync
SQLITE_LIKE_DOESNT_MATCH_BLOBS # LIKE skips BLOBs
SQLITE_MAX_EXPR_DEPTH=10000 # Complex query support
SQLITE_OMIT_DECLTYPE # Omit if not using column types
SQLITE_USE_ALLOCA # Use alloca() for temp allocations
# Security
SQLITE_DQS=0 # Disable double-quoted strings
SQLITE_MAX_LENGTH=1000000000 # 1GB max string/blob
SQLITE_MAX_SQL_LENGTH=1000000 # 1MB max SQL statement
)
// src/database.cpp — Verify compile options at runtime
#include <sqlite3.h>
#include <iostream>
void printSqliteConfig() {
std::cout << "SQLite version: " << sqlite3_libversion() << "\n";
std::cout << "Thread-safe: " << sqlite3_threadsafe() << "\n";
// Check if FTS5 is available
sqlite3* db = nullptr;
sqlite3_open(":memory:", &db);
sqlite3_stmt* stmt = nullptr;
int rc = sqlite3_prepare_v2(db,
"SELECT fts5(?1)", -1, &stmt, nullptr);
std::cout << "FTS5 available: " << (rc == SQLITE_OK ? "yes" : "no") << "\n";
sqlite3_finalize(stmt);
sqlite3_close(db);
}
SQLite Encryption Extension
SQLite's official encryption (SEE) is a commercial product. Open-source alternatives like SQLCipher provide AES-256 encryption. Integration requires replacing the amalgamation with the encrypted variant:
# CMakeLists.txt — SQLCipher (open-source encryption)
cmake_minimum_required(VERSION 3.20)
project(SecureApp LANGUAGES C CXX)
include(FetchContent)
# SQLCipher depends on OpenSSL for crypto
find_package(OpenSSL REQUIRED)
FetchContent_Declare(sqlcipher
GIT_REPOSITORY https://github.com/niccokunzmann/sqlcipher-amalgamation.git
GIT_TAG master
)
FetchContent_MakeAvailable(sqlcipher)
add_library(sqlcipher_lib STATIC
${sqlcipher_SOURCE_DIR}/sqlite3.c
)
target_include_directories(sqlcipher_lib PUBLIC
${sqlcipher_SOURCE_DIR}
)
target_compile_definitions(sqlcipher_lib PRIVATE
SQLITE_HAS_CODEC
SQLITE_THREADSAFE=1
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
)
target_link_libraries(sqlcipher_lib PRIVATE OpenSSL::Crypto)
if(UNIX)
find_package(Threads REQUIRED)
target_link_libraries(sqlcipher_lib PRIVATE Threads::Threads ${CMAKE_DL_LIBS})
endif()
add_executable(secure_app src/main.cpp)
target_link_libraries(secure_app PRIVATE sqlcipher_lib)
Building sqlite3 CLI Tool
The sqlite3 command-line tool is useful for development and debugging. Build it alongside your project from the amalgamation:
# CMakeLists.txt — Optional sqlite3 CLI tool
option(BUILD_SQLITE3_CLI "Build the sqlite3 command-line tool" OFF)
if(BUILD_SQLITE3_CLI)
add_executable(sqlite3_cli
${sqlite3_SOURCE_DIR}/shell.c
${sqlite3_SOURCE_DIR}/sqlite3.c
)
target_compile_definitions(sqlite3_cli PRIVATE
SQLITE_THREADSAFE=0
SQLITE_ENABLE_FTS5
SQLITE_ENABLE_JSON1
SQLITE_ENABLE_EXPLAIN_COMMENTS
SQLITE_ENABLE_UNKNOWN_SQL_FUNCTION
)
if(UNIX)
find_package(Threads REQUIRED)
target_link_libraries(sqlite3_cli PRIVATE
Threads::Threads
${CMAKE_DL_LIBS}
)
# readline for interactive editing
find_library(READLINE_LIB readline)
if(READLINE_LIB)
target_link_libraries(sqlite3_cli PRIVATE ${READLINE_LIB})
target_compile_definitions(sqlite3_cli PRIVATE HAVE_READLINE)
endif()
endif()
set_target_properties(sqlite3_cli PROPERTIES OUTPUT_NAME sqlite3)
install(TARGETS sqlite3_cli RUNTIME DESTINATION bin)
endif()
C++ Wrappers (SQLiteCpp)
SQLiteCpp provides a modern C++ RAII wrapper around the SQLite C API, with proper exception handling and type safety:
# CMakeLists.txt — SQLiteCpp wrapper via FetchContent
cmake_minimum_required(VERSION 3.20)
project(CppDbApp LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
# SQLiteCpp bundles its own SQLite or can use external
set(SQLITECPP_INTERNAL_SQLITE ON CACHE BOOL "Use bundled SQLite")
FetchContent_Declare(sqlitecpp
GIT_REPOSITORY https://github.com/SRombauts/SQLiteCpp.git
GIT_TAG 3.3.1
)
FetchContent_MakeAvailable(sqlitecpp)
add_executable(cpp_app src/main.cpp)
target_link_libraries(cpp_app PRIVATE SQLiteCpp)
// src/main.cpp — SQLiteCpp usage example
#include <SQLiteCpp/SQLiteCpp.h>
#include <iostream>
int main() {
try {
// Open or create database
SQLite::Database db("example.db",
SQLite::OPEN_READWRITE | SQLite::OPEN_CREATE);
// Create table
db.exec(
"CREATE TABLE IF NOT EXISTS users ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
" name TEXT NOT NULL,"
" email TEXT UNIQUE"
")"
);
// Insert with prepared statement
SQLite::Statement insert(db,
"INSERT INTO users (name, email) VALUES (?, ?)");
insert.bind(1, "Alice");
insert.bind(2, "alice@example.com");
insert.exec();
// Query
SQLite::Statement query(db, "SELECT id, name, email FROM users");
while (query.executeStep()) {
std::cout << query.getColumn(0).getInt() << " | "
<< query.getColumn(1).getText() << " | "
<< query.getColumn(2).getText() << "\n";
}
} catch (const SQLite::Exception& e) {
std::cerr << "SQLite error: " << e.what() << "\n";
return 1;
}
return 0;
}
Testing with In-Memory DB
SQLite's :memory: database is perfect for unit tests — it's fast, isolated, and requires no filesystem cleanup:
# CMakeLists.txt — Database unit tests
enable_testing()
add_executable(db_tests
tests/test_schema.cpp
tests/test_queries.cpp
tests/test_migrations.cpp
)
target_link_libraries(db_tests PRIVATE
sqlite3_lib
GTest::gtest_main
)
# Tests use :memory: database — no fixtures needed
add_test(NAME DatabaseTests COMMAND db_tests)
set_tests_properties(DatabaseTests PROPERTIES
LABELS "unit"
TIMEOUT 30
)
// tests/test_schema.cpp — In-memory database testing
#include <gtest/gtest.h>
#include <sqlite3.h>
#include <string>
class DatabaseTest : public ::testing::Test {
protected:
sqlite3* db = nullptr;
void SetUp() override {
// Fresh in-memory database for each test
int rc = sqlite3_open(":memory:", &db);
ASSERT_EQ(rc, SQLITE_OK);
// Apply schema
const char* schema = R"(
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_users_name ON users(name);
)";
char* err = nullptr;
rc = sqlite3_exec(db, schema, nullptr, nullptr, &err);
ASSERT_EQ(rc, SQLITE_OK) << "Schema error: " << (err ? err : "");
sqlite3_free(err);
}
void TearDown() override {
sqlite3_close(db);
}
};
TEST_F(DatabaseTest, InsertAndRetrieve) {
const char* sql = "INSERT INTO users (name) VALUES ('Alice')";
ASSERT_EQ(sqlite3_exec(db, sql, nullptr, nullptr, nullptr), SQLITE_OK);
sqlite3_stmt* stmt = nullptr;
sqlite3_prepare_v2(db, "SELECT name FROM users WHERE id = 1", -1, &stmt, nullptr);
ASSERT_EQ(sqlite3_step(stmt), SQLITE_ROW);
std::string name(reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0)));
EXPECT_EQ(name, "Alice");
sqlite3_finalize(stmt);
}
TEST_F(DatabaseTest, UniqueConstraint) {
sqlite3_exec(db, "CREATE UNIQUE INDEX idx_name ON users(name)", nullptr, nullptr, nullptr);
sqlite3_exec(db, "INSERT INTO users (name) VALUES ('Bob')", nullptr, nullptr, nullptr);
int rc = sqlite3_exec(db, "INSERT INTO users (name) VALUES ('Bob')", nullptr, nullptr, nullptr);
EXPECT_EQ(rc, SQLITE_CONSTRAINT);
}
std::tmpnam() or CMake's TEST_WORKING_DIRECTORY property.