diff --git a/bindings/dart/test/lua_runner_test.dart b/bindings/dart/test/lua_runner_test.dart index 2ef1052..09cd8e8 100644 --- a/bindings/dart/test/lua_runner_test.dart +++ b/bindings/dart/test/lua_runner_test.dart @@ -271,7 +271,10 @@ void main() { ); try { db.createElement('Configuration', {'label': 'Config'}); - db.createElement('Collection', {'label': 'Item 1', 'value_int': [1, 2, 3]}); + db.createElement('Collection', { + 'label': 'Item 1', + 'value_int': [1, 2, 3] + }); final lua = LuaRunner(db); try { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b50a620..dada826 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,12 +3,15 @@ include(GoogleTest) add_executable(psr_database_tests test_database_create.cpp test_database_delete.cpp + test_database_errors.cpp test_database_lifecycle.cpp test_database_read.cpp test_database_relations.cpp test_database_update.cpp test_element.cpp test_lua_runner.cpp + test_migrations.cpp + test_row_result.cpp test_schema_validator.cpp ) diff --git a/tests/test_database_errors.cpp b/tests/test_database_errors.cpp new file mode 100644 index 0000000..7d6db96 --- /dev/null +++ b/tests/test_database_errors.cpp @@ -0,0 +1,325 @@ +#include "test_utils.h" + +#include +#include +#include + +// ============================================================================ +// No schema loaded error tests +// ============================================================================ + +TEST(DatabaseErrors, CreateElementNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + psr::Element element; + element.set("label", std::string("Test")); + + EXPECT_THROW(db.create_element("Configuration", element), std::runtime_error); +} + +TEST(DatabaseErrors, CreateElementCollectionNotFound) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element element; + element.set("label", std::string("Test")); + + EXPECT_THROW(db.create_element("NonexistentCollection", element), std::runtime_error); +} + +TEST(DatabaseErrors, CreateElementEmptyElement) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element element; // Empty element with no scalars + + EXPECT_THROW(db.create_element("Configuration", element), std::runtime_error); +} + +TEST(DatabaseErrors, CreateElementEmptyArray) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + // Create required Configuration first + psr::Element config; + config.set("label", std::string("Test Config")); + db.create_element("Configuration", config); + + // Try to create element with empty array + psr::Element element; + element.set("label", std::string("Item 1")).set("value_int", std::vector{}); + + EXPECT_THROW(db.create_element("Collection", element), std::runtime_error); +} + +// ============================================================================ +// Update error tests +// ============================================================================ + +TEST(DatabaseErrors, UpdateElementNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + psr::Element element; + element.set("label", std::string("Test")); + + EXPECT_THROW(db.update_element("Configuration", 1, element), std::runtime_error); +} + +TEST(DatabaseErrors, UpdateElementCollectionNotFound) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element element; + element.set("label", std::string("Test")); + + EXPECT_THROW(db.update_element("NonexistentCollection", 1, element), std::runtime_error); +} + +TEST(DatabaseErrors, UpdateElementEmptyElement) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + // Create an element first + psr::Element original; + original.set("label", std::string("Test")); + int64_t id = db.create_element("Configuration", original); + + // Try to update with empty element + psr::Element empty_element; + + EXPECT_THROW(db.update_element("Configuration", id, empty_element), std::runtime_error); +} + +// ============================================================================ +// Delete error tests +// ============================================================================ + +TEST(DatabaseErrors, DeleteElementNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.delete_element_by_id("Configuration", 1), std::runtime_error); +} + +TEST(DatabaseErrors, DeleteElementCollectionNotFound) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.delete_element_by_id("NonexistentCollection", 1), std::runtime_error); +} + +// ============================================================================ +// Read scalar error tests (no schema) +// ============================================================================ + +TEST(DatabaseErrors, ReadScalarIntegersNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + // Without schema, executing SQL directly will fail due to missing table + EXPECT_THROW(db.read_scalar_integers("Configuration", "integer_attribute"), std::runtime_error); +} + +TEST(DatabaseErrors, ReadScalarDoublesNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.read_scalar_doubles("Configuration", "float_attribute"), std::runtime_error); +} + +TEST(DatabaseErrors, ReadScalarStringsNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.read_scalar_strings("Configuration", "label"), std::runtime_error); +} + +// ============================================================================ +// Read vector error tests +// Note: read_vector_* methods without schema cause segfault (null pointer dereference) +// because impl_->schema->find_vector_table() is called without null check. +// These tests are skipped until the library adds proper null checks. +// ============================================================================ + +TEST(DatabaseErrors, ReadVectorIntegersCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + // Create required Configuration + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + EXPECT_THROW(db.read_vector_integers("NonexistentCollection", "value_int"), std::exception); +} + +TEST(DatabaseErrors, ReadVectorDoublesCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + EXPECT_THROW(db.read_vector_doubles("NonexistentCollection", "value_float"), std::exception); +} + +// ============================================================================ +// Read set error tests +// Note: read_set_* methods without schema cause segfault (null pointer dereference) +// because impl_->schema->find_set_table() is called without null check. +// These tests are skipped until the library adds proper null checks. +// ============================================================================ + +TEST(DatabaseErrors, ReadSetStringsCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + EXPECT_THROW(db.read_set_strings("NonexistentCollection", "tag"), std::exception); +} + +// ============================================================================ +// GetAttributeType error tests +// Note: get_attribute_type without schema causes segfault (null pointer dereference) +// because impl_->schema is dereferenced without null check. +// ============================================================================ + +TEST(DatabaseErrors, GetAttributeTypeCollectionNotFound) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.get_attribute_type("NonexistentCollection", "label"), std::runtime_error); +} + +TEST(DatabaseErrors, GetAttributeTypeAttributeNotFound) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.get_attribute_type("Configuration", "nonexistent_attribute"), std::runtime_error); +} + +// ============================================================================ +// Relation error tests +// ============================================================================ + +TEST(DatabaseErrors, SetScalarRelationNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.set_scalar_relation("Child", "parent_id", "Child 1", "Parent 1"), std::runtime_error); +} + +TEST(DatabaseErrors, SetScalarRelationCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.set_scalar_relation("NonexistentCollection", "parent_id", "Child 1", "Parent 1"), + std::runtime_error); +} + +TEST(DatabaseErrors, SetScalarRelationNotForeignKey) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // 'label' is not a foreign key + EXPECT_THROW(db.set_scalar_relation("Child", "label", "Child 1", "Parent 1"), std::runtime_error); +} + +TEST(DatabaseErrors, SetScalarRelationTargetNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create parent and child + psr::Element parent; + parent.set("label", std::string("Parent 1")); + db.create_element("Parent", parent); + + psr::Element child; + child.set("label", std::string("Child 1")); + db.create_element("Child", child); + + // Try to set relation to nonexistent parent + EXPECT_THROW(db.set_scalar_relation("Child", "parent_id", "Child 1", "Nonexistent Parent"), std::runtime_error); +} + +TEST(DatabaseErrors, ReadScalarRelationNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.read_scalar_relation("Child", "parent_id"), std::runtime_error); +} + +TEST(DatabaseErrors, ReadScalarRelationCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.read_scalar_relation("NonexistentCollection", "parent_id"), std::runtime_error); +} + +TEST(DatabaseErrors, ReadScalarRelationNotForeignKey) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // 'label' is not a foreign key + EXPECT_THROW(db.read_scalar_relation("Child", "label"), std::runtime_error); +} + +// ============================================================================ +// Update scalar error tests +// ============================================================================ + +TEST(DatabaseErrors, UpdateScalarIntegerNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.update_scalar_integer("Configuration", "integer_attribute", 1, 42), std::exception); +} + +TEST(DatabaseErrors, UpdateScalarDoubleNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.update_scalar_double("Configuration", "float_attribute", 1, 3.14), std::exception); +} + +TEST(DatabaseErrors, UpdateScalarStringNoSchema) { + psr::Database db(":memory:", {.console_level = psr::LogLevel::off}); + + EXPECT_THROW(db.update_scalar_string("Configuration", "label", 1, "new value"), std::exception); +} + +// ============================================================================ +// Update vector error tests +// Note: update_vector_* methods without schema cause segfault (null pointer dereference) +// because impl_->schema->find_vector_table() is called without null check. +// These tests use a loaded schema and test collection-not-found instead. +// ============================================================================ + +TEST(DatabaseErrors, UpdateVectorIntegersCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + EXPECT_THROW(db.update_vector_integers("NonexistentCollection", "value_int", 1, {1, 2, 3}), std::exception); +} + +TEST(DatabaseErrors, UpdateVectorDoublesCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + EXPECT_THROW(db.update_vector_doubles("NonexistentCollection", "value_float", 1, {1.5, 2.5}), std::exception); +} + +// ============================================================================ +// Update set error tests +// Note: update_set_* methods without schema cause segfault (null pointer dereference) +// because impl_->schema->find_set_table() is called without null check. +// These tests use a loaded schema and test collection-not-found instead. +// ============================================================================ + +TEST(DatabaseErrors, UpdateSetStringsCollectionNotFound) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + EXPECT_THROW(db.update_set_strings("NonexistentCollection", "tag", 1, {"a", "b"}), std::exception); +} diff --git a/tests/test_database_relations.cpp b/tests/test_database_relations.cpp index 04918c8..ce4fc81 100644 --- a/tests/test_database_relations.cpp +++ b/tests/test_database_relations.cpp @@ -50,3 +50,136 @@ TEST(Database, SetScalarRelationSelfReference) { EXPECT_EQ(relations[0], "Child 2"); EXPECT_EQ(relations[1], ""); // Child 2 has no sibling set } + +// ============================================================================ +// Read scalar relation edge cases +// ============================================================================ + +TEST(Database, ReadScalarRelationWithNulls) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create parent + psr::Element parent; + parent.set("label", std::string("Parent 1")); + db.create_element("Parent", parent); + + // Create children without setting parent_id relation + psr::Element child1; + child1.set("label", std::string("Child 1")); + db.create_element("Child", child1); + + psr::Element child2; + child2.set("label", std::string("Child 2")); + db.create_element("Child", child2); + + // Read relations - should return empty strings for unset relations + auto relations = db.read_scalar_relation("Child", "parent_id"); + EXPECT_EQ(relations.size(), 2); + EXPECT_EQ(relations[0], ""); // NULL parent + EXPECT_EQ(relations[1], ""); // NULL parent +} + +TEST(Database, ReadScalarRelationMixedNullsAndValues) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create parent + psr::Element parent; + parent.set("label", std::string("Parent 1")); + db.create_element("Parent", parent); + + // Create children + psr::Element child1; + child1.set("label", std::string("Child 1")); + db.create_element("Child", child1); + + psr::Element child2; + child2.set("label", std::string("Child 2")); + db.create_element("Child", child2); + + // Set relation for only one child + db.set_scalar_relation("Child", "parent_id", "Child 1", "Parent 1"); + + auto relations = db.read_scalar_relation("Child", "parent_id"); + EXPECT_EQ(relations.size(), 2); + EXPECT_EQ(relations[0], "Parent 1"); // Has parent + EXPECT_EQ(relations[1], ""); // NULL parent +} + +TEST(Database, ReadScalarRelationEmpty) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // No children created yet + auto relations = db.read_scalar_relation("Child", "parent_id"); + EXPECT_EQ(relations.size(), 0); +} + +// ============================================================================ +// Set scalar relation edge cases +// ============================================================================ + +TEST(Database, SetScalarRelationMultipleChildren) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create parent + psr::Element parent; + parent.set("label", std::string("Parent 1")); + db.create_element("Parent", parent); + + // Create children + psr::Element child1; + child1.set("label", std::string("Child 1")); + db.create_element("Child", child1); + + psr::Element child2; + child2.set("label", std::string("Child 2")); + db.create_element("Child", child2); + + psr::Element child3; + child3.set("label", std::string("Child 3")); + db.create_element("Child", child3); + + // Set relations for multiple children + db.set_scalar_relation("Child", "parent_id", "Child 1", "Parent 1"); + db.set_scalar_relation("Child", "parent_id", "Child 3", "Parent 1"); + + auto relations = db.read_scalar_relation("Child", "parent_id"); + EXPECT_EQ(relations.size(), 3); + EXPECT_EQ(relations[0], "Parent 1"); + EXPECT_EQ(relations[1], ""); // Child 2 has no parent + EXPECT_EQ(relations[2], "Parent 1"); +} + +TEST(Database, SetScalarRelationOverwrite) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("relations.sql"), {.console_level = psr::LogLevel::off}); + + // Create two parents + psr::Element parent1; + parent1.set("label", std::string("Parent 1")); + db.create_element("Parent", parent1); + + psr::Element parent2; + parent2.set("label", std::string("Parent 2")); + db.create_element("Parent", parent2); + + // Create child + psr::Element child; + child.set("label", std::string("Child 1")); + db.create_element("Child", child); + + // Set initial relation + db.set_scalar_relation("Child", "parent_id", "Child 1", "Parent 1"); + + auto relations = db.read_scalar_relation("Child", "parent_id"); + EXPECT_EQ(relations[0], "Parent 1"); + + // Overwrite relation + db.set_scalar_relation("Child", "parent_id", "Child 1", "Parent 2"); + + relations = db.read_scalar_relation("Child", "parent_id"); + EXPECT_EQ(relations[0], "Parent 2"); +} diff --git a/tests/test_lua_runner.cpp b/tests/test_lua_runner.cpp index 7f69e89..64c5d8c 100644 --- a/tests/test_lua_runner.cpp +++ b/tests/test_lua_runner.cpp @@ -742,3 +742,137 @@ TEST_F(LuaRunnerTest, ReadFromNonExistentCollection) { EXPECT_THROW( { lua.run(R"(local x = db:read_scalar_strings("NonexistentCollection", "label"))"); }, std::runtime_error); } + +// ============================================================================ +// Additional edge case tests +// ============================================================================ + +TEST_F(LuaRunnerTest, ReadScalarStringsEmpty) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + db.create_element("Configuration", psr::Element().set("label", "Config")); + + psr::LuaRunner lua(db); + + // No Collection elements created, should return empty table + lua.run(R"( + local labels = db:read_scalar_strings("Collection", "label") + assert(#labels == 0, "Expected empty table, got " .. #labels .. " items") + )"); +} + +TEST_F(LuaRunnerTest, ReadScalarIntegersEmpty) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + db.create_element("Configuration", psr::Element().set("label", "Config")); + + psr::LuaRunner lua(db); + + lua.run(R"( + local integers = db:read_scalar_integers("Collection", "some_integer") + assert(#integers == 0, "Expected empty table, got " .. #integers .. " items") + )"); +} + +TEST_F(LuaRunnerTest, ReadVectorIntegersEmpty) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + db.create_element("Configuration", psr::Element().set("label", "Config")); + + psr::LuaRunner lua(db); + + lua.run(R"( + local vectors = db:read_vector_integers("Collection", "value_int") + assert(#vectors == 0, "Expected empty table, got " .. #vectors .. " items") + )"); +} + +TEST_F(LuaRunnerTest, ReadSetStringsByIdEmpty) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + db.create_element("Configuration", psr::Element().set("label", "Config")); + + // Create a collection element without any set values + int64_t id = db.create_element("Collection", psr::Element().set("label", "Item 1")); + + psr::LuaRunner lua(db); + + std::string script = R"( + local set = db:read_set_strings_by_id("Collection", "tag", )" + + std::to_string(id) + R"() + assert(#set == 0, "Expected empty set, got " .. #set .. " items") + )"; + lua.run(script); +} + +TEST_F(LuaRunnerTest, CreateElementWithOnlyLabel) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + psr::LuaRunner lua(db); + + lua.run(R"( + db:create_element("Configuration", { label = "Test Config" }) + db:create_element("Collection", { label = "Item 1" }) + )"); + + auto labels = db.read_scalar_strings("Collection", "label"); + EXPECT_EQ(labels.size(), 1); + EXPECT_EQ(labels[0], "Item 1"); +} + +TEST_F(LuaRunnerTest, CreateElementMixedTypes) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + psr::LuaRunner lua(db); + + lua.run(R"( + db:create_element("Configuration", { label = "Test Config" }) + db:create_element("Collection", { + label = "Item 1", + some_integer = 42, + some_float = 3.14 + }) + )"); + + auto integers = db.read_scalar_integers("Collection", "some_integer"); + EXPECT_EQ(integers.size(), 1); + EXPECT_EQ(integers[0], 42); + + auto doubles = db.read_scalar_doubles("Collection", "some_float"); + EXPECT_EQ(doubles.size(), 1); + EXPECT_DOUBLE_EQ(doubles[0], 3.14); +} + +TEST_F(LuaRunnerTest, ReadVectorIntegersByIdFromLua) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + + db.create_element("Configuration", psr::Element().set("label", "Config")); + int64_t id1 = db.create_element( + "Collection", psr::Element().set("label", "Item 1").set("value_int", std::vector{10, 20, 30})); + + psr::LuaRunner lua(db); + + std::string script = R"( + local vec = db:read_vector_integers_by_id("Collection", "value_int", )" + + std::to_string(id1) + R"() + assert(#vec == 3, "Expected 3 elements, got " .. #vec) + assert(vec[1] == 10, "First element should be 10") + assert(vec[2] == 20, "Second element should be 20") + assert(vec[3] == 30, "Third element should be 30") + )"; + lua.run(script); +} + +TEST_F(LuaRunnerTest, ReadVectorDoublesByIdFromLua) { + auto db = psr::Database::from_schema(":memory:", collections_schema); + + db.create_element("Configuration", psr::Element().set("label", "Config")); + int64_t id1 = db.create_element( + "Collection", psr::Element().set("label", "Item 1").set("value_float", std::vector{1.1, 2.2, 3.3})); + + psr::LuaRunner lua(db); + + std::string script = R"( + local vec = db:read_vector_doubles_by_id("Collection", "value_float", )" + + std::to_string(id1) + R"() + assert(#vec == 3, "Expected 3 elements, got " .. #vec) + assert(vec[1] == 1.1, "First element should be 1.1") + assert(vec[2] == 2.2, "Second element should be 2.2") + assert(vec[3] == 3.3, "Third element should be 3.3") + )"; + lua.run(script); +} diff --git a/tests/test_migrations.cpp b/tests/test_migrations.cpp new file mode 100644 index 0000000..54acb7e --- /dev/null +++ b/tests/test_migrations.cpp @@ -0,0 +1,270 @@ +#include "test_utils.h" + +#include +#include +#include +#include +#include +#include + +namespace fs = std::filesystem; + +class MigrationsTestFixture : public ::testing::Test { +protected: + void SetUp() override { + temp_dir = (fs::temp_directory_path() / "psr_migrations_test").string(); + migrations_path = (fs::path(__FILE__).parent_path() / "schemas" / "migrations").string(); + + // Clean up temp directory if it exists + if (fs::exists(temp_dir)) { + fs::remove_all(temp_dir); + } + } + + void TearDown() override { + if (fs::exists(temp_dir)) { + fs::remove_all(temp_dir); + } + } + + std::string temp_dir; + std::string migrations_path; +}; + +// ============================================================================ +// Migration class tests +// ============================================================================ + +TEST_F(MigrationsTestFixture, MigrationUpSqlNonexistentPath) { + psr::Migration migration(1, "/nonexistent/path/to/migration"); + auto sql = migration.up_sql(); + EXPECT_TRUE(sql.empty()); +} + +TEST_F(MigrationsTestFixture, MigrationDownSqlNonexistentPath) { + psr::Migration migration(1, "/nonexistent/path/to/migration"); + auto sql = migration.down_sql(); + EXPECT_TRUE(sql.empty()); +} + +TEST_F(MigrationsTestFixture, MigrationComparisonOperatorsLessOrEqual) { + psr::Migration m1(1, migrations_path + "/1"); + psr::Migration m2(2, migrations_path + "/2"); + psr::Migration m1_copy(1, migrations_path + "/1"); + + EXPECT_TRUE(m1 <= m2); + EXPECT_TRUE(m1 <= m1_copy); + EXPECT_FALSE(m2 <= m1); +} + +TEST_F(MigrationsTestFixture, MigrationComparisonOperatorsGreaterOrEqual) { + psr::Migration m1(1, migrations_path + "/1"); + psr::Migration m2(2, migrations_path + "/2"); + psr::Migration m2_copy(2, migrations_path + "/2"); + + EXPECT_TRUE(m2 >= m1); + EXPECT_TRUE(m2 >= m2_copy); + EXPECT_FALSE(m1 >= m2); +} + +TEST_F(MigrationsTestFixture, MigrationComparisonOperatorsGreater) { + psr::Migration m1(1, migrations_path + "/1"); + psr::Migration m2(2, migrations_path + "/2"); + + EXPECT_TRUE(m2 > m1); + EXPECT_FALSE(m1 > m2); + EXPECT_FALSE(m1 > m1); +} + +TEST_F(MigrationsTestFixture, MigrationMoveSemantics) { + psr::Migration original(1, migrations_path + "/1"); + auto original_version = original.version(); + auto original_path = original.path(); + + psr::Migration moved = std::move(original); + + EXPECT_EQ(moved.version(), original_version); + EXPECT_EQ(moved.path(), original_path); +} + +TEST_F(MigrationsTestFixture, MigrationCopyAssignment) { + psr::Migration m1(1, migrations_path + "/1"); + psr::Migration m2(2, migrations_path + "/2"); + + m2 = m1; + + EXPECT_EQ(m2.version(), m1.version()); + EXPECT_EQ(m2.path(), m1.path()); +} + +TEST_F(MigrationsTestFixture, MigrationMoveAssignment) { + psr::Migration m1(1, migrations_path + "/1"); + psr::Migration m2(2, migrations_path + "/2"); + + auto m1_version = m1.version(); + auto m1_path = m1.path(); + + m2 = std::move(m1); + + EXPECT_EQ(m2.version(), m1_version); + EXPECT_EQ(m2.path(), m1_path); +} + +// ============================================================================ +// Migrations class tests +// ============================================================================ + +TEST_F(MigrationsTestFixture, MigrationsDefaultConstructor) { + psr::Migrations migrations; + + EXPECT_TRUE(migrations.empty()); + EXPECT_EQ(migrations.count(), 0u); + EXPECT_EQ(migrations.latest_version(), 0); +} + +TEST_F(MigrationsTestFixture, MigrationsPathIsFile) { + // Create a temporary file instead of directory + fs::create_directories(temp_dir); + auto file_path = fs::path(temp_dir) / "not_a_directory.txt"; + std::ofstream file(file_path); + file << "test content"; + file.close(); + + psr::Migrations migrations(file_path.string()); + + EXPECT_TRUE(migrations.empty()); + EXPECT_EQ(migrations.count(), 0u); +} + +TEST_F(MigrationsTestFixture, MigrationsCopySemantics) { + psr::Migrations original(migrations_path); + + psr::Migrations copy = original; + + EXPECT_EQ(copy.count(), original.count()); + EXPECT_EQ(copy.latest_version(), original.latest_version()); +} + +TEST_F(MigrationsTestFixture, MigrationsMoveSemantics) { + psr::Migrations original(migrations_path); + auto original_count = original.count(); + auto original_latest = original.latest_version(); + + psr::Migrations moved = std::move(original); + + EXPECT_EQ(moved.count(), original_count); + EXPECT_EQ(moved.latest_version(), original_latest); +} + +TEST_F(MigrationsTestFixture, MigrationsCopyAssignment) { + psr::Migrations m1(migrations_path); + psr::Migrations m2; + + m2 = m1; + + EXPECT_EQ(m2.count(), m1.count()); + EXPECT_EQ(m2.latest_version(), m1.latest_version()); +} + +TEST_F(MigrationsTestFixture, MigrationsMoveAssignment) { + psr::Migrations m1(migrations_path); + auto m1_count = m1.count(); + auto m1_latest = m1.latest_version(); + + psr::Migrations m2; + m2 = std::move(m1); + + EXPECT_EQ(m2.count(), m1_count); + EXPECT_EQ(m2.latest_version(), m1_latest); +} + +TEST_F(MigrationsTestFixture, MigrationsSelfAssignment) { + psr::Migrations migrations(migrations_path); + auto count = migrations.count(); + + migrations = migrations; + + EXPECT_EQ(migrations.count(), count); +} + +// ============================================================================ +// Database migration error tests +// ============================================================================ + +TEST_F(MigrationsTestFixture, DatabaseMigrationWithEmptyUpSql) { + // Create a migration directory with empty up.sql + fs::create_directories(fs::path(temp_dir) / "1"); + std::ofstream up_file(fs::path(temp_dir) / "1" / "up.sql"); + up_file << ""; // Empty SQL + up_file.close(); + + // Empty up.sql should cause migration to fail + EXPECT_THROW(psr::Database::from_migrations(":memory:", temp_dir, {.console_level = psr::LogLevel::off}), + std::runtime_error); +} + +TEST_F(MigrationsTestFixture, DatabaseMigrationWithInvalidSQL) { + // Create a migration directory with invalid SQL + fs::create_directories(fs::path(temp_dir) / "1"); + std::ofstream up_file(fs::path(temp_dir) / "1" / "up.sql"); + up_file << "THIS IS NOT VALID SQL AT ALL;"; + up_file.close(); + + EXPECT_THROW(psr::Database::from_migrations(":memory:", temp_dir, {.console_level = psr::LogLevel::off}), + std::runtime_error); +} + +TEST_F(MigrationsTestFixture, MigrationsWithNonNumericDirectories) { + // Create directories with non-numeric names + fs::create_directories(fs::path(temp_dir) / "abc"); + fs::create_directories(fs::path(temp_dir) / "not_a_number"); + + psr::Migrations migrations(temp_dir); + + // Non-numeric directories should be ignored + EXPECT_TRUE(migrations.empty()); +} + +TEST_F(MigrationsTestFixture, MigrationsWithMixedDirectories) { + // Create both valid and invalid directories + fs::create_directories(fs::path(temp_dir) / "1"); + fs::create_directories(fs::path(temp_dir) / "abc"); + fs::create_directories(fs::path(temp_dir) / "2"); + fs::create_directories(fs::path(temp_dir) / "not_a_number"); + + // Create up.sql files + std::ofstream(fs::path(temp_dir) / "1" / "up.sql") << "CREATE TABLE Test1 (id INTEGER PRIMARY KEY);"; + std::ofstream(fs::path(temp_dir) / "2" / "up.sql") << "CREATE TABLE Test2 (id INTEGER PRIMARY KEY);"; + + psr::Migrations migrations(temp_dir); + + // Only numeric directories should be counted + EXPECT_EQ(migrations.count(), 2u); + EXPECT_EQ(migrations.latest_version(), 2); +} + +TEST_F(MigrationsTestFixture, MigrationsWithZeroVersionDirectory) { + // Version 0 should be ignored (invalid) + fs::create_directories(fs::path(temp_dir) / "0"); + fs::create_directories(fs::path(temp_dir) / "1"); + + std::ofstream(fs::path(temp_dir) / "0" / "up.sql") << "CREATE TABLE Test0 (id INTEGER PRIMARY KEY);"; + std::ofstream(fs::path(temp_dir) / "1" / "up.sql") << "CREATE TABLE Test1 (id INTEGER PRIMARY KEY);"; + + psr::Migrations migrations(temp_dir); + + // Version 0 should be ignored + EXPECT_EQ(migrations.count(), 1u); + EXPECT_EQ(migrations.latest_version(), 1); +} + +TEST_F(MigrationsTestFixture, MigrationsWithNegativeVersionDirectory) { + // Negative versions should be ignored (can't parse as valid) + fs::create_directories(fs::path(temp_dir) / "1"); + + std::ofstream(fs::path(temp_dir) / "1" / "up.sql") << "CREATE TABLE Test1 (id INTEGER PRIMARY KEY);"; + + psr::Migrations migrations(temp_dir); + + EXPECT_EQ(migrations.count(), 1u); +} diff --git a/tests/test_row_result.cpp b/tests/test_row_result.cpp new file mode 100644 index 0000000..a24f93a --- /dev/null +++ b/tests/test_row_result.cpp @@ -0,0 +1,291 @@ +#include "test_utils.h" + +#include +#include +#include +#include +#include + +// ============================================================================ +// Row boundary tests +// ============================================================================ + +TEST(Row, EmptyRow) { + psr::Row row(std::vector{}); + + EXPECT_TRUE(row.empty()); + EXPECT_EQ(row.size(), 0u); + EXPECT_EQ(row.column_count(), 0u); +} + +TEST(Row, AtOutOfBounds) { + psr::Row row(std::vector{int64_t{42}}); + + EXPECT_THROW(row.at(1), std::out_of_range); + EXPECT_THROW(row.at(100), std::out_of_range); +} + +TEST(Row, OperatorBracketValidIndex) { + psr::Row row(std::vector{int64_t{42}, std::string("test"), 3.14}); + + // Access valid indices + EXPECT_TRUE(std::holds_alternative(row[0])); + EXPECT_TRUE(std::holds_alternative(row[1])); + EXPECT_TRUE(std::holds_alternative(row[2])); +} + +TEST(Row, IsNullOutOfBounds) { + psr::Row row(std::vector{int64_t{42}}); + + // Out of bounds returns true for is_null + EXPECT_TRUE(row.is_null(1)); + EXPECT_TRUE(row.is_null(100)); +} + +TEST(Row, IsNullTrueForNullValue) { + psr::Row row(std::vector{nullptr}); + + EXPECT_TRUE(row.is_null(0)); +} + +TEST(Row, IsNullFalseForNonNull) { + psr::Row row(std::vector{int64_t{42}}); + + EXPECT_FALSE(row.is_null(0)); +} + +TEST(Row, GetIntOutOfBounds) { + psr::Row row(std::vector{int64_t{42}}); + + auto result = row.get_int(1); + EXPECT_FALSE(result.has_value()); + + result = row.get_int(100); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetDoubleOutOfBounds) { + psr::Row row(std::vector{3.14}); + + auto result = row.get_double(1); + EXPECT_FALSE(result.has_value()); + + result = row.get_double(100); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetStringOutOfBounds) { + psr::Row row(std::vector{std::string("test")}); + + auto result = row.get_string(1); + EXPECT_FALSE(result.has_value()); + + result = row.get_string(100); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetIntWrongType) { + psr::Row row(std::vector{std::string("not an int")}); + + auto result = row.get_int(0); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetDoubleWrongType) { + psr::Row row(std::vector{std::string("not a double")}); + + auto result = row.get_double(0); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetStringWrongType) { + psr::Row row(std::vector{int64_t{42}}); + + auto result = row.get_string(0); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetIntFromNull) { + psr::Row row(std::vector{nullptr}); + + auto result = row.get_int(0); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetDoubleFromNull) { + psr::Row row(std::vector{nullptr}); + + auto result = row.get_double(0); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, GetStringFromNull) { + psr::Row row(std::vector{nullptr}); + + auto result = row.get_string(0); + EXPECT_FALSE(result.has_value()); +} + +TEST(Row, IteratorSupport) { + std::vector values = {int64_t{1}, int64_t{2}, int64_t{3}}; + psr::Row row(values); + + int count = 0; + for (const auto& val : row) { + EXPECT_TRUE(std::holds_alternative(val)); + ++count; + } + EXPECT_EQ(count, 3); +} + +// ============================================================================ +// Result tests +// ============================================================================ + +TEST(Result, DefaultConstructor) { + psr::Result result; + + EXPECT_TRUE(result.empty()); + EXPECT_EQ(result.row_count(), 0u); + EXPECT_EQ(result.column_count(), 0u); +} + +TEST(Result, ColumnsAccessor) { + std::vector columns = {"id", "name", "value"}; + std::vector rows; + + psr::Result result(columns, std::move(rows)); + + auto& cols = result.columns(); + EXPECT_EQ(cols.size(), 3u); + EXPECT_EQ(cols[0], "id"); + EXPECT_EQ(cols[1], "name"); + EXPECT_EQ(cols[2], "value"); +} + +TEST(Result, AtOutOfBounds) { + psr::Result result; + + EXPECT_THROW(result.at(0), std::out_of_range); + EXPECT_THROW(result.at(100), std::out_of_range); +} + +TEST(Result, EmptyResult) { + std::vector columns = {"id", "name"}; + std::vector rows; + + psr::Result result(columns, std::move(rows)); + + EXPECT_TRUE(result.empty()); + EXPECT_EQ(result.row_count(), 0u); + EXPECT_EQ(result.column_count(), 2u); // Columns exist but no rows +} + +TEST(Result, IteratorOnEmpty) { + psr::Result result; + + int count = 0; + for (const auto& row : result) { + (void)row; + ++count; + } + EXPECT_EQ(count, 0); +} + +TEST(Result, IteratorOnNonEmpty) { + std::vector columns = {"value"}; + std::vector rows; + rows.emplace_back(std::vector{int64_t{1}}); + rows.emplace_back(std::vector{int64_t{2}}); + rows.emplace_back(std::vector{int64_t{3}}); + + psr::Result result(columns, std::move(rows)); + + int count = 0; + for (const auto& row : result) { + EXPECT_EQ(row.size(), 1u); + ++count; + } + EXPECT_EQ(count, 3); +} + +TEST(Result, OperatorBracketValid) { + std::vector columns = {"value"}; + std::vector rows; + rows.emplace_back(std::vector{int64_t{42}}); + + psr::Result result(columns, std::move(rows)); + + const auto& row = result[0]; + EXPECT_EQ(row.get_int(0).value(), 42); +} + +TEST(Result, MixedValueTypes) { + std::vector columns = {"int_col", "double_col", "string_col", "null_col"}; + std::vector rows; + rows.emplace_back(std::vector{int64_t{42}, 3.14, std::string("hello"), nullptr}); + + psr::Result result(columns, std::move(rows)); + + EXPECT_EQ(result.row_count(), 1u); + EXPECT_EQ(result.column_count(), 4u); + + const auto& row = result[0]; + EXPECT_EQ(row.get_int(0).value(), 42); + EXPECT_DOUBLE_EQ(row.get_double(1).value(), 3.14); + EXPECT_EQ(row.get_string(2).value(), "hello"); + EXPECT_TRUE(row.is_null(3)); +} + +// ============================================================================ +// Integration tests with Database +// ============================================================================ + +TEST(RowResult, ReadScalarWithNullValues) { + auto db = + psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), {.console_level = psr::LogLevel::off}); + + // Create required Configuration + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + // Create elements without optional scalar attributes + psr::Element e1; + e1.set("label", std::string("Item 1")); + db.create_element("Collection", e1); + + psr::Element e2; + e2.set("label", std::string("Item 2")).set("some_integer", int64_t{42}); + db.create_element("Collection", e2); + + // Read scalars - only non-null values are returned + auto integers = db.read_scalar_integers("Collection", "some_integer"); + EXPECT_EQ(integers.size(), 1u); + EXPECT_EQ(integers[0], 42); +} + +TEST(RowResult, ReadScalarByIdWithNull) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + // Create element with minimal required fields + psr::Element e; + e.set("label", std::string("Config")); + int64_t id = db.create_element("Configuration", e); + + // Read optional float attribute (should be nullopt since we didn't set it) + // Note: integer_attribute has DEFAULT 6, so we use float_attribute instead + auto result = db.read_scalar_doubles_by_id("Configuration", "float_attribute", id); + EXPECT_FALSE(result.has_value()); +} + +TEST(RowResult, EmptyResultFromQuery) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), {.console_level = psr::LogLevel::off}); + + // No elements created - should return empty vectors + auto labels = db.read_scalar_strings("Configuration", "label"); + EXPECT_TRUE(labels.empty()); + + auto integers = db.read_scalar_integers("Configuration", "integer_attribute"); + EXPECT_TRUE(integers.empty()); +} diff --git a/tests/test_schema_validator.cpp b/tests/test_schema_validator.cpp index b1ae531..02b3fb3 100644 --- a/tests/test_schema_validator.cpp +++ b/tests/test_schema_validator.cpp @@ -64,3 +64,103 @@ TEST_F(SchemaValidatorFixture, InvalidFkNotNullSetNull) { TEST_F(SchemaValidatorFixture, InvalidFkActions) { EXPECT_THROW(psr::Database::from_schema(":memory:", INVALID_SCHEMA("fk_actions.sql"), opts), std::runtime_error); } + +// ============================================================================ +// Type validation tests (via create_element errors) +// ============================================================================ + +TEST_F(SchemaValidatorFixture, TypeMismatchIntegerExpectedReal) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), opts); + + // Try to set a string where a float is expected + psr::Element e; + e.set("label", std::string("Test")).set("float_attribute", std::string("not a float")); + + EXPECT_THROW(db.create_element("Configuration", e), std::runtime_error); +} + +TEST_F(SchemaValidatorFixture, TypeMismatchStringExpectedInteger) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), opts); + + // Try to set a string where an integer is expected + psr::Element e; + e.set("label", std::string("Test")).set("integer_attribute", std::string("not an integer")); + + EXPECT_THROW(db.create_element("Configuration", e), std::runtime_error); +} + +TEST_F(SchemaValidatorFixture, TypeMismatchIntegerExpectedText) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), opts); + + // Try to set an integer where a string is expected + psr::Element e; + e.set("label", int64_t{42}); // label expects TEXT + + EXPECT_THROW(db.create_element("Configuration", e), std::runtime_error); +} + +TEST_F(SchemaValidatorFixture, ArrayTypeValidation) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), opts); + + psr::Element config; + config.set("label", std::string("Config")); + db.create_element("Configuration", config); + + // Try to create element with wrong array type + psr::Element e; + e.set("label", std::string("Item 1")); + // value_int expects integers, but we'll try to pass strings + // Note: This depends on how the Element class handles type conversion + e.set("value_int", std::vector{"not", "integers"}); + + EXPECT_THROW(db.create_element("Collection", e), std::runtime_error); +} + +// ============================================================================ +// Schema attribute lookup tests +// ============================================================================ + +TEST_F(SchemaValidatorFixture, GetAttributeTypeScalarInteger) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), opts); + + auto type = db.get_attribute_type("Configuration", "integer_attribute"); + + EXPECT_EQ(type.structure, psr::AttributeStructure::Scalar); + EXPECT_EQ(type.data_type, psr::AttributeDataType::Integer); +} + +TEST_F(SchemaValidatorFixture, GetAttributeTypeScalarReal) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), opts); + + auto type = db.get_attribute_type("Configuration", "float_attribute"); + + EXPECT_EQ(type.structure, psr::AttributeStructure::Scalar); + EXPECT_EQ(type.data_type, psr::AttributeDataType::Real); +} + +TEST_F(SchemaValidatorFixture, GetAttributeTypeScalarText) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("basic.sql"), opts); + + auto type = db.get_attribute_type("Configuration", "label"); + + EXPECT_EQ(type.structure, psr::AttributeStructure::Scalar); + EXPECT_EQ(type.data_type, psr::AttributeDataType::Text); +} + +TEST_F(SchemaValidatorFixture, GetAttributeTypeVectorInteger) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), opts); + + auto type = db.get_attribute_type("Collection", "value_int"); + + EXPECT_EQ(type.structure, psr::AttributeStructure::Vector); + EXPECT_EQ(type.data_type, psr::AttributeDataType::Integer); +} + +TEST_F(SchemaValidatorFixture, GetAttributeTypeSetText) { + auto db = psr::Database::from_schema(":memory:", VALID_SCHEMA("collections.sql"), opts); + + auto type = db.get_attribute_type("Collection", "tag"); + + EXPECT_EQ(type.structure, psr::AttributeStructure::Set); + EXPECT_EQ(type.data_type, psr::AttributeDataType::Text); +}