Import SQLite3 wrapper implementation from old repo

This commit is contained in:
TennesseeTrash 2025-06-09 01:41:27 +02:00
parent f9c6e9e7cb
commit 405b68b824
5 changed files with 826 additions and 0 deletions

28
CMake/sqlite3.cmake Normal file
View file

@ -0,0 +1,28 @@
include(FetchContent)
FetchContent_Declare(
sqlite3
URL https://www.sqlite.org/2025/sqlite-amalgamation-3490100.zip
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
FetchContent_MakeAvailable(
sqlite3
)
add_library(
sqlite3
STATIC
)
target_include_directories(
sqlite3
PUBLIC
${sqlite3_SOURCE_DIR}
)
target_sources(
sqlite3
PRIVATE
${sqlite3_SOURCE_DIR}/sqlite3.c
)

View file

@ -0,0 +1,742 @@
#ifndef GARBAGE_SQLITE_HPP
#define GARBAGE_SQLITE_HPP
#include <array>
#include <cstdint>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <tuple>
#include <vector>
#include <sqlite3.h>
namespace Garbage::SQLite
{
//////////////////////////////////////////////////////////////////////////
// TRAIT SUPPORT
template <typename T>
struct MemberTraits;
template <typename T, typename U>
struct MemberTraits<T U::*>
{
using Member = T;
using Container = U;
};
namespace Implementation
{
template <typename... Ts>
struct First;
template <typename T, typename... Ts>
struct First<T, Ts...>
{
using Type = T;
};
}
template <typename... Ts>
using First = Implementation::First<Ts...>::Type;
namespace Implementation
{
template <template <typename> typename Template, typename T>
struct IsSpecialization
{
static constexpr bool Value = false;
};
template <template <typename> typename Template, typename... Args>
struct IsSpecialization<Template, Template<Args...>>
{
static constexpr bool Value = true;
};
template <template <auto> typename Template, typename T>
struct IsValueSpecialization
{
static constexpr bool Value = false;
};
template <template <auto> typename Template, auto... Args>
struct IsValueSpecialization<Template, Template<Args...>>
{
static constexpr bool Value = true;
};
}
template <template <typename> typename Template, typename T>
concept IsSpecialization = Implementation::IsSpecialization<Template, T>::Value;
template <template <auto> typename Template, typename T>
concept IsValueSpecialization = Implementation::IsValueSpecialization<Template, T>::Value;
template <typename T1, typename T2>
concept IsSame = std::is_same_v<T1, T2>;
template <typename T, typename... Ts>
concept IsAnyOf = (IsSame<T, Ts> || ...);
template <template <typename> typename Wrapper, typename T, typename... Ts>
concept IsAnyInstanceOf = IsAnyOf<T, Wrapper<Ts>...>;
namespace Implementation
{
template <typename Arg, typename... Ts>
struct FirstWithArg
{
using Type = void;
};
template <typename Arg, template <typename> typename T, typename... Ts, typename... Args>
struct FirstWithArg<Arg, T<Args...>, Ts...>
{
using Type = std::conditional_t<IsSame<Arg, typename First<Args...>::Type>,
T<Args...>, typename FirstWithArg<Arg, Ts...>::Type>;
};
}
template <typename Arg, typename... Ts>
using FirstWithArg = Implementation::FirstWithArg<Arg, Ts...>::Type;
//////////////////////////////////////////////////////////////////////////
// UTILITY FUNCTIONS
template <typename DestinationType, typename... TupleTypes>
[[nodiscard]] constexpr
auto ToArray(std::tuple<TupleTypes...>&& tuple)
{
constexpr auto makeArray = [] (auto&&... args) {
return std::array{ std::forward(args)... };
};
return std::apply(makeArray, tuple);
}
template <typename T, std::size_t N>
[[nodiscard]] constexpr
auto ToArray(auto (&&array)[N])
{
std::array<T, N> result;
std::move(std::begin(array), std::end(array), result.begin());
return result;
}
// Note(3011): The mutating version is easily addable if need be.
template <typename... Ts>
constexpr
void ForEach(auto&& multifunc, const std::tuple<Ts...>& tuple)
{
auto conditionalInvoke = [] (auto& multifunc, const auto& item) {
if constexpr (std::invocable<decltype(multifunc), decltype(item)>) {
multifunc(item);
}
};
std::apply([&multifunc, &conditionalInvoke] (const auto&... args) {
(conditionalInvoke(multifunc, args), ...);
}, tuple);
}
//////////////////////////////////////////////////////////////////////////
// ERRORS
class DatabaseError : public std::runtime_error
{
using std::runtime_error::runtime_error;
};
//////////////////////////////////////////////////////////////////////////
// BASIC TYPES
using Integer = std::int64_t;
using String = std::string;
using Real = double;
using Blob = std::vector<std::uint8_t>;
template <typename T>
using Optional = std::optional<T>;
namespace Implementation
{
template <typename T>
struct TypeString;
template <> struct TypeString<Integer> { static constexpr std::string_view Value = "INTEGER"; };
template <> struct TypeString<String > { static constexpr std::string_view Value = "TEXT" ; };
template <> struct TypeString<Real > { static constexpr std::string_view Value = "REAL" ; };
template <> struct TypeString<Blob > { static constexpr std::string_view Value = "BLOB" ; };
template <typename T>
struct TypeString<Optional<T>>
{
static constexpr std::string_view Value = TypeString<T>::Value;
};
}
template <typename T>
static constexpr std::string_view TypeString = Implementation::TypeString<T>::Value;
template <typename T>
concept SupportedType = IsAnyOf<T, Integer, String, Real, Blob>
|| IsAnyInstanceOf<Optional, T, Integer, String, Real, Blob>;
// TODO: These are only for development, remove once prepared statements are
// implemented.
[[nodiscard]] constexpr
std::string ToString(Integer i)
{
return std::to_string(i);
}
[[nodiscard]] constexpr
std::string ToString(const String& s)
{
return "'" + s + "'";
}
//////////////////////////////////////////////////////////////////////////
// DATABASE MODEL
enum class ColumnFlags : std::uint8_t
{
Nothing = 0b0000'0000,
PrimaryKey = 0b0000'0001,
Unique = 0b0000'0010,
LowerBits = 0b0000'1111,
UpperBits = 0b1111'0000,
};
[[nodiscard]] constexpr
ColumnFlags operator| (ColumnFlags a, ColumnFlags b)
{
using Type = std::underlying_type_t<ColumnFlags>;
return ColumnFlags(static_cast<Type>(a) | static_cast<Type>(b));
}
[[nodiscard]] constexpr
ColumnFlags operator& (ColumnFlags a, ColumnFlags b)
{
using Type = std::underlying_type_t<ColumnFlags>;
return ColumnFlags(static_cast<Type>(a) & static_cast<Type>(b));
}
template <ColumnFlags Flag>
[[nodiscard]] constexpr
bool IsSet(ColumnFlags a)
{
return (a & Flag) != ColumnFlags::Nothing;
}
template <typename T>
requires std::is_member_object_pointer_v<T>
&& SupportedType<typename MemberTraits<T>::Member>
class Column
{
public:
using PtrType = T;
using DataType = MemberTraits<T>::Member;
using TableType = MemberTraits<T>::Container;
[[nodiscard]] constexpr
Column(DataType TableType::*ptr, std::string_view name, ColumnFlags flags = ColumnFlags::Nothing)
: mPtr(ptr), mFlags(flags), mName(name)
{}
std::string_view Name() const
{
return mName;
}
std::string_view Type() const
{
return TypeString<DataType>;
}
ColumnFlags Flags() const
{
return mFlags;
}
bool IsOptional() const
{
// Note: it doesn't work with the Optional alias
return IsSpecialization<std::optional, DataType>;
}
std::string Declaration() const
{
std::string result;
result += mName + " " + std::string(Type());
if (!IsOptional()) {
result += " NOT NULL";
}
switch (mFlags & ColumnFlags::LowerBits) {
break; case ColumnFlags::Nothing:
// noop
break; case ColumnFlags::PrimaryKey:
result += " PRIMARY KEY";
break; case ColumnFlags::Unique:
result += " UNIQUE";
break; default:
throw DatabaseError("Invalid column flag for: " + mName);
}
return result;
}
DataType TableType::*Ptr() const
{
return mPtr;
}
const DataType& Get(const TableType& item) const
{
return item.*mPtr;
}
private:
DataType TableType::*mPtr;
ColumnFlags mFlags;
std::string mName;
};
template <typename T>
Column(T, auto) -> Column<T>;
template <typename T>
Column(T, auto, ColumnFlags) -> Column<T>;
template <std::size_t N>
class ForeignKey
{
public:
template <typename StringLike>
requires std::is_convertible_v<StringLike, std::string>
[[nodiscard]] constexpr
ForeignKey(StringLike (&&from)[N], std::string_view table, StringLike (&&to)[N])
: mDestinationTable(table)
, mFromColumns(ToArray<std::string>(std::move(from)))
, mToColumns(ToArray<std::string>(std::move(to)))
{}
std::string Declaration() const
{
std::string fromColumns;
std::string toColumns;
for (std::size_t i = 0; i < mFromColumns.size(); ++i) {
fromColumns += mFromColumns[i];
toColumns += mToColumns[i];
if (i < N - 1) {
fromColumns += ", ";
toColumns += ", ";
}
}
return "FOREIGN KEY(" + fromColumns + ") REFERENCES " + mDestinationTable + "(" + toColumns + ")";
}
private:
std::string mDestinationTable;
std::array<std::string, N> mFromColumns;
std::array<std::string, N> mToColumns;
};
template <typename T>
concept MappableType = requires {
requires std::is_aggregate_v<T>;
requires std::is_default_constructible_v<T>;
requires !std::is_array_v<T>;
};
template <typename T, typename TableType>
concept TableEntity = IsValueSpecialization<ForeignKey, T>
|| (IsSpecialization<Column, T> && IsSame<TableType, typename T::TableType>);
template <MappableType T, TableEntity<T>... Entities>
requires (sizeof...(Entities) > 0)
class Table
{
public:
[[nodiscard]] constexpr
Table(std::string_view name, Entities&&... entities)
: mName(name), mEntities(std::move(entities)...)
{}
std::string_view Name() const
{
return mName;
}
template <typename ColumnPtr>
std::string_view Name(ColumnPtr ptr) const
{
std::string_view name;
ForEach([&name, &ptr] (const Column<ColumnPtr>& column) {
if (ptr == column.Ptr()) {
name = column.Name();
}
}, mEntities);
return name;
}
template <typename ColumnPtr>
std::string Identifier(ColumnPtr ptr) const
{
return "$" + mName + "::" + std::string(Name(ptr));
}
std::string Create() const
{
std::string result;
result += "CREATE TABLE IF NOT EXISTS " + mName + " (\n";
ForEach([&result] <typename ColumnType> (const Column<ColumnType>& column) {
result += " ";
result += column.Declaration();
result += ",\n";
}, mEntities);
ForEach([&result] <std::size_t Size> (const ForeignKey<Size>& key) {
result += " ";
result += key.Declaration();
result += ",\n";
}, mEntities);
result.erase(result.size() - 2, 1);
result.back() = '\n';
result += ");\n";
return result;
}
std::string Insert() const
{
std::string columnNames;
std::string valueNames;
ForEach([this, &columnNames, &valueNames] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
columnNames += column.Name();
columnNames += ", ";
valueNames += Identifier(column.Ptr());
valueNames += ", ";
}, mEntities);
std::string result;
result += "INSERT INTO " + mName + "(";
result += columnNames.erase(columnNames.size() - 2);
result += ") VALUES (";
result += valueNames.erase(valueNames.size() - 2);
result += ");\n";
return result;
}
// TODO: These are only for development, remove once prepared statements are
// implemented.
std::string Insert(const T& item) const
{
std::string columnNames;
std::string values;
ForEach([&item, &columnNames, &values] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
columnNames += column.Name();
columnNames += ", ";
values += ToString(column.Get(item));
values += ", ";
}, mEntities);
std::string result;
result += "INSERT INTO " + mName + "(";
result += columnNames.erase(columnNames.size() - 2);
result += ") VALUES (";
result += values.erase(values.size() - 2);
result += ");\n";
return result;
}
std::string Update() const
{
std::string result;
result += "UPDATE " + mName + " SET\n";
ForEach([this, &result] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
result += " " + std::string(column.Name()) + " = " + Identifier(column.Ptr()) + ",\n";
}, mEntities);
result.erase(result.size() - 2, 1);
ForEach([this, &result] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
result += "WHERE " + std::string(column.Name()) + " = " + Identifier(column.Ptr()) + ";\n";
}
}, mEntities);
return result;
}
// TODO: These are only for development, remove once prepared statements are
// implemented.
std::string Update(const T& item) const
{
std::string result;
result += "UPDATE " + mName + " SET\n";
ForEach([this, &result, &item] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
return;
}
result += " " + std::string(column.Name()) + " = " + ToString(column.Get(item)) + ",\n";
}, mEntities);
result.erase(result.size() - 2, 1);
ForEach([this, &result, &item] <typename ColumnType> (const Column<ColumnType>& column) {
if (IsSet<ColumnFlags::PrimaryKey>(column.Flags())) {
result += "WHERE " + std::string(column.Name()) + " = " + ToString(column.Get(item)) + ";\n";
}
}, mEntities);
return result;
}
private:
std::string mName;
std::tuple<Entities...> mEntities;
};
// Note(3011): This breaks when the first specified entity is not a column.
template <typename... Entities>
Table(auto, Entities&&...) -> Table<typename MemberTraits<typename First<Entities...>::PtrType>::Container, Entities...>;
template <typename... Tables>
requires (IsSpecialization<Table, Tables> && ...)
class Database
{
public:
[[nodiscard]] constexpr
Database(std::string_view name, Tables&&... tables)
: mName(name), mTables(std::move(tables)...)
{}
template <typename TableType>
std::string_view Name() const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Name();
}
template <typename ColumnPtr>
requires std::is_member_object_pointer_v<ColumnPtr>
std::string_view Name(ColumnPtr ptr) const
{
using TableType = MemberTraits<ColumnPtr>::Container;
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Name(ptr);
}
template <typename ColumnPtr>
requires std::is_member_object_pointer_v<ColumnPtr>
std::string Identifier(ColumnPtr ptr) const
{
using TableType = MemberTraits<ColumnPtr>::Container;
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Identifier(ptr);
}
std::string Create() const
{
std::string result;
result += "PRAGMA foreign_keys = ON;\n\n";
std::apply([&result](auto&... args) {
((result += args.Create(), result += "\n"), ...);
}, mTables);
return result;
}
template <typename TableType>
std::string Insert() const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Insert();
}
template <typename TableType>
std::string Insert(const TableType& item) const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Insert(item);
}
template <typename TableType>
std::string Update() const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Update();
}
template <typename TableType>
std::string Update(const TableType& item) const
{
using Type = FirstWithArg<TableType, Tables...>;
return std::get<Type>(mTables).Update(item);
}
private:
std::string mName;
std::tuple<Tables...> mTables;
};
template <typename... Tables>
Database(auto, Tables...) -> Database<Tables...>;
//////////////////////////////////////////////////////////////////////////
// DATABASE CONNECTION
enum class Operator : std::uint8_t
{
None = 0,
Equal = 1,
};
template <typename DatabaseType>
requires IsSpecialization<Database, DatabaseType>
class Connection;
template <typename DatabaseType, typename TableType>
class Select
{
private:
friend class Connection<DatabaseType>;
Select(Connection<DatabaseType>& connection)
: mConnection(connection)
{}
public:
// Note: Immovable object, please don't use unstoppable force.
Select(const Select&) = delete;
Select& operator=(const Select&) = delete;
Select(Select&&) = delete;
Select& operator=(Select&&) = delete;
template <typename ColumnPtr>
requires std::is_member_object_pointer_v<ColumnPtr>
Select& Where(ColumnPtr column)
{
// TODO: Append the condition.
return &this;
}
// TODO: Finalizer member function - returns the result.
std::optional<TableType> First()
{
}
// TODO: Finalizer member function - returns the result.
std::vector<TableType> All()
{
}
private:
Connection<TableType>& mConnection;
std::string mStatement;
};
template <typename DatabaseType>
requires IsSpecialization<Database, DatabaseType>
class Connection
{
public:
Connection(const DatabaseType& database, std::string path)
: mDatabase(&database), mPath(std::move(path)), mConnection(nullptr)
{
int errc = sqlite3_open(mPath.c_str(), &mConnection);
if (errc != SQLITE_OK) {
// TODO: Add more information to the error.
throw DatabaseError("Could not open a SQLite connection");
}
}
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
Connection(Connection&& other) noexcept
: mDatabase(other.mDatabase)
, mPath(std::move(other.mPath))
, mConnection(other.mConnection)
{
other.mConnection = nullptr;
}
Connection& operator= (Connection&& other)
{
if (&other == this) {
return;
}
mDatabase = other.mDatabase;
mPath = std::move(other.mPath);
mConnection = other.mConnection;
other.mDatabase = nullptr;
other.mConnection = nullptr;
}
~Connection()
{
// TODO: The current implementation will loop here forever in case
// anything isn't finalized - make sure to force a cleanup before
// that happens.
int errc = sqlite3_close(mConnection);
while (errc == SQLITE_BUSY) {
errc = sqlite3_close(mConnection);
}
}
bool Create()
{
std::string script = mDatabase->Create();
char *errmsg;
auto errc = sqlite3_exec(mConnection, script.c_str(), nullptr, nullptr, &errmsg);
if (errc != SQLITE_OK) {
// TODO: Report the error?
sqlite3_free(reinterpret_cast<void *>(errc));
return false;
}
return true;
}
template <typename TableType>
bool Insert(TableType& item)
{
std::string statement = mDatabase->Insert(item);
char *errmsg;
auto errc = sqlite3_exec(mConnection, statement.c_str(), nullptr, nullptr, &errmsg);
if (errc != SQLITE_OK) {
// TODO: Report the error?
sqlite3_free(reinterpret_cast<void *>(errc));
return false;
}
return true;
}
template <typename TableType>
bool Update(TableType& item)
{
std::string statement = mDatabase->Insert(item);
char *errmsg;
auto errc = sqlite3_exec(mConnection, statement.c_str(), nullptr, nullptr, &errmsg);
if (errc != SQLITE_OK) {
// TODO: Report the error?
sqlite3_free(reinterpret_cast<void *>(errc));
return false;
}
return true;
}
template <typename TableType>
Select<DatabaseType, TableType> Select()
{
return Select<DatabaseType, TableType>(*this);
}
private:
const DatabaseType *mDatabase;
std::string mPath;
sqlite3 *mConnection;
};
}
#endif // GARBAGE_SQLITE_HPP

