From 2d2323c3a50421c216a4f68b454ef4d0aa2e5354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Thu, 27 Aug 2026 11:59:54 +0200 Subject: [PATCH] Follow a delegating wrapper to the literal it binds A wrapper that names no MEOS call of its own passes the call info to a shared helper and binds the rest there: `Tjsonb_object_field` is `Tjsonb_object_field_common(fcinfo, false)` over a helper that calls `tjsonb_object_field(temp, key, astext, null_handle)`. The pass follows that hop, matching the literals to the helper's parameters by position, so an argument of the MEOS call naming one of them resolves to the literal behind it. Each catalog entry names one wrapper, so the sibling binding the opposite literal is a separate entry and the two stay distinct. The bound-literal count over MobilityDB master reads 153 against 87, with every previously captured literal unchanged. --- parser/boundargs.py | 77 +++++++++++++++++++++++++++++++++++++++-- tests/test_boundargs.py | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 3 deletions(-) diff --git a/parser/boundargs.py b/parser/boundargs.py index 54e42de..7d6fd3e 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -20,8 +20,18 @@ ``PG_GETARG_*`` (directly, or via a local so-assigned) is a CALLER arg and is skipped; ``&name`` is an out-param (already in ``outParams``) and is skipped; only a genuine LITERAL (``true``/``false``, a number, ``NULL`` or an UPPERCASE -enum/macro) is recorded. A wrapper that does not call the MEOS function by name -(it delegates to a shared helper) yields no ``boundArgs``. +enum/macro) is recorded. + +A wrapper that does not call the MEOS function by name delegates to a shared +helper, and the literal it binds sits at the DELEGATION rather than at the MEOS +call: ``Tjsonb_object_field`` is ``return Tjsonb_object_field_common(fcinfo, +false)`` and the helper calls ``tjsonb_object_field(temp, key, astext, +null_handle)``. Such a wrapper is followed one hop: the literals it passes are +matched to the helper's parameters by position, and an argument of the MEOS call +naming one of those parameters resolves to the literal behind it. Two wrappers +sharing a helper (``…_object_field`` and ``…_object_field_text``) bind the same +parameter to different literals, so the pair is read as the one SQL surface each +wrapper names rather than merged. """ from __future__ import annotations @@ -41,6 +51,11 @@ _NUMBER = re.compile(r"^-?\d+(?:\.\d+)?$") _ENUM = re.compile(r"^[A-Z][A-Z0-9_]+$") _IDENT = re.compile(r"^\w+$") +# A shared helper takes the call info plus the parameters the wrappers bind. +_HELPER = re.compile(r"Datum\s+(?P\w+)\s*\(\s*FunctionCallInfo\s+\w+" + r"(?P[^)]*)\)\s*\{") +# ... and a wrapper delegates to it by passing that same call info straight through. +_DELEG = re.compile(r"\b(?P\w+)\s*\(\s*fcinfo\s*(?P,[^;]*?)?\)\s*;") def _body(text: str, brace_pos: int) -> str: @@ -112,6 +127,44 @@ def extract_wrappers(mdb_src: str | Path) -> dict[str, str]: return out +def extract_helpers(mdb_src: str | Path) -> dict[str, tuple[str, list[str]]]: + """``{helper_name: (body_text, [param names after the call info])}`` for every shared + wrapper helper under ``mdb_src``.""" + out: dict[str, tuple[str, list[str]]] = {} + for cf in Path(mdb_src).rglob("*.c"): + text = cf.read_text(errors="ignore") + for m in _HELPER.finditer(text): + rest = (m.group("rest") or "").strip() + names: list[str] = [] + if rest.startswith(","): + for decl in _split_args(rest[1:]): + tok = decl.replace("*", " ").split() + if tok: + names.append(tok[-1]) + out[m.group("name")] = (_body(text, m.end() - 1), names) + return out + + +def _delegated(body: str, helpers: dict[str, tuple[str, list[str]]]): + """``(helper_body, {helper_param: literal})`` when ``body`` delegates to a shared + helper, passing the call info through and binding the rest to literals.""" + for m in _DELEG.finditer(body): + entry = helpers.get(m.group("name")) + if entry is None: + continue + hbody, hparams = entry + raw = (m.group("args") or "").strip() + vals = _split_args(raw[1:]) if raw.startswith(",") else [] + subst = {} + for pname, val in zip(hparams, vals): + lit = _literal(val) + if lit is not None: + subst[pname] = lit + if subst: + return hbody, subst + return None, {} + + def _literal(arg: str) -> str | None: """Normalise a call argument to the literal to record, or None if not a literal.""" if _TRUE.match(arg): @@ -124,7 +177,8 @@ def _literal(arg: str) -> str | None: def _wrapper_bound(body: str, func: dict, drift: list, - documented: dict[str, set]) -> dict[str, str]: + documented: dict[str, set], + subst: dict[str, str] | None = None) -> dict[str, str]: """The literals wrapper ``body`` binds in its call to ``func['name']``, keyed by ``func``'s parameter name. Empty if the wrapper does not call ``func`` by name. @@ -137,6 +191,7 @@ def _wrapper_bound(body: str, func: dict, drift: list, args = _call_args(body, func["name"]) if not args: return {} + subst = subst or {} assigned = {m.group("var") for m in _ASSIGNED.finditer(body)} doc_params = documented.get(func["name"], frozenset()) params = func.get("params", []) @@ -147,6 +202,10 @@ def _wrapper_bound(body: str, func: dict, drift: list, pname = params[i].get("name") if not pname: continue + if a in subst: + # a helper parameter the delegating wrapper bound to a literal + bound[pname] = subst[a] + continue if a.startswith("&") or "PG_GETARG" in a or a in assigned: continue # out-param or caller-sourced local lit = _literal(a) @@ -182,6 +241,7 @@ def merge_boundargs(idl: dict, mdb_src: str | Path, undocumented parameters.""" documented = documented or {} wrappers = extract_wrappers(mdb_src) + helpers = extract_helpers(mdb_src) n = 0 drift: list[tuple[str, str, str]] = [] groups: dict[str, list] = {} @@ -199,6 +259,17 @@ def merge_boundargs(idl: dict, mdb_src: str | Path, for func in group: for k, v in _wrapper_bound(body, func, drift, documented).items(): wbound.setdefault(k, v) + if not wbound: + # The wrapper names no MEOS call of its own: it delegates, and the literal it + # binds sits at that delegation. `mdbC` names ONE wrapper per catalog entry, so + # the sibling that binds the opposite literal is a different entry and the two + # never merge. + hbody, subst = _delegated(body, helpers) + if hbody is None: + continue + for func in group: + for k, v in _wrapper_bound(hbody, func, drift, documented, subst).items(): + wbound.setdefault(k, v) if not wbound: continue for func in group: diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 9d6ba39..f6d340d 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -108,6 +108,69 @@ def _idl(): ]} +DELEGATING = """ +Datum +Jsonb_field_common(FunctionCallInfo fcinfo, bool astext) +{ + Temporal *temp = PG_GETARG_TEMPORAL_P(0); + text *key = PG_GETARG_TEXT_P(1); + Temporal *result = jsonb_field(temp, key, astext); + PG_RETURN_TEMPORAL_P(result); +} + +PGDLLEXPORT Datum Jsonb_field(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(Jsonb_field); +Datum +Jsonb_field(PG_FUNCTION_ARGS) +{ + return Jsonb_field_common(fcinfo, false); +} + +PGDLLEXPORT Datum Jsonb_field_text(PG_FUNCTION_ARGS); +PG_FUNCTION_INFO_V1(Jsonb_field_text); +Datum +Jsonb_field_text(PG_FUNCTION_ARGS) +{ + return Jsonb_field_common(fcinfo, true); +} +""" + + +class DelegatingWrapperTests(unittest.TestCase): + """A wrapper that binds its literal at the DELEGATION, not at the MEOS call.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + src = Path(self.tmp.name) / "src" + src.mkdir() + (src / "deleg.c").write_text(DELEGATING) + + def tearDown(self): + self.tmp.cleanup() + + def _idl(self, wrapper): + return {"functions": [ + {"name": "jsonb_field", "mdbC": wrapper, + "params": [{"name": "temp"}, {"name": "key"}, {"name": "astext"}]}]} + + def test_literal_behind_the_helper_parameter_is_captured(self): + idl, n, drift = merge_boundargs(self._idl("Jsonb_field"), self.tmp.name) + self.assertEqual(idl["functions"][0]["shape"]["boundArgs"], {"astext": "false"}) + self.assertEqual(drift, []) + + def test_the_sibling_wrapper_binds_the_other_literal(self): + # Same helper, same MEOS function, opposite literal: the value follows the + # wrapper each catalog entry names, so the two never merge into one. + idl, n, drift = merge_boundargs(self._idl("Jsonb_field_text"), self.tmp.name) + self.assertEqual(idl["functions"][0]["shape"]["boundArgs"], {"astext": "true"}) + + def test_caller_read_args_stay_out(self): + idl, _, _ = merge_boundargs(self._idl("Jsonb_field"), self.tmp.name) + bound = idl["functions"][0]["shape"]["boundArgs"] + self.assertNotIn("temp", bound) + self.assertNotIn("key", bound) + + class BoundArgsTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory()