|
24 | 24 |
|
25 | 25 | #include <gtest/gtest.h> |
26 | 26 |
|
| 27 | +#include <string> |
| 28 | +#include <utility> |
| 29 | + |
27 | 30 | #include "person.h" |
28 | 31 | #include "pet.h" |
29 | 32 |
|
@@ -207,3 +210,56 @@ TEST(QueryPredicatesTest, LikeInsideAndSurvivesCloneWithEscapeClause) { |
207 | 210 | EXPECT_EQ(R"(%50\%%)", bindings[0].text_value); |
208 | 211 | EXPECT_EQ(30, bindings[1].int_value); |
209 | 212 | } |
| 213 | + |
| 214 | +namespace { |
| 215 | +// A plain struct that is deliberately never run through the REFLECTABLE/FIELDS registration |
| 216 | +// macros, so its type id never appears in the reflection registry. |
| 217 | +struct UnregisteredRecord { |
| 218 | + int64_t id; |
| 219 | + int64_t value; |
| 220 | +}; |
| 221 | + |
| 222 | +// A plain struct that is also never run through the registration macros, but is manually and |
| 223 | +// incompletely registered below (name only, no member metadata) to exercise the |
| 224 | +// registered-but-offset-mismatch guard, as distinct from the unregistered-type guard above. |
| 225 | +struct MismatchedRecord { |
| 226 | + int64_t id; |
| 227 | + int64_t value; |
| 228 | +}; |
| 229 | + |
| 230 | +// Erases a hand-inserted entry from the process-wide reflection registry on scope exit. Without |
| 231 | +// this, a MismatchedRecord-shaped entry with no member metadata would linger in the registry for |
| 232 | +// the rest of the test binary: Database::Database iterates every registered record and would |
| 233 | +// generate "CREATE TABLE IF NOT EXISTS MismatchedRecord ();" (empty column list) for it, failing |
| 234 | +// every later Database::Initialize() call in this process. |
| 235 | +class ScopedRegistryCleanup { |
| 236 | +public: |
| 237 | + explicit ScopedRegistryCleanup(std::string type_id) : type_id_(std::move(type_id)) {} |
| 238 | + ~ScopedRegistryCleanup() { |
| 239 | + GetReflectionRegisterInstance()->records.erase(type_id_); |
| 240 | + } |
| 241 | + |
| 242 | +private: |
| 243 | + std::string type_id_; |
| 244 | +}; |
| 245 | +} // namespace |
| 246 | + |
| 247 | +TEST(QueryPredicatesTest, PredicateConstructionThrowsForUnregisteredType) { |
| 248 | + // #23: GetRecordFromTypeId must fail fast for a type that was never registered, instead of |
| 249 | + // std::map::operator[] silently default-inserting an empty Reflection (empty table name, no |
| 250 | + // columns), which would otherwise surface later as an opaque SQLite prepare error |
| 251 | + EXPECT_THROW(Equal(&UnregisteredRecord::value, 42), std::runtime_error); |
| 252 | +} |
| 253 | + |
| 254 | +TEST(QueryPredicatesTest, PredicateConstructionThrowsWhenNoMemberMatches) { |
| 255 | + // #22: even for a registered type, if no member_metadata entry's offset matches the |
| 256 | + // pointer-to-member (here because the type was registered by hand with no members at all, |
| 257 | + // rather than via the FIELDS macro), the QueryPredicate constructor must fail fast instead |
| 258 | + // of silently leaving member_name_ empty and emitting malformed SQL like " = ?" |
| 259 | + const std::string type_id = typeid(MismatchedRecord).name(); |
| 260 | + auto& instance = *GetReflectionRegisterInstance(); |
| 261 | + instance.records[type_id].name = "MismatchedRecord"; |
| 262 | + const ScopedRegistryCleanup cleanup(type_id); |
| 263 | + |
| 264 | + EXPECT_THROW(Equal(&MismatchedRecord::value, 42), std::runtime_error); |
| 265 | +} |
0 commit comments