From 105a0b208108e71ec90f9f507fa177615a4017de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Mon, 5 Oct 2026 02:08:37 +0200 Subject: [PATCH] Answer NULL at the distance sentinel the catalog states through distance_sentinel tools/codegen_duck_udfs.py reads the guard if (datum_eq(result, distance_sentinel(basetype), basetype)), on which a PG wrapper returns NULL for a distance computed in the base type, as the test r == std::numeric_limits::max() on the result in the type MEOS returns, before its conversion to the declared float, as emit_temporal_box tests the sentinel of a box. The nearest approach distance between two temporal numbers, and between a temporal number and a number, is NULL where the two share no time, over tint, tbigint and tfloat. Why: distance_sentinel answers the maximum of the type the distance is expressed in where no distance is computed, and NAD_tnumber_tnumber and NAD_tnumber_number return NULL on it; the catalog carries that guard in shape.nullableResult, while SCALAR_NULL_GUARD read only the result == DBL_MAX form, so the six nearestApproachDistance functions over numbers returned the sentinel itself as a distance. Witness: Gen_nad_tfloat_tfloat returns (double) r with no test, r being what nad_tfloat_tfloat answers, so for two tfloat values sharing no time the row holds DBL_MAX, and over tint and tbigint the maxima of int and int64 widened to a double; with this change the row is NULL. Measured: over the catalog the shared provision derives from MobilityDB master 4c4457d2ec, the generator writes the same 1972 functions and 7389 registrations, and the six Gen_nad_{tint_int, tint_tint, tbigint_bigint, tbigint_tbigint, tfloat_float, tfloat_tfloat} take ExecuteWithNulls and the test; no other function differs. The committed surface carries the six as the generator writes them; generated_temporal_udfs_2.cpp compiles warning-free. test/sql/parity/036_tnumber_distance.test states the three NULLs. --- src/generated/generated_temporal_udfs_2.cpp | 30 ++++++++++++--------- test/sql/parity/036_tnumber_distance.test | 18 +++++++++++++ tools/codegen_duck_udfs.py | 8 ++++-- 3 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/generated/generated_temporal_udfs_2.cpp b/src/generated/generated_temporal_udfs_2.cpp index a4a4c5e6..fe35a712 100644 --- a/src/generated/generated_temporal_udfs_2.cpp +++ b/src/generated/generated_temporal_udfs_2.cpp @@ -2162,69 +2162,75 @@ static void Gen_tdistance_tnumber_tnumber(DataChunk &args, ExpressionState &, Ve static void Gen_nad_tfloat_float(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); - BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), - [&](string_t in, double a2) { + BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), + [&](string_t in, double a2, ValidityMask &mask, idx_t idx) -> double { Temporal *t = BlobToTemporal(in); double r = nad_tfloat_float(t, a2); free(t); + if (r == std::numeric_limits::max()) { mask.SetInvalid(idx); return double(); } return (double) r; }); } static void Gen_nad_tfloat_tfloat(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); - BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), - [&](string_t in1, string_t in2) { + BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), + [&](string_t in1, string_t in2, ValidityMask &mask, idx_t idx) -> double { Temporal *t1 = BlobToTemporal(in1); Temporal *t2 = BlobToTemporal(in2); double r = nad_tfloat_tfloat(t1, t2); free(t1); free(t2); + if (r == std::numeric_limits::max()) { mask.SetInvalid(idx); return double(); } return (double) r; }); } static void Gen_nad_tbigint_bigint(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); - BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), - [&](string_t in, int64_t a2) { + BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), + [&](string_t in, int64_t a2, ValidityMask &mask, idx_t idx) -> double { Temporal *t = BlobToTemporal(in); int64_t r = nad_tbigint_bigint(t, a2); free(t); + if (r == std::numeric_limits::max()) { mask.SetInvalid(idx); return double(); } return (double) r; }); } static void Gen_nad_tbigint_tbigint(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); - BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), - [&](string_t in1, string_t in2) { + BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), + [&](string_t in1, string_t in2, ValidityMask &mask, idx_t idx) -> double { Temporal *t1 = BlobToTemporal(in1); Temporal *t2 = BlobToTemporal(in2); int64_t r = nad_tbigint_tbigint(t1, t2); free(t1); free(t2); + if (r == std::numeric_limits::max()) { mask.SetInvalid(idx); return double(); } return (double) r; }); } static void Gen_nad_tint_int(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); - BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), - [&](string_t in, int32_t a2) { + BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), + [&](string_t in, int32_t a2, ValidityMask &mask, idx_t idx) -> double { Temporal *t = BlobToTemporal(in); int32_t r = nad_tint_int(t, a2); free(t); + if (r == std::numeric_limits::max()) { mask.SetInvalid(idx); return double(); } return (double) r; }); } static void Gen_nad_tint_tint(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); - BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), - [&](string_t in1, string_t in2) { + BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), + [&](string_t in1, string_t in2, ValidityMask &mask, idx_t idx) -> double { Temporal *t1 = BlobToTemporal(in1); Temporal *t2 = BlobToTemporal(in2); int32_t r = nad_tint_tint(t1, t2); free(t1); free(t2); + if (r == std::numeric_limits::max()) { mask.SetInvalid(idx); return double(); } return (double) r; }); } diff --git a/test/sql/parity/036_tnumber_distance.test b/test/sql/parity/036_tnumber_distance.test index 2caae083..30d71419 100644 --- a/test/sql/parity/036_tnumber_distance.test +++ b/test/sql/parity/036_tnumber_distance.test @@ -59,3 +59,21 @@ query I SELECT typeof("<->"('1@2000-01-01'::tint, '2@2000-01-01'::tint)); ---- tint + +# ── two values sharing no time have no nearest approach ─────────────────────── +# NAD_tnumber_tnumber returns NULL where nad_tnumber_tnumber answers the distance +# sentinel, the maximum of the type the distance is computed in. +query I +SELECT nearestApproachDistance(tint '[1@2000-01-01, 5@2000-01-05]', tint '[3@2000-02-01, 3@2000-02-05]') IS NULL; +---- +true + +query I +SELECT nearestApproachDistance(tbigint '[1@2000-01-01, 5@2000-01-05]', tbigint '[3@2000-02-01, 3@2000-02-05]') IS NULL; +---- +true + +query I +SELECT nearestApproachDistance(tfloat '[1@2000-01-01, 5@2000-01-05]', tfloat '[3@2000-02-01, 3@2000-02-05]') IS NULL; +---- +true diff --git a/tools/codegen_duck_udfs.py b/tools/codegen_duck_udfs.py index 0c0a0972..e071e73a 100644 --- a/tools/codegen_duck_udfs.py +++ b/tools/codegen_duck_udfs.py @@ -481,9 +481,13 @@ def scalar_emit3(f): # (`shape.nullableResult`, read from the wrapper's `if () PG_RETURN_NULL();`): a # three-valued predicate answering unknown, and the distance sentinel. They name the result # `result`; the generated body names it `r`. A pointer result's guard (`! result`, -# `result == NULL`) is the NULL pointer the pointer paths already map. +# `result == NULL`) is the NULL pointer the pointer paths already map. The distance sentinel of +# distance_sentinel is the maximum of the type MEOS returns, tested on `r` in that type before +# its conversion to the declared one, as #emit_temporal_box tests it. SCALAR_NULL_GUARD = {"if (result < 0)": "r < 0", - "if (result == DBL_MAX)": "r == DBL_MAX"} + "if (result == DBL_MAX)": "r == DBL_MAX", + "if (datum_eq(result, distance_sentinel(basetype), basetype))": + "r == std::numeric_limits::max()"} def scalar_exec(f, rett, rexpr): """(executor method, extra lambda parameters, lambda return annotation, return statement) for