Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/portable-parity.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: portable-aliases parity

# Gates the portable bare-name dialect (RFC #920; MEOS-API cross-repo
# handoff PR #9): the generated MEOS.NET symbol set must remain a superset
# of portableAliases.bareNames — 29/29, 0 unbacked, all six in-scope
# type families — so a regenerated binding can never silently drop a
# canonical bare name.

on:
push:
pull_request:

jobs:
parity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Parity gate (script — generated symbol set ⊇ bareNames)
run: python3 tools/portable_parity.py --check

- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'

- name: Parity gate (.NET test — language-independent mirror)
run: >
dotnet test MEOS.NET.Tests/MEOS.NET.Tests.csproj
--filter "FullyQualifiedName~PortableAliasParityTests"
-c Release
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,8 @@ ASALocalRun/
.mfractor/

# Local History for Visual Studio
.localhistory/
.localhistory/

# Generated portable-aliases parity report (regenerated by the CI gate and
# the .NET parity test; the proof is the gate, not a checked-in artifact)
tools/portable-parity.report.json
136 changes: 136 additions & 0 deletions MEOS.NET.Tests/PortableAliasParityTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
using System.Text.Json;
using System.Text.RegularExpressions;

namespace MEOS.NET.Tests
{
/// <summary>
/// Portable bare-name parity gate (RFC #920; MEOS-API cross-repo handoff
/// PR #9). A binding is done when its <b>generated symbol set ⊇
/// portableAliases.bareNames</b>, verified with the same prefix logic as
/// MEOS-API portable_parity.py — a bare name is backed iff some emitted
/// MEOS symbol <c>== bareName</c> or <c>startsWith(bareName + "_")</c>,
/// falling back to the verified <c>explicitBacking</c> prefixes
/// (<c>nearestApproachDistance</c> ← the <c>nad_*</c> family). 0 unbacked,
/// no per-binding exceptions, across all six in-scope type families
/// (cbuffer/npoint/pose/rgeo are full user-facing types — never excluded
/// from the parity headline). This is the C# mirror of
/// tools/portable_parity.py, so the verdict is identical and
/// language-independent.
/// </summary>
[TestClass]
public class PortableAliasParityTests
{
private static readonly string[] InScopeFamilies =
{ "temporal", "geo", "cbuffer", "npoint", "pose", "rgeo" };

private static string RepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir is not null &&
!File.Exists(Path.Combine(dir.FullName, "MEOS.NET.sln")))
dir = dir.Parent;
Assert.IsNotNull(dir, "Could not locate repo root (MEOS.NET.sln).");
return dir!.FullName;
}

private static HashSet<string> GeneratedSymbols(string repo)
{
var cs = Path.Combine(repo, "MEOS.NET", "Internal",
"MEOSExternalFunctions.cs");
Assert.IsTrue(File.Exists(cs),
$"Generated bindings missing: {cs}");
var rx = new Regex(@"public\s+static\s+partial\s+\S+\s+([A-Za-z_]\w*)\s*\(");
return rx.Matches(File.ReadAllText(cs))
.Select(m => m.Groups[1].Value)
.ToHashSet(StringComparer.Ordinal);
}

private static (List<(string op, string bare, string fam)> pairs,
Dictionary<string, string[]> explicitBacking)
Contract(string repo)
{
var json = Path.Combine(repo, "tools", "portable-aliases.json");
Assert.IsTrue(File.Exists(json),
$"Vendored portable-aliases SoT missing: {json}");
using var doc = JsonDocument.Parse(File.ReadAllText(json));
var root = doc.RootElement;

var pairs = new List<(string, string, string)>();
foreach (var fam in root.GetProperty("families").EnumerateObject())
foreach (var e in fam.Value.EnumerateArray())
pairs.Add((e.GetProperty("operator").GetString()!,
e.GetProperty("bareName").GetString()!,
fam.Name));

var explicitBacking = new Dictionary<string, string[]>();
if (root.TryGetProperty("explicitBacking", out var eb))
foreach (var p in eb.EnumerateObject())
explicitBacking[p.Name] =
p.Value.EnumerateArray()
.Select(x => x.GetString()!).ToArray();

return (pairs, explicitBacking);
}

