mirror of
https://github.com/latentPrion/cppbessot.git
synced 2026-04-17 18:44:23 +00:00
Add hydration tests for the ODB ORM. Apparently it works.
This commit is contained in:
52
tests/odb-orm/CMakeLists.txt
Normal file
52
tests/odb-orm/CMakeLists.txt
Normal file
@@ -0,0 +1,52 @@
|
||||
include(GoogleTest)
|
||||
|
||||
set(CPP_ODB_TEST_NAME_SUFFIX "${DB_SCHEMA_DIR_TO_GENERATE}")
|
||||
string(REPLACE "." "_" CPP_ODB_TEST_NAME_SUFFIX "${CPP_ODB_TEST_NAME_SUFFIX}")
|
||||
string(REPLACE "-" "_" CPP_ODB_TEST_NAME_SUFFIX "${CPP_ODB_TEST_NAME_SUFFIX}")
|
||||
|
||||
function(cppbessot_add_odb_orm_test backend target_suffix connstr_var source_file link_target)
|
||||
if("${${connstr_var}}" STREQUAL "")
|
||||
message(STATUS "Skipping ${backend} ODB runtime tests because ${connstr_var} is empty.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(_target_name "cpp_odb_orm_${target_suffix}_${CPP_ODB_TEST_NAME_SUFFIX}")
|
||||
add_executable(${_target_name} "${source_file}")
|
||||
add_dependencies(${_target_name} db_gen_sql_ddl)
|
||||
target_compile_features(${_target_name} PRIVATE cxx_std_20)
|
||||
target_include_directories(${_target_name} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
target_link_libraries(${_target_name}
|
||||
PRIVATE
|
||||
cppbessot::openai_model_gen
|
||||
${link_target}
|
||||
GTest::gtest_main)
|
||||
target_compile_definitions(${_target_name}
|
||||
PRIVATE
|
||||
CPPBESSOT_ODB_TEST_SQL_DIR="${PROJECT_SOURCE_DIR}/db/${DB_SCHEMA_DIR_TO_GENERATE}/generated-sql-ddl/${backend}"
|
||||
${connstr_var}_DEFAULT="${${connstr_var}}")
|
||||
|
||||
if("${backend}" STREQUAL "sqlite")
|
||||
target_link_libraries(${_target_name} PRIVATE "${CPPBESSOT_SQLITE_CLIENT_LIB}")
|
||||
elseif("${backend}" STREQUAL "postgre")
|
||||
target_include_directories(${_target_name} PRIVATE "${CPPBESSOT_PGSQL_INCLUDE_DIR}")
|
||||
target_link_libraries(${_target_name} PRIVATE "${CPPBESSOT_PGSQL_CLIENT_LIB}")
|
||||
endif()
|
||||
|
||||
gtest_discover_tests(${_target_name}
|
||||
PROPERTIES
|
||||
ENVIRONMENT "${connstr_var}=${${connstr_var}}")
|
||||
endfunction()
|
||||
|
||||
cppbessot_add_odb_orm_test(
|
||||
"sqlite"
|
||||
"sqlite"
|
||||
CPPBESSOT_ODB_TEST_SQLITE_CONNSTR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/sqlite_orm_test.cpp"
|
||||
cppbessot::odb_sqlite)
|
||||
|
||||
cppbessot_add_odb_orm_test(
|
||||
"pgsql"
|
||||
"pgsql"
|
||||
CPPBESSOT_ODB_TEST_PGSQL_CONNSTR
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/pgsql_orm_test.cpp"
|
||||
cppbessot::odb_pgsql)
|
||||
159
tests/odb-orm/orm_test_common.h
Normal file
159
tests/odb-orm/orm_test_common.h
Normal file
@@ -0,0 +1,159 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <cppbessot/model/Agent.h>
|
||||
|
||||
inline std::string cppbessot_required_env(const char* name)
|
||||
{
|
||||
const char* value = std::getenv(name);
|
||||
if(value == nullptr || value[0] == '\0')
|
||||
{
|
||||
throw std::runtime_error(std::string("missing required environment variable: ") + name);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
inline std::string cppbessot_env_or_default(const char* name, const char* fallback)
|
||||
{
|
||||
const char* value = std::getenv(name);
|
||||
if(value != nullptr && value[0] != '\0')
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
if(fallback != nullptr && fallback[0] != '\0')
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
throw std::runtime_error(std::string("missing required environment variable: ") + name);
|
||||
}
|
||||
|
||||
inline std::vector<std::filesystem::path> cppbessot_sql_files()
|
||||
{
|
||||
const std::filesystem::path sql_dir(CPPBESSOT_ODB_TEST_SQL_DIR);
|
||||
if(!std::filesystem::is_directory(sql_dir))
|
||||
{
|
||||
throw std::runtime_error("SQL DDL directory does not exist: " + sql_dir.string());
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> files;
|
||||
for(const auto& entry : std::filesystem::directory_iterator(sql_dir))
|
||||
{
|
||||
if(entry.is_regular_file() && entry.path().extension() == ".sql")
|
||||
{
|
||||
files.push_back(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(files.begin(), files.end());
|
||||
if(files.empty())
|
||||
{
|
||||
throw std::runtime_error("No SQL DDL files found in " + sql_dir.string());
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
inline std::string cppbessot_read_text_file(const std::filesystem::path& path)
|
||||
{
|
||||
std::ifstream stream(path);
|
||||
if(!stream)
|
||||
{
|
||||
throw std::runtime_error("Failed to open file: " + path.string());
|
||||
}
|
||||
|
||||
std::ostringstream buffer;
|
||||
buffer << stream.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
template <typename Database, typename Transaction>
|
||||
void cppbessot_run_agent_orm_roundtrip(Database& db)
|
||||
{
|
||||
{
|
||||
Transaction t(db.begin());
|
||||
models::Agent first{};
|
||||
first.id = "agent-orm-1";
|
||||
first.role = "REQUESTER";
|
||||
first.persistent = true;
|
||||
first.displayName = "ODB Agent";
|
||||
db.persist(first);
|
||||
|
||||
models::Agent second{};
|
||||
second.id = "agent-orm-2";
|
||||
second.role = "PROVIDER";
|
||||
second.persistent = false;
|
||||
second.displayName = "ODB Agent Secondary";
|
||||
db.persist(second);
|
||||
t.commit();
|
||||
}
|
||||
|
||||
{
|
||||
Transaction t(db.begin());
|
||||
models::Agent loaded{};
|
||||
db.template load<models::Agent>("agent-orm-1", loaded);
|
||||
EXPECT_EQ(loaded.id, "agent-orm-1");
|
||||
EXPECT_EQ(loaded.role, "REQUESTER");
|
||||
EXPECT_TRUE(loaded.persistent);
|
||||
EXPECT_EQ(loaded.displayName, "ODB Agent");
|
||||
|
||||
loaded.displayName = "ODB Agent Updated";
|
||||
db.update(loaded);
|
||||
t.commit();
|
||||
}
|
||||
|
||||
{
|
||||
Transaction t(db.begin());
|
||||
using query = odb::query<models::Agent>;
|
||||
odb::result<models::Agent> results(
|
||||
db.template query<models::Agent>(query::persistent == true || query::role == "PROVIDER"));
|
||||
bool saw_updated_agent = false;
|
||||
bool saw_secondary_agent = false;
|
||||
std::size_t count = 0;
|
||||
for(const models::Agent& row : results)
|
||||
{
|
||||
++count;
|
||||
if(row.id == "agent-orm-1")
|
||||
{
|
||||
saw_updated_agent = true;
|
||||
EXPECT_EQ(row.role, "REQUESTER");
|
||||
EXPECT_TRUE(row.persistent);
|
||||
EXPECT_EQ(row.displayName, "ODB Agent Updated");
|
||||
}
|
||||
else if(row.id == "agent-orm-2")
|
||||
{
|
||||
saw_secondary_agent = true;
|
||||
EXPECT_EQ(row.role, "PROVIDER");
|
||||
EXPECT_FALSE(row.persistent);
|
||||
EXPECT_EQ(row.displayName, "ODB Agent Secondary");
|
||||
}
|
||||
else
|
||||
{
|
||||
ADD_FAILURE() << "unexpected hydrated Agent row id: " << row.id;
|
||||
}
|
||||
}
|
||||
EXPECT_EQ(count, 2U);
|
||||
EXPECT_TRUE(saw_updated_agent);
|
||||
EXPECT_TRUE(saw_secondary_agent);
|
||||
|
||||
db.template erase<models::Agent>("agent-orm-1");
|
||||
db.template erase<models::Agent>("agent-orm-2");
|
||||
t.commit();
|
||||
}
|
||||
|
||||
{
|
||||
Transaction t(db.begin());
|
||||
EXPECT_FALSE(db.template find<models::Agent>("agent-orm-1"));
|
||||
EXPECT_FALSE(db.template find<models::Agent>("agent-orm-2"));
|
||||
t.commit();
|
||||
}
|
||||
}
|
||||
62
tests/odb-orm/pgsql_orm_test.cpp
Normal file
62
tests/odb-orm/pgsql_orm_test.cpp
Normal file
@@ -0,0 +1,62 @@
|
||||
#include <odb/pgsql/database.hxx>
|
||||
#include <odb/result.hxx>
|
||||
#include <odb/pgsql/transaction.hxx>
|
||||
#include <libpq-fe.h>
|
||||
|
||||
#include "Agent-odb.hxx"
|
||||
#include "orm_test_common.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void exec_pgsql(PGconn* conn, const std::string& sql)
|
||||
{
|
||||
PGresult* result = PQexec(conn, sql.c_str());
|
||||
if(result == nullptr)
|
||||
{
|
||||
throw std::runtime_error("PQexec returned null");
|
||||
}
|
||||
|
||||
const ExecStatusType status = PQresultStatus(result);
|
||||
if(status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK)
|
||||
{
|
||||
const std::string message = PQerrorMessage(conn);
|
||||
PQclear(result);
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
PQclear(result);
|
||||
}
|
||||
|
||||
void apply_pgsql_ddl(const std::string& connstr)
|
||||
{
|
||||
PGconn* conn = PQconnectdb(connstr.c_str());
|
||||
if(PQstatus(conn) != CONNECTION_OK)
|
||||
{
|
||||
const std::string message = PQerrorMessage(conn);
|
||||
PQfinish(conn);
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
exec_pgsql(conn, "DROP TABLE IF EXISTS \"TripAttemptResult\" CASCADE;");
|
||||
exec_pgsql(conn, "DROP TABLE IF EXISTS \"GovernmentAddress\" CASCADE;");
|
||||
exec_pgsql(conn, "DROP TABLE IF EXISTS \"Agent\" CASCADE;");
|
||||
|
||||
for(const auto& sql_file : cppbessot_sql_files())
|
||||
{
|
||||
exec_pgsql(conn, cppbessot_read_text_file(sql_file));
|
||||
}
|
||||
|
||||
PQfinish(conn);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(PgsqlOdbOrm, PersistsLoadsQueriesAndErases)
|
||||
{
|
||||
const std::string connstr = cppbessot_env_or_default(
|
||||
"CPPBESSOT_ODB_TEST_PGSQL_CONNSTR",
|
||||
CPPBESSOT_ODB_TEST_PGSQL_CONNSTR_DEFAULT);
|
||||
apply_pgsql_ddl(connstr);
|
||||
odb::pgsql::database db(connstr);
|
||||
cppbessot_run_agent_orm_roundtrip<odb::pgsql::database, odb::pgsql::transaction>(db);
|
||||
}
|
||||
52
tests/odb-orm/sqlite_orm_test.cpp
Normal file
52
tests/odb-orm/sqlite_orm_test.cpp
Normal file
@@ -0,0 +1,52 @@
|
||||
#include <odb/sqlite/database.hxx>
|
||||
#include <odb/result.hxx>
|
||||
#include <odb/sqlite/transaction.hxx>
|
||||
#include <sqlite3.h>
|
||||
|
||||
#include "Agent-odb.hxx"
|
||||
#include "orm_test_common.h"
|
||||
|
||||
namespace {
|
||||
|
||||
void apply_sqlite_ddl(const std::string& connstr)
|
||||
{
|
||||
sqlite3* handle = nullptr;
|
||||
if(sqlite3_open(connstr.c_str(), &handle) != SQLITE_OK)
|
||||
{
|
||||
const std::string message = handle != nullptr ? sqlite3_errmsg(handle) : "sqlite open failed";
|
||||
if(handle != nullptr)
|
||||
{
|
||||
sqlite3_close(handle);
|
||||
}
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
|
||||
for(const auto& sql_file : cppbessot_sql_files())
|
||||
{
|
||||
const std::string sql = cppbessot_read_text_file(sql_file);
|
||||
char* error_message = nullptr;
|
||||
const int rc = sqlite3_exec(handle, sql.c_str(), nullptr, nullptr, &error_message);
|
||||
if(rc != SQLITE_OK)
|
||||
{
|
||||
const std::string message = error_message != nullptr ? error_message : "sqlite3_exec failed";
|
||||
sqlite3_free(error_message);
|
||||
sqlite3_close(handle);
|
||||
throw std::runtime_error(message + " while applying " + sql_file.string());
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_close(handle);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(SqliteOdbOrm, PersistsLoadsQueriesAndErases)
|
||||
{
|
||||
const std::string connstr = cppbessot_env_or_default(
|
||||
"CPPBESSOT_ODB_TEST_SQLITE_CONNSTR",
|
||||
CPPBESSOT_ODB_TEST_SQLITE_CONNSTR_DEFAULT);
|
||||
std::filesystem::remove(connstr);
|
||||
apply_sqlite_ddl(connstr);
|
||||
odb::sqlite::database db(connstr, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
|
||||
cppbessot_run_agent_orm_roundtrip<odb::sqlite::database, odb::sqlite::transaction>(db);
|
||||
}
|
||||
Reference in New Issue
Block a user