Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions parser/compositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"(?<![\w.>])" + 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
8 changes: 7 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
94 changes: 93 additions & 1 deletion tests/test_compositions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Loading