Skip to content

Commit 5806a12

Browse files
committed
Implement nullable reflected fields
1 parent f53139f commit 5806a12

10 files changed

Lines changed: 448 additions & 25 deletions

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@ FetchContent_Declare(
2222
googletest
2323
URL https://github.com/google/googletest/archive/refs/tags/release-1.12.1.zip
2424
)
25-
FetchContent_MakeAvailable(googletest)
25+
FetchContent_Declare(
26+
functional_cpp
27+
URL https://github.com/jkalias/functional_cpp/archive/refs/tags/1.1.1.zip
28+
SOURCE_SUBDIR src
29+
)
30+
FetchContent_MakeAvailable(googletest functional_cpp)
2631

2732
# Set compiler settings based on the current platform
2833
if(CMAKE_HOST_UNIX)

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,17 @@ Supported field macros:
133133
| `std::wstring` | `MEMBER_TEXT(name)` |
134134
| `bool` | `MEMBER_BOOL(name)` |
135135
| `sqlite_reflection::TimePoint` | `MEMBER_DATETIME(name)` |
136+
| `fcpp::optional_t<int64_t>` | `MEMBER_INT_NULLABLE(name)` |
137+
| `fcpp::optional_t<double>` | `MEMBER_REAL_NULLABLE(name)` |
138+
| `fcpp::optional_t<std::wstring>` | `MEMBER_TEXT_NULLABLE(name)` |
139+
| `fcpp::optional_t<bool>` | `MEMBER_BOOL_NULLABLE(name)` |
140+
| `fcpp::optional_t<sqlite_reflection::TimePoint>` | `MEMBER_DATETIME_NULLABLE(name)` |
136141
| member function declaration | `FUNC(signature)` |
137142

