From 1dde972486135f2a4547789b7da41042b366492b Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sat, 3 Oct 2026 16:05:19 +0200 Subject: [PATCH] Keep a shared SQL signature on the function whose parameters it fits attach_claims_fit, run once the object model and the type relations are attached, gathers the SQL signatures two or more public functions claim; where the arguments of one fit the C input parameters of a claimant, in their order or the one other order a wrapper passes them in, with the bound parameters and the out-parameters taking no argument, as _Resolver.params of parser/compositions.py matches a call, every claimant whose parameters they do not fit gives it up. A signature no claimant or every claimant fits stays where it is, a signature one function claims is untouched, and a function left with signatures under another name takes that name as its sqlfn. test_claims_fit.py states the typed kernels of one box wrapper, a bound parameter, and the three cases left alone. Witness: Tbox_value_tiles backs valueTiles over tbox with integer, bigint and float sizes, and tintbox_value_tiles, tbigintbox_value_tiles and tfloatbox_value_tiles each claimed all three signatures, since the type scope that chose them names the box they all take. A binding calls the first claimant it meets: the Spark SQL surface answered valueTiles(tbox(tint), 1, 0, true) through tbigintbox_value_tiles, which raised "The span must be of type bigintspan". teq_bool_tbool claimed tEqual(integer, tint) the same way. Why: a binding names the C function of a signature from the catalog alone, so a signature claimed by a function whose parameters it does not fit sends the call to the wrong kernel, and a Java int widening to a long or a double hides the mismatch from the compiler. Measured: over MobilityDB 43cfd3f936 the catalog gives up 48 signatures across 15 functions: the value tiles, expand, shift and scale functions of the int, bigint and float boxes, the tEqual and tNotEqual of tbool beside the other base types, aDisjoint(tcbuffer, geometry) on adisjoint_tcbuffer_cbuffer and cellToChildren(quadbin, integer) on quadbin_cell_to_children; no other field of any function changes. pytest tests/ answers 473 passed and 11 skipped with MDB_SRC_ROOT set. --- parser/sqlfn.py | 67 ++++++++++++++++++++++++- run.py | 8 +++ tests/test_claims_fit.py | 102 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 tests/test_claims_fit.py diff --git a/parser/sqlfn.py b/parser/sqlfn.py index 428e8ce..a8188bb 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -19,8 +19,10 @@ from pathlib import Path from parser.shapeinfer import _out_count_param -from parser.typescope import (C_BASE_TYPES, TypeFacts, declared_scopes, read_bodies, - sql_spellings, +from itertools import permutations + +from parser.typescope import (C_BASE_TYPES, SQL_ALIASES, TypeFacts, declared_scopes, + read_bodies, sql_spellings, require_scopes, scoped_signatures) # A @csqlfn tag carries one OR MORE #Wrapper() references — comma- or @@ -643,6 +645,67 @@ def attach_row_sources(idl, declared=None): return idl, n +def _signature_fits(func, sig, sqlc): + """Whether the arguments of `sig` fit the C input parameters of `func`, in their order + or in the one other order a wrapper may pass them in, as #_Resolver.params of + parser/compositions.py matches the operands of a call: True or False, or None when the + two counts differ (an array passed with its count, an argument left to its default). + The parameters `sig` binds and the out-parameters take no argument.""" + shape = func.get("shape") or {} + bound = sig.get("boundArgs") or shape.get("boundArgs") or {} + out = set(shape.get("outParams") or ()) + inputs = [p for p in func.get("params") or () + if p["name"] not in out and p["name"] not in bound] + if len(inputs) != len(sig["args"]): + return None + types = [] + for a in sig["args"]: + t = re.sub(r"\s*\(.*\)$", "", a.strip().lower()) + types.append(SQL_ALIASES.get(t, t)) + + def fit(order): + return all(_fits(t, *_c_base(p.get("cType")), sqlc) for t, p in zip(types, order)) + return fit(inputs) or any(fit(o) for o in permutations(inputs)) + + +def attach_claims_fit(idl): + """(idl, number of signatures given up) once every claimant of a shared SQL signature + keeps it only if its C parameters fit it. + + Two public functions can claim one SQL signature: a wrapper serving a whole family + (`Tbox_value_tiles`) is claimed by each typed MEOS function behind it + (`tintbox_value_tiles`, `tbigintbox_value_tiles`, `tfloatbox_value_tiles`), and a type + scope naming the box all three take keeps every signature on each. A binding then + calls whichever claimant it meets first, and `valueTiles(tbox, integer, integer, + boolean)` reaches the `int64` kernel. Where another claimant's parameters fit the + signature (#_signature_fits), a claimant whose parameters do not gives it up; a + signature no claimant or every claimant fits stays where it is. Runs once the object + model and the type relations are attached, as #attach_row_sources does, since a + parameter is matched to a SQL type by its C type.""" + sqlc = _sql_ctypes(idl) + claims = {} + for f in idl["functions"]: + if f.get("api") != "public": + continue + for s in f.get("sqlSignatures") or (): + key = (s.get("sqlName") or f.get("sqlfn"), tuple(s["args"])) + claims.setdefault(key, []).append((f, s)) + n = 0 + for pairs in claims.values(): + if len(pairs) < 2: + continue + fits = [(f, s, _signature_fits(f, s, sqlc)) for f, s in pairs] + if not any(v is True for _, _, v in fits): + continue + for f, s, v in fits: + if v is False: + f["sqlSignatures"] = [x for x in f["sqlSignatures"] if x is not s] + n += 1 + # A function may keep only signatures carrying another name than its sqlfn + idl, _ = state_deployed_sqlfn(idl) + return idl, n + + def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None): m2d = _meos_to_mdb(meos_src) d2s = _mdb_to_sql(mdb_src) diff --git a/run.py b/run.py index 05558f5..0424a84 100644 --- a/run.py +++ b/run.py @@ -23,6 +23,7 @@ from parser.enrich import enrich_idl, restate_wire from parser.codecs import state_type_encodings from parser.sqlfn import (attach_sqlfn_map, attach_aggfn_map, attach_row_sources, + attach_claims_fit, attach_sqlaggfn_map, lint_container_family_csqlfn, lint_ea_sqlfn, lint_positional_sqlfn, lint_sqlfn_case_collisions, state_deployed_sqlfn) @@ -368,6 +369,13 @@ def main(): # moment a family is added. idl = attach_temporal_types(idl, MOBILITYDB_SRC) + # Keep a SQL signature two public functions claim on the one whose C parameters it + # fits, matched by type once the object model and the type relations state the C type + # of each SQL type. + idl, nclaims = attach_claims_fit(idl) + print(f" shared SQL signatures given up by a function they do not fit: {nclaims}", + file=sys.stderr) + # Name the C value feeding each column of every row a SQL signature returns, matched # by type once the object model and the type relations state the C type of each SQL # type; a row with a column nothing feeds stops the catalog. diff --git a/tests/test_claims_fit.py b/tests/test_claims_fit.py new file mode 100644 index 0000000..dcda959 --- /dev/null +++ b/tests/test_claims_fit.py @@ -0,0 +1,102 @@ +"""A SQL signature two public functions claim stays on the one whose C parameters it fits. + +A wrapper serving a whole family is claimed by each typed MEOS function behind it, and a type +scope naming the box they all take keeps every signature on each. #attach_claims_fit of +parser/sqlfn.py lets a claimant whose parameters a signature does not fit give it up when +another claimant's parameters fit it, and changes nothing else. + +The catalog is synthetic, its classes stated as #CompositionTests of tests/test_compositions.py +states them. Plain unittest, no pytest dependency. +""" +import unittest + +from parser.sqlfn import attach_claims_fit + +TB = "const TBox *" + + +def _fn(name, sqlfn, params, sigs, api="public", shape=None): + f = {"name": name, "sqlfn": sqlfn, "api": api, + "params": [{"name": n, "cType": c} for n, c in params], + "sqlSignatures": [dict(s) for s in sigs]} + if shape: + f["shape"] = shape + return f + + +# The three signatures Tbox_value_tiles deploys, each typed kernel claiming all three +TILES = [{"args": ["tbox", "integer", "integer", "boolean"], "ret": "tbox[]"}, + {"args": ["tbox", "bigint", "bigint", "boolean"], "ret": "tbox[]"}, + {"args": ["tbox", "float", "float", "boolean"], "ret": "tbox[]"}] + + +def _kernel(name, ctype): + return _fn(name, "valueTiles", [("box", TB), ("vsize", ctype), ("vorigin", ctype), + ("border_inc", "bool")], TILES) + + +def _idl(*functions): + return {"functions": list(functions), + "objectModel": {"classes": {"TBox": {"cType": "TBox *"}}}} + + +def _args(f): + return [tuple(s["args"]) for s in f["sqlSignatures"]] + + +class ClaimsFitTests(unittest.TestCase): + """Each claimant of a shared signature keeps the signatures its parameters fit.""" + + def test_each_typed_kernel_keeps_its_own_signature(self): + idl, n = attach_claims_fit(_idl(_kernel("tintbox_value_tiles", "int"), + _kernel("tbigintbox_value_tiles", "int64"), + _kernel("tfloatbox_value_tiles", "double"))) + fns = {f["name"]: f for f in idl["functions"]} + self.assertEqual(_args(fns["tintbox_value_tiles"]), + [("tbox", "integer", "integer", "boolean")]) + self.assertEqual(_args(fns["tbigintbox_value_tiles"]), + [("tbox", "bigint", "bigint", "boolean")]) + self.assertEqual(_args(fns["tfloatbox_value_tiles"]), + [("tbox", "float", "float", "boolean")]) + self.assertEqual(n, 6) + + def test_a_bound_parameter_takes_no_argument(self): + # shiftValue binds the width of the shift_scale kernel, as the catalog states + sig = {"args": ["tbox", "integer"], "ret": "tbox", + "boundArgs": {"width": "0", "hasshift": "true", "haswidth": "false"}} + params = lambda t: [("box", TB), ("shift", t), ("width", t), # noqa: E731 + ("hasshift", "bool"), ("haswidth", "bool")] + idl, n = attach_claims_fit(_idl( + _fn("tintbox_shift_scale", "shiftValue", params("int"), [sig]), + _fn("tfloatbox_shift_scale", "shiftValue", params("double"), [sig]))) + fns = {f["name"]: f for f in idl["functions"]} + self.assertEqual(_args(fns["tintbox_shift_scale"]), [("tbox", "integer")]) + self.assertEqual(fns["tfloatbox_shift_scale"]["sqlSignatures"], []) + self.assertEqual(n, 1) + + def test_a_signature_every_claimant_fits_stays_on_each(self): + sig = [{"args": ["tbox", "integer", "integer", "boolean"], "ret": "tbox[]"}] + params = [("box", TB), ("a", "int"), ("b", "int"), ("c", "bool")] + idl, n = attach_claims_fit(_idl(_fn("one", "valueTiles", params, sig), + _fn("two", "valueTiles", params, sig))) + self.assertEqual(n, 0) + self.assertTrue(all(f["sqlSignatures"] for f in idl["functions"])) + + def test_a_signature_no_claimant_fits_stays_on_each(self): + sig = [{"args": ["tbox", "text"], "ret": "tbox"}] + idl, n = attach_claims_fit(_idl( + _fn("one", "f", [("box", TB), ("a", "int")], sig), + _fn("two", "f", [("box", TB), ("a", "double")], sig))) + self.assertEqual(n, 0) + + def test_an_internal_claimant_is_left_alone(self): + idl, n = attach_claims_fit(_idl(_kernel("tintbox_value_tiles", "int"), + _kernel("internal_tiles", "int64") | {"api": "internal"})) + fns = {f["name"]: f for f in idl["functions"]} + self.assertEqual(len(fns["internal_tiles"]["sqlSignatures"]), 3) + self.assertEqual(len(fns["tintbox_value_tiles"]["sqlSignatures"]), 3) + self.assertEqual(n, 0) + + +if __name__ == "__main__": + unittest.main()