diff --git a/parser/sqlfn.py b/parser/sqlfn.py index 5372606..4616bd2 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -63,6 +63,16 @@ def _split_top_commas(s): _ARGMODE = re.compile(r"^(?:IN|OUT|INOUT|VARIADIC)\s+", re.I) +def _arg_default(decl): + """The literal default expression of an arg declaration, or None for a required arg. + Complement of `_bare_type` (same split, the other side): `integer DEFAULT 0` -> `0`, + `text DEFAULT NULL` -> `NULL`, `float` -> None. Kept verbatim from the SQL source + (no interpretation) so a consumer of an optional trailing arg has its omitted value.""" + a = _ARGMODE.sub("", decl.strip()) + parts = re.split(r"\bDEFAULT\b|=", a, maxsplit=1, flags=re.I) + return parts[1].strip() if len(parts) > 1 and parts[1].strip() else None + + def _bare_type(decl): """A CREATE FUNCTION arg declaration with its argmode and DEFAULT / `= expr` clause stripped — leaving `[argname] argtype`, argtype possibly multi-word (double precision).""" @@ -140,9 +150,11 @@ def _wrapper_sql_sigs(sql_src): if wrapper is None: continue # LANGUAGE SQL / $$ body — no C symbol args = [_arg_type(a, vocab) for a in argdecls] + arg_defaults = [_arg_default(a) for a in argdecls] required = sum(1 for a in argdecls if not re.search(r"\bDEFAULT\b", a, re.I)) out.setdefault(wrapper, []).append( - {"sqlName": sqlname, "args": args, "required": required, "ret": ret}) + {"sqlName": sqlname, "args": args, "required": required, + "argDefaults": arg_defaults, "ret": ret}) return out @@ -241,8 +253,18 @@ def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None): # its {tint,tbigint,tfloat,ttext} overloads). One wrapper backs several @sqlfn # names (Temporal_to_tinstant <- tintInst/tgeometryInst/...), so keep only the # overloads whose CREATE FUNCTION name is this function's @sqlfn. - own = [{"args": s["args"], "ret": s["ret"]} for s in sigs - if s["sqlName"] == f["sqlfn"]] + # `argDefaults` (the per-arg literal SQL default, None for a required arg) is + # attached ONLY for a signature that actually has an optional arg, so a binding + # can render the shorter overload of a SQL-optional argument with its omitted + # value; default-free signatures stay {args, ret} unchanged. + own = [] + for s in sigs: + if s["sqlName"] != f["sqlfn"]: + continue + entry = {"args": s["args"], "ret": s["ret"]} + if any(d is not None for d in s["argDefaults"]): + entry["argDefaults"] = s["argDefaults"] + own.append(entry) if own: f["sqlSignatures"] = own if pairs[0][1]: diff --git a/tests/test_sql_defaults.py b/tests/test_sql_defaults.py new file mode 100644 index 0000000..cf24b29 --- /dev/null +++ b/tests/test_sql_defaults.py @@ -0,0 +1,57 @@ +"""Regression tests for capturing per-arg SQL DEFAULT values in parser/sqlfn.py. + +An optional argument (`integer DEFAULT 0`, `text DEFAULT NULL`) is SQL-optional: a +binding must render the shorter overload with the omitted value substituted. The parser +records the literal default per arg as `argDefaults` (None for a required arg), attached +to a signature only when it has an optional arg, so default-free signatures stay +`{args, ret}` unchanged. + +Plain unittest, no pytest dependency; synthetic SQL via a temp dir. +""" +import tempfile +import unittest +from pathlib import Path + +from parser.sqlfn import _arg_default, _wrapper_sql_sigs + + +class ArgDefaultTests(unittest.TestCase): + def test_arg_default_literal_kept_verbatim(self): + self.assertEqual(_arg_default("integer DEFAULT 0"), "0") + self.assertEqual(_arg_default("val integer DEFAULT 0"), "0") # named arg + self.assertEqual(_arg_default("text DEFAULT NULL"), "NULL") + self.assertEqual(_arg_default("withbbox boolean DEFAULT FALSE"), "FALSE") + self.assertEqual(_arg_default("maxdd integer = 15"), "15") # `=` form + self.assertEqual(_arg_default("VARIADIC opts text DEFAULT NULL"), "NULL") + + def test_required_arg_has_no_default(self): + self.assertIsNone(_arg_default("floatset")) + self.assertIsNone(_arg_default("s floatset")) + self.assertIsNone(_arg_default("double precision")) + + def test_wrapper_sql_sigs_records_arg_defaults(self): + # round(floatset, integer DEFAULT 0): the trailing precision is SQL-optional. + sql = ("CREATE FUNCTION round(floatset, integer DEFAULT 0)\n" + " RETURNS floatset AS 'MODULE_PATHNAME', 'Set_round' LANGUAGE C;\n") + with tempfile.TemporaryDirectory() as d: + (Path(d) / "x.sql").write_text(sql) + sigs = _wrapper_sql_sigs(d) + self.assertIn("Set_round", sigs) + s = sigs["Set_round"][0] + self.assertEqual(s["args"], ["floatset", "integer"]) + self.assertEqual(s["argDefaults"], [None, "0"]) + self.assertEqual(s["required"], 1) + + def test_all_required_args_have_all_none_defaults(self): + sql = ("CREATE FUNCTION set_union(floatset, floatset)\n" + " RETURNS floatset AS 'MODULE_PATHNAME', 'Union_set_set' LANGUAGE C;\n") + with tempfile.TemporaryDirectory() as d: + (Path(d) / "x.sql").write_text(sql) + sigs = _wrapper_sql_sigs(d) + s = sigs["Union_set_set"][0] + self.assertEqual(s["argDefaults"], [None, None]) + self.assertEqual(s["required"], 2) + + +if __name__ == "__main__": + unittest.main()