diff --git a/parser/portable.py b/parser/portable.py index 41398ac..2ca17f4 100644 --- a/parser/portable.py +++ b/parser/portable.py @@ -43,3 +43,34 @@ def attach_portable_aliases(idl: dict, path: Path) -> dict: "count": len(pairs), } return idl + + +def classify_backing_sqlfn(idl: dict) -> dict: + """Mark the bounding-box topological BACKING ``@sqlfn`` tags. + + MobilityDB backs the five topological operators (~=/@>/<@/-|-/&&) with a SHARED C + ``@sqlfn`` tag named ``_bbox`` (same_bbox, contains_bbox, contained_bbox, + overlaps_bbox, adjacent_bbox). That tag is NEVER emitted as a ``CREATE FUNCTION`` — + the deployed, user-facing SQL name is the operator's bare portable alias + (same/contains/…). The raw ``sqlfn`` is therefore a backing name, not a public one; + a binding that registers it leaks a function MobilityDB does not expose. Flag those + records with ``sqlfnBackingOnly`` + the ``publicSqlName`` (the bare alias) so every + binding uniformly registers the bare name + operator and drops the ``_bbox`` tag. + + Not a heuristic: grounded in two catalog-native facts — the ``_bbox`` shared-backing + convention AND the operator→bareName map. ``publicSqlName`` is always defined because + every ``_bbox`` sqlfn carries one of the five topological operators. + + MUST run AFTER ``attach_sqlfn_map`` (sqlfn/sqlop) AND ``attach_portable_aliases`` + (byOperator) — it reads all three. + """ + by_operator = (idl.get("portableAliases") or {}).get("byOperator") or {} + if not by_operator: + return idl + for f in idl.get("functions", []): + sqlfn = f.get("sqlfn") or "" + op = f.get("sqlop") or "" + if sqlfn.endswith("_bbox") and op in by_operator: + f["sqlfnBackingOnly"] = True + f["publicSqlName"] = by_operator[op] + return idl diff --git a/run.py b/run.py index 5b55452..49d70ef 100644 --- a/run.py +++ b/run.py @@ -4,7 +4,7 @@ from pathlib import Path from parser.parser import parse_all_headers, merge_meta -from parser.portable import attach_portable_aliases +from parser.portable import attach_portable_aliases, classify_backing_sqlfn from parser.covering import attach_temporal_covering from parser.typerecover import recover_collapsed_types from parser.header_types import reconcile @@ -110,6 +110,15 @@ def main(): for _lo, spellings in case_bad: print(f" {' vs '.join(spellings)}", 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 + # (same_bbox/contains_bbox/…) so bindings register the bare public name, not the + # catalog-only backing tag. + idl = classify_backing_sqlfn(idl) + 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) + # 5. Attach the doxygen module group (@ingroup) from the vendored source, so # bindings organize their generated surface like the reference manual. if MEOS_SRC.exists(): diff --git a/tests/test_portable.py b/tests/test_portable.py index 5eef798..860346c 100644 --- a/tests/test_portable.py +++ b/tests/test_portable.py @@ -13,7 +13,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) -from parser.portable import attach_portable_aliases +from parser.portable import attach_portable_aliases, classify_backing_sqlfn MAP = ROOT / "meta" / "portable-aliases.json" SCHEMA = ROOT / "meta" / "portable-aliases.schema.json" @@ -120,6 +120,34 @@ def test_missing_file_is_noop(self): idl = attach_portable_aliases({"x": 1}, ROOT / "nope.json") self.assertNotIn("portableAliases", idl) + def test_backing_sqlfn_classification(self): + # A bbox-topological function carries a shared `_bbox` @sqlfn backing tag + # that is never deployed as a CREATE FUNCTION; its public name is the operator's + # bare alias. classify_backing_sqlfn must flag it and record publicSqlName. + idl = attach_portable_aliases({"functions": [ + {"name": "Same_stbox_stbox", "sqlfn": "same_bbox", "sqlop": "~="}, + {"name": "Contains_tbox_tnumber", "sqlfn": "contains_bbox", "sqlop": "@>"}, + {"name": "Left_stbox_stbox", "sqlfn": "temporal_left", "sqlop": "<<"}, + {"name": "Tpoint_trajectory", "sqlfn": "trajectory"}, + ]}, MAP) + idl = classify_backing_sqlfn(idl) + by = {f["name"]: f for f in idl["functions"]} + # the two _bbox backing tags are flagged with the bare public name + self.assertTrue(by["Same_stbox_stbox"]["sqlfnBackingOnly"]) + self.assertEqual(by["Same_stbox_stbox"]["publicSqlName"], "same") + self.assertTrue(by["Contains_tbox_tnumber"]["sqlfnBackingOnly"]) + self.assertEqual(by["Contains_tbox_tnumber"]["publicSqlName"], "contains") + # a positional op whose @sqlfn IS the deployed name (temporal_left) is untouched + self.assertNotIn("sqlfnBackingOnly", by["Left_stbox_stbox"]) + # a plain function with no operator is untouched + self.assertNotIn("sqlfnBackingOnly", by["Tpoint_trajectory"]) + + def test_backing_sqlfn_noop_without_aliases(self): + # No portableAliases attached -> nothing to classify, no crash. + idl = classify_backing_sqlfn({"functions": [ + {"name": "X", "sqlfn": "same_bbox", "sqlop": "~="}]}) + self.assertNotIn("sqlfnBackingOnly", idl["functions"][0]) + def test_duplicate_detection(self): bad = {"families": {"a": [{"operator": "&&", "bareName": "x"}, {"operator": "@>", "bareName": "x"}]},