From 1ebabf7edf8c97a8fc8f350a6517afd719a231b7 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Fri, 9 Oct 2026 01:54:14 +0200 Subject: [PATCH] State a C wrapper casting an argument before its call as a composition A C wrapper that converts an argument through a public cast before it calls its MEOS function states its signature as a composition beside those A7 reads from SQL bodies: eIntersects(tpose, geometry), whose wrapper Eintersects_tpose_geo casts with tpoint = tpose_to_tpoint(temp) and calls eintersects_tgeo_geo(tpoint, gs), is the composition of operand 0 through tpose_to_tpoint and operand 1 into eintersects_tgeo_geo, and leaves that function's sqlSignatures. Each signature is traced to the wrapper whose CREATE FUNCTION states it and each argument of the call to the SQL argument it carries; a cast is a public catalog function of one parameter, and a wrapper casting nothing keeps its signature where it is. PostgreSQL keeps such a wrapper for its planner support function: an inlinable SQL body is inlined before index matching, and PostgreSQL offers an index to a support function only for an argument that is the indexed column itself, which the cast of the column is not. Witness: on MobilityDB master e30ef41657 the six ever relationships of tpose over geometry and tpose (eIntersects, eDwithin) are C wrappers calling the internal ea_intersects_tgeo_geo and ea_dwithin_tgeo_geo, which no @csqlfn names, so the catalog states none of the six; a wrapper calling the public eintersects_tgeo_geo with a cast argument would state eIntersects(tpose, geometry) on that function, and a binding would pass it a tpose it refuses. Measured: over a MobilityDB tree whose six tpose wrappers cast with tpose_to_tpoint and call the public tgeo functions, which name them in their @csqlfn, run.py states 6 C wrapper compositions, each operand of a tpose argument through tpose_to_tpoint and the others as they are, and none of the six signatures on the tgeo functions; over master e30ef41657 it states none, and the functions, compositions, aggregates and type encodings of the catalog equal those over master without the change. The suite passes, 486 tests, 25 skipped as before, the two new tests in tests/test_compositions.py among them. Why: the bindings reach a relationship of a derived type through the cast and the function of its cast target, as for the SQL compositions, while PostgreSQL answers it through a C wrapper its index serves. --- parser/compositions.py | 72 +++++++++++++++++++++++++++++ run.py | 8 +++- tests/test_compositions.py | 94 +++++++++++++++++++++++++++++++++++++- 3 files changed, 172 insertions(+), 2 deletions(-) diff --git a/parser/compositions.py b/parser/compositions.py index 6bc78f7..9135b83 100644 --- a/parser/compositions.py +++ b/parser/compositions.py @@ -494,3 +494,75 @@ def attach_compositions(idl, sql_src): + "\n ".join(errors)) idl["compositions"] = out return idl, len(out) + + +_CAST_CALL = re.compile(r"^\s*(\w+)\s*\(\s*(\w+)\s*\)\s*$") + + +def attach_wrapper_compositions(idl, mdb_src, sql_src, meos_src): + """(idl, count): the signatures a C wrapper states by converting an argument through a + public cast before it calls its MEOS function, moved from that function's + ``sqlSignatures`` to the top-level ``compositions`` #attach_compositions builds. + + ``Eintersects_tpose_geo`` reads a tpose, casts it with ``tpoint = tpose_to_tpoint(temp)`` + and calls ``eintersects_tgeo_geo(tpoint, gs)``, which states the wrapper in its + ``@csqlfn``: ``eIntersects(tpose, geometry)`` is then the composition + ``{"operands": [{"arg": 0, "casts": ["tpose_to_tpoint"], "param": "temp"}, + {"arg": 1, "param": "gs"}], "call": "eintersects_tgeo_geo"}``, the form a SQL body + casting its argument takes, so a binding passes the function a value of the type it + reads. PostgreSQL keeps the C wrapper, whose planner support function an inlined SQL + body cannot reach. Each signature is traced to the wrapper whose CREATE FUNCTION + states it (#_signature_wrapper of parser/boundargs.py) and each argument of the call to + the SQL argument it carries (#_caller_index); a cast is a public catalog function of + one parameter. A signature whose wrapper casts nothing stays where it is.""" + from parser.boundargs import (_call_args, _caller_index, _signature_wrapper, + extract_wrappers) + from parser.sqlfn import _meos_to_mdb, _wrapper_sql_sigs + wrappers = extract_wrappers(mdb_src) + m2d = _meos_to_mdb(meos_src) + w2sig = _wrapper_sql_sigs(sql_src) + public = {f["name"]: f for f in idl["functions"] if f.get("api") == "public"} + out = idl.setdefault("compositions", []) + n = 0 + for func in idl["functions"]: + claimed = [w for w in [func.get("mdbC")] + list(m2d.get(func["name"]) or ()) if w] + sigs = func.get("sqlSignatures") or [] + if not claimed or not sigs: + continue + outs = set((func.get("shape") or {}).get("outParams") or ()) + params = [p["name"] for p in func.get("params") or () if p["name"] not in outs] + keep = [] + for sig in sigs: + w = _signature_wrapper(func, sig, claimed, w2sig) + body = wrappers.get(w) if w else None + args = _call_args(body, func["name"]) if body else None + operands = None + if args is not None and len(args) == len(params): + operands, cast_seen = [], False + for a, p in zip(args, params): + a = a.strip() + rhs = [m.group(1) for m in re.finditer( + r"(?])" + re.escape(a) + r"\s*=(?!=)\s*([^;]+);", body)] + hit = _CAST_CALL.match(rhs[0]) if len(rhs) == 1 else None + cast = hit.group(1) if hit else None + if cast in public and len(public[cast].get("params") or ()) == 1: + operands.append({"arg": _caller_index(body, hit.group(2)), + "casts": [cast], "param": p}) + cast_seen = True + else: + operands.append({"arg": _caller_index(body, a), "param": p}) + if not cast_seen or any(o["arg"] is None for o in operands): + operands = None + if operands is None: + keep.append(sig) + continue + nargs = len(sig.get("args") or ()) + dflt = list(sig.get("argDefaults") or [None] * nargs) + out.append({"sqlName": sig.get("sqlName") or func.get("sqlfn"), + "args": list(sig.get("args") or ()), + "required": sum(1 for d in dflt if d is None), + "argDefaults": dflt, "ret": sig.get("ret"), + "operands": operands, "call": func["name"]}) + n += 1 + func["sqlSignatures"] = keep + return idl, n diff --git a/run.py b/run.py index 13f7e20..5b966f1 100644 --- a/run.py +++ b/run.py @@ -19,7 +19,7 @@ resolve_bound_names, strip_call_literals) from parser.aggregates import attach_aggregates from parser.altsqlfn import attach_alt_sql_names -from parser.compositions import attach_compositions +from parser.compositions import attach_compositions, attach_wrapper_compositions from parser.indexsearch import attach_index_search from parser.enrich import enrich_idl, restate_wire from parser.codecs import state_type_encodings @@ -409,6 +409,12 @@ def main(): idl, ncomp = attach_compositions(idl, SQL_SRC) print(f" SQL compositions over another type's functions: {ncomp}", file=sys.stderr) + # A C wrapper casting an argument before its call states a composition too: + # PostgreSQL keeps the wrapper for its planner support function. + if MEOS_SRC.exists() and MDB_SRC.exists(): + idl, nwcomp = attach_wrapper_compositions(idl, MDB_SRC, SQL_SRC, MEOS_SRC) + print(f" C wrapper compositions through a public cast: {nwcomp}", + file=sys.stderr) # State each SQL aggregate: its arguments, its result type and the SQL and MEOS # function of each of its roles, read from the deployed CREATE AGGREGATE statements diff --git a/tests/test_compositions.py b/tests/test_compositions.py index 276d9da..8035f5f 100644 --- a/tests/test_compositions.py +++ b/tests/test_compositions.py @@ -14,7 +14,7 @@ import unittest from pathlib import Path -from parser.compositions import attach_compositions +from parser.compositions import attach_compositions, attach_wrapper_compositions def _fn(name, sqlfn, params, sigs, api="public", shape=None): @@ -314,5 +314,97 @@ def test_a_cast_no_create_cast_declares_stops_the_catalog(self): self.assertIn("no CREATE CAST from tnpoint to tgeompoint", str(cm.exception)) +CAST_WRAPPERS = ''' +Datum +Eintersects_tpose_geo(PG_FUNCTION_ARGS) +{ + Temporal *temp = PG_GETARG_TEMPORAL_P(0); + GSERIALIZED *gs = PG_GETARG_GSERIALIZED_P(1); + Temporal *tpoint = tpose_to_tpoint(temp); + int result = eintersects_tgeo_geo(tpoint, gs); + pfree(tpoint); + PG_RETURN_BOOL(result ? true : false); +} + +Datum +Eintersects_tgeo_geo(PG_FUNCTION_ARGS) +{ + Temporal *temp = PG_GETARG_TEMPORAL_P(0); + GSERIALIZED *gs = PG_GETARG_GSERIALIZED_P(1); + int result = eintersects_tgeo_geo(temp, gs); + PG_RETURN_BOOL(result ? true : false); +} +''' + +CAST_SQL = ''' +CREATE FUNCTION eIntersects(tpose, geometry) + RETURNS boolean + AS 'MODULE_PATHNAME', 'Eintersects_tpose_geo' + SUPPORT tspatial_supportfn + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +CREATE FUNCTION eIntersects(tgeompoint, geometry) + RETURNS boolean + AS 'MODULE_PATHNAME', 'Eintersects_tgeo_geo' + SUPPORT tspatial_supportfn + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +''' + +CAST_MEOS = ''' +/** + * @brief Return 1 if a temporal geo ever intersects a geometry + * @csqlfn #Eintersects_tgeo_geo(), #Eintersects_tpose_geo() + */ +int +eintersects_tgeo_geo(const Temporal *temp, const GSERIALIZED *gs) +{ + return 0; +} +''' + + +class WrapperCompositionTests(unittest.TestCase): + """#attach_wrapper_compositions: a C wrapper casting an argument through a public cast + states a composition, and its sibling passing the argument as it is keeps its + signature.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + root = Path(self.tmp.name) + for sub, name, text in (("src", "rels.c", CAST_WRAPPERS), ("sql", "rels.in.sql", CAST_SQL), + ("meos", "rels_meos.c", CAST_MEOS)): + (root / sub).mkdir() + (root / sub / name).write_text(text) + + def tearDown(self): + self.tmp.cleanup() + + def _attach(self): + root = Path(self.tmp.name) + idl = {"functions": [ + {"name": "eintersects_tgeo_geo", "api": "public", "mdbC": "Eintersects_tgeo_geo", + "sqlfn": "eIntersects", "params": [{"name": "temp"}, {"name": "gs"}], + "sqlSignatures": [{"args": ["tgeompoint", "geometry"], "ret": "boolean"}, + {"args": ["tpose", "geometry"], "ret": "boolean"}]}, + {"name": "tpose_to_tpoint", "api": "public", "params": [{"name": "temp"}], + "sqlSignatures": []}], + "compositions": []} + return attach_wrapper_compositions(idl, root / "src", root / "sql", root / "meos") + + def test_the_cast_wrapper_states_a_composition(self): + idl, n = self._attach() + self.assertEqual(n, 1) + self.assertEqual(idl["compositions"], [{ + "sqlName": "eIntersects", "args": ["tpose", "geometry"], "required": 2, + "argDefaults": [None, None], "ret": "boolean", + "operands": [{"arg": 0, "casts": ["tpose_to_tpoint"], "param": "temp"}, + {"arg": 1, "param": "gs"}], + "call": "eintersects_tgeo_geo"}]) + + def test_the_wrapper_casting_nothing_keeps_its_signature(self): + idl, _ = self._attach() + self.assertEqual([s["args"] for s in idl["functions"][0]["sqlSignatures"]], + [["tgeompoint", "geometry"]]) + + if __name__ == "__main__": unittest.main()