Skip to content

Commit cea5f9f

Browse files
jkaliasclaude
andauthored
Implement nullable reflected fields (#38)
* Pull in functional_cpp for its C++11-compatible optional (#2) fcpp::optional_t<T> is the nullable-field representation: it aliases std::optional<T> under C++17 and later and falls back to an equivalent C++11 implementation otherwise. Consumed header-only via FetchContent, pinned to tag 1.1.1; SOURCE_SUBDIR points at the header directory (which has no CMakeLists.txt) so only the sources are downloaded, without building functional_cpp's own library and test targets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Add MEMBER_*_NULLABLE macros and a nullability flag in member metadata (#2) Each field macro gains a _NULLABLE variant declaring the member as fcpp::optional_t of the underlying type (std::optional under C++17+). MemberMetadata carries an orthogonal nullable flag - storage_class keeps describing the underlying contained type - set via the new DEFINE_MEMBER_NULLABLE during registration, so query generation, binding, and hydration can branch on it. No behavior change yet for existing records: the flag defaults to false everywhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Round-trip SQL NULL for nullable members across schema, read, write, and predicates (#2, #20) Write side: SqlValue gains an is_null state bound via sqlite3_bind_null; GetValues reads a nullable member through its optional and produces a null SqlValue when it is empty. Read side (the #20 fix): HydrateCurrentRow hydrates a nullable member's SQL NULL into an empty optional instead of skipping it, and a present value into the contained value - an empty TEXT becomes a contained empty string, distinct from NULL, ending the NULL/empty conflation. Non-nullable members keep today's exact skip semantics, including the NULL/BLOB accessor-safety skip. Schema: CreateTableQuery emits NOT NULL for non-nullable columns and omits it for nullable ones (id keeps PRIMARY KEY AUTOINCREMENT, which is implicitly NOT NULL). Only newly created tables are affected, since tables are created with CREATE TABLE IF NOT EXISTS. The pre-existing NULL-skip-semantics test now recreates its table the way a legacy database file would look, which is the very scenario those skip semantics protect. Predicates: new IsNull()/IsNotNull() emit "<column> IS NULL"/"IS NOT NULL" with no bound value and survive Clone() inside And()/Or() compounds. Value predicates on a nullable member compare against the contained value; given an empty optional they throw std::invalid_argument, since "= NULL" never matches in SQL - IsNull()/IsNotNull() is the correct tool there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Test nullable fields end to end (#2, #20) NullableRecord mixes a non-nullable member with a nullable member of every storage class. Covered: unset fields round-trip as SQL NULL (with IS NULL matching at the SQL level as raw proof); set fields round-trip equal; NULL is distinguishable from a stored empty string and stored zero after fetch (the #20 core case); non-nullable columns reject NULL while nullable ones accept it (schema NOT NULL, asserted behaviorally); IsNull/IsNotNull select the right rows, also inside And() compounds; value predicates operate on the contained value and fail fast on an empty optional; a record without nullable members round-trips unchanged. Under C++17+ a static_assert pins that nullable fields are literally std::optional. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Document nullable fields in the README (#2) Covers the MEMBER_*_NULLABLE macros, the fcpp::optional_t representation (std::optional under C++17+, C++11 fallback otherwise), NULL round-trip semantics, IsNull/IsNotNull and value-predicate behavior, and the NOT NULL migration caveat for pre-existing database files, mirroring the existing AUTOINCREMENT caveat. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Bind IsNull predicates to locals before taking their address Apple Clang errors on &IsNull(...) with -Waddress-of-temporary (taking the address of a temporary object), failing both macOS CI jobs; GCC only tolerated it via the project's -fpermissive. Bind each predicate to a named const local first, matching how every other predicate call site in the tests already does it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Guard functional_cpp population for CMake older than 3.18 FetchContent's SOURCE_SUBDIR handling was added in CMake 3.18, but the project's minimum is 3.14; on 3.14-3.17, MakeAvailable would have add_subdirectory'd functional_cpp's top-level CMakeLists.txt, pulling its own src/tests into this build. Branch on the CMake version: 3.18+ keeps the SOURCE_SUBDIR MakeAvailable path, older versions populate the sources via FetchContent_Populate, which never calls add_subdirectory at all. The deprecated-in-3.30 Populate call sits behind the VERSION_LESS 3.18 check, so modern CMake never executes it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Support nullable members in range predicates GreaterThan, GreaterThanOrEqual, SmallerThan and SmallerThanOrEqual only exposed int64_t and double member-pointer overloads, so they could not be used with nullable numeric members at all, even though value predicates on nullable members are documented to compare against the contained value. Add fcpp::optional_t<int64_t> and fcpp::optional_t<double> overloads that route through the existing QueryPredicate constructor, which already unwraps the optional and fails fast on an empty one. Also fix the README nullness-predicate example, which took the address of a temporary (the pattern Clang and MSVC reject), and mention the range predicates in the nullable value-predicate documentation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK * Use SaveAutoIncrement in the README nullable example The example default-constructed an Employee and saved it via Save, which binds the appended int64_t id without ever writing one back - so the id stayed indeterminate and the subsequent fetch by e.id read an uninitialized value. SaveAutoIncrement assigns the id and writes it back into the object, making the example correct as written. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUt3c1wdseRtRSSXfg3MCK --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f53139f commit cea5f9f

9 files changed

Lines changed: 768 additions & 17 deletions

File tree

CMakeLists.txt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ FetchContent_Declare(
2424
)
2525
FetchContent_MakeAvailable(googletest)
2626

27+
# functional_cpp provides fcpp::optional_t, the nullable-field representation
28+
# (std::optional under C++17 and later, an equivalent C++11 fallback otherwise).
29+
# It is consumed header-only: only its sources are downloaded, and functional_cpp's
30+
# own library and test targets are never added to this build.
31+
FetchContent_Declare(
32+
functional_cpp
33+
GIT_REPOSITORY https://github.com/jkalias/functional_cpp.git
34+
GIT_TAG 1.1.1
35+
SOURCE_SUBDIR include
36+
)
37+
if(CMAKE_VERSION VERSION_LESS 3.18)
38+
# FetchContent's SOURCE_SUBDIR support was added in CMake 3.18; on older versions
39+
# MakeAvailable would add_subdirectory functional_cpp's top-level CMakeLists.txt,
40+
# pulling its src/tests into this build. Populate without adding any subdirectory.
41+
FetchContent_GetProperties(functional_cpp)
42+
if(NOT functional_cpp_POPULATED)
43+
FetchContent_Populate(functional_cpp)
44+
endif()
45+
else()
46+
# SOURCE_SUBDIR points at the header directory, which contains no CMakeLists.txt,
47+
# so MakeAvailable populates the sources without calling add_subdirectory
48+
FetchContent_MakeAvailable(functional_cpp)
49+
endif()
50+
include_directories(${functional_cpp_SOURCE_DIR}/include)
51+
2752
# Set compiler settings based on the current platform
2853
if(CMAKE_HOST_UNIX)
2954
add_definitions(-w -fpermissive -Wshadow -Wunused -Woverloaded-virtual -Wnon-virtual-dtor -Werror)

README.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,59 @@ Supported field macros:
135135
| `sqlite_reflection::TimePoint` | `MEMBER_DATETIME(name)` |
136136
| member function declaration | `FUNC(signature)` |
137137

138+
### Nullable fields
139+
140+
Each field macro has a `_NULLABLE` variant (`MEMBER_INT_NULLABLE`, `MEMBER_REAL_NULLABLE`,
141+
`MEMBER_TEXT_NULLABLE`, `MEMBER_BOOL_NULLABLE`, `MEMBER_DATETIME_NULLABLE`) declaring a field
142+
that can represent the absence of a value. The field's type is `fcpp::optional_t<T>` from
143+
[functional_cpp](https://github.com/jkalias/functional_cpp): under C++17 and later that is
144+
literally `std::optional<T>`, so you get the standard type and API; under C++11 an equivalent
145+
fallback with the core API (`has_value()`, `value()`, `operator*`/`->`, assignment from a value)
146+
is used.
147+
148+
```c++
149+
#define REFLECTABLE Employee
150+
#define FIELDS \
151+
MEMBER_TEXT(name) \
152+
MEMBER_TEXT_NULLABLE(middle_name) \
153+
MEMBER_REAL_NULLABLE(salary)
154+
#include "reflection.h"
155+
156+
Employee e;
157+
e.name = L"Ada Lovelace"; // middle_name and salary stay unset
158+
db->SaveAutoIncrement(e); // unset fields are stored as SQL NULL; e.id gets assigned
159+
160+
const auto fetched = db->Fetch<Employee>(e.id);
161+
if (fetched.salary.has_value()) { /* a real value was stored */ }
162+
```
163+
164+
Round-trip semantics: saving an unset nullable field binds SQL `NULL`
165+
(`sqlite3_bind_null`), and fetching a `NULL` column hydrates the field back to the empty
166+
state. A stored empty string and a stored zero are distinct from `NULL` and hydrate to a
167+
*present* empty/zero value — absence and emptiness are no longer conflated.
168+
169+
Two predicates cover nullness tests, since SQL's `= NULL` never matches:
170+
171+
```c++
172+
const auto is_unpaid = IsNull(&Employee::salary);
173+
const auto unpaid = db->Fetch<Employee>(&is_unpaid);
174+
const auto is_paid = IsNotNull(&Employee::salary);
175+
const auto paid = db->Fetch<Employee>(&is_paid);
176+
```
177+
178+
Value predicates (`Equal`, `Like`, `GreaterThan`, `SmallerThanOrEqual`, ...) on a nullable
179+
field compare against the contained value and take an optional as the comparison argument,
180+
e.g. `Equal(&Employee::salary, fcpp::optional_t<double>(50000.0))` or
181+
`GreaterThan(&Employee::salary, fcpp::optional_t<double>(40000.0))`. Passing an *empty*
182+
optional to a value predicate throws `std::invalid_argument` — use `IsNull`/`IsNotNull` for
183+
that instead.
184+
185+
Non-nullable columns are now created with an explicit `NOT NULL` constraint, so the schema
186+
enforces what the object model assumes. Because tables are created with
187+
`CREATE TABLE IF NOT EXISTS`, this only affects newly created tables; pre-existing database
188+
files keep their previous schema until migrated (the library has no migration layer yet) —
189+
the same caveat that applies to `AUTOINCREMENT`.
190+
138191
**Layout constraint.** Reflectable records must be simple, standard-layout structs: no base
139192
classes, no virtual functions, no virtual/multiple inheritance. Member access is computed from
140193
`offsetof`/pointer-to-member byte offsets, which are only well-defined for such types; a struct

include/query_predicates.h

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ struct REFLECTION_EXPORT SqlValue {
4242
bool bool_value;
4343
double real_value;
4444
std::string text_value;
45+
46+
/// When true, the value represents SQL NULL (an unset nullable member) and is bound
47+
/// via sqlite3_bind_null; the typed value members above are then meaningless
48+
bool is_null;
4549
};
4650

4751
/// The base class of all WHERE predicates used in SQLite queries
@@ -87,14 +91,25 @@ class REFLECTION_EXPORT QueryPredicate : public QueryPredicateBase {
8791
for (auto i = 0; i < record.member_metadata.size(); ++i) {
8892
if (record.member_metadata[i].offset == offset) {
8993
member_name_ = record.member_metadata[i].name;
90-
value_ = value_retrieval((void*)&value, record.member_metadata[i].storage_class);
94+
auto value_address = (void*)&value;
95+
if (record.member_metadata[i].nullable) {
96+
// A value predicate on a nullable member compares against the contained
97+
// value; an empty optional cannot be expressed as "= ?" in SQL (a bound
98+
// NULL never matches), so demand IsNull()/IsNotNull() for that instead
99+
value_address = NullableContainedValue(value_address, record.member_metadata[i].storage_class);
100+
if (value_address == nullptr) {
101+
throw std::invalid_argument("Value predicate on nullable member '" + member_name_ +
102+
"' was given an empty optional; use IsNull()/IsNotNull() instead");
103+
}
104+
}
105+
value_ = value_retrieval(value_address, record.member_metadata[i].storage_class);
91106
found = true;
92107
break;
93108
}
94109
}
95110
if (!found) {
96111
throw std::runtime_error("No registered member of '" + record.name +
97-
"' matches the given pointer-to-member (type id: " + typeid(T).name() + ")");
112+
"' matches the given pointer-to-member (type id: " + typeid(T).name() + ")");
98113
}
99114
}
100115

@@ -111,6 +126,14 @@ class REFLECTION_EXPORT QueryPredicate : public QueryPredicateBase {
111126
/// to be type-erased, so that the header file is not bloated with unnecessary implementation details
112127
virtual SqlValue GetSqlValue(void* v, SqliteStorageClass storage_class) const;
113128

129+
private:
130+
/// For a type-erased pointer to an fcpp::optional_t of the underlying type of the given
131+
/// storage class, returns a pointer to the contained value, or nullptr when the optional
132+
/// is empty. Lets the constructor above unwrap a nullable comparison value once, so the
133+
/// virtual GetSqlValue overloads only ever see the plain underlying types
134+
static void* NullableContainedValue(void* v, SqliteStorageClass storage_class);
135+
136+
protected:
114137
/// The symbol used for the comparison, for example "=" for equality
115138
std::string symbol_;
116139

@@ -177,6 +200,59 @@ class REFLECTION_EXPORT Like final : public QueryPredicate {
177200
SqlValue GetSqlValue(void* v, SqliteStorageClass storage_class) const override;
178201
};
179202

203+
/// A predicate testing a column for the presence or absence of SQL NULL. Unlike the value
204+
/// predicates it binds no value ("= NULL" never matches in SQL; nullness needs the dedicated
205+
/// IS NULL / IS NOT NULL syntax), so it derives from QueryPredicateBase directly
206+
class REFLECTION_EXPORT NullnessPredicate : public QueryPredicateBase {
207+
public:
208+
std::string Evaluate() const override;
209+
std::vector<SqlValue> Bindings() const override;
210+
QueryPredicateBase* Clone() const override;
211+
212+
protected:
213+
template <typename T, typename R>
214+
NullnessPredicate(R T::* fn, const std::string& symbol) : symbol_(symbol) {
215+
auto record = GetRecordFromTypeId(typeid(T).name());
216+
auto offset = OffsetFromStart(fn);
217+
auto found = false;
218+
for (auto i = 0; i < record.member_metadata.size(); ++i) {
219+
if (record.member_metadata[i].offset == offset) {
220+
member_name_ = record.member_metadata[i].name;
221+
found = true;
222+
break;
223+
}
224+
}
225+
if (!found) {
226+
throw std::runtime_error("No registered member of '" + record.name +
227+
"' matches the given pointer-to-member (type id: " + typeid(T).name() + ")");
228+
}
229+
}
230+
231+
NullnessPredicate(const std::string& symbol, const std::string& member_name)
232+
: symbol_(symbol), member_name_(member_name) {}
233+
234+
/// The nullness test, either "IS NULL" or "IS NOT NULL"
235+
std::string symbol_;
236+
237+
/// The name of the tested member, as defined in source code
238+
std::string member_name_;
239+
};
240+
241+
/// A predicate matching rows whose column holds SQL NULL, i.e. rows whose
242+
/// nullable member was saved in the unset/empty state
243+
class REFLECTION_EXPORT IsNull final : public NullnessPredicate {
244+
public:
245+
template <typename T, typename R>
246+
explicit IsNull(R T::* fn) : NullnessPredicate(fn, "IS NULL") {}
247+
};
248+
249+
/// A predicate matching rows whose column holds a present (non-NULL) value
250+
class REFLECTION_EXPORT IsNotNull final : public NullnessPredicate {
251+
public:
252+
template <typename T, typename R>
253+
explicit IsNotNull(R T::* fn) : NullnessPredicate(fn, "IS NOT NULL") {}
254+
};
255+
180256
/// A wrapper for a comparison predicate, for which the value of the
181257
/// struct member is required to be greater than a given control value
182258
class REFLECTION_EXPORT GreaterThan final : public QueryPredicate {
@@ -189,6 +265,14 @@ class REFLECTION_EXPORT GreaterThan final : public QueryPredicate {
189265

190266
template <typename T>
191267
explicit GreaterThan(double T::* fn, double value) : QueryPredicate(fn, value, ">") {}
268+
269+
template <typename T>
270+
explicit GreaterThan(fcpp::optional_t<int64_t> T::* fn, fcpp::optional_t<int64_t> value)
271+
: QueryPredicate(fn, value, ">") {}
272+
273+
template <typename T>
274+
explicit GreaterThan(fcpp::optional_t<double> T::* fn, fcpp::optional_t<double> value)
275+
: QueryPredicate(fn, value, ">") {}
192276
};
193277

194278
/// A wrapper for a comparison predicate, for which the value of the
@@ -203,6 +287,14 @@ class REFLECTION_EXPORT GreaterThanOrEqual final : public QueryPredicate {
203287

204288
template <typename T>
205289
explicit GreaterThanOrEqual(double T::* fn, double value) : QueryPredicate(fn, value, ">=") {}
290+
291+
template <typename T>
292+
explicit GreaterThanOrEqual(fcpp::optional_t<int64_t> T::* fn, fcpp::optional_t<int64_t> value)
293+
: QueryPredicate(fn, value, ">=") {}
294+
295+
template <typename T>
296+
explicit GreaterThanOrEqual(fcpp::optional_t<double> T::* fn, fcpp::optional_t<double> value)
297+
: QueryPredicate(fn, value, ">=") {}
206298
};
207299

208300
/// A wrapper for a comparison predicate, for which the value of the
@@ -217,6 +309,14 @@ class REFLECTION_EXPORT SmallerThan final : public QueryPredicate {
217309

218310
template <typename T>
219311
explicit SmallerThan(double T::* fn, double value) : QueryPredicate(fn, value, "<") {}
312+
313+
template <typename T>
314+
explicit SmallerThan(fcpp::optional_t<int64_t> T::* fn, fcpp::optional_t<int64_t> value)
315+
: QueryPredicate(fn, value, "<") {}
316+
317+
template <typename T>
318+
explicit SmallerThan(fcpp::optional_t<double> T::* fn, fcpp::optional_t<double> value)
319+
: QueryPredicate(fn, value, "<") {}
220320
};
221321

222322
/// A wrapper for a comparison predicate, for which the value of the
@@ -231,6 +331,14 @@ class REFLECTION_EXPORT SmallerThanOrEqual final : public QueryPredicate {
231331

232332
template <typename T>
233333
explicit SmallerThanOrEqual(double T::* fn, double value) : QueryPredicate(fn, value, "<=") {}
334+
335+
template <typename T>
336+
explicit SmallerThanOrEqual(fcpp::optional_t<int64_t> T::* fn, fcpp::optional_t<int64_t> value)
337+
: QueryPredicate(fn, value, "<=") {}
338+
339+
template <typename T>
340+
explicit SmallerThanOrEqual(fcpp::optional_t<double> T::* fn, fcpp::optional_t<double> value)
341+
: QueryPredicate(fn, value, "<=") {}
234342
};
235343

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

include/reflection.h

Lines changed: 45 additions & 2 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,13 @@ 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,
52+
bool _nullable = false)
5153
: name(_name),
5254
storage_class(_storage_class),
5355
sqlite_column_name(ToSqliteColumnName(_storage_class)),
54-
offset(_offset) {}
56+
offset(_offset),
57+
nullable(_nullable) {}
5558

5659
/// The struct variable member name, as defined in the source code
5760
std::string name;
@@ -66,6 +69,11 @@ struct REFLECTION_EXPORT Reflection {
6669
/// The memory offset in bytes of this member from the struct's start, including any padding bits
6770
size_t offset;
6871

72+
/// Whether this member was declared through a MEMBER_*_NULLABLE macro. A nullable member
73+
/// is an fcpp::optional_t of the underlying storage type (std::optional under C++17 and
74+
/// later); the storage_class above always describes the underlying, contained type
75+
bool nullable;
76+
6977
private:
7078
/// Helper for conversion between member storage class and SQLite column name
7179
static const char* ToSqliteColumnName(const SqliteStorageClass storage_class) {
@@ -115,6 +123,9 @@ size_t OffsetFromStart(R T::* fn) {
115123
#define DEFINE_MEMBER(R, T) \
116124
reflectable.member_metadata.push_back(Reflection::MemberMetadata(STR(R), T, offsetof(struct REFLECTABLE, R)));
117125

126+
#define DEFINE_MEMBER_NULLABLE(R, T) \
127+
reflectable.member_metadata.push_back(Reflection::MemberMetadata(STR(R), T, offsetof(struct REFLECTABLE, R), true));
128+
118129
/// A singleton object which holds all reflectable structs, and is guaranteed to be
119130
/// instantiated before main.cpp starts
120131
struct REFLECTION_EXPORT ReflectionRegister {
@@ -161,12 +172,19 @@ REFLECTION_EXPORT char* GetMemberAddress(void* p, const Reflection& record, size
161172
/// corruption, not a compile or runtime error) unless caught by the static_assert below.
162173
struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
163174
// member declaration according to the order given in source code
175+
// nullable members are declared as fcpp::optional_t of the underlying type,
176+
// which is std::optional under C++17 and later
164177
#define MEMBER_DECLARE(L, R) L R;
165178
#define MEMBER_INT(R) MEMBER_DECLARE(int64_t, R)
166179
#define MEMBER_REAL(R) MEMBER_DECLARE(double, R)
167180
#define MEMBER_TEXT(R) MEMBER_DECLARE(std::wstring, R)
168181
#define MEMBER_DATETIME(R) MEMBER_DECLARE(sqlite_reflection::TimePoint, R)
169182
#define MEMBER_BOOL(R) MEMBER_DECLARE(bool, R)
183+
#define MEMBER_INT_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<int64_t>, R)
184+
#define MEMBER_REAL_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<double>, R)
185+
#define MEMBER_TEXT_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<std::wstring>, R)
186+
#define MEMBER_DATETIME_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<sqlite_reflection::TimePoint>, R)
187+
#define MEMBER_BOOL_NULLABLE(R) MEMBER_DECLARE(fcpp::optional_t<bool>, R)
170188
#define FUNC(SIGNATURE)
171189
FIELDS
172190
#undef MEMBER_DECLARE
@@ -175,6 +193,11 @@ struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
175193
#undef MEMBER_TEXT
176194
#undef MEMBER_DATETIME
177195
#undef MEMBER_BOOL
196+
#undef MEMBER_INT_NULLABLE
197+
#undef MEMBER_REAL_NULLABLE
198+
#undef MEMBER_TEXT_NULLABLE
199+
#undef MEMBER_DATETIME_NULLABLE
200+
#undef MEMBER_BOOL_NULLABLE
178201
#undef FUNC
179202
int64_t id;
180203

@@ -184,13 +207,23 @@ struct REFLECTABLE_DLL_EXPORT REFLECTABLE {
184207
#define MEMBER_TEXT(R)
185208
#define MEMBER_DATETIME(R)
186209
#define MEMBER_BOOL(R)
210+
#define MEMBER_INT_NULLABLE(R)
211+
#define MEMBER_REAL_NULLABLE(R)
212+
#define MEMBER_TEXT_NULLABLE(R)
213+
#define MEMBER_DATETIME_NULLABLE(R)
214+
#define MEMBER_BOOL_NULLABLE(R)
187215
#define FUNC(SIGNATURE) SIGNATURE;
188216
FIELDS
189217
#undef MEMBER_INT
190218
#undef MEMBER_REAL
191219
#undef MEMBER_TEXT
192220
#undef MEMBER_DATETIME
193221
#undef MEMBER_BOOL
222+
#undef MEMBER_INT_NULLABLE
223+
#undef MEMBER_REAL_NULLABLE
224+
#undef MEMBER_TEXT_NULLABLE
225+
#undef MEMBER_DATETIME_NULLABLE
226+
#undef MEMBER_BOOL_NULLABLE
194227
#undef FUNC
195228
};
196229

@@ -232,13 +265,23 @@ static std::string CAT(Register, REFLECTABLE)() {
232265
#define MEMBER_TEXT(R) DEFINE_MEMBER(R, SqliteStorageClass::kText)
233266
#define MEMBER_DATETIME(R) DEFINE_MEMBER(R, SqliteStorageClass::kDateTime)
234267
#define MEMBER_BOOL(R) DEFINE_MEMBER(R, SqliteStorageClass::kBool)
268+
#define MEMBER_INT_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kInt)
269+
#define MEMBER_REAL_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kReal)
270+
#define MEMBER_TEXT_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kText)
271+
#define MEMBER_DATETIME_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kDateTime)
272+
#define MEMBER_BOOL_NULLABLE(R) DEFINE_MEMBER_NULLABLE(R, SqliteStorageClass::kBool)
235273
#define FUNC(SIGNATURE)
236274
FIELDS
237275
#undef MEMBER_INT
238276
#undef MEMBER_REAL
239277
#undef MEMBER_TEXT
240278
#undef MEMBER_DATETIME
241279
#undef MEMBER_BOOL
280+
#undef MEMBER_INT_NULLABLE
281+
#undef MEMBER_REAL_NULLABLE
282+
#undef MEMBER_TEXT_NULLABLE
283+
#undef MEMBER_DATETIME_NULLABLE
284+
#undef MEMBER_BOOL_NULLABLE
242285
#undef FUNC
243286
}
244287
return name;

0 commit comments

Comments
 (0)