diff --git a/parser/typerelations.py b/parser/typerelations.py index 0695b47..fd36c63 100644 --- a/parser/typerelations.py +++ b/parser/typerelations.py @@ -1,12 +1,12 @@ """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``, ``Set``, ``Span`` and ``SpanSet``. 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. +``Temporal``, ``Set``, ``Span`` and ``SpanSet``. The catalog array +``MEOS_RELTYPE_CATALOG`` in ``meos_catalog.c`` is indexed by ``MeosType`` and +names, at the entry of each type, the types related to it; ``MEOS_TYPE_NAMES`` +maps a ``MeosType`` to its public name. Reading the relations out of the entries +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`` is ``floatspanset`` @@ -18,7 +18,18 @@ 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*\}') +_ROW_RE = re.compile(r'\[\s*(T_\w+)\s*\]\s*=\s*\{(.*?)\}', re.S) +_FIELD_RE = re.compile(r'\.\s*(\w+)\s*=\s*(T_\w+)') + +#: The relation field of ``reltype_catalog_struct`` naming each role, and the field naming +#: its inverse. The catalog records both directions — a relation and its inverse are read +#: from the entry of each of the two types — so either field alone yields the same pair. +_RELATIONS = ( + # role, forward field (on the left type), inverse field (on the right type) + ("set", "basetype_settype", "settype_basetype"), + ("span", "basetype_spantype", "spantype_basetype"), + ("spanset", "spantype_spansettype", "spansettype_spantype"), +) def _names(text: str) -> dict: @@ -27,10 +38,18 @@ def _names(text: str) -> dict: 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.""" +def _rows(text: str, array: str) -> list: + """The ``[T_X] = { .field = T_Y, ... }`` entries of the type-indexed catalog array. + + Returned in file order — which is ``MeosType`` order, the ordering the catalog enforces — + so a base reached from several entries resolves to the last one, as the arrays of pairs + this array replaced did. + """ m = re.search(re.escape(array) + r'\s*\[\]\s*=\s*\{(.*?)\};', text, re.S) - return _PAIR_RE.findall(m.group(1)) if m else [] + if not m: + return [] + return [(t, dict((f, v) for f, v in _FIELD_RE.findall(body))) + for t, body in _ROW_RE.findall(m.group(1))] def _locate_catalog(src_root: Path | None) -> Path | None: @@ -54,10 +73,14 @@ def _locate_catalog(src_root: Path | None) -> Path | None: def attach_type_relations(idl: dict, src_root: Path | None) -> dict: - """Attach ``idl["typeRelations"]`` from the ``meos_catalog.c`` arrays. + """Attach ``idl["typeRelations"]`` from the ``MEOS_RELTYPE_CATALOG`` array. 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. + A located catalog that yields no relation is a parse that has lost the array, + not a catalog without types, and raises rather than attaching an empty + registry: the consumers read the registry to resolve a concrete collection + type, so an empty one silently degrades every one of them. """ catalog = _locate_catalog(src_root) if catalog is None: @@ -65,22 +88,30 @@ def attach_type_relations(idl: dict, src_root: Path | None) -> dict: 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()} + rows = _rows(text, "MEOS_RELTYPE_CATALOG") + + # Each entry names the types related to the type it is indexed by, in both directions. + related = {role: {} for role, _, _ in _RELATIONS} + temp_of_base = {} + for meos_type, fields in rows: + for role, forward, inverse in _RELATIONS: + if forward in fields: + related[role][meos_type] = fields[forward] + if inverse in fields: + related[role][fields[inverse]] = meos_type + # A base names no temporal type of its own — several temporal types share one base + # (a geometry is the base of both tgeompoint and tgeometry) — so the temporal role is + # the inverse of temptype_basetype, resolved in MeosType order. + if "temptype_basetype" in fields: + temp_of_base[fields["temptype_basetype"]] = meos_type + + set_of_base = related["set"] + span_of_base = related["span"] + # A span set reaches its base through its span. spanset_of_base = {} - for spanset, span in span_of_spanset.items(): - base = base_of_span.get(span) - if base is not None: + for base, span in span_of_base.items(): + spanset = related["spanset"].get(span) + if spanset is not None: spanset_of_base[base] = spanset by_base = {} @@ -96,5 +127,10 @@ def attach_type_relations(idl: dict, src_root: Path | None) -> dict: record[role] = names[inst] by_base[base_name] = record + if not by_base: + raise ValueError( + f"{catalog}: no type relation parsed from MEOS_RELTYPE_CATALOG — the catalog's " + "shape has changed and parser/typerelations.py no longer reads it") + idl["typeRelations"] = {"byBase": dict(sorted(by_base.items()))} return idl diff --git a/tests/test_typerelations.py b/tests/test_typerelations.py index f502f63..ccc19c6 100644 --- a/tests/test_typerelations.py +++ b/tests/test_typerelations.py @@ -15,7 +15,7 @@ ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT)) -from parser.typerelations import attach_type_relations +from parser.typerelations import _locate_catalog, attach_type_relations from parser.object_model import find_mobilitydb_src _FIXTURE = """ @@ -29,24 +29,24 @@ [T_TEXT] = "text", [T_TEXTSET] = "textset", [T_TTEXT] = "ttext", + [T_GEOMETRY] = "geometry", + [T_TGEOMPOINT] = "tgeompoint", + [T_TGEOMETRY] = "tgeometry", }; -static const settype_catalog_struct MEOS_SETTYPE_CATALOG[] = +static const reltype_catalog_struct MEOS_RELTYPE_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}, + [T_FLOAT8] = { .basetype_settype = T_FLOATSET, + .basetype_spantype = T_FLOATSPAN }, + [T_FLOATSET] = { .type_bboxtype = T_FLOATSPAN, .settype_basetype = T_FLOAT8 }, + [T_FLOATSPAN] = { .spantype_basetype = T_FLOAT8, + .spantype_spansettype = T_FLOATSPANSET }, + [T_FLOATSPANSET] = { .spansettype_spantype = T_FLOATSPAN }, + [T_TFLOAT] = { .type_bboxtype = T_TBOX, .temptype_basetype = T_FLOAT8 }, + [T_TEXT] = { .basetype_settype = T_TEXTSET }, + [T_TEXTSET] = { .settype_basetype = T_TEXT }, + [T_TTEXT] = { .type_bboxtype = T_TSTZSPAN, .temptype_basetype = T_TEXT }, + [T_TGEOMPOINT] = { .type_bboxtype = T_STBOX, .temptype_basetype = T_GEOMETRY }, + [T_TGEOMETRY] = { .type_bboxtype = T_STBOX, .temptype_basetype = T_GEOMETRY }, }; """ @@ -71,6 +71,21 @@ def test_non_orderable_base_has_set_but_no_span(self): by_base = self._attach(_FIXTURE) self.assertEqual(by_base["text"], {"temporal": "ttext", "set": "textset"}) + def test_base_shared_by_several_temporal_types_resolves_in_meostype_order(self): + # A geometry is the base of both tgeompoint and tgeometry; the catalog names no + # temporal type at the base's own entry, so the role is the inverse relation + # resolved in MeosType order — the last entry, as the arrays of pairs resolved it. + by_base = self._attach(_FIXTURE) + self.assertEqual(by_base["geometry"]["temporal"], "tgeometry") + + def test_catalog_without_the_relation_array_raises(self): + # A located catalog the parse reads no relation out of is a lost array, not a + # catalog without types: it must raise rather than attach an empty registry that + # silently degrades every consumer resolving a concrete collection type. + names_only = _FIXTURE[:_FIXTURE.index("static const reltype_catalog_struct")] + with self.assertRaises(ValueError): + self._attach(names_only) + def test_absent_source_degrades_without_fabricating(self): saved = os.environ.pop("MDB_SRC_ROOT", None) try: @@ -102,8 +117,13 @@ def test_mdb_src_root_resolves_when_object_model_root_is_absent(self): class TypeRelationsSourceTest(unittest.TestCase): def test_canonical_numeric_mappings(self): + # Resolve the tree the way the extractor does, so the live assertion runs wherever the + # extractor runs: find_mobilitydb_src reads $MOBILITYDB_SRC, while the provisioning that + # derives the catalog checks the repository out under $MDB_SRC_ROOT, which _locate_catalog + # consults. Resolving through only the first skipped this check on the build path that + # produces the catalog, which is the path whose drift it exists to catch. src = find_mobilitydb_src() - if src is None: + if src is None and _locate_catalog(None) is None: self.skipTest("MobilityDB source not available") by_base = attach_type_relations({}, src)["typeRelations"]["byBase"] self.assertEqual(by_base["float8"]["spanset"], "floatspanset")