143+
Nullable macros store SQL `NULL` as an empty optional and bind an empty optional back as SQL `NULL`; present optional values round-trip using the same storage class as their non-nullable counterparts, so `NULL`, an empty string, and numeric/boolean zero remain distinguishable after fetch. The optional type comes from [`functional_cpp`](https://github.com/jkalias/functional_cpp): under C++17 and later `fcpp::optional_t<T>` aliases `std::optional<T>`, while C++11 builds use the library fallback. Use `IsNull(&T::field)` and `IsNotNull(&T::field)` predicates for null checks. Value predicates such as `Equal(&T::nullable_field, value)` compare against a present contained value; pass the contained `T` value, not an optional.
144+
145+
**Schema nullability caveat.** Newly created tables now declare non-nullable reflected fields as `NOT NULL` and omit `NOT NULL` for nullable fields. Because initialization uses `CREATE TABLE IF NOT EXISTS`, existing database files keep their previous column definitions until you recreate or migrate those tables, just like the `AUTOINCREMENT` caveat below.
146+
138147
**Layout constraint.** Reflectable records must be simple, standard-layout structs: no base
139148
classes, no virtual functions, no virtual/multiple inheritance. Member access is computed from
140149
`offsetof`/pointer-to-member byte offsets, which are only well-defined for such types; a struct

include/query_predicates.h

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ struct REFLECTION_EXPORT SqlValue {
4242
bool bool_value;
4343
double real_value;
4444
std::string text_value;
45+
bool is_null;
4546
};
4647

4748
/// The base class of all WHERE predicates used in SQLite queries
@@ -103,13 +104,20 @@ class REFLECTION_EXPORT QueryPredicate : public QueryPredicateBase {
103104
: QueryPredicate(fn, value, symbol,
104105
[&](void* v, SqliteStorageClass storage_class) { return GetSqlValue(v, storage_class); }) {}
105106

107+
template <typename T, typename R>
108+
QueryPredicate(fcpp::optional_t<R> T::* fn, R value, const std::string& symbol)
109+
: QueryPredicate(fn, fcpp::optional_t<R>(value), symbol, [&](void* v, SqliteStorageClass storage_class) {
110+
return GetOptionalSqlValue(v, storage_class);
111+
}) {}
112+
106113
QueryPredicate(const std::string& symbol, const std::string& member_name, const SqlValue& value)
107114
: symbol_(symbol), member_name_(member_name), value_(value) {}
108115

109116
/// Returns the value used for the current query, against which the struct member
110117
/// (defined from the pointer-to-member function) will be compared. The value needs
111118
/// to be type-erased, so that the header file is not bloated with unnecessary implementation details
112119
virtual SqlValue GetSqlValue(void* v, SqliteStorageClass storage_class) const;
120+
virtual SqlValue GetOptionalSqlValue(void* v, SqliteStorageClass storage_class) const;
113121

114122
/// The symbol used for the comparison, for example "=" for equality
115123
std::string symbol_;
@@ -122,6 +130,47 @@ class REFLECTION_EXPORT QueryPredicate : public QueryPredicateBase {
122130
SqlValue value_;
123131
};
124132

133+
/// A wrapper for an empty predicate, used to fetch all elements of an SQLite table
134+
class REFLECTION_EXPORT NullPredicate : public QueryPredicateBase {
135+
public:
136+
template <typename T, typename R>
137+
NullPredicate(R T::* fn, const std::string& symbol) : symbol_(symbol) {
138+
auto record = GetRecordFromTypeId(typeid(T).name());
139+
auto offset = OffsetFromStart(fn);
140+
for (auto i = 0; i < record.member_metadata.size(); ++i) {
141+
if (record.member_metadata[i].offset == offset) {
142+
member_name_ = record.member_metadata[i].name;
143+
return;
144+
}
145+
}
146+
throw std::runtime_error("No registered member of '" + record.name +
147+
"' matches the given pointer-to-member (type id: " + typeid(T).name() + ")");
148+
}
149+
150+
std::string Evaluate() const override;
151+
std::vector<SqlValue> Bindings() const override;
152+
QueryPredicateBase* Clone() const override;
153+
154+
protected:
155+
NullPredicate(const std::string& symbol, const std::string& member_name)
156+
: symbol_(symbol), member_name_(member_name) {}
157+
158+
std::string symbol_;
159+
std::string member_name_;
160+
};
161+
162+
class REFLECTION_EXPORT IsNull final : public NullPredicate {
163+
public:
164+
template <typename T, typename R>
165+
explicit IsNull(R T::* fn) : NullPredicate(fn, "IS NULL") {}
166+
};
167+
168+
class REFLECTION_EXPORT IsNotNull final : public NullPredicate {
169+
public:
170+
template <typename T, typename R>
171+
explicit IsNotNull(R T::* fn) : NullPredicate(fn, "IS NOT NULL") {}
172+
};
173+
125174
/// A wrapper for an empty predicate, used to fetch all elements of an SQLite table
126175
class REFLECTION_EXPORT EmptyPredicate final : public QueryPredicateBase {
127176
public:
@@ -137,6 +186,9 @@ class REFLECTION_EXPORT Equal final : public QueryPredicate {
137186
template <typename T, typename R>
138187
explicit Equal(R T::* fn, R value) : QueryPredicate(fn, value, "=") {}
139188

189+
template <typename T, typename R>
190+
explicit Equal(fcpp::optional_t<R> T::* fn, R value) : QueryPredicate(fn, value, "=") {}
191+
140192
template <typename T>
141193
explicit Equal(int64_t T::* fn, int value) : Equal(fn, (int64_t)value) {}
142194

@@ -151,6 +203,9 @@ class REFLECTION_EXPORT Unequal final : public QueryPredicate {
151203
template <typename T, typename R>
152204
explicit Unequal(R T::* fn, R value) : QueryPredicate(fn, value, "!=") {}
153205

206+
template <typename T, typename R>
207+
explicit Unequal(fcpp::optional_t<R> T::* fn, R value) : QueryPredicate(fn, value, "!=") {}
208+
154209
template <typename T>
155210
explicit Unequal(int64_t T::* fn, int value) : Unequal(fn, (int64_t)value) {}
156211

@@ -167,6 +222,12 @@ class REFLECTION_EXPORT Like final : public QueryPredicate {
167222
: QueryPredicate(fn, value, "LIKE",
168223
[&](void* v, SqliteStorageClass storage_class) { return GetSqlValue(v, storage_class); }) {}
169224

225+
template <typename T, typename R>
226+
explicit Like(fcpp::optional_t<R> T::* fn, R value)
227+
: QueryPredicate(fn, fcpp::optional_t<R>(value), "LIKE", [&](void* v, SqliteStorageClass storage_class) {
228+
return GetOptionalSqlValue(v, storage_class);
229+
}) {}
230+
170231
template <typename T>
171232
explicit Like(int64_t T::* fn, int value) : Like(fn, (int64_t)value) {}
172233

@@ -189,6 +250,12 @@ class REFLECTION_EXPORT GreaterThan final : public QueryPredicate {
189250

190251
template <typename T>
191252
explicit GreaterThan(double T::* fn, double value) : QueryPredicate(fn, value, ">") {}
253+
254+
template <typename T>
255+
explicit GreaterThan(fcpp::optional_t<int64_t> T::* fn, int64_t value) : QueryPredicate(fn, value, ">") {}
256+
257+
template <typename T>
258+
explicit GreaterThan(fcpp::optional_t<double> T::* fn, double value) : QueryPredicate(fn, value, ">") {}
192259
};
193260

194261
/// A wrapper for a comparison predicate, for which the value of the
@@ -203,6 +270,12 @@ class REFLECTION_EXPORT GreaterThanOrEqual final : public QueryPredicate {
203270

204271
template <typename T>
205272
explicit GreaterThanOrEqual(double T::* fn, double value) : QueryPredicate(fn, value, ">=") {}
273+
274+
template <typename T>
275+
explicit GreaterThanOrEqual(fcpp::optional_t<int64_t> T::* fn, int64_t value) : QueryPredicate(fn, value, ">=") {}
276+
277+
template <typename T>
278+
explicit GreaterThanOrEqual(fcpp::optional_t<double> T::* fn, double value) : QueryPredicate(fn, value, ">=") {}
206279
};
207280

208281
/// A wrapper for a comparison predicate, for which the value of the
@@ -217,6 +290,12 @@ class REFLECTION_EXPORT SmallerThan final : public QueryPredicate {
217290

218291
template <typename T>
219292
explicit SmallerThan(double T::* fn, double value) : QueryPredicate(fn, value, "<") {}
293+
294+
template <typename T>
295+
explicit SmallerThan(fcpp::optional_t<int64_t> T::* fn, int64_t value) : QueryPredicate(fn, value, "<") {}
296+
297+
template <typename T>
298+
explicit SmallerThan(fcpp::optional_t<double> T::* fn, double value) : QueryPredicate(fn, value, "<") {}
220299
};
221300

222301
/// A wrapper for a comparison predicate, for which the value of the
@@ -231,6 +310,12 @@ class REFLECTION_EXPORT SmallerThanOrEqual final : public QueryPredicate {
231310

232311
template <typename T>
233312
explicit SmallerThanOrEqual(double T::* fn, double value) : QueryPredicate(fn, value, "<=") {}
313+
314+
template <typename T>
315+
explicit SmallerThanOrEqual(fcpp::optional_t<int64_t> T::* fn, int64_t value) : QueryPredicate(fn, value, "<=") {}
316+
317+
template <typename T>
318+
explicit SmallerThanOrEqual(fcpp::optional_t<double> T::* fn, double value) : QueryPredicate(fn, value, "<=") {}
234319
};
235320

236321
/// A wrapper of a compound predicate, which combines two other predicates,

include/reflection.h

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include <typeinfo>
3434
#include <vector>
3535

36+
#include "optional.h"
3637
#include "reflection_export.h"
3738

3839
/// The storage class in an SQLite column for a given member of a struct, for which reflection is enabled
@@ -47,11 +48,12 @@ struct REFLECTION_EXPORT Reflection {
4748
/// This holds the metadata of a given struct member
4849
class MemberMetadata {
4950
public:
50-
MemberMetadata(const std::string& _name, SqliteStorageClass _storage_class, size_t _offset)
51+
MemberMetadata(const std::string& _name, SqliteStorageClass _storage_class, size_t _offset, bool _nullable = false)
5152
: name(_name),
5253
storage_class(_storage_class),
5354
sqlite_column_name(ToSqliteColumnName(_storage_class)),
54-
offset(_offset) {}
55+
offset(_offset),
56+
nullable(_nullable) {}
5557

5658
/// The struct variable member name, as defined in the source code
5759
std::string name;
@@ -66,6 +68,9 @@ struct REFLECTION_EXPORT Reflection {
6668
/// The memory offset in bytes of this member from the struct's start, including any padding bits
6769
size_t offset;
6870

71+
/// Whether this member is represented by optional_t<T> and may carry SQL NULL
72+
bool nullable;
73+
6974
private:
7075
/// Helper for conversion between member storage class and SQLite column name
7176
static const char* ToSqliteColumnName(const SqliteStorageClass storage_class) {
@@ -113,7 +118,9 @@ size_t OffsetFromStart(R T::* fn) {
113118
#define CAT(A, B) CAT_NOEXPAND(A, B)
114119

115120
#define DEFINE_MEMBER(R, T) \
116-
reflectable.member_metadata.push_back(Reflection::MemberMetadata(STR(R), T, offsetof(struct REFLECTABLE, R)));
121+
reflectable.member_metadata.push_back(Reflection::MemberMetadata(STR(R), T, offsetof(struct REFLECTABLE, R), false));
122+
#define DEFINE_MEMBER_NULLABLE(R, T) \
123+
reflectable.member_metadata.push_back(Reflection::MemberMetadata(STR(R), T, offsetof(struct REFLECTABLE, R), true));
117124

118125
/// A singleton object which holds all reflectable structs, and is guaranteed to be
119126
/// instantiated before main.cpp starts
@@ -167,6 +174,11 @@ struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
167174
#define MEMBER_TEXT(R) MEMBER_DECLARE(std::wstring, R)
168175
#define MEMBER_DATETIME(R) MEMBER_DECLARE(sqlite_reflection::TimePoint, R)
169176
#define MEMBER_BOOL(R) MEMBER_DECLARE(bool, R)
177+
#define MEMBER_INT_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<int64_t>, R)
178+
#define MEMBER_REAL_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<double>, R)
179+
#define MEMBER_TEXT_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<std::wstring>, R)
180+
#define MEMBER_DATETIME_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<sqlite_reflection::TimePoint>, R)
181+
#define MEMBER_BOOL_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<bool>, R)
170182
#define FUNC(SIGNATURE)
171183
FIELDS
172184
#undef MEMBER_DECLARE
@@ -175,6 +187,11 @@ struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
175187
#undef MEMBER_TEXT
176188
#undef MEMBER_DATETIME
177189
#undef MEMBER_BOOL
190+
#undef MEMBER_INT_NULLABLE
191+
#undef MEMBER_REAL_NULLABLE
192+
#undef MEMBER_TEXT_NULLABLE
193+
#undef MEMBER_DATETIME_NULLABLE
194+
#undef MEMBER_BOOL_NULLABLE
178195
#undef FUNC
179196
int64_t id;
180197

@@ -184,13 +201,23 @@ struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
184201
#define MEMBER_TEXT(R)
185202
#define MEMBER_DATETIME(R)
186203
#define MEMBER_BOOL(R)
204+
#define MEMBER_INT_NULLABLE(R)
205+
#define MEMBER_REAL_NULLABLE(R)
206+
#define MEMBER_TEXT_NULLABLE(R)
207+
#define MEMBER_DATETIME_NULLABLE(R)
208+
#define MEMBER_BOOL_NULLABLE(R)
187209
#define FUNC(SIGNATURE) SIGNATURE;
188210
FIELDS
189211
#undef MEMBER_INT
190212
#undef MEMBER_REAL
191213
#undef MEMBER_TEXT
192214
#undef MEMBER_DATETIME
193215
#undef MEMBER_BOOL
216+
#undef MEMBER_INT_NULLABLE
217+
#undef MEMBER_REAL_NULLABLE
218+
#undef MEMBER_TEXT_NULLABLE
219+
#undef MEMBER_DATETIME_NULLABLE
220+
#undef MEMBER_BOOL_NULLABLE
194221
#undef FUNC
195222
};
196223

@@ -232,13 +259,23 @@ static std::string CAT(Register, REFLECTABLE)() {
232259
#define MEMBER_TEXT(R) DEFINE_MEMBER(R, SqliteStorageClass::kText)
233260
#define MEMBER_DATETIME(R) DEFINE_MEMBER(R, SqliteStorageClass::kDateTime)
234261
#define MEMBER_BOOL(R) DEFINE_MEMBER(R, SqliteStorageClass::kBool)
262+
#define MEMBER_INT_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kInt)
263+
#define MEMBER_REAL_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kReal)
264+
#define MEMBER_TEXT_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kText)
265+
#define MEMBER_DATETIME_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kDateTime)
266+
#define MEMBER_BOOL_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kBool)
235267
#define FUNC(SIGNATURE)
236268
FIELDS
237269
#undef MEMBER_INT
238270
#undef MEMBER_REAL
239271
#undef MEMBER_TEXT
240272
#undef MEMBER_DATETIME
241273
#undef MEMBER_BOOL
274+
#undef MEMBER_INT_NULLABLE
275+
#undef MEMBER_REAL_NULLABLE
276+
#undef MEMBER_TEXT_NULLABLE
277+
#undef MEMBER_DATETIME_NULLABLE
278+
#undef MEMBER_BOOL_NULLABLE
242279
#undef FUNC
243280
}
244281
return name;

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ source_group("internal" FILES ${HEADERS_INTERNAL})
2525

2626
# Set Properties->General->Configuration Type to Dynamic Library (.dll/.so/.dylib)
2727
add_library(${LIBNAME} SHARED ${HEADERS} ${HEADERS_INTERNAL} ${SOURCES})
28+
target_link_libraries(${LIBNAME} PUBLIC fcpp)
29+
target_include_directories(${LIBNAME} PUBLIC ${functional_cpp_SOURCE_DIR}/include)
2830

2931
if(CMAKE_HOST_UNIX AND NOT CMAKE_HOST_APPLE)
3032
target_link_libraries(${LIBNAME} PUBLIC tbb dl)

0 commit comments

Comments
 (0)