diff --git a/include/query_predicates.h b/include/query_predicates.h index 026616a..e29b275 100644 --- a/include/query_predicates.h +++ b/include/query_predicates.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -82,13 +83,19 @@ class REFLECTION_EXPORT QueryPredicate : public QueryPredicateBase { : symbol_(symbol) { auto record = GetRecordFromTypeId(typeid(T).name()); auto offset = OffsetFromStart(fn); + auto found = false; for (auto i = 0; i < record.member_metadata.size(); ++i) { if (record.member_metadata[i].offset == offset) { member_name_ = record.member_metadata[i].name; value_ = value_retrieval((void*)&value, record.member_metadata[i].storage_class); + found = true; break; } } + if (!found) { + throw std::runtime_error("No registered member of '" + record.name + + "' matches the given pointer-to-member (type id: " + typeid(T).name() + ")"); + } } template diff --git a/include/reflection.h b/include/reflection.h index d0a97c7..6b046de 100644 --- a/include/reflection.h +++ b/include/reflection.h @@ -195,7 +195,10 @@ static std::string CAT(Register, REFLECTABLE)() { ReflectionRegister& instance = *GetReflectionRegisterInstance(); auto isRecordRegisterd = instance.records.find(type_id) != instance.records.end(); if (!isRecordRegisterd) { - auto& reflectable = GetRecordFromTypeId(type_id); + // Create the entry directly (operator[] default-inserts on miss); this is registration's + // own create path, scoped by the find() guard above, and is intentionally not routed + // through GetRecordFromTypeId, which is a pure lookup that throws on a miss + auto& reflectable = instance.records[type_id]; reflectable.name = name; // store member metadata diff --git a/src/reflection.cc b/src/reflection.cc index d1e50b3..20934fa 100644 --- a/src/reflection.cc +++ b/src/reflection.cc @@ -23,6 +23,7 @@ #include "reflection.h" #include +#include static std::unique_ptr p = nullptr; @@ -35,8 +36,11 @@ ReflectionRegister* GetReflectionRegisterInstance() { Reflection& GetRecordFromTypeId(const std::string& type_id) { ReflectionRegister& instance = *GetReflectionRegisterInstance(); - auto& meta_struct = instance.records[type_id]; - return meta_struct; + auto it = instance.records.find(type_id); + if (it == instance.records.end()) { + throw std::runtime_error("Reflection lookup failed: type not registered: " + type_id); + } + return it->second; } char* GetMemberAddress(void* precord, const Reflection& record, const size_t i) { diff --git a/tests/query_predicates_test.cc b/tests/query_predicates_test.cc index f3de274..079152f 100644 --- a/tests/query_predicates_test.cc +++ b/tests/query_predicates_test.cc @@ -24,6 +24,9 @@ #include +#include +#include + #include "person.h" #include "pet.h" @@ -207,3 +210,56 @@ TEST(QueryPredicatesTest, LikeInsideAndSurvivesCloneWithEscapeClause) { EXPECT_EQ(R"(%50\%%)", bindings[0].text_value); EXPECT_EQ(30, bindings[1].int_value); } + +namespace { +// A plain struct that is deliberately never run through the REFLECTABLE/FIELDS registration +// macros, so its type id never appears in the reflection registry. +struct UnregisteredRecord { + int64_t id; + int64_t value; +}; + +// A plain struct that is also never run through the registration macros, but is manually and +// incompletely registered below (name only, no member metadata) to exercise the +// registered-but-offset-mismatch guard, as distinct from the unregistered-type guard above. +struct MismatchedRecord { + int64_t id; + int64_t value; +}; + +// Erases a hand-inserted entry from the process-wide reflection registry on scope exit. Without +// this, a MismatchedRecord-shaped entry with no member metadata would linger in the registry for +// the rest of the test binary: Database::Database iterates every registered record and would +// generate "CREATE TABLE IF NOT EXISTS MismatchedRecord ();" (empty column list) for it, failing +// every later Database::Initialize() call in this process. +class ScopedRegistryCleanup { +public: + explicit ScopedRegistryCleanup(std::string type_id) : type_id_(std::move(type_id)) {} + ~ScopedRegistryCleanup() { + GetReflectionRegisterInstance()->records.erase(type_id_); + } + +private: + std::string type_id_; +}; +} // namespace + +TEST(QueryPredicatesTest, PredicateConstructionThrowsForUnregisteredType) { + // #23: GetRecordFromTypeId must fail fast for a type that was never registered, instead of + // std::map::operator[] silently default-inserting an empty Reflection (empty table name, no + // columns), which would otherwise surface later as an opaque SQLite prepare error + EXPECT_THROW(Equal(&UnregisteredRecord::value, 42), std::runtime_error); +} + +TEST(QueryPredicatesTest, PredicateConstructionThrowsWhenNoMemberMatches) { + // #22: even for a registered type, if no member_metadata entry's offset matches the + // pointer-to-member (here because the type was registered by hand with no members at all, + // rather than via the FIELDS macro), the QueryPredicate constructor must fail fast instead + // of silently leaving member_name_ empty and emitting malformed SQL like " = ?" + const std::string type_id = typeid(MismatchedRecord).name(); + auto& instance = *GetReflectionRegisterInstance(); + instance.records[type_id].name = "MismatchedRecord"; + const ScopedRegistryCleanup cleanup(type_id); + + EXPECT_THROW(Equal(&MismatchedRecord::value, 42), std::runtime_error); +}