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
44 changes: 44 additions & 0 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
16 changes: 14 additions & 2 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
79 changes: 79 additions & 0 deletions tests/test_sqlfn_container_family.py
Original file line number Diff line number Diff line change
@@ -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()
Loading