Skip to content

Commit 0aaf966

Browse files
committed
test(v3): pin ore_cllw ROW(NULL) RAISE divergence from eql_v2
eql_v3.compare_ore_cllw_term guards with `a::text IS NULL` (true only for a genuine SQL-NULL composite), so a hand-crafted ROW(NULL) falls through to the `a.bytes IS NULL` check and RAISES the extractor-invariant violation. eql_v2 guards with `a IS NULL`, which — for a single-field composite — is also true for ROW(NULL), so v2 silently returns NULL and its RAISE branch is unreachable. No committed test exercised this divergence: both the existing v3 and v2 tests only cover the shared SQL-NULL -> RETURN NULL path. Add comparator_raises_on_row_null_composite to validate v3 raises on ROW(NULL), and clarify the sibling NULL-return test's comment to make the divergence explicit.
1 parent c8fed7b commit 0aaf966

1 file changed

Lines changed: 49 additions & 7 deletions

File tree

tests/sqlx/tests/ore_cllw_v3_opclass_tests.rs

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,10 @@ async fn shorter_sorts_first_when_prefix_equal(pool: PgPool) -> Result<()> {
165165
let lt: bool = sqlx::query_scalar(&format!("SELECT {a} < {b}"))
166166
.fetch_one(&pool)
167167
.await?;
168-
assert!(lt, "shorter term should sort before its longer prefix-extension");
168+
assert!(
169+
lt,
170+
"shorter term should sort before its longer prefix-extension"
171+
);
169172

170173
let gt: bool = sqlx::query_scalar(&format!("SELECT {b} > {a}"))
171174
.fetch_one(&pool)
@@ -175,7 +178,10 @@ async fn shorter_sorts_first_when_prefix_equal(pool: PgPool) -> Result<()> {
175178
let neq: bool = sqlx::query_scalar(&format!("SELECT {a} <> {b}"))
176179
.fetch_one(&pool)
177180
.await?;
178-
assert!(neq, "different-length terms with equal prefix are not equal");
181+
assert!(
182+
neq,
183+
"different-length terms with equal prefix are not equal"
184+
);
179185
Ok(())
180186
}
181187

@@ -278,11 +284,10 @@ async fn ore_cllw_extractor_returns_composite_when_oc_present(pool: PgPool) -> R
278284

279285
#[sqlx::test]
280286
async fn comparator_returns_null_on_null_composite(pool: PgPool) -> Result<()> {
281-
// The comparator returns SQL NULL (not a raise) when handed a SQL-NULL
282-
// composite — the shape the extractor produces for a missing-`oc` row, which
283-
// btree's NULL handling then filters from range queries. (A non-NULL
284-
// composite with a NULL `bytes` field would raise instead, but that shape is
285-
// unreachable via the extractor.)
287+
// SQL-NULL composite (the shape the extractor produces for a missing-`oc`
288+
// row): the comparator returns SQL NULL, which btree's NULL handling then
289+
// filters from range queries. `a::text IS NULL` catches this — a genuine
290+
// SQL-NULL composite casts to NULL text. This path is shared with eql_v2.
286291
let cmp: Option<i32> = sqlx::query_scalar(
287292
"SELECT eql_v3.compare_ore_cllw_term(\
288293
eql_v3.ore_cllw('{\"s\":\"x\",\"c\":\"y\",\"hm\":\"abc\"}'::jsonb), \
@@ -298,6 +303,43 @@ async fn comparator_returns_null_on_null_composite(pool: PgPool) -> Result<()> {
298303
Ok(())
299304
}
300305

306+
#[sqlx::test]
307+
async fn comparator_raises_on_row_null_composite(pool: PgPool) -> Result<()> {
308+
// EXPLICIT, VALIDATED DIVERGENCE FROM eql_v2.
309+
//
310+
// A hand-crafted `ROW(NULL)::eql_v3.ore_cllw` is NOT a SQL-NULL composite —
311+
// for a single-field composite, `ROW(NULL) IS NULL` is true (row-IS-NULL
312+
// fires when every field is NULL), but `(ROW(NULL))::text` is the non-NULL
313+
// text `()`, so `a::text IS NULL` is FALSE. The value therefore falls
314+
// through v3's first guard to the `a.bytes IS NULL` check and RAISES the
315+
// extractor-invariant violation.
316+
//
317+
// eql_v2's `compare_ore_cllw_term` guards with `a IS NULL` instead, which IS
318+
// true for `ROW(NULL)`, so eql_v2 silently RETURNs NULL here and its RAISE
319+
// branch is unreachable (see eql_v2's own `comparator_raises_on_null_bytes_
320+
// in_non_null_composite`, which documents the branch as unreachable and
321+
// asserts the NULL return). v3 deliberately uses `a::text IS NULL` so the
322+
// "raise loudly rather than silently misorder" contract is reachable. This
323+
// test pins that divergence — neither the SQL-NULL test above nor the v2
324+
// suite exercises it.
325+
let err = sqlx::query(
326+
"SELECT eql_v3.compare_ore_cllw_term(\
327+
ROW(NULL)::eql_v3.ore_cllw, \
328+
ROW(decode('00ff', 'hex'))::eql_v3.ore_cllw\
329+
)",
330+
)
331+
.execute(&pool)
332+
.await
333+
.expect_err("ROW(NULL) composite must RAISE in v3, not return NULL");
334+
335+
let msg = format!("{err:?}");
336+
assert!(
337+
msg.contains("composite has NULL bytes field"),
338+
"expected the extractor-invariant RAISE, got: {msg}"
339+
);
340+
Ok(())
341+
}
342+
301343
// ===========================================================================
302344
// Functional-index match: ORDER BY engages Index Scan, not Sort
303345
//

0 commit comments

Comments
 (0)