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
2 changes: 1 addition & 1 deletion .github/workflows/pytest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
# carries, or a change to them is not exercised until after it merges.
# Consumers use the action; this repository owns the rules.
- name: Refuse a skip, and a suite that shrank
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 304
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 307

# The rules earn their place by refusing a log that carries what they
# name. Both fixtures are written here rather than tracked, and the
Expand Down
10 changes: 8 additions & 2 deletions docs/object-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ hierarchies — `Box` (`TBox`, `STBox`, `TPCBox`), `Collection`
tstz/geo/… leaves) and `Value` — and `objectModel.algebra` records which
companion a temporal family yields.

`Value` holds the base values themselves: `Text`, `Jsonb`, `JsonPath`, `Geo` over
`Value` holds the values a method names: `Text`, `Jsonb`, `JsonPath`, `Geo` over
`Geometry` and `Geography` (both a `GSERIALIZED`, parented exactly as
`TGeo` parents `TGeometry` and `TGeography`), `Cbuffer`, `Npoint`,
`Nsegment`, `Pose`, `PoseChain`, `Pcpoint`, `Pcpatch` and `Raquet`. A
Expand All @@ -125,7 +125,13 @@ that set from the two predicates and fails until each has a class, so a
base type MEOS adds reaches the model with no edit to the file.
`Nsegment`, `Raquet` and `JsonPath` are the MeosTypes no membership
predicate admits; the model states them, and the same gate states that it
does.
does. `Pcschema`, `RTree`, `SPTree` and `MeosArray` go further: MEOS
registers them in no enum at all, and they say so with a null `temptype`
rather than by leaving the field out. To a binding a pointer to a struct
is one thing — an instance it holds and calls through — whether MEOS
publishes the struct's layout or forward-declares it, which is why they
sit here rather than in a hierarchy of their own; GoMEOS's own wrapper
table draws no such line either.

`Box` is covered the same way, from the `type_bboxtype` column of
`MEOS_RELTYPE_CATALOG`: it names the box each type stores, so its
Expand Down
39 changes: 38 additions & 1 deletion meta/object-model.json
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@
}
},
"Value": {
"_comment": "A base value crosses the MEOS boundary as a POINTER unless basetype_byvalue() names its type, and a method taking or answering one cannot be typed without a class for it. Parallel hierarchy, as Box and Collection are. temptypes gated against the MeosType enum, and the by-reference base types gated for COVERAGE against basetype_byvalue().",
"_comment": "A value crosses the MEOS boundary as a POINTER unless basetype_byvalue() names its type, and a method taking or answering one cannot be typed without a class for it. To a binding a pointer to a struct is one thing — an instance it holds and calls through — whether MEOS publishes the struct's layout or forward-declares it, so the schema, the trees and the array sit here beside the base values rather than in a hierarchy of their own. Parallel hierarchy, as Box and Collection are. temptypes gated against the MeosType enum where MEOS registers one, and the by-reference base types gated for COVERAGE against basetype_byvalue().",
"root": "Value",
"nodes": {
"Value": {
Expand Down Expand Up @@ -706,6 +706,43 @@
"temptype": "T_RAQUET",
"conditional": "RASTER",
"doc": "A raster tile. A MeosType no membership predicate admits, as Nsegment is."
},
"Pcschema": {
"kind": "leaf",
"parent": "Value",
"prefixes": [
"meos_pc_schema"
],
"temptype": null,
"conditional": "POINTCLOUD",
"doc": "The dimension schema a point cloud value is read through. Every pcpoint and pcpatch accessor takes one, and the process-global cache answers it for a pcid. MEOS registers it in no enum, so it names no temptype."
},
"RTree": {
"kind": "leaf",
"parent": "Value",
"prefixes": [
"rtree"
],
"temptype": null,
"doc": "MEOS's in-memory R-tree over spans, boxes and their kin. MEOS registers it in no enum, so it names no temptype."
},
"SPTree": {
"kind": "leaf",
"parent": "Value",
"prefixes": [
"sptree"
],
"temptype": null,
"doc": "MEOS's in-memory SP-tree, the R-tree's sibling over the same key types. MEOS registers it in no enum, so it names no temptype."
},
"MeosArray": {
"kind": "leaf",
"parent": "Value",
"prefixes": [
"meos_array"
],
"temptype": null,
"doc": "The growable array MEOS builds its own results in. MEOS registers it in no enum, so it names no temptype."
}
}
}
Expand Down
49 changes: 38 additions & 11 deletions parser/object_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ def find_mobilitydb_src(headers_dir: Path | None = None) -> Path | None:

