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
30 changes: 24 additions & 6 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@
# contains a comma. `(\S+)` ran past it and published the comma as part of the operator.
_SQLOP = re.compile(r"@sqlop\s+@p\s+([^\s,]+)")
_DATUM = re.compile(r"Datum\s+(\w+)\s*\(\s*PG_FUNCTION_ARGS")
# Every value of a tag that lists several, on the tag's own line: the names of
# `@sqlfn a(), b()` and `@sqlaggfn a(), b()`, and the operators of `@sqlop @p ->, @p ->>`.
# `_SQLFN`, `_SQLAGGFN` and `_SQLOP` find the tag, and its first value, as before.
_TAG_NAME = re.compile(r"(\w+)\s*\(\)")
_TAG_OP = re.compile(r"@p\s+([^\s,]+)")


def _tag_list(text, start, value):
"""The values `value` matches on the line of the tag starting at `start`."""
end = text.find("\n", start)
line = text[start:end if end != -1 else len(text)]
return value.findall(line[line.index(" "):]) if " " in line else []

# `CREATE [OR REPLACE] FUNCTION name(` — the SQL-facing signature; the wrapper it
# binds is in the trailing `AS 'MODULE_PATHNAME', '<Wrapper>'`.
_CREATE_FN = re.compile(r"CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(\w+)\s*\(", re.I)
Expand Down Expand Up @@ -416,17 +429,21 @@ def _mdb_to_sql(mdb_src):
for cf in Path(mdb_src).rglob("*.c"):
text = cf.read_text(errors="ignore")
for m in _SQLFN.finditer(text):
sqlfn = m.group(1)
# @sqlop lives in the SAME doxygen block (before the closing */).
close = text.find("*/", m.end())
block = text[m.start():close] if close != -1 else text[m.start():m.start() + 800]
op = _SQLOP.search(block)
# A block naming several SQL functions lists their operators in the same
# order, `@sqlop @p ->, @p ->>` beside `@sqlfn a(), b()`: the i-th name
# takes the i-th operator.
ops = _tag_list(block, op.start(), _TAG_OP) if op else []
dm = _DATUM.search(text, close if close != -1 else m.end())
if dm:
entry = (sqlfn, op.group(1) if op else None)
lst = out.setdefault(dm.group(1), [])
if entry not in lst:
lst.append(entry)
for i, sqlfn in enumerate(_tag_list(text, m.start(), _TAG_NAME)):
entry = (sqlfn, ops[i] if i < len(ops) else None)
if entry not in lst:
lst.append(entry)
return out


Expand All @@ -445,8 +462,9 @@ def _mdb_to_agg(mdb_src):
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))
for name in _tag_list(text, m.start(), _TAG_NAME):
if name not in lst:
lst.append(name)
return out


Expand Down
49 changes: 48 additions & 1 deletion tests/test_sqlaggfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import unittest
from pathlib import Path

from parser.sqlfn import _mdb_to_agg, attach_sqlaggfn_map, attach_sqlfn_map
from parser.sqlfn import _mdb_to_agg, _mdb_to_sql, attach_sqlaggfn_map, attach_sqlfn_map

MEOS_C = """
/**
Expand Down Expand Up @@ -101,6 +101,53 @@ def test_sqlfn_states_the_function_the_wrapper_backs(self):
self.assertEqual(f["sqlfn"], "tcount_transfn")
self.assertEqual(f["sqlAgg"], ["tCount"])

def test_a_member_shared_by_several_aggregates_names_each(self):
"""A final serving several aggregates lists them on one tag line, and
every name reaches the catalog, not the first alone. The shape of
#test_wrapper_map_reads_only_tagged_wrappers over a listing tag."""
src = """
/**
* @ingroup mobilitydb_temporal_agg
* @brief Generic final function for temporal aggregation
* @sqlfn mergeFinal(), tCountFinal(), tMinFinal()
* @sqlaggfn merge(), tCount(), tMin()
*/
Datum
Temporal_tagg_finalfn(PG_FUNCTION_ARGS)
{
}
"""
with tempfile.TemporaryDirectory() as d:
(Path(d) / "y.c").write_text(src)
d2a = _mdb_to_agg(d)
d2s = _mdb_to_sql(d)
self.assertEqual(d2a.get("Temporal_tagg_finalfn"), ["merge", "tCount", "tMin"])
self.assertEqual([s for s, _ in d2s.get("Temporal_tagg_finalfn")],
["mergeFinal", "tCountFinal", "tMinFinal"])

def test_listed_names_take_their_operators_in_order(self):
"""`@sqlop @p ->, @p ->>` beside `@sqlfn a(), b()`: the i-th name takes the
i-th operator. The wrapper-side twin of
#test_multi_op_tag_keeps_the_operator_without_its_comma of
test_sqlfn_direct.py, over the same tjsonb block."""
src = """
/**
* @ingroup mobilitydb_json_json
* @brief Extract a field from a temporal JSONB value
* @sqlfn tjsonbObjectField(), tjsonbObjectFieldText()
* @sqlop @p ->, @p ->>
*/
Datum
Tjsonb_object_field(PG_FUNCTION_ARGS)
{
}
"""
with tempfile.TemporaryDirectory() as d:
(Path(d) / "j.c").write_text(src)
d2s = _mdb_to_sql(d)
self.assertEqual(d2s.get("Tjsonb_object_field"),
[("tjsonbObjectField", "->"), ("tjsonbObjectFieldText", "->>")])


if __name__ == "__main__":
unittest.main()
Loading