diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index b5c899a..62af23d 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -96,7 +96,7 @@ jobs: # carries, or a change to them is not exercised until after it merges. # Consumers use the action; this repository owns the rules. - name: Refuse a skip, and a suite that shrank - run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 383 + run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 390 # The rules earn their place by refusing a log that carries what they # name. Both fixtures are written here rather than tracked, and the diff --git a/parser/sqlfn.py b/parser/sqlfn.py index 57e0487..a9796d4 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -992,3 +992,32 @@ def lint_sqlfn_case_collisions(idl, multi=None): if sf: by_lower.setdefault(sf.lower(), set()).add(sf) return sorted((lo, sorted(sp)) for lo, sp in by_lower.items() if len(sp) > 1) + + +def state_deployed_sqlfn(idl: dict) -> tuple[dict, int]: + """Set ``sqlfn`` to the one SQL name every signature of a function carries. + + A wrapper's ``@sqlfn`` names one member of the family it serves: ``Set_in`` is + tagged ``intset_in()`` and backs ``floatset_in``, ``geomset_in`` and fifteen more, + and the signatures #attach_sqlfn_map keeps for ``floatset_in`` all carry + ``sqlName: floatset_in``, the name its ``CREATE FUNCTION`` deploys. When every + signature of a function carries one name, that name is the function's ``sqlfn`` + and the signatures drop the ``sqlName`` that only restated it. A function whose + signatures carry several names keeps the tag, each signature its own name; a + ``sqlfnBackingOnly`` record, as #classify_backing_sqlfn of parser/portable.py marks + it, keeps its backing tag beside its ``publicSqlName``.""" + n = 0 + for f in idl.get("functions", []): + sigs = f.get("sqlSignatures") + if not sigs or f.get("sqlfnBackingOnly"): + continue + names = {s.get("sqlName", f["sqlfn"]) for s in sigs} + if len(names) != 1: + continue + name = names.pop() + for s in sigs: + s.pop("sqlName", None) + if name != f["sqlfn"]: + f["sqlfn"] = name + n += 1 + return idl, n diff --git a/run.py b/run.py index 9911a7f..d5e544a 100644 --- a/run.py +++ b/run.py @@ -23,7 +23,7 @@ from parser.sqlfn import (attach_sqlfn_map, attach_aggfn_map, attach_row_sources, attach_sqlaggfn_map, lint_container_family_csqlfn, lint_ea_sqlfn, lint_positional_sqlfn, - lint_sqlfn_case_collisions) + lint_sqlfn_case_collisions, state_deployed_sqlfn) from parser.doxygroup import attach_groups from parser.extractors import find_unlisted_foreign_structs from parser.families import all_families, use_headers_dir @@ -276,6 +276,10 @@ def main(): nbo = sum(1 for f in idl.get("functions", []) if f.get("sqlfnBackingOnly")) print(f" Flagged {nbo} bbox-topological backing @sqlfn tag(s) " f"(sqlfnBackingOnly)", file=sys.stderr) + # A function whose signatures all carry one SQL name takes it as its sqlfn, + # in place of the family member its wrapper's tag names. + idl, ndep = state_deployed_sqlfn(idl) + print(f" sqlfn set to the one name its signatures carry: {ndep}", file=sys.stderr) # The same map names each position operator once per class (setLeft … # stboxLeft): derive those names from its @sqlfn/@sqlop tags. idl = attach_position_names(idl) diff --git a/tests/test_sqlfn_deployed.py b/tests/test_sqlfn_deployed.py new file mode 100644 index 0000000..a9aee34 --- /dev/null +++ b/tests/test_sqlfn_deployed.py @@ -0,0 +1,96 @@ +"""A function's `sqlfn` is the one SQL name its signatures carry. + +A wrapper's @sqlfn names one member of the family it serves, so a per-type function +read through it (`floatset_in` under `Set_in`, tagged `intset_in()`) would carry a +family member's name. #state_deployed_sqlfn states the name the function's own +CREATE FUNCTION deploys, as #attach_sqlfn_map stamps it on each signature, and keeps +the tag where the signatures carry several names or the record is a backing tag +#classify_backing_sqlfn marks. + +Plain unittest, no pytest dependency; synthetic records, then the generated catalog. +""" +import json +import unittest +from pathlib import Path + +from parser.sqlfn import state_deployed_sqlfn + +IDL = Path(__file__).resolve().parents[1] / "output" / "meos-idl.json" + + +def _state(*functions): + idl, n = state_deployed_sqlfn({"functions": list(functions)}) + return {f["name"]: f for f in idl["functions"]}, n + + +class DeployedNameTests(unittest.TestCase): + + def test_the_one_name_the_signatures_carry_is_the_sqlfn(self): + fns, n = _state({"name": "floatset_in", "sqlfn": "intset_in", + "sqlSignatures": [{"args": ["cstring"], "ret": "floatset", + "sqlName": "floatset_in"}]}) + self.assertEqual(fns["floatset_in"]["sqlfn"], "floatset_in") + self.assertEqual(fns["floatset_in"]["sqlSignatures"], + [{"args": ["cstring"], "ret": "floatset"}]) + self.assertEqual(n, 1) + + def test_signatures_carrying_several_names_keep_the_tag(self): + sigs = [{"args": ["tgeompoint", "geometry", "float"], "ret": "boolean", + "sqlName": "eDwithin"}, + {"args": ["tgeompoint", "geometry", "float"], "ret": "boolean", + "sqlName": "aDwithin"}] + fns, n = _state({"name": "ea_dwithin_tgeo_geo", "sqlfn": "eDwithin", + "sqlSignatures": [dict(s) for s in sigs]}) + self.assertEqual(fns["ea_dwithin_tgeo_geo"]["sqlfn"], "eDwithin") + self.assertEqual(fns["ea_dwithin_tgeo_geo"]["sqlSignatures"], sigs) + self.assertEqual(n, 0) + + def test_a_backing_tag_stays_beside_its_public_name(self): + fns, n = _state({"name": "adjacent_tbox_tnumber", "sqlfn": "adjacent_bbox", + "sqlfnBackingOnly": True, "publicSqlName": "adjacent", + "sqlSignatures": [{"args": ["tbox", "tint"], "ret": "boolean", + "sqlName": "adjacent"}]}) + self.assertEqual(fns["adjacent_tbox_tnumber"]["sqlfn"], "adjacent_bbox") + self.assertEqual(fns["adjacent_tbox_tnumber"]["sqlSignatures"][0]["sqlName"], + "adjacent") + self.assertEqual(n, 0) + + def test_a_function_without_signatures_keeps_its_tag(self): + fns, n = _state({"name": "temporal_in", "sqlfn": "tint_in"}) + self.assertEqual(fns["temporal_in"]["sqlfn"], "tint_in") + self.assertEqual(n, 0) + + +class DeployedNameCatalogTests(unittest.TestCase): + """Over the generated catalog, the per-type functions carry their own SQL name and + no signature restates the function's sqlfn.""" + + def setUp(self): + if not IDL.exists(): + self.skipTest(f"{IDL} not generated; run `python run.py` first") + self.fns = {f["name"]: f for f in json.loads(IDL.read_text())["functions"]} + + def test_per_type_functions_carry_their_own_name(self): + for name, sqlfn in (("floatset_in", "floatset_in"), ("tfloat_in", "tfloat_in"), + ("tfloatinst_make", "tfloat"), ("tfloat_values", "valueSet"), + ("contains_cbuffer_cbuffer", "cbuffer_contains")): + self.assertEqual(self.fns[name]["sqlfn"], sqlfn, name) + + def test_backing_tags_keep_their_family_name(self): + f = self.fns["adjacent_tbox_tnumber"] + self.assertEqual((f["sqlfn"], f["publicSqlName"]), ("adjacent_bbox", "adjacent")) + + def test_no_function_names_a_family_member_its_signatures_do_not(self): + """The condition #state_deployed_sqlfn of parser/sqlfn.py reads, asked of the + whole catalog: signatures all carrying one name other than sqlfn, a backing + tag aside.""" + other = [f["name"] for f in self.fns.values() + if f.get("sqlSignatures") and not f.get("sqlfnBackingOnly") + and len(names := {s.get("sqlName", f["sqlfn"]) + for s in f["sqlSignatures"]}) == 1 + and names != {f["sqlfn"]}] + self.assertEqual(other, []) + + +if __name__ == "__main__": + unittest.main()