From 65aea2ec64cac62bef0951a2b0402dde90224794 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sun, 25 Oct 2026 17:26:48 +0100 Subject: [PATCH] State the value and the presence flag a NULL default argument feeds A wrapper may read an argument declared DEFAULT NULL as two C parameters, its value and a flag stating whether it is given: Raster_reclass passes has_nodata = ! PG_ARGISNULL(4) and nodataval = has_nodata ? PG_GETARG_FLOAT8(4) : 0.0, and Raquet_constructor, Raster_make and Raster_add_band do the same. attach_null_default_binds of parser/boundargs.py reads such a local as a parameter the argument feeds (#_presence_flag, every assignment of it `! PG_ARGISNULL(k)` or `PG_ARGISNULL(k)`), and #_null_value reads the value a flag holds when the argument is NULL and the branch `x = flag ? ... : literal` takes then. The signature states both parameters in nullDefaultBinds, and the value the flag takes when the argument is given in presentBinds: reclass(rast, nband, reclassexpr, pixeltype, nodataval float DEFAULT NULL) states nullDefaultBinds {4: {has_nodata: false, nodataval: 0.0}} and presentBinds {4: {has_nodata: true}}. Witness: over MobilityDB master bc6f2729ea the catalog states argDefaults NULL for the nodata argument of raquet, raster, addBand and reclass and nothing of what their wrappers pass for it, so a binding pairs one SQL argument with two C parameters: the typed Spark and Flink surfaces of JMEOS register none of the four, each refused as arity:sql, and #185 names raquet and reclass among the signatures whose NULL argument the rules do not read. Measured over MobilityDB master bc6f2729ea, its libmeos installed as the provision-meos action installs it: run.py states the two maps for the 4 signatures, raquet_make, raster_make, raster_add_band and raster_reclass, each value the one its wrapper passes; 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 tests/test_boundargs.py PresenceFlagTests, a flag stating the argument given and one stating it absent. Why: a call answers as PostgreSQL does only when the binding passes what the wrapper passes, for the flag as for the value, and those values are the catalog's to state. --- parser/boundargs.py | 53 ++++++++++++++++++++++--- tests/test_boundargs.py | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) diff --git a/parser/boundargs.py b/parser/boundargs.py index c2c047d..7aa68be 100644 --- a/parser/boundargs.py +++ b/parser/boundargs.py @@ -716,6 +716,26 @@ def attach_type_derived_args(idl: dict, mdb_src: str | Path, # A local a wrapper takes from argument k unless it is NULL: # `quadbin = PG_ARGISNULL(1) ? 0 : PG_GETARG_QUADBIN(1)`. _ISNULL_CHOICE = r"(?]){var}\s*=(?!=)\s*PG_ARGISNULL\s*\(\s*(\d+)\s*\)\s*\?\s*([^:;]+?)\s*:" +# A local stating whether argument k is given: `has_nodata = ! PG_ARGISNULL(4)`. +_PRESENCE = re.compile(r"^(!\s*)?PG_ARGISNULL\s*\(\s*(\d+)\s*\)$") +# A local a wrapper takes from a presence flag: +# `nodataval = has_nodata ? PG_GETARG_FLOAT8(4) : 0.0`. +_FLAG_CHOICE = r"(?]){var}\s*=(?!=)\s*(\w+)\s*\?\s*([^:;]+?)\s*:\s*([^;]+?)\s*;" + + +def _presence_flag(body: str, var: str) -> tuple[int, str] | None: + """(k, value) when the local ``var`` of wrapper ``body`` states whether SQL argument ``k`` + is given, every assignment of it reading ``! PG_ARGISNULL(k)`` (value ``true``) or + ``PG_ARGISNULL(k)`` (value ``false``), the value being the one it holds when the argument + is given; None otherwise. ``Raster_reclass`` passes ``has_nodata = ! PG_ARGISNULL(4)`` + beside the value of argument 4, so one SQL argument feeds two C parameters.""" + rhs = [m.group(1).strip() for m in + re.finditer(r"(?])" + re.escape(var) + r"\s*=(?!=)\s*([^;]+);", body)] + found = {(int(m.group(2)), "true" if m.group(1) else "false") + for m in (_PRESENCE.match(r) for r in rhs) if m} + if len(found) == 1 and len(rhs) == 1: + return found.pop() + return None def _null_value(body: str, var: str, k: int, sig: dict, linear: dict) -> str | None: @@ -723,8 +743,18 @@ def _null_value(body: str, var: str, k: int, sig: dict, linear: dict) -> str | N or None when the wrapper states none: the literal it starts from where every assignment replacing it for the number of arguments of ``sig`` reads argument ``k`` (#_guarded_default), the interpolation of the signature's temporal type it takes when - the call carries no ``k`` (#_type_choice), or the literal of - ``var = PG_ARGISNULL(k) ? literal : ...``.""" + the call carries no ``k`` (#_type_choice), the literal of + ``var = PG_ARGISNULL(k) ? literal : ...``, the value of a presence flag of ``k`` + (#_presence_flag) when the argument is not given, or the literal of the branch + ``var = flag ? ... : ...`` takes when that flag states it is not given.""" + flag = _presence_flag(body, var) + if flag is not None: + return ("false" if flag[1] == "true" else "true") if flag[0] == k else None + m = re.search(_FLAG_CHOICE.format(var=re.escape(var)), body) + if m: + f = _presence_flag(body, m.group(1)) + if f is not None and f[0] == k: + return _literal((m.group(3) if f[1] == "true" else m.group(2)).strip()) g = _guarded_default(body, var) if g is not None: nargs = len(sig.get("args") or ()) @@ -756,7 +786,11 @@ def attach_null_default_binds(idl: dict, mdb_src: str | Path, sql_src: str | Pat of a signature whose CREATE FUNCTION is not STRICT (a STRICT one answers NULL without calling the wrapper), ``nullDefaultBinds[k]`` maps every C parameter argument ``k`` feeds (#_caller_index) to that value, stated only when the wrapper gives every one of - them a value. Runs once the temporal types are attached; the wrappers are traced as + them a value. A parameter stating whether the argument is given (#_presence_flag) is + one the argument feeds too, and ``presentBinds[k]`` states the value it takes when the + argument is given: ``reclass(..., nodataval float DEFAULT NULL)`` passes ``nodataval`` + 0.0 and ``has_nodata`` false when it is NULL, the argument and ``has_nodata`` true + otherwise. Runs once the temporal types are attached; the wrappers are traced as #merge_boundargs traces them.""" from parser.sqlfn import _meos_to_mdb, _wrapper_sql_sigs, _wrapper_sql_strict wrappers = extract_wrappers(mdb_src) @@ -785,20 +819,29 @@ def attach_null_default_binds(idl: dict, mdb_src: str | Path, sql_src: str | Pat args = _call_args(body, func["name"]) or _call_args(body, "pg_" + func["name"]) if not args: continue - fed = {} + fed, flags = {}, {} for pos, a in enumerate(args): if pos < len(params) and _IDENT.match(a): k = _caller_index(body, a, nargs=len(sig.get("args") or ())) + if k is None: + flag = _presence_flag(body, a) + if flag is not None: + k = flag[0] + flags.setdefault(k, {})[params[pos]] = flag[1] if k is not None: fed.setdefault(k, []).append((params[pos], a)) - binds = {} + binds, present = {}, {} for k in nulls: vals = {p: _null_value(body, a, k, sig, linear) for p, a in fed.get(k, ())} if vals and all(v is not None for v in vals.values()): binds[str(k)] = vals + if k in flags: + present[str(k)] = flags[k] if binds: sig["nullDefaultBinds"] = binds n += len(binds) + if present: + sig["presentBinds"] = present return idl, n diff --git a/tests/test_boundargs.py b/tests/test_boundargs.py index 89ba6cc..05a0a28 100644 --- a/tests/test_boundargs.py +++ b/tests/test_boundargs.py @@ -1415,5 +1415,92 @@ def test_a_null_default_feeds_the_parameter_its_number_reads(self): "4": {"maxt": "NULL"}}) +PRESENCE_FLAG = r''' +Datum +Raster_reclass(PG_FUNCTION_ARGS) +{ + Raster *rast = (Raster *) PG_DETOAST_DATUM(PG_GETARG_DATUM(0)); + int band = PG_GETARG_INT32(1); + char *expr = text_to_cstring(PG_GETARG_TEXT_P(2)); + char *pixeltype = text_to_cstring(PG_GETARG_TEXT_P(3)); + bool has_nodata = ! PG_ARGISNULL(4); + double nodataval = has_nodata ? PG_GETARG_FLOAT8(4) : 0.0; + Raster *result = raster_reclass(rast, band, expr, pixeltype, has_nodata, + nodataval); + PG_RETURN_POINTER(result); +} + +Datum +Raster_absent(PG_FUNCTION_ARGS) +{ + Raster *rast = (Raster *) PG_DETOAST_DATUM(PG_GETARG_DATUM(0)); + bool no_value = PG_ARGISNULL(1); + double value = no_value ? -1.0 : PG_GETARG_FLOAT8(1); + Raster *result = raster_absent(rast, value, no_value); + PG_RETURN_POINTER(result); +} +''' + + +class PresenceFlagTests(unittest.TestCase): + """#attach_null_default_binds reads a local stating whether a NULL default argument is + given (#_presence_flag) as a parameter the argument feeds: ``nullDefaultBinds`` states + what it and the value take when the argument is NULL, ``presentBinds`` what the flag + takes when it is given, whichever way the flag is written.""" + + SQL = ''' +CREATE FUNCTION reclass(rast raster, nband integer, reclassexpr text, + pixeltype text, nodataval float DEFAULT NULL) + RETURNS raster + AS 'MODULE_PATHNAME', 'Raster_reclass' + LANGUAGE C IMMUTABLE PARALLEL SAFE; +CREATE FUNCTION absent(rast raster, value float DEFAULT NULL) + RETURNS raster + AS 'MODULE_PATHNAME', 'Raster_absent' + LANGUAGE C IMMUTABLE PARALLEL SAFE; +''' + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + root = Path(self.tmp.name) + (root / "src").mkdir() + (root / "src" / "temporal_raster.c").write_text(PRESENCE_FLAG) + (root / "sql").mkdir() + (root / "sql" / "500_raster.in.sql").write_text(self.SQL) + + def tearDown(self): + self.tmp.cleanup() + + def _idl(self): + return {"functions": [ + {"name": "raster_reclass", "mdbC": "Raster_reclass", "sqlfn": "reclass", + "params": [{"name": n} for n in + ("rast", "band", "expr", "pixeltype", "has_nodata", "nodataval")], + "sqlSignatures": [{"args": ["raster", "integer", "text", "text", "float"], + "ret": "raster", + "argDefaults": [None, None, None, None, "NULL"]}]}, + {"name": "raster_absent", "mdbC": "Raster_absent", "sqlfn": "absent", + "params": [{"name": n} for n in ("rast", "value", "no_value")], + "sqlSignatures": [{"args": ["raster", "float"], "ret": "raster", + "argDefaults": [None, "NULL"]}]}]} + + def test_a_flag_and_its_value_take_what_the_wrapper_passes(self): + idl, n = attach_null_default_binds(self._idl(), Path(self.tmp.name) / "src", + Path(self.tmp.name) / "sql") + sig = idl["functions"][0]["sqlSignatures"][0] + self.assertEqual(sig["nullDefaultBinds"], + {"4": {"has_nodata": "false", "nodataval": "0.0"}}) + self.assertEqual(sig["presentBinds"], {"4": {"has_nodata": "true"}}) + + def test_a_flag_stating_the_argument_is_absent_is_read_the_other_way(self): + idl, n = attach_null_default_binds(self._idl(), Path(self.tmp.name) / "src", + Path(self.tmp.name) / "sql") + sig = idl["functions"][1]["sqlSignatures"][0] + self.assertEqual(sig["nullDefaultBinds"], + {"1": {"value": "-1.0", "no_value": "true"}}) + self.assertEqual(sig["presentBinds"], {"1": {"no_value": "false"}}) + self.assertEqual(n, 2) + + if __name__ == "__main__": unittest.main()