diff --git a/parser/sqlfn.py b/parser/sqlfn.py index 7bb3b5f..133e591 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -46,6 +46,15 @@ # operators lost their SQL name that way). _FNDEF = re.compile(r"\*/\s*\n(?:[^\n(){};=]+\n)?(?:[\w\s*]+?\s)?(\w+)\s*\(") _SQLFN = re.compile(r"@sqlfn\s+(\w+)\s*\(\)") +# @sqlaggfn names the SQL AGGREGATE a PG wrapper serves, on the wrapper of one of +# its transition/combine/final members. It is the aggregate counterpart of +# @sqlfn on the SAME block: @sqlfn states the CREATE FUNCTION the wrapper backs +# (tcount_transfn), @sqlaggfn the CREATE AGGREGATE that function implements +# (tCount). Without it the aggregate name has nowhere to live but @sqlfn, which +# then holds a name no CREATE FUNCTION carries and leaves a consumer to tell an +# aggregate from a function by the C symbol's suffix. Same value grammar as +# @sqlfn — bare `name()`, never `#Name()` — so a block may name several. +_SQLAGGFN = re.compile(r"@sqlaggfn\s+(\w+)\s*\(\)") # The operator stops at a comma, mirroring `_SQLFN`'s `(\w+)\s*\(\)`: a block naming several # SQL functions lists their operators the same comma-separated way # (`@sqlop @p ->, @p ->>` beside `@sqlfn a(), b()`), and a PostgreSQL operator name never @@ -262,6 +271,26 @@ def _mdb_to_sql(mdb_src): return out +def _mdb_to_agg(mdb_src): + """MobilityDB-C wrapper name -> ordered list of SQL aggregate names. + + Mirrors `_mdb_to_sql` on the @sqlaggfn tag: the same doxygen block carries + @sqlfn for the CREATE FUNCTION and @sqlaggfn for the CREATE AGGREGATE, so a + wrapper resolves to both without either name displacing the other. A member + shared by several aggregates lists them, and duplicates collapse.""" + out = {} + for cf in Path(mdb_src).rglob("*.c"): + text = cf.read_text(errors="ignore") + for m in _SQLAGGFN.finditer(text): + close = text.find("*/", m.end()) + dm = _DATUM.search(text, close if close != -1 else m.end()) + if dm: + lst = out.setdefault(dm.group(1), []) + if m.group(1) not in lst: + lst.append(m.group(1)) + return out + + _DOXY_BLOCK = re.compile(r"/\*\*.*?\*/", re.S) @@ -483,6 +512,38 @@ def attach_aggfn_map(idl, meos_src): return idl, n +def attach_sqlaggfn_map(idl, meos_src, mdb_src): + """Attach `sqlAggregate` — the SQL AGGREGATE(s) a function's PG wrapper + serves, read from @sqlaggfn in mobilitydb/src over the same @csqlfn chain + that resolves `sqlfn`. + + This is the aggregate name a binding registers (`tCount`), and it is a + different fact from both neighbours it sits beside: + + `sqlfn` the CREATE FUNCTION the wrapper backs (`tcount_transfn`) — the + aggregate's transition member, which no user calls; + `sqlAgg` the aggregate-ROLE name from MEOS's @csqlaggfn + (`setUnionTransition`), naming the member within its aggregate. + + Keeping them apart is what lets a binding tell an aggregate from a function + without reading the C symbol's suffix, and it is why `sqlfn` can state the + function it actually backs. Faithful reader: recorded verbatim, no + derivation, and absent for every function whose wrapper carries no tag.""" + m2d = _meos_to_mdb(meos_src) + d2a = _mdb_to_agg(mdb_src) + n = 0 + for f in idl["functions"]: + names = [] + for w in m2d.get(f["name"]) or (): + for a in d2a.get(w) or (): + if a not in names: + names.append(a) + if names: + f["sqlAggregate"] = names + n += 1 + return idl, n + + # MEOS-C ever/always spatial-relationship functions are named _...; their # @csqlfn must point at the matching _... wrapper. A copy-paste @csqlfn in # meos/src (e.g. eintersects_tgeo_geo tagged #Aintersects_tgeo_geo) silently flips the diff --git a/run.py b/run.py index d18da6b..42fa392 100644 --- a/run.py +++ b/run.py @@ -14,7 +14,8 @@ from parser.outparam import extract_param_names, merge_outparams from parser.boundargs import merge_boundargs from parser.enrich import enrich_idl -from parser.sqlfn import (attach_sqlfn_map, attach_aggfn_map, lint_ea_sqlfn, +from parser.sqlfn import (attach_sqlfn_map, attach_aggfn_map, + attach_sqlaggfn_map, lint_ea_sqlfn, lint_positional_sqlfn, lint_sqlfn_case_collisions) from parser.doxygroup import attach_groups from parser.extractors import find_unlisted_foreign_structs @@ -188,6 +189,12 @@ def main(): # named binary set/span union function. One-hop, faithful to the source tag. idl, nagg = attach_aggfn_map(idl, MEOS_SRC) print(f" Attached {nagg} @csqlaggfn aggregate names", file=sys.stderr) + # The SQL aggregate a member serves, from @sqlaggfn on its PG wrapper. It + # rides the same @csqlfn chain as `sqlfn`, and holds the name a binding + # registers the aggregate under, so `sqlfn` is free to state the CREATE + # FUNCTION the wrapper backs rather than the aggregate above it. + idl, naggfn = attach_sqlaggfn_map(idl, MEOS_SRC, MDB_SRC) + print(f" Attached {naggfn} @sqlaggfn SQL aggregate names", file=sys.stderr) # Guard: a copy-paste @csqlfn in meos/src can point an ever/always function at # the opposite-prefix wrapper (eintersects_* tagged #Aintersects_*), flipping its # SQL name and breaking the binding overload dispatch. The parser is faithful, so diff --git a/tests/test_sqlaggfn.py b/tests/test_sqlaggfn.py new file mode 100644 index 0000000..444ef9e --- /dev/null +++ b/tests/test_sqlaggfn.py @@ -0,0 +1,106 @@ +"""Regression tests for @sqlaggfn — the SQL AGGREGATE a PG wrapper serves. + +An aggregate's transition/combine/final wrapper backs a CREATE FUNCTION nobody +calls (`tcount_transfn`) while implementing a CREATE AGGREGATE everybody does +(`tCount`). @sqlfn states the first and @sqlaggfn the second, so neither +displaces the other and a consumer tells an aggregate from a function without +reading the C symbol's suffix. + +Plain unittest, no pytest dependency; synthetic sources via a temp dir. +""" +import tempfile +import unittest +from pathlib import Path + +from parser.sqlfn import _mdb_to_agg, attach_sqlaggfn_map, attach_sqlfn_map + +MEOS_C = """ +/** + * @ingroup meos_temporal_agg + * @brief Transition function for temporal count aggregation + * @csqlfn #Temporal_tcount_transfn() + */ +SkipList * +temporal_tcount_transfn(SkipList *state, const Temporal *temp) +{ +} + +/** + * @ingroup meos_temporal_accessor + * @brief Return the number of instants of a temporal value + * @csqlfn #Temporal_num_instants() + */ +int +temporal_num_instants(const Temporal *temp) +{ +} +""" + +MDB_C = """ +/** + * @ingroup mobilitydb_temporal_agg + * @brief Transition function for temporal count aggregation + * @sqlfn tcount_transfn() + * @sqlaggfn tCount() + */ +Datum +Temporal_tcount_transfn(PG_FUNCTION_ARGS) +{ +} + +/** + * @ingroup mobilitydb_temporal_accessor + * @brief Return the number of instants of a temporal value + * @sqlfn numInstants() + */ +Datum +Temporal_num_instants(PG_FUNCTION_ARGS) +{ +} +""" + + +class SqlAggfnTests(unittest.TestCase): + def _trees(self, d): + meos = Path(d) / "meos" + mdb = Path(d) / "mdb" + meos.mkdir() + mdb.mkdir() + (meos / "x.c").write_text(MEOS_C) + (mdb / "y.c").write_text(MDB_C) + return str(meos), str(mdb) + + def test_wrapper_map_reads_only_tagged_wrappers(self): + with tempfile.TemporaryDirectory() as d: + _, mdb = self._trees(d) + d2a = _mdb_to_agg(mdb) + self.assertEqual(d2a.get("Temporal_tcount_transfn"), ["tCount"]) + # A wrapper carrying only @sqlfn names no aggregate and stays out. + self.assertNotIn("Temporal_num_instants", d2a) + + def test_aggregate_name_rides_the_csqlfn_chain(self): + idl = {"functions": [{"name": "temporal_tcount_transfn"}, + {"name": "temporal_num_instants"}]} + with tempfile.TemporaryDirectory() as d: + meos, mdb = self._trees(d) + idl, n = attach_sqlaggfn_map(idl, meos, mdb) + by = {f["name"]: f for f in idl["functions"]} + self.assertEqual(n, 1) + self.assertEqual(by["temporal_tcount_transfn"]["sqlAggregate"], ["tCount"]) + self.assertNotIn("sqlAggregate", by["temporal_num_instants"]) + + def test_sqlfn_states_the_function_the_wrapper_backs(self): + """The two tags coexist on one block: the aggregate name does not take + the `sqlfn` slot, which keeps naming the CREATE FUNCTION.""" + idl = {"functions": [{"name": "temporal_tcount_transfn"}]} + with tempfile.TemporaryDirectory() as d: + meos, mdb = self._trees(d) + idl, _, _ = attach_sqlfn_map(idl, meos, mdb) + idl, _ = attach_sqlaggfn_map(idl, meos, mdb) + f = idl["functions"][0] + self.assertEqual(f["sqlfn"], "tcount_transfn") + self.assertEqual(f["sqlAggregate"], ["tCount"]) + + +if __name__ == "__main__": + unittest.main()