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
53 changes: 48 additions & 5 deletions parser/boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,15 +716,45 @@ 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"(?<![\w.>]){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"(?<![\w.>]){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"(?<![\w.>])" + 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:
"""The literal local ``var`` of wrapper ``body`` holds when SQL argument ``k`` is NULL,
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 ())
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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


Expand Down
87 changes: 87 additions & 0 deletions tests/test_boundargs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Loading