_QUALIFIER_RE = re.compile(r"\b(?:const|struct)\b")

#: What a class's instances are never a pointer to. A `char *` is a string in
#: every binding and a scalar pointer is a buffer or an out-parameter, so
#: neither says anything about the type a class stands for.
_NOT_A_CLASS_POINTEE = frozenset({
"char", "void", "bool", "int", "int8", "int8_t", "uint8", "uint8_t",
"short", "int16", "int16_t", "uint16", "uint16_t", "int32", "int32_t",
"uint32", "uint32_t", "float", "Oid", "DateADT", "long", "int64",
"int64_t", "uint64", "uint64_t", "double", "float8", "Datum", "Timestamp",
"TimestampTz", "TimeADT", "TimeOffset", "size_t",
})


def _pointee(c_type: str) -> str | None:
"""The type a single-pointer C declaration points at, or None."""
Expand Down Expand Up @@ -210,8 +221,13 @@ def _role(fn_name: str) -> str:

# Editorial name fixes for the rare cases mechanical derivation gets wrong.
# Deliberately minimal — the clean derived name is canonical, so legacy
# binding spellings are not carried forward.
_OONAME_OVERRIDES: dict[str, str] = {}
# binding spellings are not carried forward. What belongs here is a function the
# derivation cannot name at all: one whose whole name IS its class's prefix, so
# dropping the prefix leaves nothing.
_OONAME_OVERRIDES: dict[str, str] = {
# `meos_pc_schema(pcid)` answers the schema the cache holds for a pcid.
"meos_pc_schema": "get",
}


def _strip_class_token(fn_name: str, cls: str, prefix: str = "") -> str:
Expand Down Expand Up @@ -445,25 +461,31 @@ def _class_ctypes(classes: dict, functions: dict, parents: dict,
of that parameter names the type, and the class's own methods answer for
it. A class whose methods build values rather than take them — the
concrete `<leaf><subtype>` classes hold constructors alone — takes the
answer of the subtype it is a product of, and any other class its parent's,
which is the same C type by construction.
answer of the subtype it is a product of, then what its own methods RETURN,
and any other class its parent's, which is the same C type by construction.
"""
own = {}
own, made = {}, {}
for cls, spec in classes.items():
seen = {}
seen, answered = {}, {}
for method in spec["methods"]:
fn = functions.get(method["function"])
if not fn:
continue
pointee = _pointee(fn.get("returnType", {}).get("c", ""))
if pointee and pointee not in _NOT_A_CLASS_POINTEE:
answered[pointee] = answered.get(pointee, 0) + 1
if method["role"] not in _RECEIVER_ROLES:
continue
fn = functions.get(method["function"])
params = fn.get("params") if fn else None
params = fn.get("params")
if not params:
continue
pointee = _pointee(params[0]["cType"])
if pointee:
seen[pointee] = seen.get(pointee, 0) + 1
ranked = sorted(seen.items(), key=lambda kv: -kv[1])
if ranked and (len(ranked) == 1 or ranked[0][1] > ranked[1][1]):
own[cls] = ranked[0][0]
for source, out in ((seen, own), (answered, made)):
ranked = sorted(source.items(), key=lambda kv: -kv[1])
if ranked and (len(ranked) == 1 or ranked[0][1] > ranked[1][1]):
out[cls] = ranked[0][0]

resolved: dict = {}

Expand All @@ -474,6 +496,11 @@ def resolve(cls, walked=frozenset()):
resolved[cls] = own[cls]
elif cls in subtype_classes:
resolved[cls] = resolve(subtype_classes[cls], walked | {cls})
elif cls in made:
# No method takes one, so what the class MAKES says what it is —
# a type MEOS only ever hands over, never calls a method on, is
# reached that way and no other.
resolved[cls] = made[cls]
else:
parent = parents.get(cls)
resolved[cls] = (resolve(parent, walked | {cls})
Expand Down
49 changes: 48 additions & 1 deletion tests/test_object_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,12 @@ def test_companions_are_well_formed_trees(self):
if s["parent"]:
self.assertIn(s["parent"], nodes)
if s["kind"] == "leaf":
# A leaf names the ONE type it models, and a null says MEOS
# registers it in no enum — stated rather than left out,
# since an absent field reads the same as one nobody wrote.
self.assertIn("temptype", s, n)
if s["temptype"] is None:
self.assertTrue(s.get("doc"), n)

def test_traits_are_not_inheritance(self):
# geometry/geodetic is a TRAIT axis, never a parent (no diamond).
Expand Down Expand Up @@ -212,6 +217,43 @@ def test_ooname_drops_the_prefix_the_classifier_matched(self):
# One token is dropped, not every repetition of it.
self.assertEqual(names["set_set_subspan"], "setSubspan")

def test_a_class_nothing_is_called_on_takes_what_it_makes(self):
# Nothing is called ON a point-cloud schema — every accessor takes one
# as an ARGUMENT — so what the class MAKES says what it is, and the
# `char *` among those answers says nothing, being a string.
om = attach_object_model({"functions": [
{"name": "meos_pc_schema", "returnType": {"c": "PCSCHEMA *"},
"params": [{"name": "pcid", "cType": "uint32_t"}]},
{"name": "meos_pc_schema_xml", "returnType": {"c": "const char *"},
"params": [{"name": "pcid", "cType": "uint32_t"}]},
{"name": "meos_pc_schema_compression",
"returnType": {"c": "const char *"},
"params": [{"name": "pcid", "cType": "uint32_t"}]},
]}, MODEL, None)["objectModel"]
self.assertEqual(om["classes"]["Pcschema"]["cType"], "PCSCHEMA")

def test_a_type_meos_registers_in_no_enum_still_gets_a_class(self):
# A pointer to a struct is one thing to a binding whether MEOS
# publishes the layout or forward-declares it, so these sit in `Value`
# beside the base values and say with a null that they name no type.
nodes = _nodes(json.loads(MODEL.read_text())["companions"]["Value"]
["nodes"])
unregistered = {n for n, s in nodes.items()
if s["kind"] == "leaf" and s["temptype"] is None}
self.assertEqual(unregistered,
{"Pcschema", "RTree", "SPTree", "MeosArray"})

def test_a_function_named_as_its_own_prefix_still_has_a_member_name(self):
# `meos_pc_schema` IS its class's prefix, so dropping the prefix leaves
# nothing — the one case the override table exists for.
om = attach_object_model({"functions": [
{"name": "meos_pc_schema"}, {"name": "meos_pc_schema_ndims"},
]}, MODEL, None)["objectModel"]
names = {m["function"]: m["ooName"]
for m in om["classes"]["Pcschema"]["methods"]}
self.assertEqual(names, {"meos_pc_schema": "get",
"meos_pc_schema_ndims": "ndims"})

def test_class_ctype_comes_from_the_receiver(self):
# A receiver-role method takes the value it is called on first, so its
# pointee names what the class's instances are — which is what a
Expand Down Expand Up @@ -568,7 +610,7 @@ def test_every_bbox_type_has_a_companion_class(self):
for fam in _nodes(self.attached["companions"])
for spec in _nodes(
self.attached["companions"][fam]["nodes"]).values()
if spec["kind"] == "leaf"}
if spec["kind"] == "leaf" and spec.get("temptype")}
self.assertEqual(boxes - claimed, set(),
"a box type the relation catalog names has no "
"companion class, so the method answering it is "
Expand All @@ -592,6 +634,11 @@ def test_every_byreference_base_type_has_a_value_class(self):
if spec["kind"] != "leaf":
continue
tt = spec["temptype"]
if tt is None:
# A type MEOS registers in no enum: a class for it is still
# needed to type the methods naming it, and the coverage this
# gate measures is over the types MEOS DOES register.
continue
self.assertNotIn(tt, seen,
f"{tt} is claimed by both {seen.get(tt)} and {node}")
seen[tt] = node
Expand Down
Loading