From be5e3984c5f39481ec308b137d222a0b6b664c5d Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Fri, 16 Oct 2026 23:29:47 +0200 Subject: [PATCH] State the name another engine calls an aggregate by An aggregate whose PostgreSQL name a function also holds cannot take that name in DuckDB, Spark or Flink, which hold one function per name: those engines call tAnd the aggregate tAndAgg. MobilityDB states that name where it states every other engine's name, in @altsqlfn, on the transition wrapper of the aggregate beside its @sqlaggfn, a transition serving one aggregate: @sqlfn tAndTransition() @sqlaggfn tAnd() @altsqlfn tAndAgg() There @altsqlfn names the aggregate, never the transition: wrapper_names leaves out a wrapper stating @sqlaggfn, so no transition signature takes an altSqlName, and attach_aggregate_alt_sql_names, run after attach_aggregates, pairs the tag with the @sqlaggfn names by position and sets altSqlName on every entry of the aggregates section of that aggregate. A wrapper whose two lists differ in length, or an aggregate two wrappers name differently, stops the catalog, as an unresolved function alternative does. Witness: over the MobilityDB branch sql/aggregates-keep-their-names, whose transition wrappers state @altsqlfn (Tbool_tand_transfn: tAndAgg), master's parser states an alternative name on 1018 function signatures against 864 over MobilityDB master: the 154 transition signatures take the aggregate's name as their own, and no aggregate entry takes it. Why: one tag and one field carry every name an engine takes in place of a PostgreSQL one, so the bindings read altSqlName for an aggregate as they do for a function, and need no rule of their own to append Agg. Measured: over that branch the catalog states altSqlName on 154 aggregate entries, the CREATE AGGREGATE count of the eleven aggregates whose name a function also holds, and on 864 function signatures, the count master's parser and this one both read over MobilityDB master 439dc9fbe3 (0 aggregate entries there, which carries no such tag); the test suite runs 558 tests, OK, the two new ones included. --- parser/altsqlfn.py | 53 ++++++++++++++++++++++++++-- run.py | 12 ++++++- tests/test_altsqlfn.py | 80 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 141 insertions(+), 4 deletions(-) diff --git a/parser/altsqlfn.py b/parser/altsqlfn.py index 28c49eb..6798f25 100644 --- a/parser/altsqlfn.py +++ b/parser/altsqlfn.py @@ -31,6 +31,18 @@ Every signature resolves to exactly one name or the catalog stops: an alternative name that no signature's base type selects, or that several select, would hand a binding a wrong or ambiguous name. + +An aggregate whose name a function also holds is named the same way. DuckDB, Spark and Flink +hold one function per name, so they call ``tAnd`` the aggregate ``tAndAgg``; the transition +wrapper of the aggregate states it beside its ``@sqlaggfn``, a transition serving one aggregate: + + @sqlfn tAndTransition() + @sqlaggfn tAnd() + @altsqlfn tAndAgg() + +There ``@altsqlfn`` pairs with the ``@sqlaggfn`` names by position and names the aggregate, +never the transition, and the catalog states it on every entry of its ``aggregates`` section +(#attach_aggregate_alt_sql_names). """ from __future__ import annotations @@ -60,16 +72,53 @@ def _tag_names(block: str, tag: str) -> list[str]: def wrapper_names(mdb_src: str | Path) -> dict[str, tuple[list[str], list[str]]]: - """``{wrapper: (@sqlfn names, @altsqlfn names)}`` for every wrapper stating ``@altsqlfn``.""" + """``{wrapper: (@sqlfn names, @altsqlfn names)}`` for every wrapper stating ``@altsqlfn`` + and no ``@sqlaggfn``: on a wrapper stating ``@sqlaggfn`` the tag names the aggregate + (#aggregate_alt_names).""" out = {} for cf in sorted(Path(mdb_src).rglob("*.c")): for m in _WRAPPER_BLOCK.finditer(cf.read_text(errors="ignore")): alts = _tag_names(m.group(1), "altsqlfn") - if alts: + if alts and not _tag_names(m.group(1), "sqlaggfn"): out[m.group(2)] = (_tag_names(m.group(1), "sqlfn"), alts) return out +def aggregate_alt_names(mdb_src: str | Path) -> tuple[dict[str, str], list[str]]: + """(``{aggregate: alternative name}``, errors): the ``@altsqlfn`` of every wrapper stating + ``@sqlaggfn``, paired with its ``@sqlaggfn`` names by position, as #attach_alt_sql_names + pairs several ``@sqlfn`` names. A wrapper whose two lists differ in length, or an aggregate + two wrappers name differently, is an error.""" + out, errors = {}, [] + for cf in sorted(Path(mdb_src).rglob("*.c")): + for m in _WRAPPER_BLOCK.finditer(cf.read_text(errors="ignore")): + alts = _tag_names(m.group(1), "altsqlfn") + aggs = _tag_names(m.group(1), "sqlaggfn") + if not (alts and aggs): + continue + if len(alts) != len(aggs): + errors.append(f"{m.group(2)}: @sqlaggfn {aggs} does not pair with @altsqlfn {alts}") + continue + for agg, alt in zip(aggs, alts): + if out.setdefault(agg, alt) != alt: + errors.append(f"{m.group(2)}: aggregate {agg} named {alt} and {out[agg]}") + return out, errors + + +def attach_aggregate_alt_sql_names(idl: dict, mdb_src: str | Path) -> tuple[dict, int, list[str]]: + """(idl, entries stated, errors): set ``altSqlName`` on every entry of the ``aggregates`` + section (#attach_aggregates of parser/aggregates.py) whose aggregate a wrapper's + ``@altsqlfn`` names (#aggregate_alt_names).""" + names, errors = aggregate_alt_names(mdb_src) + n = 0 + for a in idl.get("aggregates") or (): + alt = names.get(a["sqlName"]) + if alt: + a["altSqlName"] = alt + n += 1 + return idl, n, errors + + def _meos_name(sqltype: str) -> str: """The MEOS spelling of a SQL type, which the catalog's type names and registry use, without the type modifier (``geometry(Point)`` is ``geometry``), as #_type of diff --git a/run.py b/run.py index 736978b..9134240 100644 --- a/run.py +++ b/run.py @@ -21,7 +21,7 @@ merge_sql_arg_params, resolve_bound_names, strip_call_literals) from parser.aggregates import attach_aggregates -from parser.altsqlfn import attach_alt_sql_names +from parser.altsqlfn import attach_aggregate_alt_sql_names, attach_alt_sql_names from parser.compositions import attach_compositions, attach_wrapper_compositions from parser.indexsearch import attach_index_search from parser.enrich import enrich_idl, restate_wire @@ -446,6 +446,16 @@ def main(): if SQL_SRC.exists(): idl, nagg_sql = attach_aggregates(idl, SQL_SRC) print(f" SQL aggregates: {nagg_sql}", file=sys.stderr) + # The name DuckDB, Spark and Flink call an aggregate by where a function holds its + # PostgreSQL name, stated by @altsqlfn on its transition wrapper; a wrapper whose + # @altsqlfn does not pair with its @sqlaggfn stops the catalog. + if MDB_SRC.exists(): + idl, nagg_alt, agg_alt_errors = attach_aggregate_alt_sql_names(idl, MDB_SRC) + if agg_alt_errors: + raise ValueError("SQL aggregates whose @altsqlfn does not resolve to one name:\n " + + "\n ".join(agg_alt_errors)) + print(f" SQL aggregates stating their alternative name: {nagg_alt}", + file=sys.stderr) # 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 diff --git a/tests/test_altsqlfn.py b/tests/test_altsqlfn.py index fa93f32..43f53fe 100644 --- a/tests/test_altsqlfn.py +++ b/tests/test_altsqlfn.py @@ -14,7 +14,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1])) -from parser.altsqlfn import _tag_names, attach_alt_sql_names, wrapper_names +from parser.altsqlfn import (_tag_names, aggregate_alt_names, attach_aggregate_alt_sql_names, + attach_alt_sql_names, wrapper_names) from parser.typescope import TypeFacts IDL = Path(__file__).resolve().parents[1] / "output" / "meos-idl.json" @@ -60,6 +61,17 @@ { } +/** + * @brief Transition function for temporal and aggregation of temporal booleans + * @sqlfn tAndTransition() + * @sqlaggfn tAnd() + * @altsqlfn tAndAgg() + */ +Datum +Tbool_tand_transfn(PG_FUNCTION_ARGS) +{ +} + /** * @brief Return the start value * @sqlfn startValue() @@ -82,6 +94,7 @@ CREATE FUNCTION abs(tfloat) RETURNS tfloat AS 'MODULE_PATHNAME', 'Tnumber_abs' LANGUAGE C; CREATE FUNCTION hash(intset) RETURNS integer AS 'MODULE_PATHNAME', 'Set_hash' LANGUAGE C; CREATE FUNCTION hash(floatset) RETURNS integer AS 'MODULE_PATHNAME', 'Set_hash' LANGUAGE C; +CREATE FUNCTION tAndTransition(internal, tbool) RETURNS internal AS 'MODULE_PATHNAME', 'Tbool_tand_transfn' LANGUAGE C; """ MEOS = """ @@ -109,6 +122,14 @@ { } +/** + * @csqlfn #Tbool_tand_transfn() + */ +SkipList * +tbool_tand_transfn(SkipList *state, const Temporal *temp) +{ +} + /** * @csqlfn #Set_hash() */ @@ -159,6 +180,8 @@ def catalog(): sig(["tint"], "tint"), sig(["tfloat"], "tfloat")]}, {"name": "set_hash", "sqlfn": "hash", "sqlSignatures": [ sig(["intset"], "integer"), sig(["floatset"], "integer")]}, + {"name": "tbool_tand_transfn", "sqlfn": "tAndTransition", "sqlSignatures": [ + sig(["internal", "tbool"], "internal")]}, {"name": "temporal_start_value", "sqlfn": "startValue", "sqlSignatures": [ sig(["tint"], "integer"), sig(["tfloat"], "float"), sig(["tgeompoint"], "geometry(Point)"), sig(["tpose"], "pose"), @@ -222,6 +245,61 @@ def test_a_signature_no_name_selects_stops_the_catalog(self): def test_a_wrapper_with_no_alternative_name_states_none(self): self.assertFalse(any("altSqlName" in s for s in self.fns["temporal_start_value"])) + def test_the_tag_beside_sqlaggfn_names_the_aggregate_never_the_transition(self): + self.assertNotIn("Tbool_tand_transfn", wrapper_names(Path(self.tmp.name) / "mdb")) + self.assertFalse(any("altSqlName" in s for s in self.fns["tbool_tand_transfn"])) + idl = {"aggregates": [{"sqlName": "tAnd", "args": ["tbool"]}, + {"sqlName": "tCount", "args": ["tbool"]}]} + idl, n, errors = attach_aggregate_alt_sql_names(idl, Path(self.tmp.name) / "mdb") + self.assertEqual((n, errors), (1, [])) + self.assertEqual(idl["aggregates"][0]["altSqlName"], "tAndAgg") + self.assertNotIn("altSqlName", idl["aggregates"][1]) + + +class AggregateAltNameTests(unittest.TestCase): + """A wrapper whose @altsqlfn does not pair with its @sqlaggfn, or an aggregate two wrappers + name differently, is an error.""" + + def test_unpaired_and_conflicting_names_are_errors(self): + text = """ +/** + * @sqlfn spanUnionFinal() + * @sqlaggfn spanUnion(), spansetUnion() + * @altsqlfn spanUnionAgg() + */ +Datum +Span_union_finalfn(PG_FUNCTION_ARGS) +{ +} + +/** + * @sqlfn tMinTransition() + * @sqlaggfn tMin() + * @altsqlfn tMinAgg() + */ +Datum +Tint_tmin_transfn(PG_FUNCTION_ARGS) +{ +} + +/** + * @sqlfn tMinTransition() + * @sqlaggfn tMin() + * @altsqlfn tMinimumAgg() + */ +Datum +Tfloat_tmin_transfn(PG_FUNCTION_ARGS) +{ +} +""" + with tempfile.TemporaryDirectory() as tmp: + (Path(tmp) / "w.c").write_text(text) + names, errors = aggregate_alt_names(tmp) + self.assertEqual(names, {"tMin": "tMinAgg"}) + self.assertEqual(len(errors), 2) + self.assertIn("Span_union_finalfn", errors[0]) + self.assertIn("Tfloat_tmin_transfn", errors[1]) + class AltSqlNameContractTests(unittest.TestCase): """Over the generated catalog."""