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
88 changes: 62 additions & 26 deletions parser/typerelations.py
Original file line number Diff line number Diff line change
@@ -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<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.
``Temporal<T>``, ``Set<T>``, ``Span<T>`` and ``SpanSet<T>``. 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<float>`` is ``floatspanset``
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -54,33 +73,45 @@ 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:
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()}
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 = {}
Expand All @@ -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
56 changes: 38 additions & 18 deletions tests/test_typerelations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = """
Expand All @@ -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 },
};
"""

Expand All @@ -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:
Expand Down Expand Up @@ -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")
Expand Down
Loading