From e2ec2665fe8e0c6e53971247710bdaf1b75d542d Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Thu, 13 Aug 2026 11:39:54 +0200 Subject: [PATCH] Give each function the signatures of the types it serves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A PG wrapper commonly backs a whole per-type family: Set_values is the body behind getValues(intset), getValues(cbufferset) and fourteen more, and every one of those MEOS functions names that wrapper in its @csqlfn. The wrapper's signature list is therefore the union over its claimants, and attaching it whole told a binding that intset_values serves cbufferset. Of 1675 wrappers, 231 are claimed by more than one public function. Each function now keeps only the signatures its TYPE SCOPE covers, read from what MEOS itself states: its VALIDATE_* macro, the MeosType literals in its body, a class predicate it calls, or its C parameter types resolved through the catalog's base-type relations. The macros are read from their DEFINITIONS, so a class macro resolves too — VALIDATE_TGEO is written over ensure_tgeo_type_all, and a name-shaped reading would see only the single-type macros. Scopes are compared in both spellings, since SQL says integer where meostype_name says int4. Of the claimants, 720 resolve through a VALIDATE_* macro, 105 through C parameters and 82 through MeosType literals. The 27 that state nothing are declared in meta/type-scope.json with the reason each is unstated, and require_scopes fails generation on any claimant that is neither derived nor declared: guessing keeps the wrong signatures or drops real ones, and both are invisible downstream. A filtered function keeps its OWN overload, which carries its own SQL name (bigintset_in) rather than the representative the @sqlfn tag names (intset_in), so the single-name shortcut no longer drops it. Measured against the same MobilityDB commit: 5513 functions before and after, none gains a signature, 868 shrink to the types they serve. 20 keep none, each a wrapper whose overloads the claimant genuinely does not serve — the jsonb comparisons MEOS exports without a SQL declaration, and functions whose @csqlfn names a wrapper of another type. --- meta/type-scope.json | 126 +++++++++++++++++++ meta/type-scope.schema.json | 40 +++++++ parser/sqlfn.py | 47 +++++++- parser/typescope.py | 233 ++++++++++++++++++++++++++++++++++++ tests/test_typescope.py | 74 ++++++++++++ 5 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 meta/type-scope.json create mode 100644 meta/type-scope.schema.json create mode 100644 parser/typescope.py create mode 100644 tests/test_typescope.py diff --git a/meta/type-scope.json b/meta/type-scope.json new file mode 100644 index 0000000..77195df --- /dev/null +++ b/meta/type-scope.json @@ -0,0 +1,126 @@ +{ + "$schema": "./type-scope.schema.json", + "description": "The SQL type scope of MEOS functions whose scope MEOS itself does not state. A function sharing a PG wrapper with others receives only the wrapper's signatures that its scope covers; parser/typescope.py derives that scope from a VALIDATE_* macro, MeosType literals, a class predicate or the C parameter types, and every claimant it cannot derive must be declared here. A scope of \"*\" means the function genuinely serves every overload its wrapper declares.", + "scopes": { + "temporal_append_tinstant": { + "types": "*", + "note": "Generic over Temporal *: appends to any temporal type." + }, + "temporal_at_values": { + "types": "*", + "note": "Generic over Temporal *: restricts any temporal type to a value set." + }, + "temporal_minus_values": { + "types": "*", + "note": "Generic over Temporal *: the complement of temporal_at_values." + }, + "temporal_end_instant": { + "types": "*", + "note": "Generic over Temporal *: accessor on any temporal type." + }, + "temporal_start_instant": { + "types": "*", + "note": "Generic over Temporal *: accessor on any temporal type." + }, + "temporal_end_sequence": { + "types": "*", + "note": "Generic over Temporal *: accessor on any temporal type." + }, + "temporal_start_sequence": { + "types": "*", + "note": "Generic over Temporal *: accessor on any temporal type." + }, + "temporal_set_interp": { + "types": "*", + "note": "Generic over Temporal *: sets the interpolation of any temporal type." + }, + "temporal_shift_time": { + "types": "*", + "note": "Generic over Temporal *: shifts the time of any temporal type." + }, + "temporal_shift_scale_time": { + "types": "*", + "note": "Generic over Temporal *: shifts and scales the time of any temporal type." + }, + "spanset_span": { + "types": "*", + "note": "Generic over SpanSet *: returns the bounding span of any spanset type." + }, + "span_lower_inc": { + "types": "*", + "note": "Generic over Span *: reports whether the lower bound of any span type is inclusive." + }, + "span_upper_inc": { + "types": "*", + "note": "Generic over Span *: reports whether the upper bound of any span type is inclusive." + }, + + "trgeometry_at_values": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone. It states no validity check of its own, so the scope cannot be read from the body — see the rgeo restrict functions, which delegate without validating." + }, + "trgeometry_minus_values": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + "trgeometry_at_tstzspan": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + "trgeometry_at_tstzset": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + "trgeometry_at_tstzspanset": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + "trgeometry_minus_tstzspan": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + "trgeometry_minus_tstzset": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + "trgeometry_minus_tstzspanset": { + "types": ["trgeometry"], + "note": "Serves trgeometry alone; states no validity check." + }, + + "tintbox_shift_scale": { + "types": ["tbox"], + "note": "Shifts and scales a tbox. Its body names the value span it moves (T_INTSPAN), so the span is what a body-read derives — the type it serves is the box." + }, + "tbigintbox_shift_scale": { + "types": ["tbox"], + "note": "Shifts and scales a tbox; the body names its value span, not the box." + }, + "tfloatbox_shift_scale": { + "types": ["tbox"], + "note": "Shifts and scales a tbox; the body names its value span, not the box." + }, + + "nad_tint_tbox": { + "types": ["tint"], + "note": "The tint arm of the tnumber/tbox nearest-approach pair." + }, + "nad_tfloat_tbox": { + "types": ["tfloat"], + "note": "The tfloat arm of the tnumber/tbox nearest-approach pair." + }, + "nad_tcbuffer_stbox": { + "types": ["tcbuffer"], + "note": "Serves tcbuffer. Its @csqlfn names NAD_tcbuffer_geo, the wrapper of nad_tcbuffer_geo; the stbox form has its own wrapper NAD_tcbuffer_stbox. Upstream tag fix, recorded here so generation stays deterministic meanwhile." + }, + + "span_to_spanset": { + "types": [], + "note": "Converts span to spanset, while its @csqlfn names Spanset_to_span — the wrapper of the opposite conversion, whose overloads are all spanset-argument. No overload of that wrapper is served, hence the empty scope. Upstream tag fix." + }, + "contained_span_span": { + "types": [], + "note": "Compares span with span, while its @csqlfn names Contained_value_span, whose overloads all take a base value. No overload of that wrapper is served. Upstream tag fix." + } + } +} diff --git a/meta/type-scope.schema.json b/meta/type-scope.schema.json new file mode 100644 index 0000000..b483fdf --- /dev/null +++ b/meta/type-scope.schema.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "type-scope.schema.json", + "title": "MEOS function type scopes", + "description": "Declared SQL type scopes for MEOS functions whose scope MEOS itself does not state. Consumed by parser/typescope.py, which fails generation on any underivable claimant absent from this file.", + "type": "object", + "required": ["scopes"], + "additionalProperties": false, + "properties": { + "$schema": { "type": "string" }, + "description": { "type": "string" }, + "scopes": { + "type": "object", + "description": "Keyed by MEOS function name.", + "additionalProperties": { + "type": "object", + "required": ["types", "note"], + "additionalProperties": false, + "properties": { + "types": { + "description": "The SQL types the function serves: a list of type names, or \"*\" when it serves every overload its wrapper declares. An empty list means it serves none of them, which is what a mistagged @csqlfn looks like.", + "oneOf": [ + { "type": "string", "const": "*" }, + { + "type": "array", + "items": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "uniqueItems": true + } + ] + }, + "note": { + "type": "string", + "description": "Why the scope is what it is, and why MEOS does not state it.", + "minLength": 1 + } + } + } + } + } +} diff --git a/parser/sqlfn.py b/parser/sqlfn.py index b008ffa..d99521c 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -17,6 +17,9 @@ import re from pathlib import Path +from parser.typescope import (TypeFacts, declared_scopes, read_bodies, + require_scopes, resolve_scope, signatures_for) + # A @csqlfn tag carries one OR MORE #Wrapper() references — comma- or # space-separated, and possibly continued across doxygen lines — because a single # MEOS function can back several wrappers (the ever/always pair eDisjoint/aDisjoint @@ -285,6 +288,32 @@ def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None): w2sig = _wrapper_sql_sigs(sql_src) if sql_src else {} direct = _meos_direct_sql(meos_src) n = 0 + # One wrapper commonly backs a whole per-type family — `Set_values` is the body + # behind getValues(intset), getValues(cbufferset) and fourteen more — so a + # wrapper's signature list is the union over its claimants, not the surface of + # any one of them. Each function therefore keeps only the signatures its own + # TYPE SCOPE covers; scopes MEOS does not state are declared in + # meta/type-scope.json, and an underivable claimant fails generation rather + # than silently taking the union or nothing. + scope_facts = scope_bodies = scope_params = None + declared = {} + shared_wrappers = set() + if w2sig: + meos_root = Path(meos_src).parent + scope_facts = TypeFacts(meos_root) + scope_bodies, scope_params = read_bodies(meos_root) + declared = declared_scopes() + claimed = {} + for f in idl["functions"]: + if f.get("api") != "public": + continue + for w in m2d.get(f["name"]) or (): + claimed.setdefault(w, []).append(f["name"]) + break + shared_wrappers = {w for w, names in claimed.items() + if len(names) > 1 and len(w2sig.get(w) or ()) > 1} + require_scopes([n for w in shared_wrappers for n in claimed[w]], + scope_facts, scope_bodies, scope_params, declared) # Transient map: MEOS function name -> every SQL name it resolves to, for the # functions that fan out (a shared wrapper / ever-always pair). This is NOT # catalog output — every binding reads only the primary `sqlfn` — it is working @@ -320,6 +349,16 @@ def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None): # signature instead of the wider C one: args beyond sqlArity are SQL-optional # (DEFAULT), and C params beyond sqlArityMax are C-only out-params. sigs = w2sig.get(wrappers[0]) + # Only a public claimant of a shared wrapper is filtered: those are the + # functions a binding projects, and the ones require_scopes has proven a + # scope for. An internal function is not part of any binding surface. + scoped = False + if sigs and wrappers[0] in shared_wrappers and f.get("api") == "public": + scope, _ = resolve_scope(f["name"], scope_facts, scope_bodies, + scope_params, declared) + if scope is not None: + sigs = signatures_for(f["name"], sigs, scope) + scoped = True if sigs: f["sqlArity"] = min(s["required"] for s in sigs) f["sqlArityMax"] = max(len(s["args"]) for s in sigs) @@ -349,8 +388,14 @@ def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None): # 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. + # Scope filtering already reduced `sigs` to the overloads this function + # serves, and a per-type function's own overload carries its OWN SQL name + # (`bigintset_in`), not the representative the @sqlfn tag names + # (`intset_in`). Dropping a name that differs from `sqlfn` would discard + # exactly the signature the filter just proved belongs here, so a filtered + # function keeps all of them and stamps the name whenever it differs. fam_names = {s["sqlName"] for s in sigs} - multiname = len(fam_names) > 1 + multiname = len(fam_names) > 1 or (scoped and fam_names != {f["sqlfn"]}) own = [] for s in sigs: if not multiname and s["sqlName"] != f["sqlfn"]: diff --git a/parser/typescope.py b/parser/typescope.py new file mode 100644 index 0000000..134f7ab --- /dev/null +++ b/parser/typescope.py @@ -0,0 +1,233 @@ +"""Derive the SQL type scope of a MEOS function. + +One PG wrapper commonly backs a whole per-type family: `Set_values` is the body +behind `getValues(intset)`, `getValues(cbufferset)` and fourteen more, while the +MEOS side spells one typed function per type (`intset_values`, +`cbufferset_values`, ...). Attaching a wrapper's whole signature list to every +MEOS function that names it would tell a binding that `intset_values` serves +cbufferset — so each function's signatures are filtered to the types it actually +serves, and that set is its TYPE SCOPE. + +The scope is read from what MEOS itself states, never from the function's name: + + * its own `VALIDATE_` macro, + * the `MeosType` literals in its body, resolved through `meostype_name`, + * a class predicate it calls (`tnumber_type`, `tspatial_type`, ...), whose + members the catalog lists, + * its C parameter types, resolved through the catalog's base-type relations. + +A function whose scope none of those state is UNDERIVABLE: it is either generic +over every overload or simply unclassified, and the two are indistinguishable +from outside. Rather than guess — one guess keeps a wrong signature list, the +other drops a real registration — such a function must be listed in +`meta/type-scope.json`, and `require_scopes` fails on any that is not. +""" +from __future__ import annotations + +import json +import re +from pathlib import Path + +# A declared scope of "*" means the function serves every overload its wrapper +# declares — the legitimate generic case, stated rather than guessed. +EVERY_OVERLOAD = '*' + +_META = Path(__file__).resolve().parent.parent / 'meta' / 'type-scope.json' + +_TYPE_NAME = re.compile(r'\[(T_[A-Z0-9_]+)\]\s*=\s*"([a-z0-9_]+)"') +_PREDICATE = re.compile(r'^(\w+)\(MeosType type\)\n\{\n(.*?)\n\}', re.S | re.M) +_MEOS_TYPE = re.compile(r'\bT_[A-Z0-9_]+\b') +_VALIDATE = re.compile(r'\bVALIDATE_[A-Z0-9_]+\b') +_C_PARAM = re.compile(r'\b(?:const\s+)?(\w+)\s*\*?\s*\w+\s*[,)]') +_FN_OPEN = re.compile(r'^(\w+)\((.*)$') + +# The catalog fields relating a container type to the type it is built over. +_RELATIONS = ('settype_basetype', 'spantype_basetype', 'temptype_basetype', + 'spansettype_spantype') + +# `T_UNKNOWN` is the catalog's placeholder, not a type a function can serve. +_NOT_A_TYPE = {'unknown'} + +# The C spelling of each MEOS base type. `GSERIALIZED` covers both geometry and +# geography, which is why a C parameter of that type widens to the pair. +C_BASE_TYPES = { + 'int': 'int4', 'int32': 'int4', 'int64': 'int8', 'double': 'float8', + 'bool': 'bool', 'text': 'text', 'DateADT': 'date', + 'TimestampTz': 'timestamptz', + 'GSERIALIZED': ('geometry', 'geography'), +} + +# PostgreSQL's spelling of the MEOS base types: a SQL signature says `integer` +# where `meostype_name` says `int4`, so a scope must be compared in both. +SQL_ALIASES = {'int4': 'integer', 'int8': 'bigint', 'float8': 'float', + 'bool': 'boolean'} + + +def sql_spellings(types): + """Every SQL spelling of a scope, so it can be matched against a signature.""" + return set(types) | {SQL_ALIASES[t] for t in types if t in SQL_ALIASES} + + +class TypeFacts: + """The type vocabulary, class predicates and container relations, read from + MEOS's own catalog rather than restated here.""" + + def __init__(self, meos_src: str | Path): + src = (Path(meos_src) / 'src/temporal/meos_catalog.c').read_text(errors='ignore') + self.name = {e: n for e, n in _TYPE_NAME.findall(src) if n not in _NOT_A_TYPE} + self.names = set(self.name.values()) + self.klass = {} + for m in _PREDICATE.finditer(src): + members = {self.name[t] for t in _MEOS_TYPE.findall(m.group(2)) + if t in self.name} + if members: + self.klass[m.group(1)] = members + self.container = {} + for field in _RELATIONS: + pat = rf'\[(T_[A-Z0-9_]+)\]\s*=\s*\{{[^}}]*\.{field}\s*=\s*(T_[A-Z0-9_]+)' + for outer, inner in re.findall(pat, src): + if outer in self.name and inner in self.name: + self.container.setdefault(self.name[inner], set()).add(self.name[outer]) + self.validate = self._read_validate_macros(Path(meos_src) / 'include') + + def _read_validate_macros(self, include_dir: Path) -> dict: + """What each `VALIDATE_*` macro constrains its argument to, read from the + macro's own definition. `VALIDATE_INTSET` names a single type through + `ensure_set_isof_type(s, T_INTSET)`; `VALIDATE_TGEO` names a whole class + through `ensure_tgeo_type_all` — a name-shaped guess would see only the + first kind and miss every class macro.""" + out = {} + for hf in include_dir.rglob('*.h'): + text = hf.read_text(errors='ignore') + for m in re.finditer(r'#\s*define\s+(VALIDATE_[A-Z0-9_]+)\((.*?)\n(?=\s*#|\s*extern|\n)', + text, re.S): + name, body = m.group(1), m.group(2) + types = {self.name[t] for t in _MEOS_TYPE.findall(body) + if t in self.name} + for pred, members in self.klass.items(): + if re.search(rf'\b(?:ensure_)?{re.escape(pred)}\s*\(', body): + types |= members + if types: + out.setdefault(name, set()).update(types) + return out + + def widen(self, types: set[str]) -> set[str]: + """A type plus every container built over it: a function stating + `T_INT8` serves bigint, and the bigintset/bigintspan/tbigint built on + it, transitively.""" + out, queue = set(types), list(types) + while queue: + for c in self.container.get(queue.pop(), ()): + if c not in out: + out.add(c) + queue.append(c) + return out + + +def read_bodies(meos_src: str | Path) -> tuple[dict, dict]: + """Every MEOS function's body text and parameter list, keyed by name.""" + bodies, params = {}, {} + for cf in Path(meos_src, 'src').rglob('*.c'): + cur, buf = None, [] + for line in cf.read_text(errors='ignore').split('\n'): + m = _FN_OPEN.match(line) + if m: + cur, buf = m.group(1), [] + params.setdefault(cur, m.group(2)) + if cur is not None: + buf.append(line) + if line == '}': + bodies.setdefault(cur, '\n'.join(buf)) + cur = None + return bodies, params + + +def scope_of(name: str, facts: TypeFacts, bodies: dict, params: dict, + c_types: dict = C_BASE_TYPES) -> tuple[set | None, str]: + """This function's type scope and the signal that states it, or + ``(None, 'none')`` when MEOS states nothing.""" + body = bodies.get(name) + if body is None: + return None, 'none' + + stated = {t for macro in _VALIDATE.findall(body) + for t in facts.validate.get(macro, ())} + if stated: + return facts.widen(stated), 'validate' + + literals = {facts.name[t] for t in _MEOS_TYPE.findall(body) if t in facts.name} + if literals: + return facts.widen(literals), 'meostype' + + members = {t for pred, types in facts.klass.items() + if re.search(rf'\b(?:ensure_)?{re.escape(pred)}\s*\(', body) + for t in types} + if members: + return members, 'class' + + # A C parameter names a base type (`int64 i` -> int8, `const Cbuffer *cb` -> + # cbuffer), and the catalog says which containers are built over it. + from_params = set() + for token in _C_PARAM.findall(params.get(name, '')): + base = c_types.get(token) + if base is not None: + from_params.update((base,) if isinstance(base, str) else base) + elif token.lower() in facts.names: + from_params.add(token.lower()) + if from_params: + widened = facts.widen(from_params) + if widened - from_params: + return widened, 'cparam' + + return None, 'none' + + +def declared_scopes(path: str | Path = _META) -> dict: + """The scopes stated in `meta/type-scope.json`, keyed by function name.""" + doc = json.loads(Path(path).read_text()) + return {name: entry['types'] for name, entry in doc['scopes'].items()} + + +def resolve_scope(name, facts, bodies, params, declared): + """This function's scope, from MEOS's own signals or from the declared file. + + Returns ``(types, signal)`` where `types` is a set, or ``EVERY_OVERLOAD`` for + a function declared generic, or ``None`` when nothing states it.""" + stated = declared.get(name) + if stated is not None: + return (EVERY_OVERLOAD if stated == EVERY_OVERLOAD else set(stated)), 'declared' + return scope_of(name, facts, bodies, params) + + +def require_scopes(claimants, facts, bodies, params, declared): + """Fail on any claimant of a shared wrapper whose scope nothing states. + + Guessing here is what the whole mechanism exists to avoid: assuming "all" + reinstates the wrong-signature bug, assuming "none" silently drops real + registrations. Both are invisible downstream, so an underivable claimant is + an error the catalog refuses to emit until someone states the answer in + `meta/type-scope.json`.""" + missing = sorted(n for n in claimants + if resolve_scope(n, facts, bodies, params, declared)[0] is None) + if missing: + raise ValueError( + 'type scope underivable for %d function(s); state each in ' + 'meta/type-scope.json: %s' % (len(missing), ', '.join(missing))) + + +def signatures_for(name, sigs, scope): + """The subset of a wrapper's signatures this function serves. + + A signature belongs to the function when the scope covers any type it names — + its arguments or its return — compared in both MEOS and SQL spellings.""" + if scope == EVERY_OVERLOAD: + return list(sigs) + covered = sql_spellings(scope) + kept = [] + for sig in sigs: + named = set(sig.get('args') or ()) + if sig.get('ret'): + named.add(sig['ret']) + if named & covered: + kept.append(sig) + return kept diff --git a/tests/test_typescope.py b/tests/test_typescope.py new file mode 100644 index 0000000..9d6dddc --- /dev/null +++ b/tests/test_typescope.py @@ -0,0 +1,74 @@ +"""A function keeps only the signatures of the types it serves.""" +import json +import unittest +from pathlib import Path + +from parser.typescope import (EVERY_OVERLOAD, SQL_ALIASES, declared_scopes, + signatures_for, sql_spellings) + +META = Path(__file__).resolve().parent.parent / 'meta' / 'type-scope.json' + +# `Set_values` is the body behind getValues(intset), getValues(cbufferset) and +# fourteen more, so its signature list is the union over all of them. +SET_VALUES = [ + {'args': ['intset'], 'ret': 'integer[]', 'sqlName': 'getValues'}, + {'args': ['cbufferset'], 'ret': 'cbuffer[]', 'sqlName': 'getValues'}, + {'args': ['npointset'], 'ret': 'npoint[]', 'sqlName': 'getValues'}, +] + + +class SignatureFilterTests(unittest.TestCase): + + def test_a_typed_function_keeps_only_its_own_overload(self): + kept = signatures_for('intset_values', SET_VALUES, {'intset'}) + self.assertEqual([s['args'] for s in kept], [['intset']]) + + def test_each_sibling_keeps_a_different_overload(self): + for scope, arg in (({'cbufferset'}, 'cbufferset'), ({'npointset'}, 'npointset')): + kept = signatures_for('x', SET_VALUES, scope) + self.assertEqual([s['args'] for s in kept], [[arg]]) + + def test_a_generic_function_keeps_every_overload(self): + kept = signatures_for('temporal_shift_time', SET_VALUES, EVERY_OVERLOAD) + self.assertEqual(len(kept), len(SET_VALUES)) + + def test_a_scope_serving_none_of_them_keeps_none(self): + """What a mistagged @csqlfn looks like: the function names a wrapper + whose overloads it does not serve.""" + self.assertEqual(signatures_for('span_to_spanset', SET_VALUES, {'intspan'}), []) + + def test_the_return_type_also_places_a_signature(self): + """An I/O function is named by what it returns, not by its cstring argument.""" + sigs = [{'args': ['cstring'], 'ret': 'bigintset', 'sqlName': 'bigintset_in'}, + {'args': ['cstring'], 'ret': 'intset', 'sqlName': 'intset_in'}] + kept = signatures_for('bigintset_in', sigs, {'bigintset'}) + self.assertEqual([s['ret'] for s in kept], ['bigintset']) + + def test_a_scope_matches_the_sql_spelling_of_its_types(self): + """SQL says `integer` where meostype_name says `int4`.""" + self.assertEqual(sql_spellings({'int4'}), {'int4', 'integer'}) + sigs = [{'args': ['integer'], 'ret': 'intspan', 'sqlName': 'span'}] + self.assertEqual(len(signatures_for('int_to_span', sigs, {'int4'})), 1) + + def test_every_sql_alias_target_differs_from_its_meos_spelling(self): + for meos, sql in SQL_ALIASES.items(): + self.assertNotEqual(meos, sql) + + +class DeclaredScopeTests(unittest.TestCase): + + def test_the_declared_file_matches_its_schema_shape(self): + doc = json.loads(META.read_text()) + for name, entry in doc['scopes'].items(): + self.assertTrue(entry['note'], f'{name} states no reason') + types = entry['types'] + self.assertTrue(types == EVERY_OVERLOAD or isinstance(types, list), + f'{name} has an unusable scope') + + def test_declared_scopes_reads_every_entry(self): + doc = json.loads(META.read_text()) + self.assertEqual(set(declared_scopes()), set(doc['scopes'])) + + +if __name__ == '__main__': + unittest.main()