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
28 changes: 25 additions & 3 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)."""
Expand Down Expand Up @@ -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


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