Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,2 @@
build
.vscode

View File

@@ -0,0 +1,17 @@
load("@rules_cc//cc:defs.bzl", "cc_library")
cc_library(
name = "everest-sqlite",
srcs = glob(["lib/everest/database/sqlite/*.cpp"]),
hdrs = glob(["include/**/*.hpp"]),
deps = [
"@sqlite3",
"@com_github_nlohmann_json//:json",
"//lib/everest/log:liblog",
],
visibility = [
"//visibility:public",
],
strip_include_prefix = "include",
copts = ["-std=c++17"],
)

View File

@@ -0,0 +1,85 @@
cmake_minimum_required(VERSION 3.14)
project(everest-sqlite VERSION 0.1.5
DESCRIPTION "SQLite wrapper for EVerest"
LANGUAGES CXX C
)
find_package(everest-cmake 0.5 REQUIRED)
option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF)
option(BUILD_TESTING "Build unit tests, used if standalone project" OFF)
option(EVEREST_SQLITE_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT})
if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING)
set(EVEREST_SQLITE_BUILD_TESTING ON)
# this policy allows us to link gcov to targets defined in other directories
if(POLICY CMP0079)
set(CMAKE_POLICY_DEFAULT_CMP0079 NEW)
endif()
endif()
find_package(SQLite3 REQUIRED)
if (NOT DISABLE_EDM)
evc_setup_edm()
# In EDM mode, we can't install exports (because the dependencies usually do not install their exports)
set(EVEREST_SQLITE_INSTALL OFF)
else()
if (EVEREST_SQLITE_BUILD_TESTING)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/release-1.12.1.zip
)
FetchContent_MakeAvailable(googletest)
endif()
endif()
# options
option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF)
option(BUILD_TESTING "Build unit tests, used if standalone project" OFF)
option(EVEREST_SQLITE_USE_BOOST_FILESYSTEM "Usage of boost/filesystem.hpp instead of std::filesystem" OFF)
add_subdirectory(lib)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# packaging
if (EVEREST_SQLITE_INSTALL)
set_target_properties(everest_sqlite PROPERTIES EXPORT_NAME sqlite)
install(
TARGETS everest_sqlite
EXPORT everest_sqlite-targets
LIBRARY
)
install(
DIRECTORY include/
TYPE INCLUDE
)
install(
FILES cmake/CollectMigrationFiles.cmake
DESTINATION
${CMAKE_INSTALL_LIBDIR}/cmake/${PROJECT_NAME}/cmake
)
evc_setup_package(
NAME everest-sqlite
NAMESPACE everest
EXPORT everest_sqlite-targets
ADDITIONAL_CONTENT
"find_dependency(SQLite3)"
"include($\{CMAKE_CURRENT_LIST_DIR\}/cmake/CollectMigrationFiles.cmake)"
)
endif()
if(EVEREST_SQLITE_BUILD_TESTING)
include(CTest)
add_subdirectory(tests)
set(CMAKE_BUILD_TYPE Debug)
endif()

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,126 @@
# everest-sqlite
**everest-sqlite** is a modern C++17 wrapper around SQLite, designed to provide a safe, easy-to-use, and robust interface for embedded SQL database operations. It supports transactional database access, schema migrations, and convenient statement handling with RAII patterns.
everest-sqlite is used within several modules and libraries of the EVerest project.
## Get Involved
See the [Contribution Guideline](https://everest.github.io/nightly/project/contributing.html) of the EVerest project to get involved.
## Features
- **RAII-based transaction management** with automatic rollback on error
- **Safe, typed access to SQLite data**
- **Schema migration support** via versioned `.sql` scripts
- **Detailed exception types** for robust error handling
- **CMake-friendly** and easily embeddable
## Components
```
include/database/
├── exceptions.hpp # Custom exceptions for database errors
├── sqlite/
├──── connection.hpp # Database connection and transaction logic
├──── schema_updater.hpp # Schema migration tooling
└──── statement.hpp # RAII wrapper for sqlite3_stmt
```
## Getting Started
The recommended way to build this project is to use EVerests dependency manager EDM.
### Dependencies
- **SQLite3**
- (Optional) **[everest-cmake](https://github.com/EVerest/everest-cmake)**
- (Optional) **GTest** for unit testing
### Build
Make sure to check out this repository along with [everest-cmake](https://github.com/EVerest/everest-cmake).
```bash
git clone <this-repo>
cd everest-sqlite
mkdir build && cd build
cmake ..
make
```
To build with unit tests:
```bash
cmake -DBUILD_TESTING=ON ..
make
ctest
make everest-sqlite_gcovr_coverage # to generate a coverage report
```
To build without EDM, you can use:
```bash
cmake -DDISABLE_EDM=ON ..
make
```
## Usage
### 1. Connecting to a database
```cpp
Connection db("my_database.db");
if (!db.open_connection()) {
throw std::runtime_error("Could not open database");
}
```
### 2. Transactions
```cpp
auto tx = db.begin_transaction();
db.execute_statement("INSERT INTO table_name (col) VALUES ('data')");
tx->commit(); // or tx->rollback();
```
### 3. Prepared Statements
```cpp
auto stmt = db.new_statement("SELECT name FROM users WHERE id = :id");
stmt->bind_int(1, 1);
if (stmt->step() == SQLITE_ROW) {
std::string name = stmt->column_text(0);
}
```
### 4. Schema Migration
Place your migration SQL files in a folder:
```
migrations/
├── 1_up.sql
├── 2_up.sql
├── 2_down.sql
└── ...
```
Apply migrations:
```cpp
SchemaUpdater updater(&db);
if (!updater.apply_migration_files("migrations", 2)) {
throw std::runtime_error("Migration failed");
}
```
Check out the [migration documentation](docs/migrations.md) for more detailed information about the migration support.
## Exception Types
All exceptions inherit from `Exception`:
- `ConnectionException`
- `QueryExecutionException`
- `RequiredEntryNotFoundException`
- `MigrationException`

View File

@@ -0,0 +1 @@
_Use this file to list out any third-party dependencies used by this project. You may choose to point to a Gemfile or other language specific packaging file for details._

View File

@@ -0,0 +1,63 @@
function(collect_migration_files)
set(options "")
set(oneValueArgs LOCATION INSTALL_DESTINATION)
set(multiValueArgs "")
cmake_parse_arguments(ARG "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
if(NOT ARG_LOCATION)
message(FATAL_ERROR "No LOCATION provided, can't parse files")
endif()
if(NOT ${ARG_LOCATION} MATCHES "/$")
set(ARG_LOCATION "${ARG_LOCATION}/")
endif()
message("Parsing migration files in folder: ${ARG_LOCATION}")
file(GLOB MIGRATION_FILE_LIST RELATIVE ${ARG_LOCATION} "${ARG_LOCATION}*.sql") # ARG_LOCATION already contains the slash
list(SORT MIGRATION_FILE_LIST)
# The first file should always start with 1_up so make use of that fact.
# Next we always check the next number "down" and then "up" which come in order since we sorted alphabetically
set(CURRENT_MIGRATION_FILE_ID 1)
set(NEXT_MIGRATION_FILE_TYPE "up")
foreach(MIGRATION_FILE ${MIGRATION_FILE_LIST})
string(REGEX MATCH "^([0-9]+)_(up|down)(|-.+)\.sql$" MIGRATION_FILE_MATCHED ${MIGRATION_FILE})
if (MIGRATION_FILE_MATCHED STREQUAL "")
message(FATAL_ERROR "Migration filename does not match specification: " ${MIGRATION_FILE})
endif()
string(CONCAT NEXT_ID "^" ${CURRENT_MIGRATION_FILE_ID} "_")
if(NOT ${MIGRATION_FILE_MATCHED} MATCHES ${NEXT_ID})
message(FATAL_ERROR "Skipped migration file ID, expected " ${CURRENT_MIGRATION_FILE_ID} "_*.sql, but got " ${MIGRATION_FILE_MATCHED})
endif()
string(APPEND NEXT_ID ${NEXT_MIGRATION_FILE_TYPE})
if(NOT ${MIGRATION_FILE_MATCHED} MATCHES ${NEXT_ID})
message(FATAL_ERROR "Missing " ${NEXT_MIGRATION_FILE_TYPE} " migration file: " ${MIGRATION_FILE_MATCHED})
endif()
if(NEXT_MIGRATION_FILE_TYPE STREQUAL "up")
math(EXPR CURRENT_MIGRATION_FILE_ID "${CURRENT_MIGRATION_FILE_ID}+1")
set(NEXT_MIGRATION_FILE_TYPE "down")
elseif(NEXT_MIGRATION_FILE_TYPE STREQUAL "down")
set(NEXT_MIGRATION_FILE_TYPE "up")
endif()
endforeach()
if (NEXT_MIGRATION_FILE_TYPE STREQUAL "up")
message(FATAL_ERROR "Down migration file " ${CURRENT_MIGRATION_FILE_ID} "_*.sql is missing up migration file")
endif()
# Since we always add on the up file we need to subtract one here
math(EXPR CURRENT_MIGRATION_FILE_ID "${CURRENT_MIGRATION_FILE_ID}-1")
list(TRANSFORM MIGRATION_FILE_LIST PREPEND ${ARG_LOCATION})
if(ARG_INSTALL_DESTINATION)
install(FILES ${MIGRATION_FILE_LIST} DESTINATION ${ARG_INSTALL_DESTINATION})
endif()
set(TARGET_MIGRATION_FILE_VERSION ${CURRENT_MIGRATION_FILE_ID} PARENT_SCOPE)
set(MIGRATION_FILE_LIST ${MIGRATION_FILE_LIST} PARENT_SCOPE)
endfunction()

View File

@@ -0,0 +1,6 @@
---
gtest:
# GoogleTest now follows the Abseil Live at Head philosophy. We recommend updating to the latest commit in the main branch as often as possible.
git: https://github.com/google/googletest.git
git_tag: release-1.12.1
cmake_condition: "EVEREST_SQLITE_BUILD_TESTING"

View File

@@ -0,0 +1,117 @@
# Database migrations
## Introduction
Updating the schema of a database that is in production should always be done with great care. To facilitate this we make use of migration files. These are files that can be executed in sequence to get to the desired schema version that works with that version of the software.
### File format
The filenames must have the following format:
`x_up[-description].sql` and `x_down[-description].sql`
The description is optional and x needs to be the next number in the sequence.
The files always exist in pairs except for the initial "up" file used to start the database. The "up" file contains the changes needed to update the database schema so that it can be used with the new firmware. The "down" file must undo all the changes of the "up" file so a perfect downgrade is possible.
CMake will validate the completeness of the migration pairs and the filenames. If this is not correct CMake will fail to initialize.
### Schema changes
The schema changes should be done in such a way that the data present in the databases will persist unless it is really necessary to remove stuff.
### Unit testing
We recommend to write specific unit tests for your migration that validate that the changes you have made have the expected outcome. Examples for this are located in
[test_database_schema_updater.cpp](../tests/test_database_schema_updater.cpp)
## Design consideration
- We start out with an SQL init file that is used to start the database with. This file should not be changed anymore once this functionality is implemented.
- Every change we want to make to the database is done through migration files. These files are simply more SQL files that can be applied to the database in the right order.
- Every change should consist of an up and a down file.
- The up files are used to update the database to the latest schema.
- The down files are used to downgrade a database to a previous schema. It is very important that these files stay on the target so that all older versions can apply these.
- If for whatever reason we need to do an operation in a down file that was not supported before we are making a breaking change which should be carefully considered.
- The up/down migration file combination shall have a "version" number assigned in sequence. The version numbers of the files will be used together with the database's user_version field to determine which migrations files to run to get to the target version.
- The target version needs to be compiled into the firmware so that older versions can know which "down" migration files to apply to get back to their version of the database. This is done by having CMake generate a compile time definition based on the content of the folder with migrations.
- Each migration needs to be done in a single SQL transaction so we don't end up with changes being applied only part of the way.
- Before applying migrations a backup shall be made of the database so that in case we fail we can rollback to that version.
- Add a CICD check that validates if all the migrations can be executed.
## How to use
Using the database migrations is fairly straightforward. Most of the details are handled by the library itself. To set up these migrations, there are a few things to consider:
- **Responsibility of the consuming project**:
- The consuming library or application is responsible for **shipping the appropriate migration files** with the target system (e.g., embedded device or deployment target).
- These files **must be present** during upgrades *and* downgrades, since rolling back requires access to older down-migration scripts.
- It is **strongly recommended** to back up the database before initiating a migration. While migrations are wrapped in transactions and should roll back on failure, this is not guaranteed in all edge cases.
- **Old database files should be removed** when reinitializing a database to ensure a clean start with a well-defined schema.
- All migrations should be performed **before any logic that depends on the schema is executed**.
## Integration for libraries using everest-sqlite
If your library or module integrates `everest-sqlite`, here's how you can hook into the migration support to safely handle schema changes between releases:
### 1. Place your migration files
Organize your SQL migration scripts in a dedicated folder in your repository, for example:
```
my_library/
├── migrations/
│ ├── 1_up.sql
│ ├── 2_up.sql
│ ├── 2_down.sql
│ └── ...
```
Follow the filename convention:
- `X_up.sql` applies a schema change to version X.
- `X_down.sql` undoes that same schema change.
### 2. Use `SchemaUpdater` during initialization
Before you use any tables or schema-specific logic in your module, run the schema updater:
```cpp
#include <database/sqlite/schema_updater.hpp>
Connection db("path/to/database.db");
db.open_connection();
SchemaUpdater updater(&db);
const uint32_t TARGET_SCHEMA_VERSION = /* set via CMake or hardcoded */;
if (!updater.apply_migration_files("path/to/migrations", TARGET_SCHEMA_VERSION)) {
throw std::runtime_error("Migration failed");
}
```
You can derive `TARGET_SCHEMA_VERSION` from a CMake definition if you use `CollectMigrationFiles.cmake`.
### 3. Add `CollectMigrationFiles.cmake` to your build
In your CMake project:
```cmake
include(${CMAKE_CURRENT_LIST_DIR}/cmake/CollectMigrationFiles.cmake)
collect_migration_files(
LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/migrations"
INSTALL_DESTINATION "share/my_library/migrations"
)
# Now TARGET_MIGRATION_FILE_VERSION is available to use:
target_compile_definitions(my_library
PRIVATE TARGET_SCHEMA_VERSION=${TARGET_MIGRATION_FILE_VERSION}
)
```
This ensures that the latest schema version is compiled into your library, which is crucial for downgrade support.
We recommend:
- Testing that your target schema version can be reached from any older version.
- Testing rollback paths.
- Using real data snapshots where applicable.

View File

@@ -0,0 +1,54 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <exception>
#include <string>
namespace everest::db {
/// \brief Base class for database-related exceptions
class Exception : public std::exception {
public:
explicit Exception(const std::string& message) : msg(message) {
}
~Exception() noexcept override = default;
[[nodiscard]] const char* what() const noexcept override {
return msg.c_str();
}
protected:
std::string msg;
};
/// \brief Exception for database connection errors
class ConnectionException : public Exception {
public:
explicit ConnectionException(const std::string& message) : Exception(message) {
}
};
/// \brief Exception that is used if expected table entries are not found
class RequiredEntryNotFoundException : public Exception {
public:
explicit RequiredEntryNotFoundException(const std::string& message) : Exception(message) {
}
};
/// \brief Exception for errors during database migration
class MigrationException : public Exception {
public:
explicit MigrationException(const std::string& message) : Exception(message) {
}
};
/// \brief Exception for errors during query execution
class QueryExecutionException : public Exception {
public:
explicit QueryExecutionException(const std::string& message) : Exception(message) {
}
};
} // namespace everest::db

View File

@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <atomic>
#ifndef EVEREST_SQLITE_USE_BOOST_FILESYSTEM
#include <filesystem>
#else
#include <boost/filesystem.hpp>
#endif
#include <memory>
#include <mutex>
#include <sqlite3.h>
#include <everest/database/sqlite/statement.hpp>
#ifndef EVEREST_SQLITE_USE_BOOST_FILESYSTEM
namespace fs = std::filesystem;
#else
namespace fs = boost::filesystem;
#endif
namespace everest::db::sqlite {
/// \brief Helper class for transactions. Will lock the database interface from new transaction until commit() or
/// rollback() is called or the object destroyed
class TransactionInterface {
public:
/// \brief Destructor of transaction: Will by default rollback unless commit() is called
virtual ~TransactionInterface() = default;
/// \brief Commits the transaction and release the lock on the database interface
virtual void commit() = 0;
/// \brief Aborts the transaction and release the lock on the database interface
virtual void rollback() = 0;
};
class ConnectionInterface {
public:
virtual ~ConnectionInterface() = default;
/// \brief Opens the database connection. Returns true if succeeded.
virtual bool open_connection() = 0;
/// \brief Closes the database connection. Returns true if succeeded.
virtual bool close_connection() = 0;
/// \brief Start a transaction on the database. Returns an object holding the transaction.
/// \note This function can block until the previous transaction is finished.
[[nodiscard]] virtual std::unique_ptr<TransactionInterface> begin_transaction() = 0;
/// \brief Immediately executes \p statement. Returns true if succeeded.
virtual bool execute_statement(const std::string& statement) = 0;
/// \brief Returns a new StatementInterface to be used to perform more advanced sql statements.
/// \note Will throw an std::runtime_error if the statement can't be prepared
virtual std::unique_ptr<StatementInterface> new_statement(const std::string& sql) = 0;
/// \brief Returns the latest error message from sqlite3.
virtual const char* get_error_message() = 0;
/// \brief Clears the table with name \p table. Returns true if succeeded.
virtual bool clear_table(const std::string& table) = 0;
/// \brief Gets the last inserted rowid.
virtual int64_t get_last_inserted_rowid() = 0;
/// \brief Helper function to set the user version of the database to \p version
virtual void set_user_version(uint32_t version) = 0;
/// \brief Helper function to get the user version of the database.
virtual uint32_t get_user_version() = 0;
};
class Connection : public ConnectionInterface {
private:
sqlite3* db;
const fs::path database_file_path;
std::atomic_uint32_t open_count;
std::timed_mutex transaction_mutex;
bool close_connection_internal(bool force_close);
public:
explicit Connection(const fs::path& database_file_path) noexcept;
~Connection() override;
bool open_connection() override;
bool close_connection() override;
[[nodiscard]] std::unique_ptr<TransactionInterface> begin_transaction() override;
bool execute_statement(const std::string& statement) override;
std::unique_ptr<StatementInterface> new_statement(const std::string& sql) override;
const char* get_error_message() override;
bool clear_table(const std::string& table) override;
int64_t get_last_inserted_rowid() override;
uint32_t get_user_version() override;
void set_user_version(uint32_t version) override;
};
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,12 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#pragma once
#include <limits>
namespace everest::db::sqlite {
template <typename T, typename U> T constexpr clamp_to(U len) {
return (len <= std::numeric_limits<T>::max()) ? static_cast<T>(len) : std::numeric_limits<T>::max();
}
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,27 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <everest/database/sqlite/connection.hpp>
namespace everest::db::sqlite {
class SchemaUpdater {
private:
ConnectionInterface* database;
public:
/// \brief Class that can apply migration files to a database to update the schema
/// \param database Interface for the database connection
explicit SchemaUpdater(ConnectionInterface* database) noexcept;
/// \brief Apply migration files to a database to update the schema
/// \param sql_migration_files_path Filesystem path to migration file folder
/// \param target_schema_version The target schema version of the database
/// \return True if migrations applied successfully, false otherwise. Database is not modified when the migration
/// fails.
bool apply_migration_files(const fs::path& migration_file_directory, uint32_t target_schema_version);
};
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,92 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <cstdint>
#include <optional>
#include <string>
#include <variant>
#include <sqlite3.h>
namespace everest::db::sqlite {
/// @brief Type used to indicate if SQLite should make a internal copy of a string
enum class SQLiteString {
Static, /// Indicates string will be valid for the whole statement
Transient /// Indicates string might change during statement, SQLite should make a copy
};
/// @bried Variant type for possible return value based on name getter
using SqliteVariant = std::variant<std::monostate, int, double, int64_t, std::string>;
/// \brief Interface for Statement wrapper class that handles finalization, step, binding and column access of
/// sqlite3_stmt
class StatementInterface {
public:
virtual ~StatementInterface() = default;
virtual int step() = 0;
virtual int reset() = 0;
virtual int changes() = 0;
virtual int bind_text(const int idx, const std::string& val, SQLiteString lifetime = SQLiteString::Static) = 0;
virtual int bind_text(const std::string& param, const std::string& val,
SQLiteString lifetime = SQLiteString::Static) = 0;
virtual int bind_int(const int idx, const int val) = 0;
virtual int bind_int(const std::string& param, const int val) = 0;
virtual int bind_int64(const int idx, const int64_t val) = 0;
virtual int bind_int64(const std::string& param, const int64_t val) = 0;
virtual int bind_double(const int idx, const double val) = 0;
virtual int bind_double(const std::string& param, const double val) = 0;
virtual int bind_null(const int idx) = 0;
virtual int bind_null(const std::string& param) = 0;
virtual int get_number_of_rows() = 0;
virtual int column_type(const int idx) = 0;
virtual SqliteVariant column_variant(const std::string& name) = 0;
virtual std::string column_text(const int idx) = 0;
virtual std::optional<std::string> column_text_nullable(const int idx) = 0;
virtual int column_int(const int idx) = 0;
virtual int64_t column_int64(const int idx) = 0;
virtual double column_double(const int idx) = 0;
};
/// \brief RAII wrapper class that handles finalization, step, binding and column access of sqlite3_stmt
class Statement : public StatementInterface {
private:
sqlite3_stmt* stmt;
sqlite3* db;
public:
Statement(sqlite3* db, const std::string& query);
~Statement() override;
int step() override;
int reset() override;
int changes() override;
int bind_text(const int idx, const std::string& val, SQLiteString lifetime = SQLiteString::Static) override;
int bind_text(const std::string& param, const std::string& val,
SQLiteString lifetime = SQLiteString::Static) override;
int bind_int(const int idx, const int val) override;
int bind_int(const std::string& param, const int val) override;
int bind_double(const int idx, const double val) override;
int bind_double(const std::string& param, const double val) override;
int bind_int64(const int idx, const int64_t val) override;
int bind_int64(const std::string& param, const int64_t val) override;
int bind_null(const int idx) override;
int bind_null(const std::string& param) override;
int get_number_of_rows() override;
int column_type(const int idx) override;
SqliteVariant column_variant(const std::string& name) override;
std::string column_text(const int idx) override;
std::optional<std::string> column_text_nullable(const int idx) override;
int column_int(const int idx) override;
int64_t column_int64(const int idx) override;
double column_double(const int idx) override;
};
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,78 @@
add_library(everest_sqlite)
add_library(everest::sqlite ALIAS everest_sqlite)
target_sources(everest_sqlite
PRIVATE
everest/database/sqlite/statement.cpp
everest/database/sqlite/connection.cpp
everest/database/sqlite/schema_updater.cpp
)
target_link_libraries(everest_sqlite
PRIVATE
SQLite::SQLite3
)
if (EVEREST_SQLITE_USE_BOOST_FILESYSTEM)
find_package(Boost REQUIRED COMPONENTS filesystem)
target_link_libraries(everest_sqlite
PRIVATE
Boost::filesystem
)
target_compile_definitions(everest_sqlite
PRIVATE
EVEREST_SQLITE_USE_BOOST_FILESYSTEM
)
endif()
target_include_directories(everest_sqlite
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
set_target_properties(everest_sqlite
PROPERTIES
POSITION_INDEPENDENT_CODE ON
)
#############
# Logging configuration
#############
if (EVEREST_CUSTOM_LOGGING_LIBRARY)
if(NOT TARGET ${EVEREST_CUSTOM_LOGGING_LIBRARY})
message(FATAL_ERROR "${EVEREST_CUSTOM_LOGGING_LIBRARY} is not a valid library")
else()
target_link_libraries(everest_sqlite
PUBLIC
${EVEREST_CUSTOM_LOGGING_LIBRARY}
)
message(STATUS "Using custom logging library: ${EVEREST_CUSTOM_LOGGING_LIBRARY}")
endif()
elseif (EVEREST_CUSTOM_LOGGING_INCLUDE_PATH)
if (NOT EXISTS "${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}/everest/logging.hpp")
message(FATAL_ERROR "everest/logging.hpp not found in directory ${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}")
else()
target_include_directories(everest_sqlite
PUBLIC
include
${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}
)
endif()
message(STATUS "Using the following logging header: ${EVEREST_CUSTOM_LOGGING_INCLUDE_PATH}/everest/logging.hpp")
endif()
if (NOT EVEREST_CUSTOM_LOGGING_INCLUDE_PATH)
target_link_libraries(everest_sqlite
PUBLIC
everest::log
)
message(STATUS "Using the default logging header")
endif()
#############
# End logging configuration
#############
target_compile_features(everest_sqlite PRIVATE cxx_std_17)

View File

@@ -0,0 +1,190 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <algorithm>
#include <cctype>
#include <chrono>
#include <everest/database/exceptions.hpp>
#include <everest/database/sqlite/connection.hpp>
#include <everest/logging.hpp>
using namespace std::chrono_literals;
using namespace std::string_literals;
namespace everest::db::sqlite {
class DatabaseTransaction : public TransactionInterface {
private:
Connection& database;
std::unique_lock<std::timed_mutex> mutex;
public:
DatabaseTransaction(Connection& database, std::unique_lock<std::timed_mutex> mutex) :
database{database}, mutex{std::move(mutex)} {
this->database.execute_statement("BEGIN TRANSACTION");
}
// Will by default rollback the transaction if destructed
~DatabaseTransaction() override {
if (this->mutex.owns_lock()) {
this->rollback();
}
}
void commit() override {
const auto retval = this->database.execute_statement("COMMIT TRANSACTION");
this->mutex.unlock();
if (not retval) {
throw QueryExecutionException(this->database.get_error_message());
}
}
void rollback() override {
const auto retval = this->database.execute_statement("ROLLBACK TRANSACTION");
this->mutex.unlock();
if (not retval) {
throw QueryExecutionException(this->database.get_error_message());
}
}
};
Connection::Connection(const fs::path& database_file_path) noexcept :
db(nullptr), database_file_path(database_file_path), open_count(0) {
}
Connection::~Connection() {
// There could still be a transaction active and we have no way to abort it,
// so wait a few seconds to give it time to finish
auto lock = std::unique_lock(this->transaction_mutex, 2s);
close_connection_internal(true);
}
bool Connection::open_connection() {
if (this->open_count.fetch_add(1) != 0) {
EVLOG_debug << "Connection already opened";
return true;
}
// Add special exception for databases in ram; we don't need to create a path
// for them and we must not attempt to enable WAL on them.
const bool in_memory = this->database_file_path.string().find(":memory:") != std::string::npos ||
this->database_file_path.string().find("mode=memory") != std::string::npos;
if (!in_memory && !fs::exists(this->database_file_path.parent_path())) {
fs::create_directories(this->database_file_path.parent_path());
}
if (sqlite3_open_v2(this->database_file_path.c_str(), &this->db,
SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_URI, nullptr) != SQLITE_OK) {
EVLOG_error << "Error opening database at " << this->database_file_path << ": " << sqlite3_errmsg(db);
return false;
}
// Retry briefly on SQLITE_BUSY instead of failing immediately, so a reader that races with a
// concurrent writer on the same database waits for the lock to clear rather than surfacing a
// transient SQLITE_BUSY to the caller.
constexpr int busy_timeout_ms = 5000;
sqlite3_busy_timeout(this->db, busy_timeout_ms);
// Enable WAL journal mode on file-based databases so concurrent readers and a single writer
// stop serializing. Best-effort: if WAL cannot be established (e.g. on a filesystem that does
// not support it) we log and continue with whatever journal mode SQLite reports rather than
// failing the open.
if (!in_memory) {
auto statement = this->new_statement("PRAGMA journal_mode=WAL");
if (statement->step() != SQLITE_ROW) {
EVLOG_warning << "Could not query journal mode while enabling WAL on " << this->database_file_path << ": "
<< this->get_error_message();
} else {
const std::string journal_mode = statement->column_text(0);
std::string lowered = journal_mode;
std::transform(lowered.begin(), lowered.end(), lowered.begin(),
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
if (lowered != "wal") {
EVLOG_warning << "Could not enable WAL journal mode on " << this->database_file_path
<< ", continuing with '" << journal_mode << "'";
}
}
}
EVLOG_debug << "Established connection to database: " << this->database_file_path;
return true;
}
bool Connection::close_connection() {
return this->close_connection_internal(false);
}
bool Connection::close_connection_internal(bool force_close) {
if (!force_close && this->open_count.fetch_sub(1) != 1) {
EVLOG_debug << "Connection should remain open for other users";
return true;
}
if (this->db == nullptr) {
EVLOG_info << "Database file " << this->database_file_path << " is already closed";
return true;
}
// forcefully finalize all statements before calling sqlite3_close
sqlite3_stmt* stmt = nullptr;
while ((stmt = sqlite3_next_stmt(db, stmt)) != nullptr) {
sqlite3_finalize(stmt);
}
if (sqlite3_close_v2(this->db) != SQLITE_OK) {
EVLOG_error << "Error closing database file " << this->database_file_path << ": " << this->get_error_message();
return false;
}
EVLOG_debug << "Successfully closed database: " << this->database_file_path;
this->db = nullptr;
return true;
}
bool Connection::execute_statement(const std::string& statement) {
char* err_msg = nullptr;
if (sqlite3_exec(this->db, statement.c_str(), nullptr, nullptr, &err_msg) != SQLITE_OK) {
EVLOG_error << "Could not execute statement \"" << statement << "\": " << err_msg;
sqlite3_free(err_msg);
return false;
}
return true;
}
const char* Connection::get_error_message() {
return sqlite3_errmsg(this->db);
}
std::unique_ptr<TransactionInterface> Connection::begin_transaction() {
return std::make_unique<DatabaseTransaction>(*this, std::unique_lock(this->transaction_mutex));
}
std::unique_ptr<StatementInterface> Connection::new_statement(const std::string& sql) {
return std::make_unique<Statement>(this->db, sql);
}
bool Connection::clear_table(const std::string& table) {
return this->execute_statement("DELETE FROM "s + table);
}
int64_t Connection::get_last_inserted_rowid() {
return sqlite3_last_insert_rowid(this->db);
}
uint32_t Connection::get_user_version() {
auto statement = this->new_statement("PRAGMA user_version");
if (statement->step() != SQLITE_ROW) {
throw std::runtime_error("Could not get user_version from database");
}
return statement->column_int(0);
}
void Connection::set_user_version(uint32_t version) {
using namespace std::string_literals;
if (!this->execute_statement("PRAGMA user_version = "s + std::to_string(version))) {
throw std::runtime_error("Could not set user_version in database");
}
}
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,210 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <everest/database/sqlite/schema_updater.hpp>
#include <everest/logging.hpp>
#include <fstream>
#include <regex>
namespace everest::db::sqlite {
enum class Direction {
Up,
Down
};
struct MigrationFile {
fs::path path;
uint32_t version;
Direction direction;
};
std::ostream& operator<<(std::ostream& os, const MigrationFile& info) {
os << "Migration file [" << (info.direction == Direction::Up ? "up" : "down") << "] version " << info.version
<< ", path: " << info.path.c_str();
return os;
}
namespace {
std::vector<MigrationFile> get_migration_file_list(const fs::path& migration_file_directory) {
const std::regex filename_pattern{R"(^(\d+)_(up|down)(-[ \S]+|)\.sql$)"};
std::vector<MigrationFile> result;
for (const auto& entry : fs::directory_iterator(migration_file_directory)) {
if (entry.is_regular_file() and fs::file_size(entry) > 0) {
const fs::path& path = entry.path();
std::cmatch match;
const std::string filename =
path.filename().string(); // Store in a variable otherwise after the match the string temporary is gone
if (std::regex_match(filename.c_str(), match, filename_pattern) and match.size() == 4) {
// [0] = whole match
// [1] = version id
// [2] = up or down
// [3] = description or empty
result.push_back(MigrationFile{path, static_cast<uint32_t>(std::stoul(match[1].str())),
match[2] == "up" ? Direction::Up : Direction::Down});
}
}
}
return result;
}
void filter_and_sort_migration_file_list(std::vector<MigrationFile>& list, Direction direction, uint32_t min_version,
uint32_t max_version) {
auto filter = [direction, min_version, max_version](const MigrationFile& item) {
return item.direction != direction or item.version < min_version or item.version > max_version;
};
list.erase(std::remove_if(list.begin(), list.end(), filter), list.end());
std::sort(list.begin(), list.end(), [direction](const auto& a, const auto& b) {
if (direction == Direction::Up) {
return a.version < b.version;
}
return b.version < a.version;
});
}
bool is_migration_file_list_valid(std::vector<MigrationFile>& list, uint32_t max_version) {
auto expected_files = (max_version * 2) - 1;
if (list.size() < expected_files) {
EVLOG_error << "Expected " << expected_files << " files but only found: " << list.size();
return false;
}
if (list.size() % 2 == 0) {
EVLOG_error << "Nr of migration files should always be uneven: 1 initial file + n pairs";
return false;
}
std::sort(list.begin(), list.end(), [](const auto& a, const auto& b) {
return std::tie(a.version, a.direction) < std::tie(b.version, b.direction);
});
if (list.at(0).version != 1 or list.at(0).direction != Direction::Up) {
EVLOG_error << "Invalid initial migration file";
return false;
}
for (size_t i = 1; i < list.size(); i += 2) {
const uint32_t expected_version = (i / 2) + 2;
const auto& up = list.at(i);
const auto& down = list.at(i + 1);
if (up.version != expected_version || up.direction != Direction::Up) {
EVLOG_error << "Expected migration file " << expected_version << "_up.sql but got: " << up.path.filename();
return false;
}
if (down.version != expected_version || down.direction != Direction::Down) {
EVLOG_error << "Expected migration file " << expected_version
<< "_down.sql but got: " << down.path.filename();
return false;
}
}
return true;
}
std::optional<std::vector<MigrationFile>> get_migration_file_sequence(const fs::path& migration_file_directory,
Direction direction, uint32_t current_version,
uint32_t target_version) {
auto list = get_migration_file_list(migration_file_directory);
EVLOG_debug << "Migration list:";
for (auto& item : list) {
EVLOG_debug << item;
}
if (!is_migration_file_list_valid(list, std::max(current_version, target_version))) {
return std::nullopt;
}
const auto lowest = std::min(current_version, target_version) + 1;
const auto highest = std::max(current_version, target_version);
filter_and_sort_migration_file_list(list, direction, lowest, highest);
EVLOG_info << "Migration files to apply:";
for (auto& item : list) {
EVLOG_info << item;
}
return list;
}
} // namespace
SchemaUpdater::SchemaUpdater(ConnectionInterface* database) noexcept : database(database) {
}
bool SchemaUpdater::apply_migration_files(const fs::path& migration_file_directory, uint32_t target_schema_version) {
if (!fs::is_directory(migration_file_directory)) {
EVLOG_error << "Migration files must be in a directory: " << migration_file_directory.c_str();
return false;
}
if (target_schema_version == 0) {
EVLOG_error << "Migration target_version 0 is invalid";
return false;
}
uint32_t current_version = 0;
try {
this->database->open_connection();
current_version = this->database->get_user_version();
EVLOG_info << "Target version: " << target_schema_version << ", current version: " << current_version;
} catch (std::runtime_error& e) {
EVLOG_error << "Failure during migration file apply: " << e.what();
return false;
}
if (current_version == target_schema_version) {
EVLOG_info << "No migrations to apply since versions match";
this->database->close_connection();
return true;
}
Direction direction = Direction::Up;
if (current_version > target_schema_version) {
direction = Direction::Down;
}
auto list =
get_migration_file_sequence(migration_file_directory, direction, current_version, target_schema_version);
if (!list.has_value()) {
EVLOG_error << "Missing migration files in sequence, no actions performed";
this->database->close_connection();
return false;
}
bool retval = true;
try {
auto transaction = this->database->begin_transaction();
for (const auto& item : list.value()) {
const std::ifstream stream{item.path.string()};
std::stringstream init_sql;
init_sql << stream.rdbuf();
if (!this->database->execute_statement(init_sql.str())) {
EVLOG_error << "Could not apply migration file " << item.path;
throw std::runtime_error("Database access error");
}
}
this->database->set_user_version(target_schema_version);
transaction->commit();
} catch (std::exception& e) {
EVLOG_error << "Failure during migration file apply: " << e.what();
retval = false;
}
this->database->close_connection();
return retval;
}
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,177 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <cstddef>
#include <everest/database/exceptions.hpp>
#include <everest/database/sqlite/helpers.hpp>
#include <everest/database/sqlite/statement.hpp>
#include <everest/logging.hpp>
#include <sqlite3.h>
namespace everest::db::sqlite {
Statement::Statement(sqlite3* db, const std::string& query) : db(db), stmt(nullptr) {
if (sqlite3_prepare_v2(db, query.c_str(), clamp_to<int>(query.size()), &this->stmt, nullptr) != SQLITE_OK) {
EVLOG_error << sqlite3_errmsg(db);
throw QueryExecutionException("Could not prepare statement for database.");
}
}
Statement::~Statement() {
if (this->stmt != nullptr) {
if (sqlite3_finalize(this->stmt) != SQLITE_OK) {
EVLOG_error << "Error finalizing statement: " << sqlite3_errmsg(this->db);
}
}
}
int Statement::step() {
return sqlite3_step(this->stmt);
}
int Statement::reset() {
return sqlite3_reset(this->stmt);
}
int Statement::changes() {
// Rows affected by the last INSERT, UPDATE, DELETE
return sqlite3_changes(this->db);
}
int Statement::bind_text(const int idx, const std::string& val, SQLiteString lifetime) {
return sqlite3_bind_text(this->stmt, idx, val.c_str(), clamp_to<int>(val.length()),
lifetime == SQLiteString::Static ? SQLITE_STATIC : SQLITE_TRANSIENT);
}
int Statement::bind_text(const std::string& param, const std::string& val, SQLiteString lifetime) {
const int index = sqlite3_bind_parameter_index(this->stmt, param.c_str());
if (index <= 0) {
throw std::out_of_range("Parameter not found in SQL query");
}
return bind_text(index, val, lifetime);
}
int Statement::bind_int(const int idx, const int val) {
return sqlite3_bind_int(this->stmt, idx, val);
}
int Statement::bind_int(const std::string& param, const int val) {
const int index = sqlite3_bind_parameter_index(this->stmt, param.c_str());
if (index <= 0) {
throw std::out_of_range("Parameter not found in SQL query");
}
return bind_int(index, val);
}
int Statement::bind_int64(const int idx, const int64_t val) {
return sqlite3_bind_int64(this->stmt, idx, val);
}
int Statement::bind_int64(const std::string& param, const int64_t val) {
const int index = sqlite3_bind_parameter_index(this->stmt, param.c_str());
if (index <= 0) {
throw std::out_of_range("Parameter not found in SQL query");
}
return bind_int64(index, val);
}
int Statement::bind_double(const int idx, const double val) {
return sqlite3_bind_double(this->stmt, idx, val);
}
int Statement::bind_double(const std::string& param, const double val) {
const int index = sqlite3_bind_parameter_index(this->stmt, param.c_str());
if (index <= 0) {
throw std::out_of_range("Parameter not found in SQL query");
}
return bind_double(index, val);
}
int Statement::bind_null(const int idx) {
return sqlite3_bind_null(this->stmt, idx);
}
int Statement::bind_null(const std::string& param) {
const int index = sqlite3_bind_parameter_index(this->stmt, param.c_str());
if (index <= 0) {
throw std::out_of_range("Parameter not found in SQL query");
}
return bind_null(index);
}
int Statement::get_number_of_rows() {
return sqlite3_data_count(this->stmt);
}
int Statement::column_type(const int idx) {
return sqlite3_column_type(this->stmt, idx);
}
SqliteVariant Statement::column_variant(const std::string& name) {
SqliteVariant ret{};
const int column_count = sqlite3_column_count(this->stmt);
for (int i = 0; i < column_count; ++i) {
const auto val = sqlite3_column_name(this->stmt, i);
if (val == nullptr) {
return ret;
}
const std::string column_name{val};
if (name == column_name) {
switch (sqlite3_column_type(this->stmt, i)) {
case SQLITE_INTEGER:
ret = column_int64(i);
break;
case SQLITE_FLOAT:
ret = column_double(i);
break;
case SQLITE_BLOB:
// For now we will treat blob as text until this feature is needed
case SQLITE_TEXT:
ret = column_text(i);
break;
case SQLITE_NULL:
default:
break;
}
break;
}
}
return ret;
}
std::string Statement::column_text(const int idx) {
const auto* p = sqlite3_column_text(this->stmt, idx);
if (p == nullptr) {
// sqlite3_column_text returns NULL for a NULL column value, or when the statement
// is not positioned on a row (e.g. step() returned SQLITE_BUSY/ERROR/MISUSE and a
// caller misuses the result). Constructing std::string from nullptr is undefined
// and aborts the process; surface as a typed exception instead so callers can log
// and recover.
throw QueryExecutionException("column_text(" + std::to_string(idx) +
") returned NULL; use column_text_nullable for nullable columns");
}
return reinterpret_cast<const char*>(p);
}
std::optional<std::string> Statement::column_text_nullable(const int idx) {
auto p = sqlite3_column_text(this->stmt, idx);
if (p != nullptr) {
return reinterpret_cast<const char*>(p);
}
return std::optional<std::string>{};
}
int Statement::column_int(const int idx) {
return sqlite3_column_int(this->stmt, idx);
}
int64_t Statement::column_int64(const int idx) {
return sqlite3_column_int64(this->stmt, idx);
}
double Statement::column_double(const int idx) {
return sqlite3_column_double(this->stmt, idx);
}
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,40 @@
set(TEST_TARGET_NAME ${PROJECT_NAME}_tests)
set(GTEST_LIBRARIES GTest::gmock_main GTest::gtest_main)
add_executable(${TEST_TARGET_NAME})
target_sources(${TEST_TARGET_NAME} PRIVATE
test_connection.cpp
test_database_schema_updater.cpp
test_sqlite_statement.cpp
)
target_include_directories(${TEST_TARGET_NAME} PRIVATE
"${PROJECT_SOURCE_DIR}/include"
)
target_link_libraries(${TEST_TARGET_NAME} PRIVATE
everest::sqlite
${GTEST_LIBRARIES}
)
add_test(${TEST_TARGET_NAME} ${TEST_TARGET_NAME})
if (EVEREST_SQLITE_BUILD_TESTING AND NOT DISABLE_EDM)
evc_include(CodeCoverage)
append_coverage_compiler_flags_to_target(everest_sqlite)
setup_target_for_coverage_gcovr_html(
NAME ${PROJECT_NAME}_gcovr_coverage
EXECUTABLE ctest
DEPENDENCIES ${TEST_TARGET_NAME}
EXCLUDE "tests/*"
)
setup_target_for_coverage_gcovr_xml(
NAME ${PROJECT_NAME}_gcovr_coverage_xml
EXECUTABLE ctest
DEPENDENCIES ${TEST_TARGET_NAME}
EXCLUDE "tests/*"
)
endif()

View File

@@ -0,0 +1,50 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#pragma once
#include <everest/database/sqlite/connection.hpp>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
using namespace std::string_literals;
namespace everest::db::sqlite {
class DatabaseTestingUtils : public ::testing::Test {
protected:
std::unique_ptr<ConnectionInterface> database;
public:
DatabaseTestingUtils() : database(std::make_unique<Connection>("file::memory:?cache=shared")) {
EXPECT_TRUE(this->database->open_connection());
}
void ExpectUserVersion(uint32_t expected_version) {
auto statement = this->database->new_statement("PRAGMA user_version");
EXPECT_EQ(statement->step(), SQLITE_ROW);
EXPECT_EQ(statement->column_int(0), expected_version);
}
void SetUserVersion(uint32_t user_version) {
EXPECT_TRUE(this->database->execute_statement("PRAGMA user_version = "s + std::to_string(user_version)));
}
bool DoesTableExist(std::string_view table) {
const std::string statement = "SELECT name FROM sqlite_master WHERE type='table' AND name=@table_name";
std::unique_ptr<StatementInterface> table_exists_statement = this->database->new_statement(statement);
table_exists_statement->bind_text("@table_name", std::string(table), SQLiteString::Transient);
const int status = table_exists_statement->step();
const int number_of_rows = table_exists_statement->get_number_of_rows();
return status != SQLITE_ERROR && number_of_rows == 1;
}
bool DoesColumnExist(std::string_view table, std::string_view column) {
return this->database->execute_statement("SELECT "s + column.data() + " FROM " + table.data() + " LIMIT 1;");
}
};
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,76 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <everest/database/sqlite/connection.hpp>
#include <everest/database/sqlite/statement.hpp>
#include <gtest/gtest.h>
#include <sqlite3.h>
#include <filesystem>
#include <string>
namespace fs = std::filesystem;
namespace everest::db::sqlite {
class ConnectionWalTest : public ::testing::Test {
protected:
fs::path db_path;
void SetUp() override {
const auto unique = "everest_sqlite_conn_test_" + std::to_string(reinterpret_cast<std::uintptr_t>(this)) + "_" +
std::to_string(::testing::UnitTest::GetInstance()->random_seed()) + ".db";
db_path = fs::temp_directory_path() / unique;
std::error_code ec;
fs::remove(db_path, ec);
fs::remove(db_path.string() + "-wal", ec);
fs::remove(db_path.string() + "-shm", ec);
}
void TearDown() override {
std::error_code ec;
fs::remove(db_path, ec);
fs::remove(db_path.string() + "-wal", ec);
fs::remove(db_path.string() + "-shm", ec);
}
};
TEST_F(ConnectionWalTest, FileDatabaseUsesWalJournalMode) {
Connection db(db_path);
ASSERT_TRUE(db.open_connection());
{
auto stmt = db.new_statement("PRAGMA journal_mode");
ASSERT_EQ(stmt->step(), SQLITE_ROW);
EXPECT_EQ(stmt->column_text(0), "wal");
}
db.close_connection();
}
TEST_F(ConnectionWalTest, InMemoryDatabaseOpensWithoutWalFailure) {
Connection db(fs::path("file::memory:?cache=shared"));
ASSERT_TRUE(db.open_connection());
db.close_connection();
}
TEST_F(ConnectionWalTest, ExistingRollbackDatabaseMigratesToWal) {
sqlite3* raw = nullptr;
ASSERT_EQ(sqlite3_open_v2(db_path.c_str(), &raw, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr), SQLITE_OK);
ASSERT_EQ(sqlite3_exec(raw, "PRAGMA journal_mode=DELETE; CREATE TABLE t(id INTEGER);", nullptr, nullptr, nullptr),
SQLITE_OK);
sqlite3_close(raw);
Connection db(db_path);
ASSERT_TRUE(db.open_connection());
{
auto stmt = db.new_statement("PRAGMA journal_mode");
ASSERT_EQ(stmt->step(), SQLITE_ROW);
EXPECT_EQ(stmt->column_text(0), "wal");
}
db.close_connection();
}
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,313 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include "database_testing_utils.hpp"
#include <everest/database/sqlite/schema_updater.hpp>
#include <fstream>
namespace everest::db::sqlite {
struct MigrationFile {
std::string_view name;
std::string_view content;
};
static constexpr MigrationFile migration_file_up_1_valid{
"1_up-initial.sql",
"PRAGMA foreign_keys = ON; CREATE TABLE TEST_TABLE1(FIELD1 TEXT PRIMARY KEY NOT NULL, FIELD2 INT NOT NULL);"};
static constexpr MigrationFile migration_file_up_1_valid_empty_name{
"1_up.sql",
"PRAGMA foreign_keys = ON; CREATE TABLE TEST_TABLE1(FIELD1 TEXT PRIMARY KEY NOT NULL, FIELD2 INT NOT NULL);"};
static constexpr MigrationFile migration_file_up_1_invalid{
"1_up-initial.sql", "PRAGMA foreign_keys = ON; CREATE TABLE <invalid> TEST_TABLE1(FIELD1 TEXT PRIMARY KEY NOT "
"NULL, FIELD2 INT NOT NULL);"};
static constexpr MigrationFile migration_file_up_2_valid{
"2_up-add_table.sql", "CREATE TABLE TEST_TABLE2(FIELD1 TEXT PRIMARY KEY NOT NULL, FIELD2 INT NOT NULL);"};
static constexpr MigrationFile migration_file_down_2_valid{"2_down-drop_table.sql", "DROP TABLE TEST_TABLE2;"};
static constexpr MigrationFile migration_file_up_3_valid{
"3_up-add_table.sql", "CREATE TABLE TEST_TABLE3(FIELD1 TEXT PRIMARY KEY NOT NULL, FIELD2 INT NOT NULL);"};
static constexpr MigrationFile migration_file_down_3_valid{"3_down-drop_table.sql", "DROP TABLE TEST_TABLE3;"};
static constexpr MigrationFile migration_file_up_4_valid{
"4_up-add_table.sql", "CREATE TABLE TEST_TABLE4(FIELD1 TEXT PRIMARY KEY NOT NULL, FIELD2 INT NOT NULL);"};
static constexpr MigrationFile migration_file_down_4_valid{"4_down-drop_table.sql", "DROP TABLE TEST_TABLE4;"};
static constexpr std::string_view table1{"TEST_TABLE1"};
static constexpr std::string_view table2{"TEST_TABLE2"};
static constexpr std::string_view table3{"TEST_TABLE3"};
static constexpr std::string_view table4{"TEST_TABLE3"};
class DatabaseSchemaUpdaterTest : public DatabaseTestingUtils {
protected:
std::filesystem::path migration_files_path;
public:
DatabaseSchemaUpdaterTest() :
DatabaseTestingUtils(),
migration_files_path(std::filesystem::temp_directory_path() / "database_schema_test" / "core_migrations") {
std::filesystem::create_directories(migration_files_path);
EXPECT_TRUE(this->database->open_connection());
}
~DatabaseSchemaUpdaterTest() {
std::filesystem::remove_all(migration_files_path);
}
void WriteMigrationFile(const MigrationFile& file) {
std::ofstream stream{this->migration_files_path / file.name};
stream << file.content;
}
};
TEST_F(DatabaseSchemaUpdaterTest, FolderDoesNotExist) {
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path / "invalid", 1));
}
TEST_F(DatabaseSchemaUpdaterTest, TargetVersionInvalid) {
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 0));
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyInitialMigrationFile) {
this->WriteMigrationFile(migration_file_up_1_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_TRUE(this->DoesTableExist(table1));
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyInitialMigrationFileEmptyName) {
this->WriteMigrationFile(migration_file_up_1_valid_empty_name);
SchemaUpdater updater{this->database.get()};
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_TRUE(this->DoesTableExist(table1));
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyInitialMigrationFileAlreadyUpToDate) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->SetUserVersion(1);
SchemaUpdater updater{this->database.get()};
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyInitialMigrationFileVersionToHigh) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->SetUserVersion(2);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(2);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyInvalidInitialMigrationFile) {
this->WriteMigrationFile(migration_file_up_1_invalid);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(0);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, MissingInitialMigrationFile) {
this->WriteMigrationFile(migration_file_up_2_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(0);
EXPECT_FALSE(this->DoesTableExist(table1));
}
TEST_F(DatabaseSchemaUpdaterTest, SequenceNotValidUnevenNrOfFiles) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_up_3_valid);
this->WriteMigrationFile(migration_file_down_3_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 1));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 2));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 3));
this->ExpectUserVersion(0);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, SequenceNotValidNotEnoughFiles) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_down_2_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 3));
this->ExpectUserVersion(0);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, SequenceNotValidMissingDownFile) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_up_3_valid);
this->WriteMigrationFile(migration_file_up_4_valid);
this->WriteMigrationFile(migration_file_down_3_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 1));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 2));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 3));
this->ExpectUserVersion(0);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, SequenceNotValidMissingUpFile) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_down_2_valid);
this->WriteMigrationFile(migration_file_down_3_valid);
this->WriteMigrationFile(migration_file_down_4_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 1));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 2));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 3));
this->ExpectUserVersion(0);
EXPECT_FALSE(this->DoesTableExist(table1)); // Database was not changed
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyMultipleMigrationFilesStepByStep) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_up_3_valid);
this->WriteMigrationFile(migration_file_down_2_valid);
this->WriteMigrationFile(migration_file_down_3_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_FALSE(this->DoesTableExist(table2));
EXPECT_FALSE(this->DoesTableExist(table3));
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 2));
this->ExpectUserVersion(2);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_TRUE(this->DoesTableExist(table2));
EXPECT_FALSE(this->DoesTableExist(table3));
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 3));
this->ExpectUserVersion(3);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_TRUE(this->DoesTableExist(table2));
EXPECT_TRUE(this->DoesTableExist(table3));
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 2));
this->ExpectUserVersion(2);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_TRUE(this->DoesTableExist(table2));
EXPECT_FALSE(this->DoesTableExist(table3));
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_FALSE(this->DoesTableExist(table2));
EXPECT_FALSE(this->DoesTableExist(table3));
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyMultipleMigrationFilesAtOnce) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_up_3_valid);
this->WriteMigrationFile(migration_file_down_2_valid);
this->WriteMigrationFile(migration_file_down_3_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 3));
this->ExpectUserVersion(3);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_TRUE(this->DoesTableExist(table2));
EXPECT_TRUE(this->DoesTableExist(table3));
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_FALSE(this->DoesTableExist(table2));
EXPECT_FALSE(this->DoesTableExist(table3));
}
TEST_F(DatabaseSchemaUpdaterTest, ApplyMultipleMigrationFilesAtOnceWithFailure) {
this->WriteMigrationFile(migration_file_up_1_valid);
this->WriteMigrationFile(migration_file_up_2_valid);
this->WriteMigrationFile(migration_file_up_3_valid);
this->WriteMigrationFile(migration_file_down_2_valid);
this->WriteMigrationFile(migration_file_down_3_valid);
SchemaUpdater updater{this->database.get()};
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
this->ExpectUserVersion(1);
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_FALSE(this->DoesTableExist(table2));
EXPECT_TRUE(this->database->execute_statement(migration_file_up_2_valid.content.data()));
EXPECT_TRUE(this->DoesTableExist(table2));
EXPECT_FALSE(updater.apply_migration_files(this->migration_files_path, 3));
EXPECT_TRUE(this->DoesTableExist(table1));
EXPECT_TRUE(this->DoesTableExist(table2));
EXPECT_FALSE(this->DoesTableExist(table3));
this->ExpectUserVersion(1);
}
} // namespace everest::db::sqlite

