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
52 changes: 49 additions & 3 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,46 @@ def _mdb_to_sql(mdb_src):
return out


_DOXY_BLOCK = re.compile(r"/\*\*.*?\*/", re.S)


def _meos_direct_sql(meos_src):
"""MEOS-C function name -> (sqlfn|None, sqlop|None) from a DIRECT @sqlfn /
@sqlop tag in meos/src — one hop, mirroring @csqlaggfn. This is the tag form
for a surface whose PostgreSQL registration is DEFERRED to a host extension
(the h3index scalar functions defer to h3-pg), so no PG wrapper exists to
carry the tag: the canonical SQL name lives with the MEOS function itself.
A block that also carries @csqlfn keeps the two-hop wrapper chain (skipped
here), and attach_sqlfn_map consults this map ONLY for functions the wrapper
chain did not resolve — fill-only, never an override."""
out = {}
for cf in Path(meos_src).rglob("*.c"):
text = cf.read_text(errors="ignore")
for bm in _DOXY_BLOCK.finditer(text):
block = bm.group(0)
if "@csqlfn" in block or "@csqlaggfn" in block:
continue
# Anchor on @sqlfn exactly as the wrapper-side scan does (_mdb_to_sql):
# an @sqlop-only block carries no SQL NAME and stays out of the map on
# both sides, so the fallback restores names without inventing new
# operator-only attributions.
sm = _SQLFN.search(block)
if not sm:
continue
om = _SQLOP.search(block)
fm = _FNDEF.search(text, bm.end() - 2)
if not fm:
continue
out.setdefault(fm.group(1),
(sm.group(1), om.group(1) if om else None))
return out


def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None):
m2d = _meos_to_mdb(meos_src)
d2s = _mdb_to_sql(mdb_src)
w2sig = _wrapper_sql_sigs(sql_src) if sql_src else {}
direct = _meos_direct_sql(meos_src)
n = 0
# Transient map: MEOS function name -> every SQL name it resolves to, for the
# functions that fan out (a shared wrapper / ever-always pair). This is NOT
Expand All @@ -256,17 +292,27 @@ def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None):
multi = {}
for f in idl["functions"]:
wrappers = m2d.get(f["name"])
if not wrappers:
continue
# A MEOS function can back several wrappers (the ever/always pair), each
# carrying its own @sqlfn; collect the (sqlfn, sqlop) pairs across all of
# them in order, keeping the primary (first) wrapper for back-compat.
pairs = []
for w in wrappers:
for w in wrappers or []:
for entry in d2s.get(w, []):
if entry not in pairs:
pairs.append(entry)
if not pairs:
# Fill-only fallback: a direct @sqlfn / @sqlop on the MEOS function
# itself (no PG wrapper — the PG registration is deferred to a host
# extension). Never reached when the wrapper chain resolves.
dsql = direct.get(f["name"])
if not dsql:
continue
sqlfn, sqlop = dsql
if sqlfn:
f["sqlfn"] = sqlfn
if sqlop:
f["sqlop"] = sqlop
n += 1
continue
f["mdbC"] = wrappers[0]
f["sqlfn"] = pairs[0][0]
Expand Down
98 changes: 98 additions & 0 deletions tests/test_sqlfn_direct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Regression tests for the direct one-hop @sqlfn fallback in parser/sqlfn.py.

A surface whose PostgreSQL registration is deferred to a host extension (the
h3index scalar functions defer to h3-pg) has no PG wrapper to carry @sqlfn, so
the MEOS function carries the tag itself. attach_sqlfn_map consults that direct
map ONLY for functions the @csqlfn wrapper chain does not resolve — fill-only,
never an override.

Plain unittest, no pytest dependency; synthetic sources via a temp dir.
"""
import tempfile
import unittest
from pathlib import Path

from parser.sqlfn import _meos_direct_sql, attach_sqlfn_map

MEOS_C = """
/**
* @ingroup meos_h3_base_inout
* @brief Parse a string into an H3Index
* @sqlfn h3index_in()
*/
H3Index
h3index_in(const char *str)
{
}

/**
* @ingroup meos_h3_base_comp
* @brief Return true if two h3index values are equal
* @sqlop @p =
*/
bool
h3index_eq(H3Index a, H3Index b)
{
}

/**
* @ingroup meos_setspan_inout
* @brief Return a set from its string representation
* @sqlfn set_in_direct_should_lose()
* @csqlfn #Set_in()
*/
Set *
set_in(const char *str)
{
}
"""

MDB_C = """
/**
* @brief Return a set from its string representation
* @sqlfn set_in()
*/
Datum
Set_in(PG_FUNCTION_ARGS)
{
}
"""


class DirectSqlfnTests(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_direct_map_needs_sqlfn_and_skips_csqlfn_blocks(self):
with tempfile.TemporaryDirectory() as d:
meos, _ = self._trees(d)
direct = _meos_direct_sql(meos)
# Name-bearing block resolves; @sqlop-only block stays out (same anchor
# the wrapper-side scan uses); a block with @csqlfn keeps the two-hop chain.
self.assertEqual(direct.get("h3index_in"), ("h3index_in", None))
self.assertNotIn("h3index_eq", direct)
self.assertNotIn("set_in", direct)

def test_attach_fills_only_unresolved_functions(self):
idl = {"functions": [{"name": "h3index_in"}, {"name": "set_in"}]}
with tempfile.TemporaryDirectory() as d:
meos, mdb = self._trees(d)
idl, n, _ = attach_sqlfn_map(idl, meos, mdb)
by = {f["name"]: f for f in idl["functions"]}
# Deferred surface: filled from the direct tag.
self.assertEqual(by["h3index_in"]["sqlfn"], "h3index_in")
self.assertNotIn("mdbC", by["h3index_in"])
# Wrapper-resolved surface: the two-hop chain wins over the direct tag.
self.assertEqual(by["set_in"]["sqlfn"], "set_in")
self.assertEqual(by["set_in"]["mdbC"], "Set_in")
self.assertEqual(n, 2)


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