diff --git a/README.md b/README.md index 0bcad9f..0fdb0c5 100644 --- a/README.md +++ b/README.md @@ -229,8 +229,9 @@ bare-name mapping that MobilityDB now registers natively (PR #1075). The pipeline folds it into the catalog as `portableAliases` (with `byOperator` / `byBareName` lookups), so **every binding/engine generates the identical bare names** and a user learns one reference and assumes the rest. A -position operator has one name per class instead (`setLeft` … `stboxLeft`), -which the catalog derives from the `@sqlfn` tags as `positionNames`. +position or a topological operator has one name per class instead +(`setLeft` … `stboxLeft`, `setOverlaps` … `stboxOverlaps`), which the catalog +derives from the `@sqlfn` tags as `positionNames`. It is curated canonical data, kept verbatim (only bijective lookups are derived — no C-symbol guessing; upstream aliases reuse each operator's own diff --git a/docs/cross-repo-handoff.md b/docs/cross-repo-handoff.md index 9f88418..86ae1ff 100644 --- a/docs/cross-repo-handoff.md +++ b/docs/cross-repo-handoff.md @@ -17,7 +17,7 @@ python tools/portable_parity.py # -> output/meos-portable-parity.json (bare-nam | Artifact | Contents | |---|---| -| `meos-idl.json#/portableAliases` | canonical operator→bare-name dialect: `byOperator`, `byBareName`, `families`, `explicitBacking`, `scope`; the position operators' names by class: `positionFamilies`, `byPositionOperator`, `positionNames` (operator → class → SQL name) | +| `meos-idl.json#/portableAliases` | canonical operator→bare-name dialect: `byOperator`, `byBareName`, `families`, `explicitBacking`, `scope`; the position and topological operators' names by class: `positionFamilies`, `byPositionOperator`, `positionNames` (operator → class → SQL name) | | `meos-idl.json#/functions[].{network,wire,api}` | per-function projectability + decode/encode/array/out-param wire model | | `meos-idl.json#/temporalTypes` | per `Temporal`: its `base`, its `bbox`, the `mfjson` type token `asMFJSON` writes, and the `number` / `spatial` / `linear` classes | | `meos-idl.json#/typeRelations/byBase` | each base type's `set`, `span`, `spanset` and its `temporal` types, the last a list since a base carries several | @@ -28,12 +28,12 @@ python tools/portable_parity.py # -> output/meos-portable-parity.json (bare-nam ## What each consumer produces **MobilityDuck, MobilitySpark** — register the **exact bare names** from -`portableAliases.byOperator` (drop type-qualified forms like -`spanOverlaps`). Done = every operator in `byOperator` is callable by its -bare name, parity-checked with the same prefix logic as -`portable_parity.py`, **0 unbacked**. A position operator registers its -names by class from `portableAliases.positionNames` (`stboxLeft`, -`tboxBefore`), which are the `sqlfn` of its functions. +`portableAliases.byOperator`. Done = every operator in `byOperator` is +callable by its bare name, parity-checked with the same prefix logic as +`portable_parity.py`, **0 unbacked**. A position or a topological operator +registers its names by class from `portableAliases.positionNames` +(`stboxLeft`, `tboxBefore`, `spanOverlaps`, `stboxContains`), which are the +`sqlfn` of its functions. **PyMEOS, JMEOS, MEOS.NET** — code-generate from `meos-idl.json` (`functions` + `portableAliases`) so every binding emits **identical** bare diff --git a/docs/portable-aliases.md b/docs/portable-aliases.md index 26cce47..3e1d115 100644 --- a/docs/portable-aliases.md +++ b/docs/portable-aliases.md @@ -16,38 +16,41 @@ family, and is **type-agnostic** (it applies to every temporal type): | Family | Operator → bare name | |---|---| -| Topology | `&&`→`overlaps` `@>`→`contains` `<@`→`contained` `-\|-`→`adjacent` | | Temporal comparison | `#=`→`tEqual` `#<>`→`tNotEqual` `#<`→`tLessThan` `#<=`→`tLessEqual` `#>`→`tGreaterThan` `#>=`→`tGreaterEqual` | | Ever comparison | `?=`→`eEqual` `?<>`→`eNotEqual` `?<`→`eLessThan` `?<=`→`eLessEqual` `?>`→`eGreaterThan` `?>=`→`eGreaterEqual` | | Always comparison | `%=`→`aEqual` `%<>`→`aNotEqual` `%<`→`aLessThan` `%<=`→`aLessEqual` `%>`→`aGreaterThan` `%>=`→`aGreaterEqual` | | Distance | `<->`→`tDistance` `\|=\|`→`nearestApproachDistance` | | Same | `~=`→`same` | -25 operator→bare-name pairs. Already-canonical (no aliasing needed): +21 operator→bare-name pairs. Already-canonical (no aliasing needed): `eIntersects`, `atTime`, restriction and spatial-relationship functions. -## Position operators +## Position and topological operators -A position operator has one name per class of its operands instead: the -class followed by the position, a temporal operand taking the class of its -bounding box (MobilityDB#2717). `<<` is `setLeft`, `spanLeft`, -`spansetLeft`, `tboxLeft`, `stboxLeft` and `tpcboxLeft`; the Y and Z -positions exist for `stbox` and `tpcbox`. The mapping holds each operator -with its position, the stem those names and the MEOS C functions share -(`left_set_set`, `left_tspatial_tspatial`), under `positionFamilies`: +A position or a topological operator has one name per class of its operands +instead: the class followed by the stem, a temporal operand taking the class +of its bounding box. `<<` is `setLeft`, `spanLeft`, `spansetLeft`, +`tboxLeft`, `stboxLeft` and `tpcboxLeft` (MobilityDB#2717); the Y and Z +positions exist for `stbox` and `tpcbox`. `&&` is `setOverlaps`, +`spanOverlaps`, `spansetOverlaps`, `tboxOverlaps`, `stboxOverlaps` and +`tpcboxOverlaps` (MobilityDB#2930). The mapping holds each operator with its +stem, which those names and the MEOS C functions share (`left_set_set`, +`overlaps_tspatial_tspatial`), under `positionFamilies`, where the key +`position` names the stem: -| Family | Operator → position | +| Family | Operator → stem | |---|---| +| Topology | `&&`→`overlaps` `@>`→`contains` `<@`→`contained` `-\|-`→`adjacent` | | Time position | `<<#`→`before` `#>>`→`after` `&<#`→`overbefore` `#&>`→`overafter` | | Space X | `<<`→`left` `>>`→`right` `&<`→`overleft` `&>`→`overright` | | Space Y | `<<\|`→`below` `\|>>`→`above` `&<\|`→`overbelow` `\|&>`→`overabove` | | Space Z | `<>`→`back` `&`→`overback` | The catalog derives the names by class from the `@sqlfn` tags of the -functions whose `@sqlop` is the operator (`positionNames`, 64 names for the -16 operators). A name that is not the class followed by the position, or an -operator that no function carries, stops the pipeline: either means the -tags and the mapping disagree. +functions whose `@sqlop` is the operator (`positionNames`, 88 names for the +20 operators over MobilityDB `96d665686e`). A name that is not the class +followed by the stem, or an operator that no function carries, stops the +pipeline: either means the tags and the mapping disagree. ## In the catalog @@ -56,12 +59,14 @@ plus derived lookups for codegen: ```json "portableAliases": { - "byOperator": { "&&": "overlaps", "#=": "tEqual", "~=": "same", ... }, - "byBareName": { "overlaps": "&&", "tEqual": "#=", "same": "~=", ... }, + "byOperator": { "#=": "tEqual", "<->": "tDistance", "~=": "same", ... }, + "byBareName": { "tEqual": "#=", "tDistance": "<->", "same": "~=", ... }, "bareNames": ["aEqual", "aGreaterEqual", ..., "tLessEqual", "tLessThan", "tNotEqual"], - "count": 25, - "byPositionOperator": { "<<": "left", "<<#": "before", ... }, + "count": 21, + "byPositionOperator": { "&&": "overlaps", "<<": "left", "<<#": "before", ... }, "positionNames": { + "&&": { "set": "setOverlaps", "span": "spanOverlaps", "spanset": "spansetOverlaps", + "stbox": "stboxOverlaps", "tbox": "tboxOverlaps", "tpcbox": "tpcboxOverlaps" }, "<<": { "set": "setLeft", "span": "spanLeft", "spanset": "spansetLeft", "stbox": "stboxLeft", "tbox": "tboxLeft", "tpcbox": "tpcboxLeft" }, "<<|": { "stbox": "stboxBelow", "tpcbox": "tpcboxBelow" }, ... @@ -94,8 +99,9 @@ end state. `portable_parity.py` is the meos-api.json analogue of MobilityDB's `tools/portable_aliases/generate.py --check`: it cross-references every bare name against the catalog's function families (by the MEOS bare-name -prefix convention), and every position operator against the family its -position prefixes (`left_*`, `before_*`) with its SQL names by class, and +prefix convention), and every position or topological operator against the +family its stem prefixes (`left_*`, `before_*`, `overlaps_*`) with its SQL +names by class, and writes `output/meos-portable-parity.json`. A bare name whose C family prefix differs is backed by the functions whose `@sqlfn` is the bare name (`tEqual` by the `teq_*` family, `eEqual` by `ever_eq_*`, `tDistance` by diff --git a/meta/portable-aliases.json b/meta/portable-aliases.json index 8272344..41a79f1 100644 --- a/meta/portable-aliases.json +++ b/meta/portable-aliases.json @@ -1,5 +1,5 @@ { - "_comment": "Canonical portable bare-name dialect \u2014 the single codegen source of truth (RFC #920). Every binding/engine generates the SAME bare names from this mapping so users learn one reference and assume the rest. Operators are SQL operator symbols; bareName is the portable function name. The mapping is type-agnostic: it applies to EVERY temporal type family. A position operator (`positionFamilies`) has instead one name per class of its operands.", + "_comment": "Canonical portable bare-name dialect \u2014 the single codegen source of truth (RFC #920). Every binding/engine generates the SAME bare names from this mapping so users learn one reference and assume the rest. Operators are SQL operator symbols; bareName is the portable function name. The mapping is type-agnostic: it applies to EVERY temporal type family. An operator of `positionFamilies`, a position or a topological operator, has instead one name per class of its operands.", "provenance": { "discussion": "MobilityDB#861", "rfc": "MobilityDB RFC #920 (doc/rfc/sql-portability/README.md, branch rfc/sql-portability)", @@ -7,24 +7,6 @@ "manualChapter": "MobilityDB#1078" }, "families": { - "topology": [ - { - "operator": "&&", - "bareName": "overlaps" - }, - { - "operator": "@>", - "bareName": "contains" - }, - { - "operator": "<@", - "bareName": "contained" - }, - { - "operator": "-|-", - "bareName": "adjacent" - } - ], "temporalComparison": [ { "operator": "#=", "bareName": "tEqual" }, { "operator": "#<>", "bareName": "tNotEqual" }, @@ -66,8 +48,14 @@ } ] }, - "_positionFamiliesComment": "A position operator has one SQL name per class of its operands, the class followed by the position: `<<` is setLeft, spanLeft, spansetLeft, tboxLeft, stboxLeft and tpcboxLeft (MobilityDB#2717). A temporal operand takes the class of its bounding box. `position` is the stem those names and the MEOS C functions share (left_set_set, left_tspatial_tspatial). The catalog derives the names by class from the @sqlfn tags of the functions whose @sqlop is the operator (`portableAliases.positionNames`).", + "_positionFamiliesComment": "A position or a topological operator has one SQL name per class of its operands, the class followed by the stem: `<<` is setLeft, spanLeft, spansetLeft, tboxLeft, stboxLeft and tpcboxLeft (MobilityDB#2717), `&&` is setOverlaps, spanOverlaps, tboxOverlaps, stboxOverlaps and tpcboxOverlaps (MobilityDB#2930). A temporal operand takes the class of its bounding box. `position` is the stem those names and the MEOS C functions share (left_set_set, overlaps_tspatial_tspatial). The catalog derives the names by class from the @sqlfn tags of the functions whose @sqlop is the operator (`portableAliases.positionNames`).", "positionFamilies": { + "topology": [ + { "operator": "&&", "position": "overlaps" }, + { "operator": "@>", "position": "contains" }, + { "operator": "<@", "position": "contained" }, + { "operator": "-|-", "position": "adjacent" } + ], "timePosition": [ { "operator": "<<#", "position": "before" }, { "operator": "#>>", "position": "after" }, @@ -125,6 +113,6 @@ "notes": [ "Generate aliases by reusing each operator's own backing C function (equivalence by construction), never by reimplementing; mirror MobilityDB tools/portable_aliases/generate.py + its 100%-coverage audit.", "User-facing API uses the full name `trgeometry`; internal functions keep the `trgeo_` prefix \u2014 do NOT normalize the internal prefix.", - "Goal: 100% parity ecosystem-wide \u2014 every operator has its portable name (its bare name, or its name by class for a position operator) on every engine, no gaps, no headline exclusions." + "Goal: 100% parity ecosystem-wide \u2014 every operator has its portable name (its bare name, or its name by class for a position or topological operator) on every engine, no gaps, no headline exclusions." ] } diff --git a/meta/portable-aliases.schema.json b/meta/portable-aliases.schema.json index 54500db..2cda999 100644 --- a/meta/portable-aliases.schema.json +++ b/meta/portable-aliases.schema.json @@ -43,7 +43,7 @@ "positionFamilies": { "type": "object", - "description": "Position operators, whose SQL names are the class of their operands followed by the position (setLeft … stboxLeft). `position` is the stem the names and the MEOS C functions share; the catalog derives the names by class.", + "description": "Position and topological operators, whose SQL names are the class of their operands followed by the stem (setLeft … stboxLeft, setOverlaps … stboxOverlaps). `position` is the stem the names and the MEOS C functions share; the catalog derives the names by class.", "minProperties": 1, "additionalProperties": { "type": "array", diff --git a/parser/portable.py b/parser/portable.py index c794f28..1757328 100644 --- a/parser/portable.py +++ b/parser/portable.py @@ -3,9 +3,10 @@ `meta/portable-aliases.json` is the curated, authoritative operator → bare-name mapping (RFC #920; native in MobilityDB via PR #1075). Folding it into the catalog means every binding/engine generates the *identical* bare -names, so a user learns one reference and assumes the rest. A position -operator has one name per class instead (setLeft … stboxLeft), derived from -the catalog's @sqlfn/@sqlop tags by ``attach_position_names``. +names, so a user learns one reference and assumes the rest. A position or a +topological operator has one name per class instead (setLeft … stboxLeft, +setOverlaps … stboxOverlaps), derived from the catalog's @sqlfn/@sqlop tags by +``attach_position_names``. This is curated canonical data, not a heuristic — it is preserved verbatim and only *derived* lookups are added (no guessing of C symbols: upstream @@ -32,9 +33,9 @@ def attach_portable_aliases(idl: dict, path: Path) -> dict: if len(by_operator) != len(pairs) or len(by_bare_name) != len(pairs): raise ValueError("portable-aliases: duplicate operator or bareName") - # A position operator has one SQL name per class of its operands (setLeft … - # stboxLeft), so it has no bare name: it is absent from the families above, and - # its operator and position are each unique. + # A position or a topological operator has one SQL name per class of its operands + # (setLeft … stboxLeft, setOverlaps … stboxOverlaps), so it has no bare name: it is + # absent from the families above, and its operator and stem are each unique. positions = [p for fam in data["positionFamilies"].values() for p in fam] by_position_operator = {p["operator"]: p["position"] for p in positions} if (len(by_position_operator) != len(positions) @@ -50,26 +51,28 @@ def attach_portable_aliases(idl: dict, path: Path) -> dict: "explicitBacking": data.get("explicitBacking", {}), "scope": data["scope"], # cbuffer/npoint/pose/rgeo in scope "notes": data["notes"], - "byOperator": by_operator, # "&&" -> "overlaps" - "byBareName": by_bare_name, # "overlaps" -> "&&" + "byOperator": by_operator, # "#=" -> "tEqual" + "byBareName": by_bare_name, # "tEqual" -> "#=" "bareNames": sorted(by_bare_name), "count": len(pairs), - "byPositionOperator": by_position_operator, # "<<" -> "left" + "byPositionOperator": by_position_operator, # "<<" -> "left", "&&" -> "overlaps" } return idl def attach_position_names(idl: dict) -> dict: - """Derive the SQL names of each position operator, by class. - - A position operator has one SQL name per class of its operands, the class - followed by the position: ``<<`` is setLeft, spanLeft, spansetLeft, tboxLeft, - stboxLeft and tpcboxLeft (MobilityDB#2717), a temporal operand taking the class of - its bounding box. The names are the ``@sqlfn`` tags of the functions whose - ``@sqlop`` is the operator, and the class is the name less its position. Adds - ``portableAliases.positionNames``: operator -> class -> SQL name. - - A name that does not end with its operator's position, or an operator that no + """Derive the SQL names of each position or topological operator, by class. + + A position or a topological operator has one SQL name per class of its operands, + the class followed by the stem: ``<<`` is setLeft, spanLeft, spansetLeft, tboxLeft, + stboxLeft and tpcboxLeft (MobilityDB#2717), ``&&`` is setOverlaps, spanOverlaps, + spansetOverlaps, tboxOverlaps, stboxOverlaps and tpcboxOverlaps (MobilityDB#2930), + a temporal operand taking the class of its bounding box. The names are the + ``@sqlfn`` tags of the functions whose ``@sqlop`` is the operator, and the class is + the name less its stem. Adds ``portableAliases.positionNames``: operator -> class + -> SQL name. + + A name that does not end with its operator's stem, or an operator that no function carries, raises: either means the @sqlfn/@sqlop tags and the mapping disagree, which a binding would otherwise inherit silently. @@ -103,20 +106,22 @@ def attach_position_names(idl: dict) -> dict: 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 + """Mark the shared bounding-box BACKING ``@sqlfn`` tags of a bare name. + + A function behind an operator with a bare name (``~=``, ``same``) may carry a + SHARED C ``@sqlfn`` tag named ``_bbox`` (same_bbox). That tag is NEVER emitted + as a ``CREATE FUNCTION`` — the deployed, user-facing SQL name is the operator's bare + portable alias. 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. + binding uniformly registers the bare name + operator and drops the ``_bbox`` tag. A + topological operator (``&&``, ``@>``, ``<@``, ``-|-``) has names by class + (MobilityDB#2930): its ``@sqlfn`` tags are its public names, and a ``_bbox`` tag on + it is refused by #attach_position_names. 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. + convention AND the operator→bareName map. ``publicSqlName`` is defined for every + record flagged, since only an operator of that map is flagged. MUST run AFTER ``attach_sqlfn_map`` (sqlfn/sqlop) AND ``attach_portable_aliases`` (byOperator) — it reads all three. diff --git a/run.py b/run.py index bf8fd33..736978b 100644 --- a/run.py +++ b/run.py @@ -281,8 +281,8 @@ def main(): 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 - # (same_bbox/contains_bbox/…) so bindings register the bare public name, not the + # (step 3) are attached, classify the shared bounding-box BACKING tags of a bare + # name (same_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")) @@ -299,12 +299,13 @@ def main(): raise ValueError("index searches that do not resolve to one operator each:\n " + "\n ".join(idx_errors)) print(f" signatures stating their index search: {nidx}", file=sys.stderr) - # The same map names each position operator once per class (setLeft … - # stboxLeft): derive those names from its @sqlfn/@sqlop tags. + # The same map names each position or topological operator once per class + # (setLeft … stboxLeft, setOverlaps … stboxOverlaps): derive those names from + # its @sqlfn/@sqlop tags. idl = attach_position_names(idl) pn = idl.get("portableAliases", {}).get("positionNames", {}) print(f" Derived {sum(len(c) for c in pn.values())} position SQL " - f"name(s) by class for {len(pn)} position operator(s) " + f"name(s) by class for {len(pn)} position or topological operator(s) " f"(positionNames)", file=sys.stderr) # A PG wrapper can BIND a MEOS input to a fixed literal instead of exposing diff --git a/tests/test_portable.py b/tests/test_portable.py index 5ab5685..74fe778 100644 --- a/tests/test_portable.py +++ b/tests/test_portable.py @@ -20,11 +20,11 @@ MAP = ROOT / "meta" / "portable-aliases.json" SCHEMA = ROOT / "meta" / "portable-aliases.schema.json" _EXPECTED_FAMILY_SIZES = { - "topology": 4, "temporalComparison": 6, "everComparison": 6, + "temporalComparison": 6, "everComparison": 6, "alwaysComparison": 6, "distance": 2, "same": 1, } _EXPECTED_POSITION_SIZES = { - "timePosition": 4, "spaceX": 4, "spaceY": 4, "spaceZ": 4, + "topology": 4, "timePosition": 4, "spaceX": 4, "spaceY": 4, "spaceZ": 4, } @@ -37,8 +37,8 @@ def _attach_map(data): def _stbox_position_functions(skip=()): - """One stbox function per position operator, named as MobilityDB names it - (`left_stbox_stbox`, @sqlfn stboxLeft, @sqlop <<).""" + """One stbox function per position or topological operator, named as MobilityDB + names it (`left_stbox_stbox`, @sqlfn stboxLeft, @sqlop <<).""" d = json.loads(MAP.read_text()) return [{"name": f"{p['position']}_stbox_stbox", "sqlfn": "stbox" + p["position"][0].upper() + p["position"][1:], @@ -66,24 +66,25 @@ def test_position_families_complete_and_sized(self): def test_known_mappings_verbatim(self): flat = {p["operator"]: p["bareName"] for fam in self.d["families"].values() for p in fam} - for op, bn in [("&&", "overlaps"), ("@>", "contains"), - ("-|-", "adjacent"), ("#=", "tEqual"), + for op, bn in [("#=", "tEqual"), ("#<>", "tNotEqual"), ("?=", "eEqual"), ("%=", "aEqual"), ("|=|", "nearestApproachDistance"), ("~=", "same")]: self.assertEqual(flat[op], bn) - self.assertEqual(sum(_EXPECTED_FAMILY_SIZES.values()), 25) - self.assertEqual(len(flat), 25) + self.assertEqual(sum(_EXPECTED_FAMILY_SIZES.values()), 21) + self.assertEqual(len(flat), 21) def test_known_positions_verbatim(self): flat = {p["operator"]: p["position"] for fam in self.d["positionFamilies"].values() for p in fam} - for op, pos in [("<<#", "before"), ("#&>", "overafter"), + for op, pos in [("&&", "overlaps"), ("@>", "contains"), + ("<@", "contained"), ("-|-", "adjacent"), + ("<<#", "before"), ("#&>", "overafter"), ("<<", "left"), ("&>", "overright"), ("|&>", "overabove"), ("/&>", "overback")]: self.assertEqual(flat[op], pos) - self.assertEqual(sum(_EXPECTED_POSITION_SIZES.values()), 16) - self.assertEqual(len(flat), 16) - # a position operator has names by class, never a bare name + self.assertEqual(sum(_EXPECTED_POSITION_SIZES.values()), 20) + self.assertEqual(len(flat), 20) + # a position or topological operator has names by class, never a bare name bare_ops = {p["operator"] for fam in self.d["families"].values() for p in fam} self.assertFalse(set(flat) & bare_ops) @@ -146,22 +147,25 @@ class AttachTests(unittest.TestCase): def test_attach_and_derive(self): idl = attach_portable_aliases({"functions": []}, MAP) pa = idl["portableAliases"] - self.assertEqual(pa["count"], 25) - self.assertEqual(pa["byOperator"]["&&"], "overlaps") - self.assertEqual(pa["byBareName"]["overlaps"], "&&") + self.assertEqual(pa["count"], 21) + self.assertEqual(pa["byOperator"]["#="], "tEqual") + self.assertEqual(pa["byBareName"]["tEqual"], "#=") self.assertEqual(pa["bareNames"], sorted(pa["byBareName"])) - # bijective: 25 distinct operators and 25 distinct bare names - self.assertEqual(len(pa["byOperator"]), 25) - self.assertEqual(len(pa["byBareName"]), 25) + # bijective: 21 distinct operators and 21 distinct bare names + self.assertEqual(len(pa["byOperator"]), 21) + self.assertEqual(len(pa["byBareName"]), 21) self.assertIn("cbuffer", pa["scope"]["inScopeTypeFamilies"]) self.assertEqual(pa["explicitBacking"], {"nearestApproachDistance": ["nad"]}) - # the 16 position operators, with their position, and no bare name + # the 20 operators named by class, with their stem, and no bare name self.assertEqual(pa["byPositionOperator"]["<<"], "left") self.assertEqual(pa["byPositionOperator"]["<<#"], "before") - self.assertEqual(len(pa["byPositionOperator"]), 16) - self.assertNotIn("<<", pa["byOperator"]) - self.assertNotIn("left", pa["byBareName"]) + self.assertEqual(pa["byPositionOperator"]["&&"], "overlaps") + self.assertEqual(pa["byPositionOperator"]["-|-"], "adjacent") + self.assertEqual(len(pa["byPositionOperator"]), 20) + for op, name in (("<<", "left"), ("&&", "overlaps"), ("@>", "contains")): + self.assertNotIn(op, pa["byOperator"]) + self.assertNotIn(name, pa["byBareName"]) self.assertEqual(set(pa["positionFamilies"]), set(_EXPECTED_POSITION_SIZES)) @@ -170,9 +174,11 @@ def test_missing_file_is_noop(self): 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. + # A function behind an operator with a bare name may carry 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. A topological operator has names by class, so its tag is not a + # backing tag of a bare name. idl = attach_portable_aliases({"functions": [ {"name": "Same_stbox_stbox", "sqlfn": "same_bbox", "sqlop": "~="}, {"name": "Contains_tbox_tnumber", "sqlfn": "contains_bbox", "sqlop": "@>"}, @@ -181,11 +187,11 @@ def test_backing_sqlfn_classification(self): ]}, 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 + # the _bbox backing tag of a bare name is flagged with that 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 topological operator has no bare name to stand for + self.assertNotIn("sqlfnBackingOnly", by["Contains_tbox_tnumber"]) # a position op whose @sqlfn IS the deployed name (stboxLeft) is untouched self.assertNotIn("sqlfnBackingOnly", by["Left_stbox_stbox"]) # a plain function with no operator is untouched @@ -198,19 +204,25 @@ def test_backing_sqlfn_noop_without_aliases(self): self.assertNotIn("sqlfnBackingOnly", idl["functions"][0]) def test_position_names_by_class(self): - # Each position operator's names by class come from the @sqlfn of the - # functions whose @sqlop it is; the class is the name less the position. + # Each position or topological operator's names by class come from the + # @sqlfn of the functions whose @sqlop it is; the class is the name less + # the stem. fns = _stbox_position_functions() + [ {"name": "left_set_set", "sqlfn": "setLeft", "sqlop": "<<"}, {"name": "left_tnumber_tnumber", "sqlfn": "tboxLeft", "sqlop": "<<"}, {"name": "left_tbox_tbox", "sqlfn": "tboxLeft", "sqlop": "<<"}, + {"name": "contains_set_value", "sqlfn": "setContains", "sqlop": "@>"}, + {"name": "overlaps_span_span", "sqlfn": "spanOverlaps", "sqlop": "&&"}, {"name": "same_stbox_stbox", "sqlfn": "same_bbox", "sqlop": "~="}, ] idl = attach_portable_aliases({"functions": fns}, MAP) pn = attach_position_names(idl)["portableAliases"]["positionNames"] - self.assertEqual(len(pn), 16) + self.assertEqual(len(pn), 20) self.assertEqual(pn["<<"], {"set": "setLeft", "stbox": "stboxLeft", "tbox": "tboxLeft"}) + self.assertEqual(pn["&&"], {"span": "spanOverlaps", "stbox": "stboxOverlaps"}) + self.assertEqual(pn["@>"], {"set": "setContains", "stbox": "stboxContains"}) + self.assertEqual(pn["-|-"], {"stbox": "stboxAdjacent"}) self.assertEqual(pn["<<#"], {"stbox": "stboxBefore"}) self.assertEqual(pn["&Left: the tags and the # mapping disagree, which must fail loudly rather than reach a binding. - for sqlfn in ("temporal_left", "Left"): + for sqlfn, op in (("temporal_left", "<<"), ("Left", "<<"), + ("contains_bbox", "@>"), ("overlaps", "&&")): fns = _stbox_position_functions() + [ - {"name": "left_temporal_temporal", "sqlfn": sqlfn, "sqlop": "<<"}] + {"name": "x_temporal_temporal", "sqlfn": sqlfn, "sqlop": op}] idl = attach_portable_aliases({"functions": fns}, MAP) with self.assertRaises(ValueError): attach_position_names(idl) diff --git a/tests/test_portable_parity.py b/tests/test_portable_parity.py index 193293f..5a79977 100644 --- a/tests/test_portable_parity.py +++ b/tests/test_portable_parity.py @@ -31,7 +31,7 @@ def _catalog(fns): class ParityLogicTests(unittest.TestCase): def test_backed_vs_needs_explicit(self): cat = _catalog([ - "overlaps_span_span", "overlaps_tbox_tbox", # backs `overlaps` + "overlaps_span_span", "overlaps_tbox_tbox", # back `&&` {"name": "teq_temporal_temporal", # backs `tEqual` "sqlfn": "tEqual"}, # by its SQL name {"name": "ever_eq_temporal_temporal"}, # no SQL name: backs @@ -44,9 +44,6 @@ def test_backed_vs_needs_explicit(self): ]) r = build_parity(cat) self.assertEqual(r["total"], 41) - self.assertEqual(r["byBareName"]["overlaps"]["status"], "backed") - self.assertEqual(r["byBareName"]["overlaps"]["via"], "prefix") - self.assertEqual(r["byBareName"]["overlaps"]["backedBy"], 2) self.assertEqual(r["byBareName"]["same"]["status"], "backed") # different C prefix -> resolved via the *verified* explicit map, # not a fake verdict and not a false gap @@ -54,7 +51,6 @@ def test_backed_vs_needs_explicit(self): self.assertEqual(nad["status"], "backed") self.assertEqual(nad["via"], "explicit") self.assertNotIn("nearestApproachDistance", r["unbacked"]) - self.assertEqual(r["byBareName"]["overlaps"]["family"], "topology") self.assertEqual(r["byBareName"]["tEqual"]["operator"], "#=") # a C family whose prefix differs is backed by the functions carrying # the bare name as their @sqlfn, and only by those @@ -73,6 +69,14 @@ def test_backed_vs_needs_explicit(self): self.assertIn("<<#", r["unbackedPositions"]) self.assertNotIn("<<", r["unbackedPositions"]) self.assertNotIn("left", r["byBareName"]) + # a topological operator is named by class too, and backed the same way + overlaps = r["byPosition"]["&&"] + self.assertEqual(overlaps["status"], "backed") + self.assertEqual(overlaps["via"], "prefix") + self.assertEqual(overlaps["backedBy"], 2) + self.assertEqual(overlaps["family"], "topology") + self.assertIn("@>", r["unbackedPositions"]) + self.assertNotIn("overlaps", r["byBareName"]) def test_position_sql_names_reported(self): # With the catalog's positionNames derived, each position operator @@ -90,8 +94,8 @@ def test_every_bare_name_classified(self): self.assertEqual(r["total"], 41) self.assertEqual(r["backed"], 0) self.assertEqual(r["needsExplicitBacking"], 41) - self.assertEqual(len(r["unbacked"]), 25) # all flagged, 0 dropped - self.assertEqual(len(r["unbackedPositions"]), 16) + self.assertEqual(len(r["unbacked"]), 21) # all flagged, 0 dropped + self.assertEqual(len(r["unbackedPositions"]), 20) self.assertTrue(all(v["status"] in ("backed", "needs-explicit-backing") for v in list(r["byBareName"].values()) diff --git a/tools/portable_parity.py b/tools/portable_parity.py index ce14be8..950afd0 100644 --- a/tools/portable_parity.py +++ b/tools/portable_parity.py @@ -5,7 +5,7 @@ # python tools/portable_parity.py # -> output/meos-portable-parity.json # # For every canonical bare name it reports the catalog functions that back it: by the MEOS bare-name prefix convention -# (`overlaps_*`, `same_*`, …), else by the functions whose @sqlfn IS the bare +# (`same_*`, …), else by the functions whose @sqlfn IS the bare # name (`tEqual` on the `teq_*` family, `eEqual` on `ever_eq_*`: the SQL name # MobilityDB declares for them), else by a verified `explicitBacking` prefix. # A bare name none of them resolves is **not** asserted to be an API gap: it @@ -13,10 +13,11 @@ # explicit operator→C-family entry — an honest signal, never a fabricated # verdict. # -# A position operator has no bare name: its SQL names are one per class -# (`setLeft` … `stboxLeft`, the catalog's `positionNames`), and its MEOS C -# functions share its position as their prefix (`left_*`, `before_*`), which is -# what backs it. The report lists it under `byPosition`, keyed by operator. +# A position or a topological operator has no bare name: its SQL names are one +# per class (`setLeft` … `stboxLeft`, `setOverlaps` … `stboxOverlaps`, the +# catalog's `positionNames`), and its MEOS C functions share its stem as their +# prefix (`left_*`, `before_*`, `overlaps_*`), which is what backs it. The report +# lists it under `byPosition`, keyed by operator. import json import sys