Background
Every field of a reflected record is declared through one of the typed macros in include/reflection.h — MEMBER_INT, MEMBER_REAL, MEMBER_TEXT, MEMBER_DATETIME, MEMBER_BOOL — and each expands to a concrete, always-present C++ type (int64_t, double, std::wstring, TimePoint, bool). There is no way to express that a field may have no value.
This is a real limitation: nullable columns are a normal part of relational schema design (an optional middle name, an as-yet-unset completion date, a price that is "unknown" rather than 0). Today every field is forced to carry a default-constructed value, so the model cannot distinguish "explicitly empty/zero" from "absent".
Current behavior
- On the C++ side, a field always holds a value of its underlying type. There is no representation for SQL
NULL, so a record round-tripped through the database can never report a missing field.
- On the SQL side, column definitions are generated in
CreateTableQuery::CustomizedColumnName (src/queries.cc) as simply <name> <TYPE> (plus PRIMARY KEY AUTOINCREMENT for id). No NOT NULL constraint is ever emitted, so the columns are physically nullable, but the library has no typed way to read or write a NULL into them. The two layers are out of sync: the schema permits NULL, the object model does not.
Goal
Allow a reflected field to be declared as nullable, so that:
- A nullable field can represent the absence of a value in C++ (the natural fit is
std::optional<T>, which requires raising the project's minimum standard to C++17 — see note below — or otherwise a library-provided nullable wrapper for C++11).
- Saving a record with an unset nullable field writes SQL
NULL (bound via sqlite3_bind_null), and fetching a row with a NULL column hydrates the field back to the empty/unset state.
- Non-nullable fields gain an explicit
NOT NULL constraint in the generated CREATE TABLE, so the schema enforces the contract the object model already assumes.
Open questions / design notes
- API surface. Options include new macros (e.g.
MEMBER_TEXT_NULLABLE(name)) or a wrapping macro applied to existing declarations. The choice should keep the X-macro expansion in reflection.h readable and keep MemberMetadata aware of nullability so query generation and binding can branch on it.
- C++ standard.
std::optional is the obvious representation but is C++17. The README currently advertises a C++11 minimum, so this needs an explicit decision: raise the minimum, or ship a small Nullable<T> type for the C++11 build.
- Predicates. Nullable fields imply
IS NULL / IS NOT NULL tests in query_predicates.h; these should be considered as part of, or as a fast follow to, this work since Equal(&T::field, value) cannot express a NULL comparison in SQL.
- Migration. Because tables are created with
CREATE TABLE IF NOT EXISTS, adding NOT NULL to existing non-nullable columns only affects newly created tables; pre-existing database files keep their current schema until migrated. This caveat mirrors the one already documented for AUTOINCREMENT in the README.
Acceptance criteria
- A field can be declared nullable and round-trips an unset value as SQL
NULL through save/fetch.
- Non-nullable fields emit
NOT NULL in CREATE TABLE.
IS NULL / IS NOT NULL predicate support is available (or tracked in a linked follow-up).
- Tests cover save/fetch of both set and unset nullable fields of each storage class, and the README "Defining records" section documents the nullable macro(s) and the C++-standard requirement.
Background
Every field of a reflected record is declared through one of the typed macros in
include/reflection.h—MEMBER_INT,MEMBER_REAL,MEMBER_TEXT,MEMBER_DATETIME,MEMBER_BOOL— and each expands to a concrete, always-present C++ type (int64_t,double,std::wstring,TimePoint,bool). There is no way to express that a field may have no value.This is a real limitation: nullable columns are a normal part of relational schema design (an optional middle name, an as-yet-unset completion date, a price that is "unknown" rather than
0). Today every field is forced to carry a default-constructed value, so the model cannot distinguish "explicitly empty/zero" from "absent".Current behavior
NULL, so a record round-tripped through the database can never report a missing field.CreateTableQuery::CustomizedColumnName(src/queries.cc) as simply<name> <TYPE>(plusPRIMARY KEY AUTOINCREMENTforid). NoNOT NULLconstraint is ever emitted, so the columns are physically nullable, but the library has no typed way to read or write aNULLinto them. The two layers are out of sync: the schema permits NULL, the object model does not.Goal
Allow a reflected field to be declared as nullable, so that:
std::optional<T>, which requires raising the project's minimum standard to C++17 — see note below — or otherwise a library-provided nullable wrapper for C++11).NULL(bound viasqlite3_bind_null), and fetching a row with aNULLcolumn hydrates the field back to the empty/unset state.NOT NULLconstraint in the generatedCREATE TABLE, so the schema enforces the contract the object model already assumes.Open questions / design notes
MEMBER_TEXT_NULLABLE(name)) or a wrapping macro applied to existing declarations. The choice should keep the X-macro expansion inreflection.hreadable and keepMemberMetadataaware of nullability so query generation and binding can branch on it.std::optionalis the obvious representation but is C++17. The README currently advertises a C++11 minimum, so this needs an explicit decision: raise the minimum, or ship a smallNullable<T>type for the C++11 build.IS NULL/IS NOT NULLtests inquery_predicates.h; these should be considered as part of, or as a fast follow to, this work sinceEqual(&T::field, value)cannot express a NULL comparison in SQL.CREATE TABLE IF NOT EXISTS, addingNOT NULLto existing non-nullable columns only affects newly created tables; pre-existing database files keep their current schema until migrated. This caveat mirrors the one already documented forAUTOINCREMENTin the README.Acceptance criteria
NULLthrough save/fetch.NOT NULLinCREATE TABLE.IS NULL/IS NOT NULLpredicate support is available (or tracked in a linked follow-up).