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
77 changes: 74 additions & 3 deletions parser/boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<name>\w+)\s*\(\s*FunctionCallInfo\s+\w+"
r"(?P<rest>[^)]*)\)\s*\{")
# ... and a wrapper delegates to it by passing that same call info straight through.
_DELEG = re.compile(r"\b(?P<name>\w+)\s*\(\s*fcinfo\s*(?P<args>,[^;]*?)?\)\s*;")


def _body(text: str, brace_pos: int) -> str:
Expand Down Expand Up @@ -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):
Expand All @@ -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.

Expand All @@ -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", [])
Expand All @@ -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)
Expand Down Expand Up @@ -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] = {}
Expand All @@ -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:
Expand Down
63 changes: 63 additions & 0 deletions tests/test_boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading