From 04477416cf959f3f3182cde2f7922900fc6c0007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Sat, 10 Oct 2026 20:41:35 +0200 Subject: [PATCH] Give each topological operator its SQL names by class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The topological operators &&, @>, <@ and -|- have one SQL name per class of their operands, as the position operators have: MobilityDB names the functions behind them by the class they compare, && being setOverlaps, spanOverlaps, spansetOverlaps, tboxOverlaps, stboxOverlaps and tpcboxOverlaps, a temporal operand taking the class of its bounding box. No operator of MobilityDB master is backed by a function named overlaps, contains, contained or adjacent: its one contains, contains(geometry, geometry), is the spatial relationship, which carries no operator and which Spark and Flink call geoContains. The mapping holds them under positionFamilies as the family topology, each operator with its stem overlaps, contains, contained or adjacent, and not as bare names under families, so byOperator and byBareName carry 21 pairs and byPositionOperator 20 operators. attach_position_names derives their names from the @sqlfn tags of the functions whose @sqlop is the operator, as it derives the position names, and stops the pipeline on a tag that is not the class followed by the stem, such as a shared contains_bbox tag. classify_backing_sqlfn flags the shared backing tag of a bare name, which ~= alone has; MobilityDB master carries no such tag. The parity audit lists the four under byPosition, backed by the MEOS functions their stem prefixes (overlaps_*, contains_*), and the documentation, the schema description and the cross-repository handoff state the topological operators beside the position ones. Measured over MobilityDB master 96d665686e: run.py derives 88 names by class for the 20 operators, 24 of them topological: setOverlaps … tpcboxOverlaps, the seven @> names including tjsonbContains, the six <@ names and the five -|- names, each one a function MobilityDB master's .in.sql files declare. portable_parity.py reads 41 of 41 operators backed. The suite collects 534 tests, the portable ones pass with none skipped, and the floor of .github/workflows/pytest.yml stays 534. JMEOS's Spark UDF surface, generated from this catalog, registers none of the bare overlaps, contains, contained and adjacent, of which contains would replace Spark's own function; its typed Spark and Flink SQL surfaces are unchanged. Why: a binding registers what the catalog states, and the catalog states the names MobilityDB declares for these operators. --- README.md | 5 +- docs/cross-repo-handoff.md | 14 +++--- docs/portable-aliases.md | 48 +++++++++++-------- meta/portable-aliases.json | 30 ++++-------- meta/portable-aliases.schema.json | 2 +- parser/portable.py | 65 +++++++++++++------------ run.py | 11 +++-- tests/test_portable.py | 79 ++++++++++++++++++------------- tests/test_portable_parity.py | 18 ++++--- tools/portable_parity.py | 11 +++-- 10 files changed, 151 insertions(+), 132 deletions(-) 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