View file

@ -1,3 +1,4 @@
include(${PROJECT_SOURCE_DIR}/CMake/Catch2.cmake)
add_subdirectory(Base64)
add_subdirectory(SQLite)

View file

@ -0,0 +1,20 @@
include(${PROJECT_SOURCE_DIR}/CMake/sqlite3.cmake)
add_executable(TestSQLite)
target_compile_features(TestSQLite
PRIVATE
cxx_std_20
)
target_link_libraries(TestSQLite
PRIVATE
Garbage
sqlite3
Catch2::Catch2WithMain
)
target_sources(TestSQLite
PRIVATE
Main.cpp
)

35
Tests/SQLite/Main.cpp Normal file
View file

@ -0,0 +1,35 @@
#include <Garbage/SQLite.hpp>
using namespace Garbage::SQLite;
struct User
{
std::int64_t Id;
std::string Username;
std::string Email;
std::string PasswordHash;
};
Database db(
"UserDb",
Table(
"Users",
Column(&User::Id, "Id", ColumnFlags::PrimaryKey),
Column(&User::Username, "Username", ColumnFlags::Unique),
Column(&User::Email, "Email", ColumnFlags::Unique),
Column(&User::PasswordHash, "PasswordHash")
)
);
int main() {
User u {
.Id = 25,
.Username = "noob",
.Email = "noob@noob.io",
.PasswordHash = "ASDASDASDASDASDA",
};
Connection c(db, "Test.db");
c.Create();
c.Insert(u);
}