Skip to content

Commit c162223

Browse files
committed
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
1 parent 3ee9551 commit c162223

3 files changed

Lines changed: 87 additions & 6 deletions

File tree

README.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,14 +169,18 @@ state. A stored empty string and a stored zero are distinct from `NULL` and hydr
169169
Two predicates cover nullness tests, since SQL's `= NULL` never matches:
170170
171171
```c++
172-
const auto unpaid = db->Fetch<Employee>(&IsNull(&Employee::salary));
173-
const auto paid = db->Fetch<Employee>(&IsNotNull(&Employee::salary));
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);
174176
```
175177

176-
Value predicates (`Equal`, `Like`, ...) on a nullable field compare against the contained
177-
value and take an optional as the comparison argument, e.g.
178-
`Equal(&Employee::salary, fcpp::optional_t<double>(50000.0))`. Passing an *empty* optional to a
179-
value predicate throws `std::invalid_argument` — use `IsNull`/`IsNotNull` for that instead.
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.
180184

181185
Non-nullable columns are now created with an explicit `NOT NULL` constraint, so the schema
182186
enforces what the object model assumes. Because tables are created with

include/query_predicates.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,14 @@ class REFLECTION_EXPORT GreaterThan final : public QueryPredicate {
265265

266266
template <typename T>
267267
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, ">") {}
268276
};
269277

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

280288
template <typename T>
281289
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, ">=") {}
282298
};
283299

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

294310
template <typename T>
295311
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, "<") {}
296320
};
297321

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

308332
template <typename T>
309333
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, "<=") {}
310342
};
311343

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

tests/nullable_test.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,51 @@ TEST_F(NullableTest, ValuePredicatesOperateOnTheContainedValue) {
217217
EXPECT_THROW(Equal(&NullableRecord::maybe_int, fcpp::optional_t<int64_t>()), std::invalid_argument);
218218
}
219219

220+
TEST_F(NullableTest, RangePredicatesOperateOnTheContainedValue) {
221+
const auto db = Database::Instance();
222+
223+
NullableRecord low;
224+
low.label = L"low";
225+
low.maybe_int = static_cast<int64_t>(3);
226+
low.maybe_real = 1.5;
227+
low.id = 1;
228+
db->Save(low);
229+
230+
NullableRecord high;
231+
high.label = L"high";
232+
high.maybe_int = static_cast<int64_t>(9);
233+
high.maybe_real = 9.5;
234+
high.id = 2;
235+
db->Save(high);
236+
237+
// A row with NULL columns never matches a range comparison in SQL
238+
NullableRecord unset;
239+
unset.label = L"unset";
240+
unset.id = 3;
241+
db->Save(unset);
242+
243+
const auto greater = GreaterThan(&NullableRecord::maybe_int, fcpp::optional_t<int64_t>(static_cast<int64_t>(5)));
244+
const auto greater_rows = db->Fetch<NullableRecord>(&greater);
245+
ASSERT_EQ(1, greater_rows.size());
246+
EXPECT_EQ(2, greater_rows[0].id);
247+
248+
const auto greater_equal =
249+
GreaterThanOrEqual(&NullableRecord::maybe_int, fcpp::optional_t<int64_t>(static_cast<int64_t>(3)));
250+
EXPECT_EQ(2, db->Fetch<NullableRecord>(&greater_equal).size());
251+
252+
const auto smaller = SmallerThan(&NullableRecord::maybe_real, fcpp::optional_t<double>(9.5));
253+
const auto smaller_rows = db->Fetch<NullableRecord>(&smaller);
254+
ASSERT_EQ(1, smaller_rows.size());
255+
EXPECT_EQ(1, smaller_rows[0].id);
256+
257+
const auto smaller_equal = SmallerThanOrEqual(&NullableRecord::maybe_real, fcpp::optional_t<double>(9.5));
258+
EXPECT_EQ(2, db->Fetch<NullableRecord>(&smaller_equal).size());
259+
260+
// Like every value predicate, an empty optional is a misuse and fails fast
261+
EXPECT_THROW(GreaterThan(&NullableRecord::maybe_int, fcpp::optional_t<int64_t>()), std::invalid_argument);
262+
EXPECT_THROW(SmallerThan(&NullableRecord::maybe_real, fcpp::optional_t<double>()), std::invalid_argument);
263+
}
264+
220265
TEST_F(NullableTest, NonNullableRecordsRoundTripUnchanged) {
221266
const auto db = Database::Instance();
222267

0 commit comments

Comments
 (0)