Support for CREATE TABLE in sqlite

This commit is contained in:
Dr. Patrick Urbanke
2025-04-02 08:18:09 +02:00
parent 9b40b301fc
commit 081a0414c7
13 changed files with 257 additions and 15 deletions

View File

@@ -22,11 +22,14 @@ struct Connection {
/// Executes a statement. Note that in order for the statement to take effect,
/// you must call .commit() afterwards.
virtual Result<Nothing> execute(const dynamic::Statement& _stmt) = 0;
virtual Result<Nothing> execute(const std::string& _sql) = 0;
/// Reads the results of a SelectFrom statement.
virtual Result<Ref<Iterator>> read(const dynamic::SelectFrom& _query) = 0;
/// Transpiles a statement to a particular SQL dialect.
virtual std::string to_sql(const dynamic::Statement& _stmt) = 0;
/// Starts the write operation.
virtual Result<Nothing> start_write(const dynamic::Insert& _stmt) = 0;

View File

@@ -0,0 +1,6 @@
#ifndef SQLGEN_INTERNAL_COLLECT_HPP_
#define SQLGEN_INTERNAL_COLLECT_HPP_
namespace sqlgen::internal::collect {}
#endif

View File

@@ -0,0 +1,20 @@
#ifndef SQLGEN_INTERNAL_COLLECT_VECTOR_HPP_
#define SQLGEN_INTERNAL_COLLECT_VECTOR_HPP_
#include <ranges>
namespace sqlgen::internal::collect {
template <class RangeType>
auto vector(RangeType _r) {
using T = std::ranges::range_value_t<RangeType>;
std::vector<T> res;
for (auto e : _r) {
res.emplace_back(std::move(e));
}
return res;
}
} // namespace sqlgen::internal::collect
#endif

View File

@@ -0,0 +1,24 @@
#ifndef SQLGEN_INTERNAL_STRINGS_STRINGS_HPP_
#define SQLGEN_INTERNAL_STRINGS_STRINGS_HPP_
#include <string>
#include <vector>
namespace sqlgen::internal::strings {
char to_lower(const char ch);
char to_upper(const char ch);
std::string join(const std::string& _delimiter,
const std::vector<std::string>& _strings);
std::string replace_all(const std::string& _str, const std::string& _from,
const std::string& _to);
std::vector<std::string> split(const std::string& _str,
const std::string& _delimiter);
} // namespace sqlgen::internal::strings
#endif

View File

@@ -0,0 +1,7 @@
#ifndef SQLGEN_SQLITE_HPP_
#define SQLGEN_SQLITE_HPP_
#include "../sqlgen.hpp"
#include "sqlite/connect.hpp"
#endif

View File

@@ -1,5 +1,5 @@
#ifndef SQLGEN_SQLITE3_CONNECTION_HPP_
#define SQLGEN_SQLITE3_CONNECTION_HPP_
#ifndef SQLGEN_SQLITE_CONNECTION_HPP_
#define SQLGEN_SQLITE_CONNECTION_HPP_
#include <sqlite3.h>
@@ -11,6 +11,7 @@
#include "../Connection.hpp"
#include "../Ref.hpp"
#include "../Result.hpp"
#include "../dynamic/Column.hpp"
#include "../dynamic/Statement.hpp"
namespace sqlgen::sqlite {
@@ -30,11 +31,9 @@ class Connection : public sqlgen::Connection {
~Connection();
Result<Nothing> commit() final { return exec("COMMIT;"); }
Result<Nothing> commit() final { return execute("COMMIT;"); }
Result<Nothing> execute(const dynamic::Statement& _stmt) final {
return exec(to_sql(_stmt));
}
Result<Nothing> execute(const std::string& _sql) noexcept final;
Connection& operator=(const Connection& _other) = delete;
@@ -44,6 +43,8 @@ class Connection : public sqlgen::Connection {
return error("TODO");
}
std::string to_sql(const dynamic::Statement& _stmt) noexcept final;
Result<Nothing> start_write(const dynamic::Insert& _stmt) final {
return error("TODO");
}
@@ -56,18 +57,22 @@ class Connection : public sqlgen::Connection {
}
private:
/// Transforms a dynamic::Column to an SQL string that defines the column in a
/// CREATE TABLE statement.
std::string column_to_sql_definition(const dynamic::Column& _col) noexcept;
/// Transforms a CreateTable Statement to an SQL string.
std::string create_table_to_sql(const dynamic::CreateTable& _stmt) noexcept;
/// Wrapper around sqlite3_exec.
Result<Nothing> exec(const std::string& _sql) noexcept;
/// Transforms a Statement to an SQL string.
std::string to_sql(const dynamic::Statement& _stmt) noexcept;
/// Generates the underlying connection.
static sqlite3* make_conn(const std::string& _fname);
/// Expresses the properies as SQL.
std::string properties_to_sql(const dynamic::types::Properties& _p) noexcept;
/// Expresses the type as SQL.
std::string type_to_sql(const dynamic::Type& _type) noexcept;
private:
/// The underlying sqlite3 connection.
sqlite3* conn_;

View File

@@ -0,0 +1,16 @@
#ifndef SQLGEN_SQLITE_CONNECT_HPP_
#define SQLGEN_SQLITE_CONNECT_HPP_
#include <string>
#include "Connection.hpp"
namespace sqlgen::sqlite {
inline auto connect(const std::string& _fname = ":memory:") {
return Connection::make(_fname);
}
} // namespace sqlgen::sqlite
#endif

View File

@@ -0,0 +1 @@
#include "sqlgen/internal/strings/strings.cpp"

View File

@@ -0,0 +1,58 @@
#include "sqlgen/internal/strings/strings.hpp"
namespace sqlgen::internal::strings {
char to_lower(const char ch) {
if (ch >= 'A' && ch <= 'Z') {
return ch + ('a' - 'A');
} else {
return ch;
}
}
char to_upper(const char ch) {
if (ch >= 'a' && ch <= 'z') {
return ch + ('A' - 'a');
} else {
return ch;
}
}
std::string join(const std::string& _delimiter,
const std::vector<std::string>& _strings) {
if (_strings.size() == 0) {
return "";
}
auto res = _strings[0];
for (size_t i = 1; i < _strings.size(); ++i) {
res += _delimiter + _strings[i];
}
return res;
}
std::string replace_all(const std::string& _str, const std::string& _from,
const std::string& _to) {
auto str = _str;
size_t pos = 0;
while ((pos = str.find(_from, pos)) != std::string::npos) {
str.replace(pos, _from.length(), _to);
pos += _to.length();
}
return str;
}
std::vector<std::string> split(const std::string& _str,
const std::string& _delimiter) {
auto str = _str;
size_t pos = 0;
std::vector<std::string> result;
while ((pos = str.find(_delimiter)) != std::string::npos) {
result.emplace_back(str.substr(0, pos));
str.erase(0, pos + _delimiter.length());
}
result.emplace_back(std::move(str));
return result;
}
} // namespace sqlgen::internal::strings

View File

@@ -1,7 +1,12 @@
#include "sqlgen/sqlite/Connection.hpp"
#include <ranges>
#include <rfl.hpp>
#include <sstream>
#include "sqlgen/internal/collect/vector.hpp"
#include "sqlgen/internal/strings/strings.hpp"
namespace sqlgen::sqlite {
Connection::~Connection() {
@@ -10,8 +15,21 @@ Connection::~Connection() {
}
}
std::string Connection::column_to_sql_definition(
const dynamic::Column& _col) noexcept {
return "\"" + _col.name + "\"" + " " + type_to_sql(_col.type) +
properties_to_sql(
_col.type.visit([](const auto& _t) { return _t.properties; }));
}
std::string Connection::create_table_to_sql(
const dynamic::CreateTable& _stmt) noexcept {
using namespace std::ranges::views;
const auto col_to_sql = [&](const auto& _col) {
return column_to_sql_definition(_col);
};
std::stringstream stream;
stream << "CREATE TABLE ";
if (_stmt.table.schema) {
@@ -21,9 +39,12 @@ std::string Connection::create_table_to_sql(
if (_stmt.if_not_exists) {
stream << "IF NOT EXISTS ";
}
stream << "(";
stream << "(";
stream << internal::strings::join(
", ", internal::collect::vector(_stmt.columns | transform(col_to_sql)));
stream << ");";
return stream.str();
}
@@ -36,7 +57,7 @@ rfl::Result<Ref<sqlgen::Connection>> Connection::make(
}
}
Result<Nothing> Connection::exec(const std::string& _sql) noexcept {
Result<Nothing> Connection::execute(const std::string& _sql) noexcept {
char* errmsg = nullptr;
sqlite3_exec(conn_, _sql.c_str(), nullptr, nullptr, &errmsg);
if (errmsg) {
@@ -57,6 +78,12 @@ sqlite3* Connection::make_conn(const std::string& _fname) {
return conn;
}
std::string Connection::properties_to_sql(
const dynamic::types::Properties& _p) noexcept {
return std::string(_p.primary ? " PRIMARY KEY" : "") +
std::string(_p.nullable ? "" : " NOT NULL");
}
Connection& Connection::operator=(Connection&& _other) {
if (this == &_other) {
return *this;
@@ -78,4 +105,32 @@ std::string Connection::to_sql(const dynamic::Statement& _stmt) noexcept {
});
}
std::string Connection::type_to_sql(const dynamic::Type& _type) noexcept {
return _type.visit([](const auto _t) -> std::string {
using T = std::remove_cvref_t<decltype(_t)>;
if constexpr (std::is_same_v<T, dynamic::types::Boolean> ||
std::is_same_v<T, dynamic::types::Int8> ||
std::is_same_v<T, dynamic::types::Int16> ||
std::is_same_v<T, dynamic::types::Int32> ||
std::is_same_v<T, dynamic::types::Int64> ||
std::is_same_v<T, dynamic::types::UInt8> ||
std::is_same_v<T, dynamic::types::UInt16> ||
std::is_same_v<T, dynamic::types::UInt32> ||
std::is_same_v<T, dynamic::types::UInt64>) {
return "INTEGER";
} else if constexpr (std::is_same_v<T, dynamic::types::Float32> ||
std::is_same_v<T, dynamic::types::Float64>) {
return "REAL";
} else if constexpr (std::is_same_v<T, dynamic::types::Unknown> ||
std::is_same_v<T, dynamic::types::Text> ||
std::is_same_v<T, dynamic::types::VarChar> ||
std::is_same_v<T, dynamic::types::Timestamp> ||
std::is_same_v<T, dynamic::types::TimestampWithTZ>) {
return "TEXT";
} else {
static_assert(rfl::always_false_v<T>, "Not all cases were covered.");
}
});
}
} // namespace sqlgen::sqlite

View File

@@ -22,3 +22,7 @@ target_link_libraries(
find_package(GTest)
gtest_discover_tests(sqlgen-tests)
if(SQLGEN_SQLITE3)
add_subdirectory(sqlite)
endif()

View File

@@ -0,0 +1,19 @@
project(sqlgen-sqlite-tests)
file(GLOB_RECURSE SOURCES CONFIGURE_DEPENDS "*.cpp")
add_executable(
sqlgen-sqlite-tests
${SOURCES}
)
target_precompile_headers(sqlgen-sqlite-tests PRIVATE [["sqlgen.hpp"]] <iostream> <string> <functional> <gtest/gtest.h>)
target_link_libraries(
sqlgen-sqlite-tests
PRIVATE
"${SQLGEN_GTEST_LIB}"
)
find_package(GTest)
gtest_discover_tests(sqlgen-sqlite-tests)

View File

@@ -0,0 +1,24 @@
#include <gtest/gtest.h>
#include <sqlgen.hpp>
#include <sqlgen/parsing/to_create_table.hpp>
#include <sqlgen/sqlite.hpp>
namespace test_to_create_table {
struct TestTable {
std::string field1;
int32_t field2;
sqlgen::PrimaryKey<uint32_t> id;
std::optional<std::string> nullable;
};
TEST(sqlite, test_to_create_table) {
const auto create_table_stmt = sqlgen::parsing::to_create_table<TestTable>();
const auto conn = sqlgen::sqlite::connect().value();
const auto expected =
R"(CREATE TABLE "TestTable" IF NOT EXISTS ("field1" TEXT NOT NULL, "field2" INTEGER NOT NULL, "id" INTEGER PRIMARY KEY NOT NULL, "nullable" TEXT);)";
EXPECT_EQ(conn->to_sql(create_table_stmt), expected);
}
} // namespace test_to_create_table