From 498d793a52b3110fb0e3d56ab99ac39e7a000448 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sun, 25 Oct 2026 17:46:25 +0100 Subject: [PATCH] Read the order of the SQL arguments through an expression a wrapper passes _sql_arg_params of parser/boundargs.py reads the order in which a wrapper reads the SQL arguments for the C parameters off its call, one SQL argument per call argument. An argument passed as an expression over a local, as Raster_make passes (const uint8_t *) VARDATA_ANY(pixels), carries the argument that local carries (#_expr_caller_index, the one argument the locals the expression reads carry, through #_caller_index), and the length of an input array carries none, since it travels with its array: Raster_make reads its bytea first and passes it last with its length, so raster_make states shape.sqlArgParams pixels, width, height, upperleftx, upperlefty, scalex, scaley, skewx, skewy, srid, pixeltype, nodataval, as its twin raquet_make states pixels first. Witness: over MobilityDB master bc6f2729ea the catalog states no sqlArgParams for raster_make, whose bytea argument 0 then carries no parameter, so a binding pairs the SQL arguments with the C parameters in their C order: the typed Spark and Flink surfaces of JMEOS pair the bytea with the width and refuse raster(bytea, integer, integer, float, float, float, float, float, float, integer, text, float). Measured over MobilityDB master bc6f2729ea, its libmeos installed as the provision-meos action installs it: run.py states sqlArgParams for raster_make, and every other key of the 5359 functions, and every top-level section but the path of the checkout the object model names, equals that of master's parser. The suite passes 558 tests with 26 skipped, the engine tests needing MEOS_LIBRARY_PATH, as master's passes 556 with the same 26 skipped; the two new ones are in tests/test_boundargs.py SqlArgParamsTests, a bytea passed last as an expression with its length beside it, which master's parser reads as no order, and an expression reading two arguments, which carries none. --- parser/boundargs.py | 26 +++++++++++++++++++++++++- tests/test_boundargs.py | 24 ++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/parser/boundargs.py b/parser/boundargs.py index c2c047d..565f0d3 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -910,6 +910,24 @@ def _kernel_args(body: str, func: dict, meos_body: str | None) -> list[str | Non return None +def _expr_caller_index(body: str, expr: str) -> int | None: + """The SQL argument an expression handed to the kernel carries, the one argument the + locals it reads carry (#_caller_index): ``Raster_make`` passes + ``(const uint8_t *) VARDATA_ANY(pixels)``, which carries argument 0 through + ``pixels = PG_GETARG_BYTEA_PP(0)``. None for a bare local, which #_caller_index reads, + and where the locals carry no argument or several.""" + expr = _CAST.sub("", expr.strip()) + if _IDENT.match(expr): + return None + found = set() + for ident in set(re.findall(r"\b[a-z_]\w*\b", expr)): + if re.search(r"(?])" + re.escape(ident) + r"\s*=(?!=)", body): + k = _caller_index(body, ident) + if k is not None: + found.add(k) + return found.pop() if len(found) == 1 else None + + def _sql_arg_params(body: str, func: dict, meos_body: str | None = None) -> list[str] | None: """The C parameters of ``func`` in the order of the SQL arguments the wrapper ``body`` reads for them, or None when that is their C order or cannot be read: the call @@ -920,11 +938,17 @@ def _sql_arg_params(body: str, func: dict, meos_body: str | None = None) -> list if not args: return None params = func.get("params", []) + # The length of an input array travels with it, so it carries no argument of its own + lengths = {(a.get("lengthFrom") or {}).get("name") + for a in (func.get("shape") or {}).get("inputArrays") or []} by_k: dict[int, str] = {} for a, p in zip(args, params): - if a is None or a.strip().startswith("&") or _literal(a.strip()) is not None: + if a is None or a.strip().startswith("&") or _literal(a.strip()) is not None \ + or p["name"] in lengths: continue k = _caller_index(body, a) + if k is None: + k = _expr_caller_index(body, a) if k is not None: if k in by_k: return None diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 89ba6cc..7b9847c 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -1093,6 +1093,18 @@ def test_the_literal_is_attached_then_stripped(self): return (int) nad_tnumber_tbox(temp, box); """ NAD_TINT_TBOX = {"name": "nad_tint_tbox", "params": [{"name": "temp"}, {"name": "box"}]} +# A wrapper reading a bytea first and passing it last, as an expression over its local. +BYTES_LAST = """ + bytea *pixels = PG_GETARG_BYTEA_PP(0); + int width = PG_GETARG_INT32(1); + char *pixeltype = text_to_cstring(PG_GETARG_TEXT_PP(3)); + Raster *result = raster_make(width, PG_GETARG_INT32(2), pixeltype, + (const uint8_t *) VARDATA_ANY(pixels), (size_t) VARSIZE_ANY_EXHDR(pixels)); +""" +RASTER_MAKE = {"name": "raster_make", "params": [ + {"name": n} for n in ("width", "height", "pixeltype", "pixels", "size")], + "shape": {"inputArrays": [{"param": "pixels", + "lengthFrom": {"kind": "param", "name": "size"}}]}} class SqlArgParamsTests(unittest.TestCase): @@ -1131,6 +1143,18 @@ def test_without_the_public_body_nothing_is_stated(self): """#test_a_shared_kernel_states_the_order without the body of the public function.""" self.assertIsNone(_sql_arg_params(KERNEL_WRAPPER, DISTANCE_SET_INT)) + def test_an_expression_argument_carries_what_its_local_carries(self): + """A bytea read first and passed last, as an expression over its local, with its + length beside it: the length travels with its array and carries no argument.""" + self.assertEqual(_sql_arg_params(BYTES_LAST, RASTER_MAKE), + ["pixels", "width", "height", "pixeltype"]) + + def test_an_expression_reading_two_arguments_carries_none(self): + """#test_an_expression_argument_carries_what_its_local_carries where the expression + reads two locals carrying different arguments, so the order cannot be read.""" + body = BYTES_LAST.replace("VARDATA_ANY(pixels)", "VARDATA_ANY(pixels) + width") + self.assertIsNone(_sql_arg_params(body, RASTER_MAKE)) + IDL = Path(__file__).resolve().parent.parent / "output" / "meos-idl.json"