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
53 changes: 51 additions & 2 deletions parser/altsqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
80 changes: 79 additions & 1 deletion tests/test_altsqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
Expand All @@ -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 = """
Expand Down Expand Up @@ -109,6 +122,14 @@
{
}

/**
* @csqlfn #Tbool_tand_transfn()
*/
SkipList *
tbool_tand_transfn(SkipList *state, const Temporal *temp)
{
}

/**
* @csqlfn #Set_hash()
*/
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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."""
Expand Down
Loading