Skip to content

Implement nullable reflected fields#37

Closed
jkalias wants to merge 5 commits into
mainfrom
codex/implement-nullable-reflected-fields
Closed

Implement nullable reflected fields#37
jkalias wants to merge 5 commits into
mainfrom
codex/implement-nullable-reflected-fields

Conversation

@jkalias

@jkalias jkalias commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Motivation

  • Two issues (Support nullable fields in reflected records #2 and NULL and empty string are indistinguishable, and empty values are skipped on hydrate #20) share the same root cause: reflected fields could not represent SQL NULL (all members were always-present value types), so stored NULL was indistinguishable from default/empty values and dropped on fetch.
  • Provide a typed “absent” representation so reading, writing, schema generation, and predicates can express true nullability and preserve existing safety semantics.
  • Reuse the existing cross-std optional implementation from functional_cpp so the type aliases to std::optional on C++17+ toolchains while keeping C++11 compatibility.

Description

  • Add functional_cpp as a FetchContent dependency (pinned to tag 1.1.1) and expose it to the src target so public headers can use fcpp::optional_t<T>.
  • Extend reflection metadata with a nullable flag and introduce MEMBER_INT_NULLABLE, MEMBER_REAL_NULLABLE, MEMBER_TEXT_NULLABLE, MEMBER_DATETIME_NULLABLE, and MEMBER_BOOL_NULLABLE macros that declare fcpp::optional_t<T> members and register them as nullable.
  • Make schema generation emit NOT NULL for non-nullable reflected columns and omit NOT NULL for nullable columns (the id column still receives PRIMARY KEY AUTOINCREMENT).
  • Implement write-side null binding and read-side null hydration: empty optionals bind as SQL NULL (sqlite3_bind_null), non-empty optionals bind their contained value, and HydrateCurrentRow sets an optional to empty on SQLITE_NULL while preserving the existing NULL/BLOB safety path for non-nullable members.
  • Add IsNull(&T::field) and IsNotNull(&T::field) predicates (no bindings) and extend value predicates to accept member-pointers to nullable fields (they operate on the contained value); provide an GetOptionalSqlValue helper to extract contained values for bindings.
  • Update the README Defining records section to document the nullable macros, round-trip semantics, NOT NULL migration caveat, and that fcpp::optional_t<T> aliases std::optional<T> under C++17+.
  • Add unit tests (tests/nullable_record.h plus tests updates) that cover round-trip null vs default/empty distinction across all storage classes, schema NOT NULL behavior, IsNull/IsNotNull predicates, nullable value predicates, and a C++17 static_assert that the optional type aliases std::optional when available.

Testing

  • cmake -S . -B build could not complete because the environment proxy returned HTTP 403 while FetchContent attempted to download external archives (GoogleTest/FetchContent step), so a full build/test run was not possible in this environment (network error).
  • Lightweight/compile-time checks performed locally in the tree succeeded: g++ -std=c++11 -Iinclude -Isrc -I/tmp/fcpp -fsyntax-only src/queries.cc src/query_predicates.cc succeeded (checked binding/hydration/predicate codepaths).
  • Tests compilation checks (with a local test harness for gtest and the temporary functional_cpp header) succeeded for tests/database_test.cc and tests/query_predicates_test.cc under both C++11 and C++17 compile-time checks, including the static_assert for std::optional aliasing under C++17.
  • Ran git diff --check and basic repository checks; no whitespace/diff-check errors reported.

Closes #2
Closes #20


Codex Task

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5806a127c9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/queries.cc
Comment on lines +250 to +251
return is_id ? name + " PRIMARY KEY AUTOINCREMENT"
: (record_.member_metadata[index].nullable ? name : name + " NOT NULL");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update legacy NULL insert coverage before enforcing NOT NULL

When a fresh test database is initialized, this now creates existing non-nullable records such as Company with name/address declared NOT NULL. The existing FetchPreservesNullAndEmptyStringSkipSemantics test still uses UnsafeSql to omit those columns (INSERT INTO Company (id, name, age, salary) ... and INSERT INTO Company (id, age, address, salary) ...), so sqlite3_exec will fail on the constraint before the fetch behavior is exercised. Unless that coverage is migrated to a pre-existing/legacy schema or nullable fields, CI will fail on a clean database.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6296389c75

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/query_predicates.cc Outdated
SqlValue QueryPredicate::GetOptionalSqlValue(void* v, SqliteStorageClass storage_class) const {
switch (storage_class) {
case SqliteStorageClass::kInt:
return GetSqlValue(&reinterpret_cast<fcpp::optional_t<int64_t>*>(v)->value(), storage_class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use const-safe extraction for C++11 optional values

In C++11 builds that use functional_cpp's fallback optional, value() returns const T&, so &...value() is a const T*. Passing that to GetSqlValue(void*) is rejected by Clang/MSVC in the supported C++11 workflow jobs (GCC only masks it with -fpermissive -w), so nullable predicate support breaks those builds. Either make the value extraction const-safe or avoid taking a mutable pointer here.

Useful? React with 👍 / 👎.

template <typename T, typename R>
explicit Like(fcpp::optional_t<R> T::* fn, R value)
: QueryPredicate(fn, fcpp::optional_t<R>(value), "LIKE", [&](void* v, SqliteStorageClass storage_class) {
return GetOptionalSqlValue(v, storage_class);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply LIKE binding semantics to nullable fields

When a Like predicate is created for a nullable field, this path unwraps the optional through the base helper and never applies Like::GetSqlValue, which is the code that converts the binding to text, wraps it in %...%, and escapes user-supplied %/_. In C++17/GCC builds where this compiles, Like(&T::optional_text, std::wstring(L"%")) binds raw % and matches every non-NULL row instead of a literal percent, while ordinary substring searches become exact matches.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aefc4f9482

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

explicit Equal(R T::* fn, R value) : QueryPredicate(fn, value, "=") {}

template <typename T, typename R>
explicit Equal(fcpp::optional_t<R> T::* fn, R value) : QueryPredicate(fn, value, "=") {}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject optional control values for nullable predicates

When callers pass an fcpp::optional_t<T> control value to a nullable field, e.g. Equal(&NullableRecord::optional_text, fcpp::optional_t<std::wstring>{}), overload resolution bypasses this contained-value overload and selects the generic Equal(R T::*, R) overload instead. That path treats the optional object's storage as the contained T, so empty optionals can bind garbage or throw rather than performing a null-aware comparison; either delete optional-valued controls or route them to IS NULL/contained-value handling.

Useful? React with 👍 / 👎.

@jkalias jkalias closed this Jul 19, 2026
@jkalias

jkalias commented Jul 19, 2026

Copy link
Copy Markdown
Owner Author

Fixed by PR #38

@jkalias
jkalias deleted the codex/implement-nullable-reflected-fields branch July 19, 2026 17:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NULL and empty string are indistinguishable, and empty values are skipped on hydrate Support nullable fields in reflected records

1 participant