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 390
run: tools/check-test-outcome.py "$RUNNER_TEMP/pytest.log" --min-tests 395

# 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
5 changes: 5 additions & 0 deletions docs/enrichment.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ literals are in the catalog.
`*_from_hexwkb`, …); **encoder** — a public `T (+ aux) → char *` (`*_out`,
`*_as_mfjson`, `*_as_hexwkb`, …). A size the function writes back is an
out-parameter, stated in its `shape.outParams` as for any other function.
- **A PostgreSQL type passed by value** (`TimestampTz`, `Timestamp`, `TimeADT`,
`DateADT`) is a class like a pointer type, its decoder returning it and its
encoder taking it without a pointer (`timestamptz_in` / `timestamptz_out`).
PostgreSQL's spelling `pg_X` yields to the name MEOS states it under, `X`,
when both serve a class (`timetz_in`, never `pg_timetz_in`).
- **`readers` / `writers`** — a class several SQL types share (`Set`, `Span`,
`SpanSet`, `Temporal`) reads and writes each type through that type's own public
function, keyed by the SQL type its signature returns (a reader) or takes (a writer).
Expand Down
32 changes: 25 additions & 7 deletions parser/enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ def _aux_specs(params: list):
return specs


def build_type_encodings(functions: list, structs: set) -> dict:
def build_type_encodings(functions: list, structs: set,
values: frozenset = frozenset()) -> dict:
"""Scan the catalog for the in/out functions of every opaque struct.

A *decoder* turns a wire string into an object (returns ``struct T *``,
Expand All @@ -216,6 +217,11 @@ def build_type_encodings(functions: list, structs: set) -> dict:
*formatting* scalars are allowed and defaulted (see ``_aux_specs``); a
non-defaultable trailing arg disqualifies the helper. Only declared
structs qualify, so primitives never register by accident.

A name in ``values`` is a type passed by value, as PostgreSQL passes its
``TimestampTz`` and ``DateADT``: its decoder returns it and its encoder takes it
without a pointer (``TimestampTz timestamptz_in(const char *, int32_t)``,
``char *timestamptz_out(TimestampTz)``).
"""
enc: dict[str, dict] = {}

Expand All @@ -235,7 +241,7 @@ def slot(b: str) -> dict:
aux = _aux_specs(params[1:]) # None => non-defaultable trailing arg

# Decoder: const char* (+ defaultable scalar aux) -> opaque struct
if (aux is not None and rd >= 1 and rb in structs
if (aux is not None and (rd >= 1 or rb in values) and rb in structs
and pd == 1 and pb in _STRING_PTR_BASES):
for rx, encoding in _DECODERS:
if rx.search(name):
Expand All @@ -246,7 +252,7 @@ def slot(b: str) -> dict:

# Encoder: const struct T* (+ defaultable scalar aux) -> char*
if (aux is not None and rd == 1 and rb in _STRING_PTR_BASES
and pd >= 1 and pb in structs):
and (pd >= 1 or pb in values) and pb in structs):
for rx, encoding in _ENCODERS:
if rx.search(name):
s = slot(pb)
Expand All @@ -265,9 +271,13 @@ def slot(b: str) -> dict:
enc_suffix = {"text": "_out", "mfjson": "_as_mfjson",
"wkb": "_as_hexwkb"}

# PostgreSQL's own spelling `pg_X` yields to the name MEOS states it under,
# `X`, when both serve a type: `timetz_in` calls `pg_timetz_in`, and only the
# first is the MEOS function a binding calls.
def choose(cands: dict, base: str, suffix: str) -> str:
pool = [c for c in cands if not (c.startswith("pg_") and c[3:] in cands)]
generic = base.lower() + suffix
return generic if generic in cands else sorted(cands)[0]
return generic if generic in pool else sorted(pool)[0]

