diff --git a/src/generated/generated_temporal_udfs.cpp b/src/generated/generated_temporal_udfs.cpp index 56efc60e..ffcd3d95 100644 --- a/src/generated/generated_temporal_udfs.cpp +++ b/src/generated/generated_temporal_udfs.cpp @@ -10264,6 +10264,17 @@ static void Gen_temporal_hash(DataChunk &args, ExpressionState &, Vector &result }); } +static void Gen_temporal_hash_extended(DataChunk &args, ExpressionState &, Vector &result) { + EnsureMeosThreadInitialized(); + BinaryExecutor::Execute(args.data[0], args.data[1], result, args.size(), + [&](string_t in, uint64_t a2) { + Temporal *t = BlobToTemporal(in); + uint64_t r = temporal_hash_extended(t, a2); + free(t); + return r; + }); +} + static void Gen_temporal_instant_n(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), @@ -17946,6 +17957,21 @@ static void RegisterGenerated_meos_temporal_accessor(ExtensionLoader &loader) { RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash", {TJsonbTypes::tjsonb()}, LogicalType::UINTEGER, Gen_temporal_hash)); RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash", {TPcpointTypes::tpcpoint()}, LogicalType::UINTEGER, Gen_temporal_hash)); RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash", {TPcpatchTypes::tpcpatch()}, LogicalType::UINTEGER, Gen_temporal_hash)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TemporalTypes::tint(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TemporalTypes::tbigint(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TemporalTypes::tbool(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TemporalTypes::tfloat(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TemporalTypes::ttext(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TgeompointType::tgeompoint(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TgeogpointType::tgeogpoint(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TGeometryTypes::tgeometry(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TGeographyTypes::tgeography(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {CbufferTypes::tcbuffer(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {H3indexTypes::th3index(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {QuadbinTypes::tquadbin(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TJsonbTypes::tjsonb(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TPcpointTypes::tpcpoint(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); + RegisterSerializedScalarFunction(loader, ScalarFunction("temporal_hash_extended", {TPcpatchTypes::tpcpatch(), LogicalType::UBIGINT}, LogicalType::UBIGINT, Gen_temporal_hash_extended)); RegisterSerializedScalarFunction(loader, ScalarFunction("instantN", {TemporalTypes::tint(), LogicalType::INTEGER}, TemporalTypes::tint(), Gen_temporal_instant_n)); RegisterSerializedScalarFunction(loader, ScalarFunction("instantN", {TemporalTypes::tbigint(), LogicalType::INTEGER}, TemporalTypes::tbigint(), Gen_temporal_instant_n)); RegisterSerializedScalarFunction(loader, ScalarFunction("instantN", {TemporalTypes::tbool(), LogicalType::INTEGER}, TemporalTypes::tbool(), Gen_temporal_instant_n)); diff --git a/test/sql/temporal_hash.test b/test/sql/temporal_hash.test new file mode 100644 index 00000000..27c17ee3 --- /dev/null +++ b/test/sql/temporal_hash.test @@ -0,0 +1,43 @@ +# name: test/sql/temporal_hash.test +# description: Generated hash surface for the temporal types — temporal_hash (uint32) +# and temporal_hash_extended (uint64 + seed). Both use DuckDB's native +# unsigned types (UINTEGER / UBIGINT): a MEOS hash fills the full unsigned +# range, so a signed INTEGER/BIGINT would be out of range (DuckDB range- +# checks the cast, unlike PostgreSQL/C which bit-reinterpret). + +require mobilityduck + +# temporal_hash returns the uint32 hash as the native UINTEGER +query I +SELECT temporal_hash(tint '1@2000-01-01'); +---- +157457912 + +query I +SELECT typeof(temporal_hash(tint '1@2000-01-01')); +---- +UINTEGER + +# temporal_hash_extended returns the uint64 hash as the native UBIGINT. This value +# (> 2**63) does NOT fit in a signed BIGINT — proof that UBIGINT is required. +query I +SELECT temporal_hash_extended(tint '1@2000-01-01', 0); +---- +11445401048662056440 + +query I +SELECT typeof(temporal_hash_extended(tint '1@2000-01-01', 0)); +---- +UBIGINT + +# the seed changes the hash (0 vs 42) +query I +SELECT temporal_hash_extended(tint '1@2000-01-01', 42); +---- +10824501823241357556 + +# inherited over every temporal family via Temporal +query I +SELECT temporal_hash_extended(tfloat '1.5@2000-01-01', 42); +---- +17681145013286498633 diff --git a/tools/codegen_duck_udfs.py b/tools/codegen_duck_udfs.py index 3f6469da..4d589182 100644 --- a/tools/codegen_duck_udfs.py +++ b/tools/codegen_duck_udfs.py @@ -94,6 +94,13 @@ def base(canon): # SCALAR_RET_CPP note below. The uint64 cell ids keep the CELL_UINT path. "uint32": ("LogicalType::UINTEGER", "uint32_t"), "uint32_t": ("LogicalType::UINTEGER", "uint32_t"), + # NB: uint64_t is deliberately NOT a generic SCALAR arg/return. A bare uint64 in + # ARG position is a Tcell cell-id base value (h3index/quadbin) already typed by + # the CELL_BASEVAL comparison shape; adding it here makes a SECOND shape emit those + # comparisons -> a duplicate Gen_ body (compile error: redefinition). The only + # uint64 surface is *_hash_extended's (Temporal, seed) -> uint64, handled by + # shape_binary via SCALAR_ARG / SCALAR_RET_CPP (UBIGINT); supported() lets the + # SCALAR_ARG seed through even though arg_type has no generic uint64 mapping. "int64": ("LogicalType::BIGINT", "int64_t"), "int64_t": ("LogicalType::BIGINT", "int64_t"), "double": ("LogicalType::DOUBLE", "double"), @@ -152,11 +159,13 @@ def ret_type(f, out_canon): return None rc = f["returnType"]["canonical"]; nc = norm(rc); b = base(rc) if b in PTR_RET and nc.endswith("*"): return (PTR_RET[b][0], "ptr") + if b in CELL_UINT and "*" not in nc: # a bare uint64 RETURN: either a Tcell cell id or a hash + sc = reg_scope(f["name"]) # (checked here, not via SCALAR, so uint64 is NOT a generic + if sc and sc[0] == "types" and len(sc[1]) == 1 and sc[1][0] in CELL_BASEVAL: # scalar ARG — that + return (CELL_BASEVAL[sc[1][0]], "scalar") # would shadow the cell comparisons). A single + return ("LogicalType::UBIGINT", "scalar") # cell type -> its cell DuckDB type; any other + # uint64 (*_hash_extended) -> native UBIGINT. if b in SCALAR and "*" not in nc: return (SCALAR[b][0], "scalar") - if b in CELL_UINT and "*" not in nc: # Tcell cell-id base value (startValue/endValue) - sc = reg_scope(f["name"]) - if sc and sc[0] == "types" and len(sc[1]) == 1 and sc[1][0] in CELL_BASEVAL: - return (CELL_BASEVAL[sc[1][0]], "scalar") if b == "GSERIALIZED" and nc.endswith("*"): # owned geometry -> DuckDB GEOMETRY (geo marshaller) return ("GeoTypes::GEOMETRY()", "geo") if b == "Interval" and nc.endswith("*"): # owned ptr -> by-value interval_t (TakeInterval) @@ -242,7 +251,16 @@ def supported(f): if ret_type(f, out) is None: return "ret:" + norm(f["returnType"]["canonical"]) for p in in_params: - if arg_type(p["canonical"]) is None: + # A by-value uint64 seed of a *_hash_extended fn has no generic arg_type mapping + # (uint64 is kept out of SCALAR so it does not shadow the cell-id comparisons), + # but it IS marshalled by shape_binary via SCALAR_ARG. Accept it ONLY for the + # *_hash_extended functions so temporal_hash_extended is emittable; a uint64 in + # any OTHER function is a cell-id base value that must STAY rejected here, else a + # second (generic ever/always) shape shadows the specialized cell comparison and + # emits a duplicate Gen_ body (redefinition). + if arg_type(p["canonical"]) is None and not ( + base(p["canonical"]) in SCALAR_ARG and "*" not in norm(p["canonical"]) + and name.endswith("_hash_extended")): return "arg:" + norm(p["canonical"]) return None @@ -265,11 +283,14 @@ def family(f): "int64_t": ("int64_t", "LogicalType::BIGINT"), # The MEOS *_hash functions return uint32 (PG hash width) and # must register as UINTEGER, not a signed INTEGER that flips - # the sign of hashes >= 2**31. (uint64 returns — hash seeds and - # the H3Index/Quadbin cell ids — are handled elsewhere: cell - # ids via the CELL_UINT path; the *_hash_extended seed width is - # left to a follow-up so it does not surface here yet.) + # the sign of hashes >= 2**31 (DuckDB RANGE-CHECKS the cast, it + # does NOT bit-reinterpret the way PG/C does; PG only *declares* + # integer because it lacks unsigned SQL types). The uint64 + # *_hash_extended value returns UBIGINT analogously (native + # unsigned, holds the full uint64 hash; the seed arg is UBIGINT + # too — see SCALAR_ARG). uint64 cell ids keep the CELL_UINT path. "uint32_t": ("uint32_t", "LogicalType::UINTEGER"), + "uint64_t": ("uint64_t", "LogicalType::UBIGINT"), "double": ("double", "LogicalType::DOUBLE"), "bool": ("bool", "LogicalType::BOOLEAN")} # By-value scalar returns the TEMPORAL detectors accept: the identity-marshalled ones @@ -818,14 +839,16 @@ def shape_emittable(f): # Take only the owned (non-const) return so the body's free(t)+TemporalToBlob stays correct. if rb in TEMPORAL_PTR_RET and rn.endswith("*") and "const" not in f["returnType"]["canonical"]: return ("temporal", "MD_TEMPORAL") # ret type resolved per-accessor via ret_temporal_type - if rb in BYVAL_RET and "*" not in rn: - return ("scalar:" + rb, scalar_ret_duck(f)) # Tcell cell-id base value: a per-type cell accessor (reg_scope keys the single cell - # temporal type) returns the collapsed uint64 cell id -> the family's BIGINT-backed cell - # DuckDB type (startValue/endValue on th3index -> h3index()). See CELL_BASEVAL. + # temporal type) returns the collapsed uint64 cell id -> the family's cell DuckDB type + # (startValue/endValue on th3index -> h3index()). Checked BEFORE the generic by-value + # scalar branch so a uint64 cell id keeps its cell type (BYVAL_RET now includes uint64 + # for *_hash_extended). See CELL_BASEVAL. if rb in CELL_UINT and "*" not in rn and sc[0] == "types" and len(sc[1]) == 1 \ and sc[1][0] in CELL_BASEVAL: return ("cellscalar", CELL_BASEVAL[sc[1][0]]) + if rb in BYVAL_RET and "*" not in rn: + return ("scalar:" + rb, scalar_ret_duck(f)) if rb in ("Interval", "text", "char") and rn.endswith("*"): # owned-pointer scalar return (duration / text / cstring) return ("scalar:" + rb, scalar_ret_duck(f)) # base-value pointer return (Temporal accessor: startValue/endValue -> the family base value, @@ -916,6 +939,7 @@ def emit_body(f, kind): "int": ("LogicalType::INTEGER", "int32_t", "a2"), "int32_t": ("LogicalType::INTEGER", "int32_t", "a2"), "int64_t": ("LogicalType::BIGINT", "int64_t", "a2"), + "uint64_t": ("LogicalType::UBIGINT", "uint64_t", "a2"), # *_hash_extended seed (native unsigned) "double": ("LogicalType::DOUBLE", "double", "a2"), "bool": ("LogicalType::BOOLEAN", "bool", "a2"), "TimestampTz": ("LogicalType::TIMESTAMP_TZ", "timestamp_tz_t", "DuckDBToMeosTimestamp(a2).value"),