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
79 changes: 53 additions & 26 deletions parser/boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,45 +123,72 @@ 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
signal to inspect, never trusted as a bound value."""
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
17 changes: 15 additions & 2 deletions tests/test_boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]},
]}


Expand Down Expand Up @@ -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__":
Expand Down
Loading