From 49c3e7d7a15855dbe39a011e21fd7518fb7d7fe5 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Mon, 26 Oct 2026 17:33:23 +0100 Subject: [PATCH] State what a wrapper passes for a C parameter its SQL signature omits The catalog states the value a MobilityDB wrapper passes for a C parameter the SQL signature leaves out, and the order of the SQL arguments, where the wrapper writes them in forms parser/boundargs.py did not read. A wrapper's own choice of a value by the number of arguments, bool segmentize = (PG_NARGS() == 2) ? PG_GETARG_BOOL(1) : false, reads as an inlined helper's does (_nargs_choices), and a literal behind a comment naming its parameter, union_tpcbox_tpcbox(a, b, /* strict */ false), reads as the literal. A shared helper a wrapper delegates to reads as the wrapper's call makes it run (_specialize): each flag the call passes stands in the helper, the tests and choices it decides keep their branch alone, a function-pointer parameter calls the function the wrapper hands it (Raster_terrain_index(fcinfo, false, &raster_tpi)), a running index, PG_GETARG_FLOAT8(i++), numbers the arguments read by read, a branch reading one argument in each arm included, and an index assigned once adds into the argument it offsets. A local the call leaves at its literal initialiser binds that literal, a declaration of several locals ending each initialiser at its comma, and the SQL argument order of a delegating wrapper is read in the helper as specialised. The helper reader closes a parameter list at its balanced parenthesis, so a helper taking a function pointer is read. Witness: over the catalog derived from MobilityDB 7b6f72c037, the typed Spark and Flink surfaces of JMEOS main refuse stboxX(float, float, float, float, integer), whose wrapper reads xmin, ymin, xmax, ymax and srid through Stbox_constructor(fcinfo, true, false, false, false), while stbox_make takes srid, xmin, xmax, ymin, ymax, then zmin, zmax and the period the helper leaves at 0, 0 and NULL. With this parser the signature binds zmin 0, zmax 0 and s NULL and states its order xmin, ymin, xmax, ymax, srid, so a binding pairs each argument with its parameter by name. Measured: over MobilityDB 7b6f72c037, the catalog changes in 38 of 6724 functions against master's parser, every change a value the wrapper passes or an order it reads: stbox_make in 11 signatures (the constants and the order), tpoint_tfloat_to_geomeas (segmentize false for one argument), union_tpcbox_tpcbox (strict false), jsonbset_set and tjsonb_set (the null treatment NULL), raster_tpi and raster_tri (the extent NULL), tnumber_value_time_boxes (no duration or time origin for the value boxes), the ever and always spatial relationships of the temporal geometries and circular buffers (ever EVER or ALWAYS, read through their function-pointer helper), and four commuted orders of edwithin, adwithin, teq and tne. The typed surfaces register 14 more user-facing signatures and refuse none they registered: the stbox and geodstbox constructors but their timestamp forms, geometry(tgeompoint), geography(tgeogpoint), jsonbsetSet, tjsonbSet, tpcboxUnion, tpi and tri. The six new tests fail on master's parser; tests/test_boundargs.py passes 69, and the suite fails the three tests it fails on master in this environment, which CI passes. Why: a binding generated from the catalog passes what the wrapper passes, and pairs the SQL arguments with the C parameters by name, only where the catalog states both. --- parser/boundargs.py | 207 ++++++++++++++++++++++++++++++++++++---- tests/test_boundargs.py | 167 +++++++++++++++++++++++++++++++- 2 files changed, 352 insertions(+), 22 deletions(-) diff --git a/parser/boundargs.py b/parser/boundargs.py index 4d07270..222d502 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -84,8 +84,7 @@ # A shared helper takes the call info plus the parameters the wrappers bind, whatever it # returns: a `Datum` helper answers for the wrapper, a `void` one lays the state a # set-returning wrapper then streams (`Tgeo_split_start`, `Stbox_tiles_start`). -_HELPER = re.compile(r"\b(?P[A-Za-z_]\w*)\s*\(\s*FunctionCallInfo\s+\w+" - r"(?P[^)]*)\)\s*\{") +_HELPER_HEAD = re.compile(r"\b(?P[A-Za-z_]\w*)\s*\(\s*FunctionCallInfo\s+\w+") # ... 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*;") @@ -168,15 +167,24 @@ def _raw_helpers(mdb_src: str) -> dict[str, tuple[str, list[str]]]: out: dict[str, tuple[str, list[str]]] = {} for cf in built_files(mdb_src, "*.c"): text = cf.read_text(errors="ignore") - for m in _HELPER.finditer(text): - rest = (m.group("rest") or "").strip() + for m in _HELPER_HEAD.finditer(text): + # the parameter list closes at its balanced parenthesis, so a function-pointer + # parameter, `Raster * (*func)(const Raster *, int, ...)`, stays one parameter + close = _close_paren(text, m.end("name") + text[m.end("name"):].index("(")) + brace = _skip_space(text, close + 1) + if not text.startswith("{", brace): + continue + rest = text[m.end():close].strip() names: list[str] = [] if rest.startswith(","): for decl in _split_args(rest[1:]): + fnptr = re.search(r"\(\s*\*\s*(\w+)\s*\)", decl) tok = decl.replace("*", " ").split() - if tok: + if fnptr: + names.append(fnptr.group(1)) + elif tok: names.append(tok[-1]) - out[m.group("name")] = (_body(text, m.end() - 1), names) + out[m.group("name")] = (_body(text, brace), names) return out @@ -199,6 +207,16 @@ def extract_helpers(mdb_src: str | Path) -> dict[str, tuple[str, list[str]]]: r"(?:==|!=|>=|<=|>|<)\s*\d+)\s*\)?\s*\?\s*(?P[^:;]+?)\s*:\s*(?P[^;]+?)\s*;") +def _nargs_choices(text: str) -> str: + """``text`` with each value chosen by the number of arguments written as the value of + the other numbers followed by its replacement under that test, the shape + #_guarded_default reads: ``bool segmentize = (PG_NARGS() == 2) ? + PG_GETARG_BOOL(1) : false;`` reads as ``bool segmentize = false; if (PG_NARGS() == 2) + segmentize = PG_GETARG_BOOL(1);``.""" + return _NARGS_CHOICE.sub(lambda c: f"{c['lhs']} = {c['else']}; " + f"if ({c['test']}) {c['lhs']} = {c['then']};", text) + + def _inline_arg_helper(hbody: str, hparams: list[str], args: list[str]) -> str: """The statements of the argument helper ``hbody`` called with ``args`` (the arguments after the call info, matched to ``hparams``), written as statements of its caller. @@ -231,9 +249,7 @@ def _inline_arg_helper(hbody: str, hparams: list[str], args: list[str]) -> str: if lit is not None: text = re.sub(r"(?])" + re.escape(p) + r"\b", lit, text) text = re.sub(r"\breturn\b[^;]*;", ";", text) - text = _NARGS_CHOICE.sub(lambda c: f"{c['lhs']} = {c['else']}; " - f"if ({c['test']}) {c['lhs']} = {c['then']};", text) - return "{" + text + "}" + return "{" + _nargs_choices(text) + "}" def _expand_arg_helpers(body: str, helpers: dict[str, tuple[str, list[str]]]) -> str: @@ -242,7 +258,8 @@ def _expand_arg_helpers(body: str, helpers: dict[str, tuple[str, list[str]]]) -> reading as ``true``. An argument helper is one the call hands the address of a local for each parameter it writes through: ``jsonb_path_args(fcinfo, &vars, &silent)`` sets ``*vars`` and ``*silent``. A call handing it no address is a delegation - (#_delegated) and stays.""" + (#_delegated) and stays. A value ``body`` itself chooses by the number of arguments + reads as an inlined helper's does (#_nargs_choices).""" for _ in range(8): for m in _ARG_CALL.finditer(body): entry = helpers.get(m.group("name")) @@ -261,8 +278,8 @@ def _expand_arg_helpers(body: str, helpers: dict[str, tuple[str, list[str]]]) -> body[start:m.start()] + "true" + body[close + 1:]) break else: - return body - return body + return _nargs_choices(body) + return _nargs_choices(body) def _delegated(body: str, helpers: dict[str, tuple[str, list[str]]]): @@ -270,7 +287,10 @@ def _delegated(body: str, helpers: dict[str, tuple[str, list[str]]]): ``body`` delegates to a shared helper, passing the call info through: the helper parameters the wrapper binds to literals, and those it binds to a local it reads from an argument only for the numbers of arguments ``arities`` (#_guarded_default), the - literal the local starts from being what a signature of another number passes on.""" + literal the local starts from being what a signature of another number passes on. + The helper body comes as the call makes it run (#_specialize), a function the wrapper + hands a function-pointer parameter, ``&raster_tpi``, read as the function it calls.""" + assigned = {a.group("var") for a in _ASSIGNED.finditer(body)} for m in _DELEG.finditer(body): entry = helpers.get(m.group("name")) if entry is None: @@ -278,17 +298,21 @@ def _delegated(body: str, helpers: dict[str, tuple[str, list[str]]]): hbody, hparams = entry raw = (m.group("args") or "").strip() vals = _split_args(raw[1:]) if raw.startswith(",") else [] - subst, gsubst = {}, {} + subst, gsubst, fnptrs = {}, {}, {} for pname, val in zip(hparams, vals): + val = val.strip() lit = _literal(val) if lit is not None: subst[pname] = lit + elif (val.startswith("&") and _IDENT.match(val[1:].strip()) + and val[1:].strip() not in assigned): + fnptrs[pname] = val[1:].strip() elif _IDENT.match(val): dflt = _guarded_default(body, val) if dflt is not None: gsubst[pname] = dflt - if subst or gsubst: - return hbody, subst, gsubst + if subst or gsubst or fnptrs: + return _specialize(hbody, subst, fnptrs), subst, gsubst return None, {}, {} @@ -327,7 +351,10 @@ def extract_param_lists(meos_src: str | Path) -> dict[str, list[str]]: def _literal(arg: str) -> str | None: - """Normalise a call argument to the literal to record, or None if not a literal.""" + """Normalise a call argument to the literal to record, or None if not a literal. A + comment naming the parameter, as ``union_tpcbox_tpcbox(a, b, /* strict */ false)`` + writes it, is read as the blank #_guards reads a comment as.""" + arg = _COMMENT.sub(" ", arg).strip() if _TRUE.match(arg): return "true" if _FALSE.match(arg): @@ -449,6 +476,128 @@ def _runs_at(body: str, pos: int) -> frozenset: return runs +def _decided(cond: str) -> bool | None: + """The truth of condition ``cond`` once a wrapper's literals stand in it, or None when + it still depends on the call: ``false && ! PG_ARGISNULL(4)`` is false and + ``true || x`` true, as #_guards splits a test on its top-level operators.""" + c = cond.strip() + while c.startswith("(") and _close_paren(c, 0) == len(c) - 1: + c = c[1:-1].strip() + if c in ("true", "false"): + return c == "true" + if c.startswith("!") and not c.startswith("!="): + v = _decided(c[1:]) + return None if v is None else not v + ors = _split_top(c, "||") + if len(ors) > 1: + vals = [_decided(o) for o in ors] + return True if True in vals else (False if all(v is False for v in vals) else None) + ands = _split_top(c, "&&") + if len(ands) > 1: + vals = [_decided(a) for a in ands] + return False if False in vals else (True if all(vals) else None) + return None + + +# An assignment of a value chosen by a condition: `int i = has_extent ? 1 : 0;`. +_CHOICE = re.compile( + r"=(?!=)\s*(?P\((?:[^()]|\([^()]*\))*\)|!?\s*\w+)\s*\?\s*(?P[^:;]+?)\s*:" + r"\s*(?P[^;]+?)\s*;") + + +def _number_reads(seg: str, inc: re.Pattern, c: int) -> tuple[str, int] | None: + """``seg`` with each read of a running index, ``i++``, written as the number it holds, + starting from ``c``, and the number after the last read, or None when a branch leaves + it undecided: both arms of a test advance it by as many reads, as + ``Stbox_constructor`` reads the period from a span or a timestamp, one argument + either way.""" + out, i = "", 0 + test = re.compile(r"\bif\s*\(") + while True: + mi, mf = inc.search(seg, i), test.search(seg, i) + if not mi: + return out + seg[i:], c + if mf and mf.start() < mi.start(): + close = _close_paren(seg, mf.end() - 1) + j = _skip_space(seg, close + 1) + end = _statement_end(seg, j) + e = _skip_space(seg, end) + has_else = re.match(r"else\b", seg[e:]) is not None + k = _skip_space(seg, e + 4) if has_else else end + stop = _statement_end(seg, k) if has_else else end + a = _number_reads(seg[j:end], inc, c) + b = _number_reads(seg[k:stop], inc, c) if has_else else ("", c) + if a is None or b is None or a[1] != b[1]: + return None + out += seg[i:j] + a[0] + seg[end:k] + b[0] + i, c = stop, a[1] + continue + out += seg[i:mi.start()] + str(c) + i, c = mi.end(), c + 1 + + +def _number_indices(text: str) -> str: + """``text`` with the SQL arguments it reads through an index local stated by number: + a running index, ``int i = 0; ... PG_GETARG_FLOAT8(i++)``, numbered read by read + (#_number_reads), and an index assigned once, ``int i = 0; ... + PG_GETARG_TEXT_P(2 + i)``, added in, so #_direct_indices reads each argument.""" + for m in re.finditer(r"\bint\s+(?P\w+)\s*=\s*(?P\d+)\s*;", text): + v, n = m.group("v"), int(m.group("n")) + inc = re.compile(r"(?])" + re.escape(v) + r"\s*\+\+") + rest = text[m.end():] + if re.search(r"(?])" + re.escape(v) + r"\s*(?:=(?!=)|--|[-+*/]=)", rest): + continue + if inc.search(rest): + numbered = _number_reads(rest, inc, n) + if numbered is not None: + text = text[:m.end()] + numbered[0] + continue + text = text[:m.end()] + re.sub( + r"(PG_GETARG_\w+\s*\(\s*)(\d+)\s*\+\s*" + re.escape(v) + r"\b|" + r"(PG_GETARG_\w+\s*\(\s*)" + re.escape(v) + r"\s*\+\s*(\d+)", + lambda x: (x.group(1) or x.group(3)) + str(int(x.group(2) or x.group(4)) + n), + rest) + return text + + +def _specialize(hbody: str, subst: dict[str, str], fnptrs: dict[str, str]) -> str: + """The shared helper ``hbody`` as the wrapper's call makes it run: each parameter + ``subst`` binds reads as its literal, each function-pointer parameter ``fnptrs`` binds + calls the function the wrapper hands it, and each test and choice those literals + decide keeps its branch alone (#_decided). ``Raster_terrain_index(fcinfo, false, + &raster_tpi)`` runs ``i = 0``, ``extent = NULL`` and ``raster_tpi(rast, band, extent, + ...)``, and ``Jsonbset_set_common(fcinfo, false)`` never reads the null treatment of + its lax twin. Comments are read as blanks, one for each character, as #_guards reads + them.""" + text = _COMMENT.sub(lambda c: " " * len(c.group()), hbody) + for p, fn in fnptrs.items(): + text = re.sub(r"(?])" + re.escape(p) + r"\s*\(", fn + "(", text) + for p, lit in subst.items(): + text = re.sub(r"(?])" + re.escape(p) + r"\b", lit, text) + text = _CHOICE.sub(lambda c: c.group() if _decided(c["cond"]) is None else + "= " + (c["then"] if _decided(c["cond"]) else c["else"]) + ";", + text) + pos = 0 + while True: + m = re.compile(r"\bif\s*\(").search(text, pos) + if not m: + return _number_indices(text) + close = _close_paren(text, m.end() - 1) + verdict = _decided(text[m.end():close]) + j = _skip_space(text, close + 1) + end = _statement_end(text, j) + e = _skip_space(text, end) + has_else = re.match(r"else\b", text[e:]) is not None + k = _skip_space(text, e + 4) if has_else else end + stop = _statement_end(text, k) if has_else else end + if verdict is None: + pos = m.end() + continue + kept = text[j:end] if verdict else (text[k:stop] if has_else else ";") + text = text[:m.start()] + kept + text[stop:] + pos = m.start() + + def _guarded_default(body: str, var: str) -> tuple[frozenset, str] | None: """``(arities, literal)`` when local ``var`` starts from a literal and every later assignment of it runs only for the numbers of arguments ``arities`` (#_guards). A SQL @@ -463,12 +612,18 @@ def _guarded_default(body: str, var: str) -> tuple[frozenset, str] | None: body): return None assigns = list(re.finditer(r"(?])" + v + r"\s*=(?!=)", body)) - if len(assigns) < 2: + if not assigns: return None - init = re.match(r"\s*([^;]+?)\s*;", body[assigns[0].end():]) + # an initialiser ends at the comma of a declaration of several locals, `double xmin = + # 0, xmax = 0;`, as at its semicolon + init = re.match(r"\s*([^;,]+?)\s*[;,]", body[assigns[0].end():]) lit = _literal(init.group(1)) if init else None if lit is None or _runs_at(body, assigns[0].start()) != _ARITIES: return None + if len(assigns) == 1: + # assigned once, from a literal, by every call: the literal for every number of + # arguments, as `period` stays NULL in `Stbox_constructor` for a box without T + return frozenset(), lit runs: frozenset = frozenset() for a in assigns[1:]: r = _runs_at(body, a.start()) @@ -1222,9 +1377,21 @@ def merge_sql_arg_params(idl: dict, mdb_src: str | Path, parameters.""" from parser.sqlfn import _meos_to_mdb, _wrapper_sql_sigs wrappers = extract_wrappers(mdb_src) + helpers = extract_helpers(mdb_src) m2d = _meos_to_mdb(meos_src) if meos_src else {} w2sig = _wrapper_sql_sigs(sql_src) if sql_src else {} bodies = extract_meos_bodies(meos_src) if meos_src else {} + + def reading_body(w: str, func: dict) -> str: + # a wrapper calling no MEOS function of its own reads its arguments in the shared + # helper it delegates to, as the call makes it run, as #merge_boundargs reads the + # literals there: `Stbox_constructor_x` reads xmin, ymin, xmax, ymax, srid + body = wrappers[w] + if _call_args(body, func["name"]) or _call_args(body, "pg_" + func["name"]): + return body + hbody, _, _ = _delegated(body, helpers) + return hbody or body + n = 0 for func in idl["functions"]: primary = func.get("mdbC") @@ -1233,7 +1400,7 @@ def merge_sql_arg_params(idl: dict, mdb_src: str | Path, ws = [primary] + [w for w in m2d.get(func["name"]) or () if w != primary] sigs = func.get("sqlSignatures") or [] sig_ws = [_signature_wrapper(func, s, ws, w2sig) or primary for s in sigs] or [primary] - orders = [_sql_arg_params(wrappers[w], func, bodies.get(func["name"])) + orders = [_sql_arg_params(reading_body(w, func), func, bodies.get(func["name"])) if w in wrappers else None for w in sig_ws] if len({tuple(o or ()) for o in orders}) == 1: if orders[0]: diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index e15b0f9..553475a 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -15,8 +15,8 @@ from parser.boundargs import (_sql_arg_params, attach_call_literals, attach_null_default_binds, attach_type_derived_args, extract_call_literals, - extract_wrappers, merge_boundargs, resolve_bound_names, - strip_call_literals) + extract_wrappers, merge_boundargs, merge_sql_arg_params, + resolve_bound_names, strip_call_literals) # A synthetic MobilityDB wrapper source (mobilitydb/src/**/*.c shape). SAMPLE = ''' @@ -176,6 +176,124 @@ def test_caller_read_args_stay_out(self): self.assertNotIn("key", bound) +SPECIALIZED = """ +Datum +Box_constructor(FunctionCallInfo fcinfo, bool hasx, bool hast) +{ + double xmin = 0, xmax = 0, zmin = 0; + int32_t srid = 0; + Span *period = NULL; + int i = 0; + if (hasx) + { + xmin = PG_GETARG_FLOAT8(i++); + xmax = PG_GETARG_FLOAT8(i++); + } + if (hast) + { + MeosType basetype = oid_meostype(get_fn_expr_argtype(fcinfo->flinfo, i)); + if (basetype == T_TSTZSPAN) + period = PG_GETARG_SPAN_P(i++); + else + { + TimestampTz t = PG_GETARG_TIMESTAMPTZ(i++); + period = span_make(t, t, true, true, T_TIMESTAMPTZ); + } + } + if (hasx) + srid = PG_GETARG_INT32(i++); + PG_RETURN_POINTER(box_make(hasx, srid, xmin, xmax, zmin, period)); +} + +Datum +Box_constructor_x(PG_FUNCTION_ARGS) +{ + return Box_constructor(fcinfo, true, false); +} + +Datum +Box_constructor_xt(PG_FUNCTION_ARGS) +{ + return Box_constructor(fcinfo, true, true); +} + +Datum +Terrain_index(FunctionCallInfo fcinfo, bool has_extent, + Raster * (*func)(const Raster *, int, const Raster *, const char *)) +{ + int i = has_extent ? 1 : 0; + Raster *rast = (Raster *) PG_DETOAST_DATUM(PG_GETARG_DATUM(0)); + int band = PG_GETARG_INT32(1); + Raster *extent = (has_extent && ! PG_ARGISNULL(2)) ? + (Raster *) PG_DETOAST_DATUM(PG_GETARG_DATUM(2)) : NULL; + char *pixeltype = text_to_cstring(PG_GETARG_TEXT_P(2 + i)); + PG_RETURN_POINTER(func(rast, band, extent, pixeltype)); +} + +Datum +Raster_tpi(PG_FUNCTION_ARGS) +{ + return Terrain_index(fcinfo, false, &raster_tpi); +} +""" + + +class SpecializedHelperTests(unittest.TestCase): + """A shared helper read as the wrapper's call makes it run, as + #DelegatingWrapperTests reads the literal a wrapper hands its helper: the flags + decide the branches, a running index numbers the arguments, a function-pointer + parameter calls the function the wrapper hands it.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + src = Path(self.tmp.name) / "src" + src.mkdir() + (src / "specialized.c").write_text(SPECIALIZED) + + def tearDown(self): + self.tmp.cleanup() + + @staticmethod + def _box(wrapper): + return {"functions": [ + {"name": "box_make", "mdbC": wrapper, + "params": [{"name": n} for n in + ("hasx", "srid", "xmin", "xmax", "zmin", "s")]}]} + + def test_constants_the_flags_leave_unread_are_bound(self): + # per signature, as #test_signature_omitting_the_argument_carries_the_default + # reads a default the call leaves the local at + idl = self._box("Box_constructor_x") + idl["functions"][0]["sqlSignatures"] = [ + {"args": ["float", "float", "integer"], "ret": "box"}] + idl, _, _ = merge_boundargs(idl, self.tmp.name) + self.assertEqual(idl["functions"][0]["shape"]["boundArgs"], + {"hasx": "true", "zmin": "0", "s": "NULL"}) + + def test_a_running_index_states_the_argument_order(self): + idl, n = merge_sql_arg_params(self._box("Box_constructor_x"), self.tmp.name) + self.assertEqual(idl["functions"][0]["shape"]["sqlArgParams"], + ["xmin", "xmax", "srid"]) + self.assertEqual(n, 1) + + def test_a_branch_reading_one_argument_either_way_keeps_the_order(self): + idl, _ = merge_sql_arg_params(self._box("Box_constructor_xt"), self.tmp.name) + self.assertEqual(idl["functions"][0]["shape"]["sqlArgParams"], + ["xmin", "xmax", "s", "srid"]) + + def test_the_function_a_pointer_parameter_receives_is_read(self): + idl, _, _ = merge_boundargs( + {"functions": [{"name": "raster_tpi", "mdbC": "Raster_tpi", + "params": [{"name": "rast"}, {"name": "band"}, + {"name": "customextent"}, + {"name": "pixeltype"}], + "sqlSignatures": [{"args": ["raster", "integer", "text"], + "ret": "raster"}]}]}, + self.tmp.name) + self.assertEqual(idl["functions"][0]["shape"]["boundArgs"], + {"customextent": "NULL"}) + + PG_TWIN = """ static Datum Jsonb_path_exists_common(FunctionCallInfo fcinfo, bool tz) @@ -689,6 +807,27 @@ def test_without_the_meos_sources_the_generic_is_not_read(self): GSERIALIZED *result = tgeo_union(temp, unary_union); PG_RETURN_GSERIALIZED_P(result); } + +/* The default #Tgeo_scale guards with an if, chosen by a ternary */ +Datum +Tpoint_to_geomeas(PG_FUNCTION_ARGS) +{ + Temporal *tpoint = PG_GETARG_TEMPORAL_P(0); + bool segmentize = (PG_NARGS() == 2) ? PG_GETARG_BOOL(1) : false; + GSERIALIZED *result; + tpoint_tfloat_to_geomeas(tpoint, NULL, segmentize, &result); + PG_RETURN_GSERIALIZED_P(result); +} + +/* A literal as #Tgeo_scale passes NULL, behind a comment naming its parameter */ +Datum +Union_tpcbox_tpcbox(PG_FUNCTION_ARGS) +{ + TPCBox *a = PG_GETARG_TPCBOX_P(0); + TPCBox *b = PG_GETARG_TPCBOX_P(1); + TPCBox *result = union_tpcbox_tpcbox(a, b, /* strict */ false); + PG_RETURN_TPCBOX_P(result); +} ''' @@ -759,6 +898,30 @@ def test_assignment_outside_the_guard_is_not_a_default(self): self.assertNotIn("shape", idl["functions"][0]) self.assertEqual(n, 0) + def test_value_chosen_by_the_number_of_arguments_is_the_default(self): + # #test_signature_omitting_the_argument_carries_the_default for the wrapper's own + # ternary, which reads as an inlined helper's does + idl, _, _ = self._merge( + {"name": "tpoint_tfloat_to_geomeas", "mdbC": "Tpoint_to_geomeas", + "params": [{"name": "tpoint"}, {"name": "measure"}, {"name": "segmentize"}, + {"name": "result"}], + "sqlSignatures": [{"args": ["tgeompoint"], "ret": "geometry"}, + {"args": ["tgeompoint", "boolean"], "ret": "geometry"}]}) + f = idl["functions"][0] + self.assertEqual([s.get("boundArgs") for s in f["sqlSignatures"]], + [{"measure": "NULL", "segmentize": "false"}, + {"measure": "NULL"}]) + + def test_literal_behind_a_comment_naming_its_parameter_is_bound(self): + # #test_literal_behind_the_helper_parameter_is_captured for a literal the wrapper + # writes behind a comment + idl, n, _ = self._merge( + {"name": "union_tpcbox_tpcbox", "mdbC": "Union_tpcbox_tpcbox", + "params": [{"name": "box1"}, {"name": "box2"}, {"name": "strict"}], + "sqlSignatures": [{"args": ["tpcbox", "tpcbox"], "ret": "tpcbox"}]}) + self.assertEqual(idl["functions"][0]["shape"]["boundArgs"], {"strict": "false"}) + self.assertEqual(n, 1) + def test_computed_initializer_is_not_a_default(self): idl, n, _ = self._merge( {"name": "tgeo_union", "mdbC": "Tgeo_union",