# The byte codec of a type, beside its wire encodings: a reader
# ``T *f(const uint8_t *wkb, size_t size)`` and a writer
Expand Down Expand Up @@ -527,7 +537,7 @@ def assess(fn: dict, type_encodings: dict, enums: set) -> tuple:
else:
base, depth = _base(ret), _ptr_depth(ret)
te = type_encodings.get(base)
if depth == 1 and te and te["out"]:
if depth <= 1 and te and te["out"]:
wire_result = {"kind": "serialized", "cType": ret,
"encode": te["out"],
"encode_aux": te.get("out_aux", []),
Expand Down Expand Up @@ -565,15 +575,23 @@ def enrich_idl(idl: dict) -> dict:
# wrappers can register a codec instead of being dead `no-decoder`s.
_scalarish = (_INT_BASES | _FLOAT_BASES | _BOOL_BASES | _STRING_PTR_BASES
| enum_names | {"void", "text"})
# A named type passed by value that is no scalar is PostgreSQL's
# (`TimestampTz`, `DateADT`, `TimeADT`, `Timestamp`): the slot keeps the name,
# as #normalize_canonical of parser/typerecover.py states it, and the type
# registers a codec from its own in/out functions like an opaque one.
opaque_names = set(struct_names)
value_names = set()
for fn in functions:
for c in ([fn["returnType"]["canonical"]]
+ [p["canonical"] for p in fn.get("params", [])]):
b = _base(c)
if _ptr_depth(c) >= 1 and b and b not in _scalarish:
if b and b not in _scalarish:
opaque_names.add(b)
if _ptr_depth(c) == 0:
value_names.add(b)

type_encodings = build_type_encodings(functions, opaque_names)
type_encodings = build_type_encodings(functions, opaque_names,
frozenset(value_names))

for fn in functions:
group = fn.get("group")
Expand Down
65 changes: 65 additions & 0 deletions tests/test_enrich.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,53 @@ def test_lifecycle_and_index_not_exposable(self):
self.assertIn("index", self.n("rtree_insert")["reason"])


class ValueTypeTests(unittest.TestCase):
"""A PostgreSQL type passed by value (`TimestampTz`, `DateADT`) registers a codec
from its own in/out functions, as a pointer type does in #TypeEncodingTests, and a
function taking or returning it is exposable."""

def setUp(self):
idl = {"functions": [
fn("timestamptz_in", "TimestampTz", ("const char *", "str"),
("int32_t", "typmod")),
fn("timestamptz_out", "char *", ("TimestampTz", "tstz")),
fn("timetz_in", "TimeTzADT *", ("const char *", "str"),
("int32_t", "typmod")),
fn("pg_timetz_in", "TimeTzADT *", ("const char *", "str"),
("int32_t", "typmod")),
fn("timetz_out", "char *", ("const TimeTzADT *", "timetz")),
fn("pg_timetz_out", "char *", ("const TimeTzADT *", "timetz")),
fn("temporal_start_timestamptz", "TimestampTz", (T, "temp")),
fn("tbool_at_timestamptz", "struct Temporal *", (T, "temp"),
("TimestampTz", "t")),
fn("tbool_in", "struct Temporal *", ("const char *", "str")),
fn("temporal_out", "char *", (T, "temp")),
fn("datum_hash", "uint32_t", ("Datum", "d")),
], "structs": [{"name": "Temporal", "fields": []}], "enums": []}
self.idl = enrich_idl(idl)
self.te = self.idl["typeEncodings"]
self.fns = by_name(self.idl)

def test_a_value_type_reads_and_writes_through_its_functions(self):
self.assertEqual((self.te["TimestampTz"]["in"], self.te["TimestampTz"]["out"]),
("timestamptz_in", "timestamptz_out"))

def test_a_value_parameter_and_result_are_on_the_wire(self):
self.assertTrue(self.fns["tbool_at_timestamptz"]["network"]["exposable"])
self.assertEqual(self.fns["tbool_at_timestamptz"]["wire"]["params"][1]["decode"],
"timestamptz_in")
self.assertEqual(self.fns["temporal_start_timestamptz"]["wire"]["result"]["encode"],
"timestamptz_out")

def test_the_postgresql_spelling_yields_to_the_meos_name(self):
self.assertEqual((self.te["TimeTzADT"]["in"], self.te["TimeTzADT"]["out"]),
("timetz_in", "timetz_out"))

def test_a_value_type_without_a_codec_registers_nothing(self):
self.assertNotIn("Datum", self.te)
self.assertEqual(self.fns["datum_hash"]["network"]["reason"], "no-decoder:Datum")


class StandardIntegerTests(unittest.TestCase):
"""An integer the catalog states by its C standard name (``int64_t``, ``uint8_t``),
as #normalize_canonical of parser/typerecover.py states every integer typedef,
Expand Down Expand Up @@ -302,6 +349,24 @@ def test_a_type_modifier_reads_minus_one(self):
[{"name": "typmod", "kind": "integer", "default": -1}], cls)


class ValueTypeCatalogTests(unittest.TestCase):
"""Over the generated catalog, PostgreSQL's time types read and write through the
MEOS functions #ValueTypeTests states, never their `pg_` spelling."""

def test_the_postgresql_time_types_read_through_their_meos_functions(self):
idl_path = Path(__file__).resolve().parents[1] / "output" / "meos-idl.json"
if not idl_path.exists():
self.skipTest(f"{idl_path} not generated; run `python run.py` first")
te = json.loads(idl_path.read_text())["typeEncodings"]
self.assertEqual({c: (te[c]["in"], te[c]["out"]) for c in
("TimestampTz", "Timestamp", "TimeADT", "DateADT", "TimeTzADT")},
{"TimestampTz": ("timestamptz_in", "timestamptz_out"),
"Timestamp": ("timestamp_in", "timestamp_out"),
"TimeADT": ("time_in", "time_out"),
"DateADT": ("date_in", "date_out"),
"TimeTzADT": ("timetz_in", "timetz_out")})


class ApiClassificationTests(unittest.TestCase):
def setUp(self):
self.fns = by_name(make_idl())
Expand Down
Loading