From 0f0e57bfa56d1f9d642ce5e51686d90fcfaf9b54 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 29 Oct 2026 19:21:41 +0100 Subject: [PATCH] Read the arguments a wrapper gathers into a C array as one operand A C wrapper gathering its trailing arguments into a local array, Pcpoint_make filling the values of pcpoint_make with PG_GETARG_FLOAT8(i + 1) for i < count and count = PG_NARGS() - 1, states a composition as a wrapper casting an argument does: attach_wrapper_compositions reads the array of a signature of n arguments as a gather operand of the arguments 1 to n - 1 and its count as the value n - 1 (#_gathers). pcpoint(integer, double precision, double precision) is the composition {"operands": [{"arg": 0, "param": "pcid"}, {"gather": [1, 2], "param": "values"}, {"value": "2", "param": "count"}], "call": "pcpoint_make"}, and pcpoint(integer, double precision[]), whose wrapper passes its array as it is, keeps its signature. Witness: tests/test_compositions.py GatherCompositionTests, the wrappers and CREATE FUNCTION statements of pcpoint as MobilityDB states them, fails its 2 tests on master and passes them here. Why: J15d of MobilityDB #2861, the coordinates the two pcpoint constructors gather, which the typed JVM surfaces refuse while the catalog states their C call. JMEOS reads the operand from JMEOS #206 on; its generator on main raises KeyError: 'arg' on it, so this change merges after JMEOS #206. Measured: over the headers MobilityDB master cce310aca2 installs, the catalog holds 361 compositions against 359, the two pcpoint signatures, which leave the sqlSignatures of pcpoint_make; no other function, composition or section changes. The 594 tests pass, master's 592 and the 2 above. --- parser/compositions.py | 40 +++++++++++++- tests/test_compositions.py | 108 +++++++++++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) diff --git a/parser/compositions.py b/parser/compositions.py index 3c3201d..91aee36 100644 --- a/parser/compositions.py +++ b/parser/compositions.py @@ -533,6 +533,29 @@ def attach_compositions(idl, sql_src): _CAST_CALL = re.compile(r"^\s*(\w+)\s*\(\s*(\w+)\s*\)\s*$") _C_LITERAL = re.compile(r"^(?:true|false|-?\d+(?:\.\d+)?)$") +# A loop filling a local array from the arguments past the first k, `values[i] = +# PG_GETARG_FLOAT8(i + 1)` for `i < count`, and the count of those arguments, +# `count = PG_NARGS() - 1` +_GATHER_LOOP = re.compile( + r"for\s*\(\s*int\s+(\w+)\s*=\s*0\s*;\s*\1\s*<\s*(\w+)\s*;\s*\1\s*\+\+\s*\)\s*" + r"(\w+)\s*\[\s*\1\s*\]\s*=\s*PG_GETARG_\w+\s*\(\s*\1\s*\+\s*(\d+)\s*\)\s*;") +_NARGS_LESS = r"(?]){}\s*=\s*PG_NARGS\s*\(\s*\)\s*-\s*(\d+)\s*;" + + +def _gathers(body, nargs): + """{array local: (the SQL arguments it gathers, its count local)} of a wrapper ``body`` + called with ``nargs`` arguments: ``Pcpoint_make`` gathers arguments 1 and 2 of + pcpoint(pcid, x, y) into ``values`` and counts 2 in ``count``. #_caller_index reads the + one argument a local carries and #_nargs_choices of parser/boundargs.py a value the + number of arguments chooses; no reader there follows a loop over the arguments, which + this one does for the loop counting them from ``PG_NARGS()``.""" + out = {} + for m in _GATHER_LOOP.finditer(body): + _, count, array, k = m.groups() + c = re.search(_NARGS_LESS.format(re.escape(count)), body) + if c and c.group(1) == k and nargs > int(k): + out[array] = (list(range(int(k), nargs)), count) + return out def attach_wrapper_compositions(idl, mdb_src, sql_src, meos_src): @@ -551,7 +574,11 @@ def attach_wrapper_compositions(idl, mdb_src, sql_src, meos_src): states it (#_signature_wrapper of parser/boundargs.py) and each argument of the call to the SQL argument it carries (#_caller_index); a cast is a public catalog function of one parameter. A C literal the wrapper passes, as ``true`` for the ``spheroid`` of - ``edwithin_tgeo_geo``, is a value operand. A signature whose wrapper casts nothing + ``edwithin_tgeo_geo``, is a value operand. The arguments a wrapper gathers into a C + array (#_gathers) are a gather operand and their count a value operand: + ``pcpoint(integer, double precision, double precision)`` is ``{"operands": [{"arg": 0, + "param": "pcid"}, {"gather": [1, 2], "param": "values"}, {"value": "2", "param": + "count"}], "call": "pcpoint_make"}``. A signature whose wrapper casts or gathers nothing stays where it is.""" from parser.boundargs import (_call_args, _caller_index, _delegated, _signature_wrapper, extract_helpers, extract_wrappers, specialize_argtypes) @@ -588,6 +615,8 @@ def attach_wrapper_compositions(idl, mdb_src, sql_src, meos_src): operands = None if args is not None and len(args) == len(params): operands, cast_seen = [], False + gathers = _gathers(body, len(sig.get("args") or ())) + counts = {c: str(len(ks)) for ks, c in gathers.values()} for a, p in zip(args, params): a = a.strip() rhs = [m.group(1) for m in re.finditer( @@ -595,7 +624,14 @@ def attach_wrapper_compositions(idl, mdb_src, sql_src, meos_src): if m.group(1).strip() != "NULL"] hit = _CAST_CALL.match(rhs[0]) if len(rhs) == 1 else None cast = hit.group(1) if hit else None - if _C_LITERAL.match(a): + if a in gathers: + # The arguments the wrapper gathers into a C array, which a binding + # passes as it passes a SQL array of them + operands.append({"gather": gathers[a][0], "param": p}) + cast_seen = True + elif a in counts: + operands.append({"value": counts[a], "param": p}) + elif _C_LITERAL.match(a): operands.append({"value": a, "param": p}) elif p in bound and not hit: # A local the wrapper leaves at the literal it starts from diff --git a/tests/test_compositions.py b/tests/test_compositions.py index f4a0b3b..a63066b 100644 --- a/tests/test_compositions.py +++ b/tests/test_compositions.py @@ -676,5 +676,113 @@ def test_the_span_signatures_keep_their_place(self): ["tstzspan", "tstzspan"]) + +GATHER_WRAPPERS = """ +Datum +Pcpoint_make(PG_FUNCTION_ARGS) +{ + uint32_t pcid = (uint32_t) PG_GETARG_INT32(0); + int count = PG_NARGS() - 1; + assert(count > 0 && count <= PCPOINT_MAX_COORDS); + double values[PCPOINT_MAX_COORDS]; + for (int i = 0; i < count; i++) + values[i] = PG_GETARG_FLOAT8(i + 1); + PG_RETURN_PCPOINT_P(pcpoint_make(pcid, values, count)); +} + +Datum +Pcpoint_make_coords(PG_FUNCTION_ARGS) +{ + uint32_t pcid = (uint32_t) PG_GETARG_INT32(0); + ArrayType *array = PG_GETARG_ARRAYTYPE_P(1); + int count; + const double *values = float8arr_extract(array, &count); + Pcpoint *result = pcpoint_make(pcid, values, count); + PG_FREE_IF_COPY(array, 1); + PG_RETURN_PCPOINT_P(result); +} +""" + +GATHER_SQL = """ +-- The vocabulary of multi-word types comes from a RETURNS clause, as cellArea(quadbin) states +-- double precision in the SQL of MobilityDB +CREATE FUNCTION cellArea(quadbin) + RETURNS double precision + AS 'MODULE_PATHNAME', 'Quadbin_cell_area' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +CREATE FUNCTION pcpoint(pcid integer, x double precision, y double precision) + RETURNS pcpoint + AS 'MODULE_PATHNAME', 'Pcpoint_make' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +CREATE FUNCTION pcpoint(pcid integer, x double precision, y double precision, + z double precision) + RETURNS pcpoint + AS 'MODULE_PATHNAME', 'Pcpoint_make' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +CREATE FUNCTION pcpoint(pcid integer, coordinates double precision[]) + RETURNS pcpoint + AS 'MODULE_PATHNAME', 'Pcpoint_make_coords' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +""" + +GATHER_MEOS = """ +/** + * @brief Return a pcpoint from a point cloud identifier and coordinates + * @csqlfn #Pcpoint_make(), #Pcpoint_make_coords() + */ +Pcpoint * +pcpoint_make(uint32_t pcid, const double *values, int count) +{ + return NULL; +} +""" + + +class GatherCompositionTests(unittest.TestCase): + """#attach_wrapper_compositions over a wrapper gathering its trailing arguments into a C + array (#_gathers): pcpoint(pcid, x, y) and pcpoint(pcid, x, y, z) pass the coordinates + as the values of pcpoint_make and their number as its count, while pcpoint(pcid, + coordinates) passes its SQL array as it is.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + root = Path(self.tmp.name) + for sub, name, text in (("src", "pcset.c", GATHER_WRAPPERS), + ("sql", "pcpoint.in.sql", GATHER_SQL), + ("meos", "pcpoint.c", GATHER_MEOS)): + (root / sub).mkdir() + (root / sub / name).write_text(text) + + def tearDown(self): + self.tmp.cleanup() + + def _attach(self): + root = Path(self.tmp.name) + double = "double precision" + idl = {"functions": [ + {"name": "pcpoint_make", "api": "public", "mdbC": "Pcpoint_make", "sqlfn": "pcpoint", + "params": [{"name": "pcid"}, {"name": "values"}, {"name": "count"}], + "sqlSignatures": [{"args": ["integer", double, double], "ret": "pcpoint"}, + {"args": ["integer", double, double, double], "ret": "pcpoint"}, + {"args": ["integer", double + "[]"], "ret": "pcpoint"}]}], + "compositions": []} + return attach_wrapper_compositions(idl, root / "src", root / "sql", root / "meos") + + def test_the_coordinates_are_gathered_into_the_values(self): + idl, n = self._attach() + self.assertEqual(n, 2) + self.assertEqual([c["operands"] for c in idl["compositions"]], [ + [{"arg": 0, "param": "pcid"}, {"gather": [1, 2], "param": "values"}, + {"value": "2", "param": "count"}], + [{"arg": 0, "param": "pcid"}, {"gather": [1, 2, 3], "param": "values"}, + {"value": "3", "param": "count"}]]) + self.assertEqual({c["call"] for c in idl["compositions"]}, {"pcpoint_make"}) + + def test_the_array_signature_keeps_its_place(self): + idl, _ = self._attach() + self.assertEqual([s["args"] for s in idl["functions"][0]["sqlSignatures"]], + [["integer", "double precision[]"]]) + + if __name__ == "__main__": unittest.main()