private static List<string> Backing(string bare,
HashSet<string> symbols, Dictionary<string, string[]> explicitBacking)
{
bool M(string s, string p) => s == p || s.StartsWith(p + "_",
StringComparison.Ordinal);
var hits = symbols.Where(s => M(s, bare)).ToList();
if (hits.Count == 0 &&
explicitBacking.TryGetValue(bare, out var prefixes))
foreach (var pref in prefixes)
hits.AddRange(symbols.Where(s => M(s, pref)));
return hits;
}

private static string FamilyOf(string name)
{
var n = name.ToLowerInvariant();
if (n.Contains("rgeo")) return "rgeo";
if (n.Contains("cbuffer")) return "cbuffer";
if (n.Contains("npoint")) return "npoint";
if (n.Contains("pose")) return "pose";
if (n.Contains("geo") || n.Contains("geom") || n.Contains("geog")
|| n.Contains("point") || n.Contains("spatial")) return "geo";
return "temporal";
}

[TestMethod]
public void GeneratedApi_Superset_Of_PortableBareNames_ZeroUnbacked()
{
var repo = RepoRoot();
var symbols = GeneratedSymbols(repo);
var (pairs, explicitBacking) = Contract(repo);

Assert.AreEqual(29, pairs.Count,
"The canonical contract must carry exactly 29 operator→bareName pairs.");

var unbacked = new List<string>();
var famTotals = InScopeFamilies.ToDictionary(f => f, _ => 0);
foreach (var (op, bare, fam) in pairs)
{
var hits = Backing(bare, symbols, explicitBacking);
if (hits.Count == 0) { unbacked.Add($"{bare} ({op}, {fam})"); continue; }
foreach (var h in hits)
{
var k = FamilyOf(h);
if (famTotals.ContainsKey(k)) famTotals[k]++;
}
}

Assert.AreEqual(0, unbacked.Count,
"Unbacked canonical bare names (generated symbol set must be a "
+ "superset, 0 unbacked): " + string.Join(", ", unbacked));

var missing = InScopeFamilies.Where(f => famTotals[f] == 0).ToList();
Assert.AreEqual(0, missing.Count,
"In-scope user-facing families absent from the generated binding "
+ "(cbuffer/npoint/pose/rgeo are never excluded from the parity "
+ "headline): " + string.Join(", ", missing)
+ ". Coverage: "
+ string.Join(", ", famTotals.Select(kv => $"{kv.Key}={kv.Value}")));
}
}
}
42 changes: 42 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,45 @@ hand-written wrappers in `MEOS.NET/Types/` need updates for:
Running `dotnet build MEOS.NET/MEOS.NET.csproj` against the new
bindings surfaces every adaptation point as a compiler error — work
through the list, no hidden runtime breakage.

## Portable bare-name dialect (RFC #920)