View File

@@ -0,0 +1,199 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
#include <everest/database/exceptions.hpp>
#include <everest/database/sqlite/connection.hpp>
#include <everest/database/sqlite/statement.hpp>
#include <gtest/gtest.h>
#include <filesystem>
#include <string>
namespace fs = std::filesystem;
namespace everest::db::sqlite {
class SQLiteStatementTest : public ::testing::Test {
protected:
std::unique_ptr<Connection> db;
void SetUp() override {
fs::path db_path = "file::memory:?cache=shared";
db = std::make_unique<Connection>(db_path);
ASSERT_TRUE(db->open_connection());
ASSERT_TRUE(db->execute_statement(
"CREATE TABLE test_table (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, value INTEGER, score REAL);"));
}
void TearDown() override {
db->close_connection();
}
};
TEST_F(SQLiteStatementTest, InsertAndQueryRow) {
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (:name, :value, :score);");
insert_stmt->bind_text(":name", "test_name", SQLiteString::Transient);
insert_stmt->bind_int(":value", 42);
insert_stmt->bind_double(":score", 98.6);
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT name, value, score FROM test_table WHERE id = 1;");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_text(0), "test_name");
EXPECT_EQ(select_stmt->column_int(1), 42);
EXPECT_DOUBLE_EQ(select_stmt->column_double(2), 98.6);
}
TEST_F(SQLiteStatementTest, NullBindingAndOptionalResult) {
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (?, ?, ?);");
insert_stmt->bind_null(1);
insert_stmt->bind_int(2, 100);
insert_stmt->bind_null(3);
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT name, score FROM test_table WHERE id = 1;");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_FALSE(select_stmt->column_text_nullable(0).has_value());
EXPECT_FALSE(select_stmt->column_text_nullable(1).has_value());
}
TEST_F(SQLiteStatementTest, InvalidParameterThrows) {
auto stmt = db->new_statement("SELECT * FROM test_table WHERE name = :name;");
EXPECT_THROW(stmt->bind_int(":invalid", 1), std::out_of_range);
}
TEST_F(SQLiteStatementTest, ResetAndReuseStatement) {
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (?, ?, ?);");
insert_stmt->bind_text(1, "row1");
insert_stmt->bind_int(2, 1);
insert_stmt->bind_double(3, 1.1);
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
ASSERT_EQ(insert_stmt->reset(), SQLITE_OK);
insert_stmt->bind_text(1, "row2");
insert_stmt->bind_int(2, 2);
insert_stmt->bind_double(3, 2.2);
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT COUNT(*) FROM test_table;");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_int(0), 2);
}
TEST_F(SQLiteStatementTest, BindByNameAndIndexConsistency) {
auto stmt_by_index = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (?, ?, ?);");
auto stmt_by_name =
db->new_statement("INSERT INTO test_table (name, value, score) VALUES (:name, :value, :score);");
stmt_by_index->bind_text(1, "index_row");
stmt_by_index->bind_int(2, 123);
stmt_by_index->bind_double(3, 1.23);
ASSERT_EQ(stmt_by_index->step(), SQLITE_DONE);
stmt_by_name->bind_text(":name", "name_row");
stmt_by_name->bind_int(":value", 321);
stmt_by_name->bind_double(":score", 3.21);
ASSERT_EQ(stmt_by_name->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT COUNT(*) FROM test_table;");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_int(0), 2);
}
TEST_F(SQLiteStatementTest, BindInt64AndReadBack) {
int64_t large_value = 9223372036854775807LL; // max int64
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (?, ?, ?);");
insert_stmt->bind_text(1, "int64_test");
insert_stmt->bind_int64(2, large_value);
insert_stmt->bind_double(3, 0.0);
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT value FROM test_table WHERE name = 'int64_test';");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_int64(0), large_value);
}
TEST_F(SQLiteStatementTest, BindInt64NamedParameter) {
int64_t big_value = 1234567890123456789LL;
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (:name, :value, :score);");
insert_stmt->bind_text(":name", "int64_named", SQLiteString::Transient);
insert_stmt->bind_int64(":value", big_value);
insert_stmt->bind_double(":score", 42.42);
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT value FROM test_table WHERE name = 'int64_named';");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_int64(0), big_value);
}
TEST_F(SQLiteStatementTest, StatementDestructorFinalizesStatement) {
{
auto stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES ('temp', 0, 0.0);");
ASSERT_EQ(stmt->step(), SQLITE_DONE);
// stmt goes out of scope here
}
auto select_stmt = db->new_statement("SELECT COUNT(*) FROM test_table WHERE name = 'temp';");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_int(0), 1);
}
TEST_F(SQLiteStatementTest, GetNumberOfColumnsInRow) {
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES ('check_cols', 1, 1.0);");
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT id, name, value, score FROM test_table WHERE name = 'check_cols';");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->get_number_of_rows(), 4);
}
TEST_F(SQLiteStatementTest, ColumnTypeChecks) {
auto insert_stmt =
db->new_statement("INSERT INTO test_table (name, value, score) VALUES ('type_test', 999, 9.99);");
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT name, value, score FROM test_table WHERE name = 'type_test';");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_EQ(select_stmt->column_type(0), SQLITE_TEXT);
EXPECT_EQ(select_stmt->column_type(1), SQLITE_INTEGER);
EXPECT_EQ(select_stmt->column_type(2), SQLITE_FLOAT);
}
TEST_F(SQLiteStatementTest, BindNullByName) {
auto insert_stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES (:name, :value, :score);");
insert_stmt->bind_null(":name");
insert_stmt->bind_int(":value", 5);
insert_stmt->bind_null(":score");
ASSERT_EQ(insert_stmt->step(), SQLITE_DONE);
auto select_stmt = db->new_statement("SELECT name, score FROM test_table WHERE value = 5;");
ASSERT_EQ(select_stmt->step(), SQLITE_ROW);
EXPECT_FALSE(select_stmt->column_text_nullable(0).has_value());
EXPECT_FALSE(select_stmt->column_text_nullable(1).has_value());
}
TEST_F(SQLiteStatementTest, ResetPreservesPreparedStatement) {
auto stmt = db->new_statement("INSERT INTO test_table (name, value, score) VALUES ('x', 1, 1.0);");
ASSERT_EQ(stmt->step(), SQLITE_DONE);
ASSERT_EQ(stmt->reset(), SQLITE_OK);
stmt->bind_text(1, "x"); // optional rebind test
stmt->bind_int(2, 2);
stmt->bind_double(3, 2.0);
ASSERT_EQ(stmt->step(), SQLITE_DONE);
}
} // namespace everest::db::sqlite