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()