The MobilityDB ecosystem defines a canonical, type-agnostic
operator → bare-name dialect so one query runs identically on every
engine and binding (a user learns one reference and assumes the rest).
The contract is **29 operator → bareName pairs** across eight families
(topology, time-position, space X/Y/Z, temporal-comparison, distance,
same), the single source of truth being MEOS-API
`meta/portable-aliases.json` (discussion MobilityDB#861 · RFC #920 ·
native MobilityDB#1075 · manual MobilityDB#1078).

`tools/portable-aliases.json` is that contract, vendored **byte-identical**
so this binding is self-contained until MEOS-API folds `portableAliases`
into `meos-idl.json` (after which the catalog copy is preferred
automatically).

A bare name is **backed** when the generated symbol set contains a MEOS
function whose name `== bareName` or `startsWith(bareName + "_")`, with
`nearestApproachDistance` backed by the verified `nad_*` family
(`explicitBacking`). MEOS C already names every operator's backing
function this way, so the generated bindings expose the dialect by
construction — each portable name reuses the operator's own backing
function, never a reimplementation. No type-qualified or per-binding
forms are introduced.

### Verifying parity

```
python3 tools/portable_parity.py --check
```

Exits non-zero unless **29/29 bare names are backed, 0 unbacked**, across
all six in-scope user-facing type families — `temporal`, `geo`,
`cbuffer`, `npoint`, `pose`, `rgeo` (`cbuffer`/`npoint`/`pose`/`rgeo` are
full temporal types and are never excluded from the parity headline).
The same check runs as the `PortableAliasParityTests` MSTest case and in
the `portable-aliases parity` CI workflow.

Note: the MEOS 1.4 surface is required for full six-family coverage —
the MEOS 1.3 catalog does not expose `cbuffer`/`pose`/`rgeo` operator
functions.
60 changes: 60 additions & 0 deletions tools/portable-aliases.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"_comment": "Canonical portable bare-name dialect — 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.",
"provenance": {
"discussion": "MobilityDB#861",
"rfc": "MobilityDB RFC #920 (doc/rfc/sql-portability/README.md, branch rfc/sql-portability)",
"nativePR": "MobilityDB#1075 (1303 operator-overload aliases, each reusing the operator's own C symbol — identical by construction; CI-gated by tools/portable_aliases/generate.py --check)",
"manualChapter": "MobilityDB#1078"
},
"families": {
"topology": [{"operator": "&&", "bareName": "overlaps"},
{"operator": "@>", "bareName": "contains"},
{"operator": "<@", "bareName": "contained"},
{"operator": "-|-", "bareName": "adjacent"}],
"timePosition": [{"operator": "<<#", "bareName": "before"},
{"operator": "#>>", "bareName": "after"},
{"operator": "&<#", "bareName": "overbefore"},
{"operator": "#&>", "bareName": "overafter"}],
"spaceX": [{"operator": "<<", "bareName": "left"},
{"operator": ">>", "bareName": "right"},
{"operator": "&<", "bareName": "overleft"},
{"operator": "&>", "bareName": "overright"}],
"spaceY": [{"operator": "<<|", "bareName": "below"},
{"operator": "|>>", "bareName": "above"},
{"operator": "&<|", "bareName": "overbelow"},
{"operator": "|&>", "bareName": "overabove"}],
"spaceZ": [{"operator": "<</", "bareName": "front"},
{"operator": "/>>", "bareName": "back"},
{"operator": "&</", "bareName": "overfront"},
{"operator": "/&>", "bareName": "overback"}],
"temporalComparison": [{"operator": "#=", "bareName": "teq"},
{"operator": "#<>", "bareName": "tne"},
{"operator": "#<", "bareName": "tlt"},
{"operator": "#<=", "bareName": "tle"},
{"operator": "#>", "bareName": "tgt"},
{"operator": "#>=", "bareName": "tge"}],
"distance": [{"operator": "<->", "bareName": "tdistance"},
{"operator": "|=|", "bareName": "nearestApproachDistance"}],
"same": [{"operator": "~=", "bareName": "same"}]
},
"alreadyCanonical": [
{"family": "ever", "operators": ["?="], "pattern": "ever_*"},
{"family": "always", "operators": ["%="], "pattern": "always_*"},
{"functions": ["eIntersects", "atTime", "restriction functions",
"spatial-relationship functions"]}
],
"_explicitBackingComment": "Bare names whose MEOS C family prefix differs from the bare name itself. Verified against the catalog (not guessed): `nearestApproachDistance` is backed by the `nad_*` family (35 functions). Lets the parity audit resolve 100% honestly instead of false-flagging a real, present family.",
"explicitBacking": {
"nearestApproachDistance": ["nad"]
},
"scope": {
"inScopeTypeFamilies": ["temporal", "geo", "cbuffer", "npoint", "pose",
"rgeo"],
"note": "cbuffer / npoint / pose / rgeo are FULL user-facing temporal types and ARE in scope — covered like every other type. PR #1075 already aliases all six families (1303 aliases). They must NOT be excluded from any parity headline; an upstream/audit note that 'defers' or 'jointly excludes' them is a known error being corrected — where another engine defers them, that is incomplete work to close (a gap with a plan), never an accepted exclusion."
},
"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 — do NOT normalize the internal prefix.",
"Goal: 100% parity ecosystem-wide — every operator has its bare name on every engine, no gaps, no headline exclusions."
]
}
Loading
Loading