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
61 changes: 61 additions & 0 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)


Expand Down Expand Up @@ -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 <e|a><verb>_...; their
# @csqlfn must point at the matching <E|A><verb>_... wrapper. A copy-paste @csqlfn in
# meos/src (e.g. eintersects_tgeo_geo tagged #Aintersects_tgeo_geo) silently flips the
Expand Down
9 changes: 8 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
106 changes: 106 additions & 0 deletions tests/test_sqlaggfn.py
Original file line number Diff line number Diff line change
@@ -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()
Loading