From 796c7c755bfcaf9fb32bc24e7a92e32691b81192 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Wed, 2 Sep 2026 21:38:21 +0200 Subject: [PATCH] Report a @csqlfn naming a wrapper over another container MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A MEOS function's name ends in the container it takes, and its @csqlfn names the PG wrapper it corresponds to. Where the two disagree about the container, every existing check still passes: the wrapper exists, nothing is unreachable, and the arity matches. What the catalog then derives is the SQL surface of a different overload, so lint_container_family_csqlfn reports the disagreement beside the ever/always, positional and case lints run over the same map. Reading the suffixes literally reports the tree rather than a defect. A concrete function name over a generic wrapper is how the value surface is written throughout — adjacent_span_timestamptz names Adjacent_span_value because one wrapper serves every base type — and forty names disagree that way. Both sides are therefore read as a container family, mapping tstzset to set and timestamptz to value, which leaves the seven that named a wrapper over another container and which MobilityDB now corrects. The lint reports none against a MobilityDB tree carrying that correction. Its test states the case it must catch and four it must not, and neutering the comparison fails the first while leaving the others passing, so the suite discriminates the lint rather than agreeing with it either way. --- parser/sqlfn.py | 44 ++++++++++++++++ run.py | 16 +++++- tests/test_sqlfn_container_family.py | 79 ++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 tests/test_sqlfn_container_family.py diff --git a/parser/sqlfn.py b/parser/sqlfn.py index e26099e..988ec80 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -617,6 +617,50 @@ def lint_positional_sqlfn(idl): return bad +# A MEOS-C function's name ends in the container it takes — `_tstzset` a timestamptz +# set, `_tstzspanset` a span set — and its @csqlfn must name a wrapper over that same +# container. A copy-paste from the neighbouring block names the sibling container's +# wrapper instead, and nothing catches it: the wrapper exists, it is reachable, and its +# arity matches, so the catalog silently carries the SQL surface of the wrong overload +# (trgeometry_at_tstzset answering atTime(trgeometry, tstzspanset), leaving the +# timestamptz-set overload named by nothing). +# A CONCRETE function name over a GENERIC wrapper is the norm rather than a mistag — +# `adjacent_span_timestamptz` names `Adjacent_span_value` because one wrapper serves +# every base type — so both sides are read as a container FAMILY and only a +# disagreement between families is reported. That is what separates the seven real +# mistags from the forty names whose suffixes merely differ. +_CONTAINER_FAMILY = { + "tstzspanset": "spanset", "tstzspan": "span", "tstzset": "set", + "timestamptz": "value", "spanset": "spanset", "span": "span", + "set": "set", "value": "value", +} +_CONTAINER_SUFFIX = tuple(sorted(_CONTAINER_FAMILY, key=len, reverse=True)) + + +def _container_family(name): + """The container family the name ends in, or None when it names no container.""" + low = name.lower() + for suf in _CONTAINER_SUFFIX: + if low.endswith("_" + suf): + return _CONTAINER_FAMILY[suf] + return None + + +def lint_container_family_csqlfn(idl): + """Return [(meos_c_name, wrapper)] where the container family the function's name + ends in contradicts the family its resolved wrapper ends in — a source @csqlfn + mistag naming the sibling container's wrapper.""" + bad = [] + for f in idl["functions"]: + wrapper = f.get("mdbC") + if not wrapper: + continue + fam_fn, fam_wrapper = _container_family(f["name"]), _container_family(wrapper) + if fam_fn and fam_wrapper and fam_fn != fam_wrapper: + bad.append((f["name"], wrapper)) + return bad + + def lint_sqlfn_case_collisions(idl, multi=None): """Return [(lower, [spelling, ...])] for @sqlfn names that collide case-insensitively but differ in case (e.g. tDistance vs tdistance). diff --git a/run.py b/run.py index 1cada55..906d4db 100644 --- a/run.py +++ b/run.py @@ -15,8 +15,9 @@ from parser.boundargs import merge_boundargs from parser.enrich import enrich_idl from parser.sqlfn import (attach_sqlfn_map, attach_aggfn_map, - attach_sqlaggfn_map, lint_ea_sqlfn, - lint_positional_sqlfn, lint_sqlfn_case_collisions) + attach_sqlaggfn_map, lint_container_family_csqlfn, + lint_ea_sqlfn, lint_positional_sqlfn, + lint_sqlfn_case_collisions) from parser.doxygroup import attach_groups from parser.extractors import find_unlisted_foreign_structs from parser.families import all_families, use_headers_dir @@ -233,6 +234,17 @@ def main(): file=sys.stderr) for cname, sf in pos_bad: print(f" {cname} -> @sqlfn {sf}", file=sys.stderr) + # Guard: a function whose name ends in the container it takes (_tstzset, _spanset) + # resolved to a wrapper over a DIFFERENT container. The wrapper exists and is + # reachable, so the name checks pass while the catalog carries the SQL surface of + # the sibling overload. The function name is the SoT; fix the @csqlfn at source. + fam_bad = lint_container_family_csqlfn(idl) + if fam_bad: + print(f" ⚠ {len(fam_bad)} @csqlfn container-family mismatch(es) (the wrapper " + f"takes a different container than the function — fix at source):", + file=sys.stderr) + for cname, wrapper in fam_bad: + print(f" {cname} -> {wrapper}", file=sys.stderr) # Now that both the @sqlfn/@sqlop map (step 4) and the portable bare-name map # (step 3) are attached, classify the shared bbox-topological BACKING tags diff --git a/tests/test_sqlfn_container_family.py b/tests/test_sqlfn_container_family.py new file mode 100644 index 0000000..c0638b0 --- /dev/null +++ b/tests/test_sqlfn_container_family.py @@ -0,0 +1,79 @@ +"""Regression tests for the container-family @csqlfn lint in parser/sqlfn.py. + +A MEOS function's name ends in the container it takes, and its @csqlfn must name +a wrapper over that same container. Naming the sibling container's wrapper passes +every other check — the wrapper exists, it is reachable, its arity matches — so +the catalog silently carries the SQL surface of a different overload. + +The refuting cases matter as much as the flagged one. A CONCRETE function name +over a GENERIC wrapper is how the whole value surface is written, so a lint that +reads suffixes literally reports the tree rather than a defect: forty names +disagree by suffix and seven by container family. + +Plain unittest, no pytest dependency; synthetic catalog records. +""" +import unittest + +from parser.sqlfn import _container_family, lint_container_family_csqlfn + + +def _idl(records): + return {"functions": [dict(name=n, mdbC=w) for n, w in records]} + + +class ContainerFamilyTests(unittest.TestCase): + + def test_sibling_container_wrapper_is_flagged(self): + """The defect: a Set function naming the span-set wrapper.""" + bad = lint_container_family_csqlfn(_idl([ + ("trgeometry_at_tstzset", "Temporal_at_tstzspanset"), + ])) + self.assertEqual(bad, [("trgeometry_at_tstzset", "Temporal_at_tstzspanset")]) + + def test_generic_wrapper_of_a_concrete_name_is_not_flagged(self): + """A timestamptz function naming the generic value wrapper is the norm. + + This is the case that makes a literal suffix comparison useless: one + wrapper serves every base type, so its name says `value` where the + function's says `timestamptz`. + """ + self.assertEqual(lint_container_family_csqlfn(_idl([ + ("adjacent_span_timestamptz", "Adjacent_span_value"), + ("union_set_timestamptz", "Union_set_value"), + ])), []) + + def test_same_family_spelled_differently_is_not_flagged(self): + """`tstzset` and `set` are one family, as are `tstzspanset` and `spanset`.""" + self.assertEqual(lint_container_family_csqlfn(_idl([ + ("distance_tstzset_tstzset", "Distance_set_set"), + ("distance_tstzspanset_tstzspan", "Distance_spanset_span"), + ])), []) + + def test_a_name_ending_in_no_container_is_not_flagged(self): + """`union_set_pcpoint` ends in a type, not a container, so it says nothing. + + Its commuted twin `union_pcpoint_set` does end in one, which is why the + pointcloud pairs flag one side and not the other. + """ + self.assertEqual(lint_container_family_csqlfn(_idl([ + ("union_set_pcpoint", "Union_set_value"), + ("temporal_start_instant", "Temporal_start_instant"), + ])), []) + + def test_a_function_naming_no_wrapper_is_not_flagged(self): + """An untagged function has nothing to disagree with.""" + self.assertEqual(lint_container_family_csqlfn( + {"functions": [{"name": "trgeometry_at_tstzset"}]}), []) + + def test_the_families_a_name_can_end_in(self): + self.assertEqual(_container_family("x_tstzspanset"), "spanset") + self.assertEqual(_container_family("x_tstzspan"), "span") + self.assertEqual(_container_family("x_tstzset"), "set") + self.assertEqual(_container_family("x_timestamptz"), "value") + self.assertEqual(_container_family("X_Set_Value"), "value") + self.assertIsNone(_container_family("x_pcpoint")) + self.assertIsNone(_container_family("tstzset")) + + +if __name__ == "__main__": + unittest.main()