Skip to content
Merged
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
81 changes: 81 additions & 0 deletions parser/typerelations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Attach the base-to-collection type-relation registry from ``meos_catalog.c``.

A base type ``T`` is the single parameter of four independent template classes —
``Temporal<T>``, ``Set<T>``, ``Span<T>`` and ``SpanSet<T>``. The positional
catalog arrays in ``meos_catalog.c`` pair each template instance's ``MeosType``
with its base (a span set with its span), and ``MEOS_TYPE_NAMES`` maps a
``MeosType`` to its public name. Inverting the arrays and resolving through the
names yields, for each base type name, the names of its set, span, span set and
temporal types.

This is the static metadata a binding generator needs to pick the concrete
collection type of a value-domain result — ``SpanSet<float>`` is ``floatspanset``
— with no hand-coding: every binding is a projection of the catalog, so the
mapping belongs in the catalog rather than in each generator.
"""
import re
from pathlib import Path

_NAME_RE = re.compile(r'\[\s*(T_\w+)\s*\]\s*=\s*"([^"]+)"')
_PAIR_RE = re.compile(r'\{\s*(T_\w+)\s*,\s*(T_\w+)\s*\}')


def _names(text: str) -> dict:
"""The ``MeosType`` enum-name to public-name map from ``MEOS_TYPE_NAMES``."""
m = re.search(r'MEOS_TYPE_NAMES\s*\[\]\s*=\s*\{(.*?)\};', text, re.S)
return dict(_NAME_RE.findall(m.group(1))) if m else {}


def _pairs(text: str, array: str) -> list:
"""The ``{T_A, T_B}`` rows of a positional catalog array, in order."""
m = re.search(re.escape(array) + r'\s*\[\]\s*=\s*\{(.*?)\};', text, re.S)
return _PAIR_RE.findall(m.group(1)) if m else []


def attach_type_relations(idl: dict, src_root: Path | None) -> dict:
"""Attach ``idl["typeRelations"]`` from the ``meos_catalog.c`` arrays.

Degrades to no attachment — never a fabricated map — when the source tree is
not available, mirroring the honest-signal contract of the object-model scan.
"""
if src_root is None:
return idl
catalog = Path(src_root) / "temporal" / "meos_catalog.c"
if not catalog.exists():
return idl

text = re.sub(r"//.*", "", catalog.read_text(errors="ignore"))
names = _names(text)

# Each array pairs an instance type with the type it is built over: a set,
# span or temporal with its base; a span set with its span.
base_of_set = {inst: base for inst, base in _pairs(text, "MEOS_SETTYPE_CATALOG")}
base_of_span = {inst: base for inst, base in _pairs(text, "MEOS_SPANTYPE_CATALOG")}
span_of_spanset = {inst: span for inst, span in _pairs(text, "MEOS_SPANSETTYPE_CATALOG")}
base_of_temp = {inst: base for inst, base in _pairs(text, "MEOS_TEMPTYPE_CATALOG")}

# Invert to base -> instance; a span set reaches its base through its span.
set_of_base = {base: inst for inst, base in base_of_set.items()}
span_of_base = {base: inst for inst, base in base_of_span.items()}
temp_of_base = {base: inst for inst, base in base_of_temp.items()}
spanset_of_base = {}
for spanset, span in span_of_spanset.items():
base = base_of_span.get(span)
if base is not None:
spanset_of_base[base] = spanset

by_base = {}
for base in set(set_of_base) | set(span_of_base) | set(temp_of_base):
base_name = names.get(base)
if base_name is None:
continue
record = {}
for role, mapping in (("temporal", temp_of_base), ("set", set_of_base),
("span", span_of_base), ("spanset", spanset_of_base)):
inst = mapping.get(base)
if inst is not None and names.get(inst) is not None:
record[role] = names[inst]
by_base[base_name] = record

idl["typeRelations"] = {"byBase": dict(sorted(by_base.items()))}
return idl
6 changes: 6 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from parser.doxygroup import attach_groups
from parser.extractors import find_unlisted_foreign_structs
from parser.object_model import attach_object_model, find_mobilitydb_src
from parser.typerelations import attach_type_relations


HEADERS_DIR = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("./meos/include")
Expand Down Expand Up @@ -264,6 +265,11 @@ def main():
f"(error scan: {MOBILITYDB_SRC})...", file=sys.stderr)
idl = attach_object_model(idl, OBJMODEL_PATH, MOBILITYDB_SRC)

# Attach the base-to-collection type-relation registry (a base T to its set, span, span set
# and temporal types), derived from the meos_catalog.c positional arrays. A binding projects
# the concrete collection type of a value-domain result from this, never hard-coding it.
idl = attach_type_relations(idl, MOBILITYDB_SRC)

# Stamp the MobilityDB source commit so the catalog is SELF-DESCRIBING about its freshness:
# a consumer proves it is current by comparing sourceCommit to live upstream master, never by
# inspecting whatever directory a vendored copy sits in. None when the source is not a git
Expand Down
94 changes: 94 additions & 0 deletions tests/test_typerelations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""Unit tests for the base-to-collection type-relation registry.

Runs without libclang or pytest: python3 tests/test_typerelations.py

A hermetic fixture exercises the parse and the inversion; a source check
asserts the canonical numeric mappings against the live MobilityDB tree when it
is available (skipped, never fabricated, when it is not).
"""
import sys
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from parser.typerelations import attach_type_relations
from parser.object_model import find_mobilitydb_src

