From fea60cbd57b36350f320cceeabe13b1967671fd5 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Fri, 9 Oct 2026 23:20:17 +0200 Subject: [PATCH] State the interpolation a wrapper takes from the temporal type of a signature A SQL signature carries in boundArgs the interpolation its wrapper derives from the temporal type of the call where the call carries none: interp = temptype_supports_linear(temptype) ? LINEAR : STEP, temptype the type the function returns (Tsequenceset_constructor_gaps) or the type of one of its arguments (Temporal_append_tinstant), with the wrapper reading argument k instead only when the call carries it. attach_type_derived_args of parser/boundargs.py reads the choice and the source of the type in the wrapper, takes k from #_caller_index, and binds LINEAR or STEP on each signature stating at most k arguments by the linear class the temporal types state, read from temptype_supports_linear by attach_temporal_types, after which it runs; the wrappers are traced as #merge_boundargs traces them. Witness: on MEOS-API master f8f63bd over MobilityDB master a0480a7d38 no signature of temporal_append_tinstant, trgeometry_append_tinstant, temporal_app_tinst_transfn or tsequenceset_make_gaps binds interp, so a binding takes it for an argument appendInstant(tfloat, tfloat) and tintSeqSetGaps(tint[], interval, float) do not carry, and the typed Spark and Flink surfaces of JMEOS refuse 49 such signatures as arity:sql. Measured over MobilityDB master a0480a7d38: run.py binds interp on 49 signatures, 17 to LINEAR and 32 to STEP, each the class of its temporal type: 21 of appendInstant, 20 of appendInstantTransition and the 8 of the SeqSetGaps constructors without an interpolation; every other key of every function equals that of master's parser. With JMEOS main's generator both typed surfaces register 1162 SQL functions and 8802 overloads, against 1154 and 8754, the refusals as arity:sql falling from 121 to 72. The suite passes 523 tests with none skipped, among them the three of tests/test_boundargs.py TypeDerivedArgTests. Why: a binding passes the interpolation the PostgreSQL function would, and the class of a temporal type is the catalog's to state, not each binding's to derive. --- parser/boundargs.py | 82 +++++++++++++++++++++++++++++++ run.py | 12 ++++- tests/test_boundargs.py | 105 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 197 insertions(+), 2 deletions(-) diff --git a/parser/boundargs.py b/parser/boundargs.py index dabf623..16d9bcb 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -535,6 +535,88 @@ def own(w, sig=None): return idl, n, list(dict.fromkeys(drift)) +# A local a wrapper sets from the class of a temporal type: `interp = +# temptype_supports_linear(temptype) ? LINEAR : STEP`. +_TYPE_CHOICE = r"(?]){var}\s*=(?!=)\s*temptype_supports_linear\s*\(\s*(\w+)\s*\)" \ + r"\s*\?\s*(\w+)\s*:\s*(\w+)\s*;" +# The temporal type of the call, read from its return or from argument i: +# `temptype = oid_meostype(get_fn_expr_rettype(fcinfo->flinfo))`. +_CALL_TYPE = r"(?]){var}\s*=(?!=)\s*oid_meostype\s*\(\s*get_fn_expr_(rettype|argtype)" \ + r"\s*\(\s*fcinfo\s*->\s*flinfo\s*(?:,\s*(\d+)\s*)?\)\s*\)" + + +def _type_choice(body: str, var: str) -> tuple[int | None, str, int | None, str, str] | None: + """``(k, source, i, linear, other)`` when local ``var`` of wrapper ``body`` is set to + ``temptype_supports_linear(t) ? linear : other`` and ``t`` is the temporal type of the + call's return (``source`` "ret") or of its argument ``i`` (``source`` "arg"); ``k`` is + the SQL argument the wrapper otherwise reads ``var`` from (#_caller_index), None when it + reads none.""" + m = re.search(_TYPE_CHOICE.format(var=re.escape(var)), body) + if not m: + return None + t = re.search(_CALL_TYPE.format(var=re.escape(m.group(1))), body) + if not t: + return None + k = _caller_index(body, var) + return (k, "ret" if t.group(1) == "rettype" else "arg", + int(t.group(2)) if t.group(2) else None, m.group(2), m.group(3)) + + +def attach_type_derived_args(idl: dict, mdb_src: str | Path, + sql_src: str | Path | None = None, + meos_src: str | Path | None = None) -> tuple[dict, int]: + """(idl, number of arguments bound) once each SQL signature carries in ``boundArgs`` the + value a wrapper derives from the temporal type of the signature. + + A wrapper that reads no interpolation from the call takes the one the type supports, + ``interp = temptype_supports_linear(temptype) ? LINEAR : STEP`` with ``temptype`` the + type the function returns (``Tsequenceset_constructor_gaps``) or the type of one of + its arguments (``Temporal_append_tinstant``), and reads argument ``k`` instead only + when the call carries it. A signature stating at most ``k`` arguments therefore passes + LINEAR or STEP as fixed as a literal, the class #attach_temporal_types states for its + temporal type (``linear``, read from ``temptype_supports_linear``), so it is bound on + the signature as #merge_boundargs binds a literal the wrapper starts a local from. Runs + once the temporal types are attached; the wrappers are traced as #merge_boundargs + traces them.""" + from parser.sqlfn import _meos_to_mdb, _wrapper_sql_sigs + wrappers = extract_wrappers(mdb_src) + m2d = _meos_to_mdb(meos_src) if meos_src else {} + w2sig = _wrapper_sql_sigs(sql_src) if sql_src else {} + linear = {t: rec.get("linear") for t, rec in (idl.get("temporalTypes") or {}).items()} + n = 0 + for func in idl["functions"]: + primary = func.get("mdbC") + if not primary: + continue + ws = [primary] + [w for w in m2d.get(func["name"]) or () if w != primary] + params = [p.get("name") for p in func.get("params", [])] + for sig in func.get("sqlSignatures") or []: + w = _signature_wrapper(func, sig, ws, w2sig) or ws[0] + body = wrappers.get(w) + if not body: + continue + args = _call_args(body, func["name"]) or _call_args(body, "pg_" + func["name"]) + for pos, a in enumerate(args or ()): + if pos >= len(params) or not _IDENT.match(a): + continue + choice = _type_choice(body, a) + if choice is None: + continue + k, source, i, lin, other = choice + sargs = sig.get("args") or [] + if k is not None and len(sargs) > k: + continue + t = sig.get("ret") if source == "ret" else ( + sargs[i] if i is not None and i < len(sargs) else None) + if t not in linear: + continue + bound = sig.setdefault("boundArgs", {}) + if params[pos] not in bound: + bound[params[pos]] = lin if linear[t] else other + n += 1 + return idl, n + + # The SQL argument a wrapper reads: `PG_GETARG_(k)`, or a helper handed the call info # with the index, as `input_interp_string(fcinfo, 1)` reads argument 1. _GETARG = re.compile(r"PG_GETARG_\w+\s*\(\s*(\d+)\s*\)|\bfcinfo\s*,\s*(\d+)\s*\)") diff --git a/run.py b/run.py index 1cd0333..5ea6f09 100644 --- a/run.py +++ b/run.py @@ -15,7 +15,8 @@ from parser.nullable import merge_nullable, extract_param_docs from parser.nullresult import attach_null_result from parser.outparam import extract_param_names, merge_outparams -from parser.boundargs import (attach_call_literals, merge_boundargs, merge_sql_arg_params, +from parser.boundargs import (attach_call_literals, attach_type_derived_args, merge_boundargs, + merge_sql_arg_params, resolve_bound_names, strip_call_literals) from parser.aggregates import attach_aggregates from parser.altsqlfn import attach_alt_sql_names @@ -380,6 +381,15 @@ def main(): # moment a family is added. idl = attach_temporal_types(idl, MOBILITYDB_SRC) + # State on each SQL signature the value a wrapper derives from its temporal type where + # the call carries none, the interpolation `temptype_supports_linear(temptype) ? LINEAR : + # STEP` of appendInstant and the sequence set constructors: it reads the classes the + # temporal types above state. + if MDB_SRC.exists(): + idl, ntd = attach_type_derived_args(idl, MDB_SRC, sql_src=SQL_SRC, meos_src=MEOS_SRC) + print(f" arguments a wrapper derives from the signature's temporal type: {ntd}", + file=sys.stderr) + # 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. diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 1f17495..25da7f5 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -12,7 +12,8 @@ import unittest from pathlib import Path -from parser.boundargs import (_sql_arg_params, attach_call_literals, extract_call_literals, +from parser.boundargs import (_sql_arg_params, attach_call_literals, attach_type_derived_args, + extract_call_literals, extract_wrappers, merge_boundargs, resolve_bound_names, strip_call_literals) @@ -1132,5 +1133,107 @@ def test_the_number_first_reads_through_the_shared_kernel(self): self.assertEqual([s.get("sqlArgParams") for s in sigs], [None, ["d", "temp"]]) +# The interpolation two wrappers take from the class of a temporal type, of the call's +# argument 1 and of its return, where the call carries none (Temporal_append_tinstant, +# Tsequenceset_constructor_gaps). +TYPE_DERIVED = ''' +PGDLLEXPORT Datum Temporal_append_tinstant(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(Temporal_append_tinstant); +Datum +Temporal_append_tinstant(PG_FUNCTION_ARGS) +{ + Temporal *temp = PG_GETARG_TEMPORAL_P(0); + TInstant *inst = PG_GETARG_TINSTANT_P(1); + interpType interp; + if (PG_NARGS() == 2 || PG_ARGISNULL(2)) + { + MeosType temptype = oid_meostype(get_fn_expr_argtype(fcinfo->flinfo, 1)); + interp = temptype_supports_linear(temptype) ? LINEAR : STEP; + } + else + interp = input_interp_string(fcinfo, 2); + Temporal *result = temporal_append_tinstant(temp, inst, interp, 0.0, NULL, false); + PG_RETURN_TEMPORAL_P(result); +} + +PGDLLEXPORT Datum Tsequenceset_constructor_gaps(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(Tsequenceset_constructor_gaps); +Datum +Tsequenceset_constructor_gaps(PG_FUNCTION_ARGS) +{ + ArrayType *array = PG_GETARG_ARRAYTYPE_P(0); + double maxdist = -1.0; + Interval *maxt = NULL; + MeosType temptype = oid_meostype(get_fn_expr_rettype(fcinfo->flinfo)); + interpType interp = temptype_supports_linear(temptype) ? LINEAR : STEP; + if (PG_NARGS() > 1 && ! PG_ARGISNULL(1)) + maxt = PG_GETARG_INTERVAL_P(1); + if (PG_NARGS() > 2 && ! PG_ARGISNULL(2)) + maxdist = PG_GETARG_FLOAT8(2); + if (PG_NARGS() > 3 && ! PG_ARGISNULL(3)) + interp = input_interp_string(fcinfo, 3); + int count; + TInstant **instants = (TInstant **) temparr_extract(array, &count); + TSequenceSet *result = tsequenceset_make_gaps(instants, count, interp, maxt, maxdist); + PG_RETURN_TSEQUENCESET_P(result); +} +''' + + +class TypeDerivedArgTests(unittest.TestCase): + """#attach_type_derived_args binds the interpolation a wrapper derives from the + temporal type of a signature, as #DelegatingWrapperTests reads a literal behind a + helper, only on the signatures that do not carry it.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + src = Path(self.tmp.name) / "src" + src.mkdir() + (src / "temporal.c").write_text(TYPE_DERIVED) + + def tearDown(self): + self.tmp.cleanup() + + @staticmethod + def _idl(): + def sig(name, args, ret): + return {"sqlName": name, "args": args, "ret": ret} + return { + "temporalTypes": {"tfloat": {"linear": True}, "tint": {"linear": False}}, + "functions": [ + {"name": "temporal_append_tinstant", "mdbC": "Temporal_append_tinstant", + "params": [{"name": n} for n in + ("temp", "inst", "interp", "maxdist", "maxt", "expand")], + "sqlSignatures": [sig("appendInstant", ["tfloat", "tfloat"], "tfloat"), + sig("appendInstant", ["tint", "tint"], "tint"), + sig("appendInstant", ["tfloat", "tfloat", "text"], + "tfloat")]}, + {"name": "tsequenceset_make_gaps", "mdbC": "Tsequenceset_constructor_gaps", + "params": [{"name": n} for n in + ("instants", "count", "interp", "maxt", "maxdist")], + "sqlSignatures": [sig("tintSeqSetGaps", ["tint[]", "interval", "float"], + "tint"), + sig("tfloatSeqSetGaps", + ["tfloat[]", "interval", "float", "text"], + "tfloat")]}]} + + def test_the_class_of_the_argument_type_is_bound(self): + idl, n = attach_type_derived_args(self._idl(), self.tmp.name) + sigs = idl["functions"][0]["sqlSignatures"] + self.assertEqual(sigs[0]["boundArgs"], {"interp": "LINEAR"}) + self.assertEqual(sigs[1]["boundArgs"], {"interp": "STEP"}) + + def test_the_class_of_the_return_type_is_bound(self): + idl, _ = attach_type_derived_args(self._idl(), self.tmp.name) + self.assertEqual(idl["functions"][1]["sqlSignatures"][0]["boundArgs"], + {"interp": "STEP"}) + + def test_a_signature_carrying_the_interpolation_binds_none(self): + idl, n = attach_type_derived_args(self._idl(), self.tmp.name) + self.assertNotIn("boundArgs", idl["functions"][0]["sqlSignatures"][2]) + self.assertNotIn("boundArgs", idl["functions"][1]["sqlSignatures"][1]) + self.assertEqual(n, 3) + + if __name__ == "__main__": unittest.main()