From 3cff0a55697469e16a0ba41424d93ed26ec964da Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 16 Jul 2026 14:58:01 +0200 Subject: [PATCH] Propagate wrapper-bound literals to per-base-type siblings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A per-base-type accessor (tbool/tint/tfloat/ttext_value_at_timestamptz) shares its PG wrapper (Temporal_value_at_timestamptz) with the generic function but is never called by name — the wrapper calls the generic, which dispatches internally. So only the generic received shape.boundArgs, while a binding that dispatches to the typed sibling for a typed result (every binding does) saw no bound literal and fell back to a wrong default (strict=false). Group functions by the wrapper they share and apply the wrapper's bound literals to every group member by parameter name, so each typed sibling carries boundArgs={strict:true} too. --- parser/boundargs.py | 79 +++++++++++++++++++++++++++-------------- tests/test_boundargs.py | 17 +++++++-- 2 files changed, 68 insertions(+), 28 deletions(-) diff --git a/parser/boundargs.py b/parser/boundargs.py index 7e0fc33..2944100 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -123,9 +123,44 @@ def _literal(arg: str) -> str | None: return None +def _wrapper_bound(body: str, func: dict, drift: list) -> 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.""" + args = _call_args(body, func["name"]) + if not args: + return {} + assigned = {m.group("var") for m in _ASSIGNED.finditer(body)} + params = func.get("params", []) + bound: dict[str, str] = {} + for i, a in enumerate(args): + if i >= len(params): + break + pname = params[i].get("name") + if not pname: + continue + if a.startswith("&") or "PG_GETARG" in a or a in assigned: + continue # out-param or caller-sourced local + lit = _literal(a) + if lit is not None: + bound[pname] = lit + elif _IDENT.match(a): + # a bare identifier that is not caller-sourced and not a literal — + # cannot be trusted as a bound value; report for a look. + drift.append((func["name"], pname, "unclassified-arg: " + a)) + return bound + + def merge_boundargs(idl: dict, mdb_src: str | Path) -> tuple[dict, int, list]: """Fold wrapper-bound literals into each function's ``shape.boundArgs``. + Functions are grouped by the PG wrapper they share (``mdbC``). Every function in a + group has the SAME SQL contract, so a literal the wrapper binds (keyed by parameter + name) applies to ALL of them — crucially the per-base-type collapse siblings + (``tbool``/``tint``/… ``_value_at_timestamptz``) that a binding dispatches to for a + typed result but that the wrapper never calls by name (it calls the generic + ``temporal_value_at_timestamptz``). Only members that actually own a parameter of that + name receive the literal. + Returns ``(idl, count, drift)`` where ``drift`` lists ``(function, param, reason)`` call arguments the pass could not classify as a caller arg / out-param / literal (a bare identifier that is neither) — a @@ -133,35 +168,27 @@ def merge_boundargs(idl: dict, mdb_src: str | Path) -> tuple[dict, int, list]: wrappers = extract_wrappers(mdb_src) n = 0 drift: list[tuple[str, str, str]] = [] + groups: dict[str, list] = {} for func in idl["functions"]: - wname = func.get("mdbC") - if not wname: - continue + w = func.get("mdbC") + if w: + groups.setdefault(w, []).append(func) + for wname, group in groups.items(): body = wrappers.get(wname) if body is None: continue - args = _call_args(body, func["name"]) - if not args: + # the wrapper's bound literals, keyed by param name, from whichever group member(s) + # the wrapper calls by name (branches — e.g. the RGEO ternary — agree, first wins) + wbound: dict[str, str] = {} + for func in group: + for k, v in _wrapper_bound(body, func, drift).items(): + wbound.setdefault(k, v) + if not wbound: continue - assigned = {m.group("var") for m in _ASSIGNED.finditer(body)} - params = func.get("params", []) - bound: dict[str, str] = {} - for i, a in enumerate(args): - if i >= len(params): - break - pname = params[i].get("name") - if not pname: - continue - if a.startswith("&") or "PG_GETARG" in a or a in assigned: - continue # out-param or caller-sourced local - lit = _literal(a) - if lit is not None: - bound[pname] = lit - elif _IDENT.match(a): - # a bare identifier that is not caller-sourced and not a literal — - # cannot be trusted as a bound value; report for a look. - drift.append((func["name"], pname, "unclassified-arg: " + a)) - if bound: - func.setdefault("shape", {})["boundArgs"] = bound - n += len(bound) + for func in group: + pnames = {p.get("name") for p in func.get("params", [])} + bound = {k: v for k, v in wbound.items() if k in pnames} + if bound: + func.setdefault("shape", {})["boundArgs"] = bound + n += len(bound) return idl, n, drift diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 2b9118d..2c4aa79 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -83,6 +83,10 @@ def _idl(): {"name": "maxdist"}, {"name": "maxt"}, {"name": "expand"}]}, # a function with no wrapper mapping -> untouched {"name": "orphan_fn", "params": [{"name": "x"}]}, + # a per-base-type collapse sibling: SHARES the wrapper (mdbC) with the generic + # but the wrapper never calls it by name -> inherits {strict:true} by param name + {"name": "tbool_value_at_timestamptz", "mdbC": "Temporal_value_at_timestamptz", + "params": [{"name": "temp"}, {"name": "t"}, {"name": "strict"}, {"name": "value"}]}, ]} @@ -128,11 +132,20 @@ def test_multiple_literals_including_null_and_number(self): # `interp` (a derived enum local) is not recorded as a literal self.assertNotIn("interp", app["shape"]["boundArgs"]) + def test_base_type_sibling_inherits_bound_literal(self): + # tbool_value_at_timestamptz shares the wrapper but is never called by name; it + # still inherits {strict:true} (its out-param `value` is not a bound literal) + idl, n, drift = merge_boundargs(_idl(), self.tmp.name) + sib = idl["functions"][5] + self.assertEqual(sib["name"], "tbool_value_at_timestamptz") + self.assertEqual(sib["shape"]["boundArgs"], {"strict": "true"}) + def test_orphan_and_count(self): idl, n, drift = merge_boundargs(_idl(), self.tmp.name) - # orphan (no mdbC) untouched; total = strict + 3 append literals = 4 + # orphan (no mdbC) untouched; total = generic strict + 3 append literals + + # sibling strict = 5 self.assertNotIn("shape", idl["functions"][4]) - self.assertEqual(n, 4) + self.assertEqual(n, 5) if __name__ == "__main__":