_FIXTURE = """
static const char *MEOS_TYPE_NAMES[] =
{
[T_FLOAT8] = "float8",
[T_FLOATSET] = "floatset",
[T_FLOATSPAN] = "floatspan",
[T_FLOATSPANSET] = "floatspanset",
[T_TFLOAT] = "tfloat",
[T_TEXT] = "text",
[T_TEXTSET] = "textset",
[T_TTEXT] = "ttext",
};
static const settype_catalog_struct MEOS_SETTYPE_CATALOG[] =
{
{T_FLOATSET, T_FLOAT8},
{T_TEXTSET, T_TEXT},
};
static const spantype_catalog_struct MEOS_SPANTYPE_CATALOG[] =
{
{T_FLOATSPAN, T_FLOAT8},
};
static const spansettype_catalog_struct MEOS_SPANSETTYPE_CATALOG[] =
{
{T_FLOATSPANSET, T_FLOATSPAN},
};
static const temptype_catalog_struct MEOS_TEMPTYPE_CATALOG[] =
{
{T_TFLOAT, T_FLOAT8},
{T_TTEXT, T_TEXT},
};
"""


class TypeRelationsParseTest(unittest.TestCase):

def _attach(self, text):
with tempfile.TemporaryDirectory() as d:
catalog = Path(d) / "temporal"
catalog.mkdir()
(catalog / "meos_catalog.c").write_text(text)
return attach_type_relations({}, Path(d))["typeRelations"]["byBase"]

def test_full_numeric_base_resolves_all_four_templates(self):
by_base = self._attach(_FIXTURE)
self.assertEqual(by_base["float8"], {
"temporal": "tfloat", "set": "floatset",
"span": "floatspan", "spanset": "floatspanset"})

def test_non_orderable_base_has_set_but_no_span(self):
# text has a set and a temporal type but no span/span set.
by_base = self._attach(_FIXTURE)
self.assertEqual(by_base["text"], {"temporal": "ttext", "set": "textset"})

def test_absent_source_degrades_without_fabricating(self):
self.assertNotIn("typeRelations", attach_type_relations({}, None))
self.assertNotIn("typeRelations", attach_type_relations({}, Path("/no/such/tree")))


class TypeRelationsSourceTest(unittest.TestCase):

def test_canonical_numeric_mappings(self):
src = find_mobilitydb_src()
if src is None:
self.skipTest("MobilityDB source not available")
by_base = attach_type_relations({}, src)["typeRelations"]["byBase"]
self.assertEqual(by_base["float8"]["spanset"], "floatspanset")
self.assertEqual(by_base["int4"], {
"temporal": "tint", "set": "intset",
"span": "intspan", "spanset": "intspanset"})
self.assertEqual(by_base["int8"]["span"], "bigintspan")
self.assertNotIn("span", by_base["text"])


if __name__ == "__main__":
unittest.main()
Loading