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
67 changes: 65 additions & 2 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
from pathlib import Path

from parser.shapeinfer import _out_count_param
from parser.typescope import (C_BASE_TYPES, TypeFacts, declared_scopes, read_bodies,
sql_spellings,
from itertools import permutations

from parser.typescope import (C_BASE_TYPES, SQL_ALIASES, TypeFacts, declared_scopes,
read_bodies, sql_spellings,
require_scopes, scoped_signatures)

# A @csqlfn tag carries one OR MORE #Wrapper() references — comma- or
Expand Down Expand Up @@ -643,6 +645,67 @@ def attach_row_sources(idl, declared=None):
return idl, n


def _signature_fits(func, sig, sqlc):
"""Whether the arguments of `sig` fit the C input parameters of `func`, in their order
or in the one other order a wrapper may pass them in, as #_Resolver.params of
parser/compositions.py matches the operands of a call: True or False, or None when the
two counts differ (an array passed with its count, an argument left to its default).
The parameters `sig` binds and the out-parameters take no argument."""
shape = func.get("shape") or {}
bound = sig.get("boundArgs") or shape.get("boundArgs") or {}
out = set(shape.get("outParams") or ())
inputs = [p for p in func.get("params") or ()
if p["name"] not in out and p["name"] not in bound]
if len(inputs) != len(sig["args"]):
return None
types = []
for a in sig["args"]:
t = re.sub(r"\s*\(.*\)$", "", a.strip().lower())
types.append(SQL_ALIASES.get(t, t))

def fit(order):
return all(_fits(t, *_c_base(p.get("cType")), sqlc) for t, p in zip(types, order))
return fit(inputs) or any(fit(o) for o in permutations(inputs))


def attach_claims_fit(idl):
"""(idl, number of signatures given up) once every claimant of a shared SQL signature
keeps it only if its C parameters fit it.

Two public functions can claim one SQL signature: a wrapper serving a whole family
(`Tbox_value_tiles`) is claimed by each typed MEOS function behind it
(`tintbox_value_tiles`, `tbigintbox_value_tiles`, `tfloatbox_value_tiles`), and a type
scope naming the box all three take keeps every signature on each. A binding then
calls whichever claimant it meets first, and `valueTiles(tbox, integer, integer,
boolean)` reaches the `int64` kernel. Where another claimant's parameters fit the
signature (#_signature_fits), a claimant whose parameters do not gives it up; a
signature no claimant or every claimant fits stays where it is. Runs once the object
model and the type relations are attached, as #attach_row_sources does, since a
parameter is matched to a SQL type by its C type."""
sqlc = _sql_ctypes(idl)
claims = {}
for f in idl["functions"]:
if f.get("api") != "public":
continue
for s in f.get("sqlSignatures") or ():
key = (s.get("sqlName") or f.get("sqlfn"), tuple(s["args"]))
claims.setdefault(key, []).append((f, s))
n = 0
for pairs in claims.values():
if len(pairs) < 2:
continue
fits = [(f, s, _signature_fits(f, s, sqlc)) for f, s in pairs]
if not any(v is True for _, _, v in fits):
continue
for f, s, v in fits:
if v is False:
f["sqlSignatures"] = [x for x in f["sqlSignatures"] if x is not s]
n += 1
# A function may keep only signatures carrying another name than its sqlfn
idl, _ = state_deployed_sqlfn(idl)
return idl, n


def attach_sqlfn_map(idl, meos_src, mdb_src, sql_src=None):
m2d = _meos_to_mdb(meos_src)
d2s = _mdb_to_sql(mdb_src)
Expand Down
8 changes: 8 additions & 0 deletions run.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from parser.enrich import enrich_idl, restate_wire
from parser.codecs import state_type_encodings
from parser.sqlfn import (attach_sqlfn_map, attach_aggfn_map, attach_row_sources,
attach_claims_fit,
attach_sqlaggfn_map, lint_container_family_csqlfn,
lint_ea_sqlfn, lint_positional_sqlfn,
lint_sqlfn_case_collisions, state_deployed_sqlfn)
Expand Down Expand Up @@ -368,6 +369,13 @@ def main():
# moment a family is added.
idl = attach_temporal_types(idl, MOBILITYDB_SRC)

# Keep a SQL signature two public functions claim on the one whose C parameters it
# fits, matched by type once the object model and the type relations state the C type
# of each SQL type.
idl, nclaims = attach_claims_fit(idl)
print(f" shared SQL signatures given up by a function they do not fit: {nclaims}",
file=sys.stderr)

# Name the C value feeding each column of every row a SQL signature returns, matched
# by type once the object model and the type relations state the C type of each SQL
# type; a row with a column nothing feeds stops the catalog.
Expand Down
102 changes: 102 additions & 0 deletions tests/test_claims_fit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""A SQL signature two public functions claim stays on the one whose C parameters it fits.

A wrapper serving a whole family is claimed by each typed MEOS function behind it, and a type
scope naming the box they all take keeps every signature on each. #attach_claims_fit of
parser/sqlfn.py lets a claimant whose parameters a signature does not fit give it up when
another claimant's parameters fit it, and changes nothing else.

The catalog is synthetic, its classes stated as #CompositionTests of tests/test_compositions.py
states them. Plain unittest, no pytest dependency.
"""
import unittest

from parser.sqlfn import attach_claims_fit

TB = "const TBox *"


def _fn(name, sqlfn, params, sigs, api="public", shape=None):
f = {"name": name, "sqlfn": sqlfn, "api": api,
"params": [{"name": n, "cType": c} for n, c in params],
"sqlSignatures": [dict(s) for s in sigs]}
if shape:
f["shape"] = shape
return f


# The three signatures Tbox_value_tiles deploys, each typed kernel claiming all three
TILES = [{"args": ["tbox", "integer", "integer", "boolean"], "ret": "tbox[]"},
{"args": ["tbox", "bigint", "bigint", "boolean"], "ret": "tbox[]"},
{"args": ["tbox", "float", "float", "boolean"], "ret": "tbox[]"}]


def _kernel(name, ctype):
return _fn(name, "valueTiles", [("box", TB), ("vsize", ctype), ("vorigin", ctype),
("border_inc", "bool")], TILES)


def _idl(*functions):
return {"functions": list(functions),
"objectModel": {"classes": {"TBox": {"cType": "TBox *"}}}}


def _args(f):
return [tuple(s["args"]) for s in f["sqlSignatures"]]


class ClaimsFitTests(unittest.TestCase):
"""Each claimant of a shared signature keeps the signatures its parameters fit."""

def test_each_typed_kernel_keeps_its_own_signature(self):
idl, n = attach_claims_fit(_idl(_kernel("tintbox_value_tiles", "int"),
_kernel("tbigintbox_value_tiles", "int64"),
_kernel("tfloatbox_value_tiles", "double")))
fns = {f["name"]: f for f in idl["functions"]}
self.assertEqual(_args(fns["tintbox_value_tiles"]),
[("tbox", "integer", "integer", "boolean")])
self.assertEqual(_args(fns["tbigintbox_value_tiles"]),
[("tbox", "bigint", "bigint", "boolean")])
self.assertEqual(_args(fns["tfloatbox_value_tiles"]),
[("tbox", "float", "float", "boolean")])
self.assertEqual(n, 6)

def test_a_bound_parameter_takes_no_argument(self):
# shiftValue binds the width of the shift_scale kernel, as the catalog states
sig = {"args": ["tbox", "integer"], "ret": "tbox",
"boundArgs": {"width": "0", "hasshift": "true", "haswidth": "false"}}
params = lambda t: [("box", TB), ("shift", t), ("width", t), # noqa: E731
("hasshift", "bool"), ("haswidth", "bool")]
idl, n = attach_claims_fit(_idl(
_fn("tintbox_shift_scale", "shiftValue", params("int"), [sig]),
_fn("tfloatbox_shift_scale", "shiftValue", params("double"), [sig])))
fns = {f["name"]: f for f in idl["functions"]}
self.assertEqual(_args(fns["tintbox_shift_scale"]), [("tbox", "integer")])
self.assertEqual(fns["tfloatbox_shift_scale"]["sqlSignatures"], [])
self.assertEqual(n, 1)

def test_a_signature_every_claimant_fits_stays_on_each(self):
sig = [{"args": ["tbox", "integer", "integer", "boolean"], "ret": "tbox[]"}]
params = [("box", TB), ("a", "int"), ("b", "int"), ("c", "bool")]
idl, n = attach_claims_fit(_idl(_fn("one", "valueTiles", params, sig),
_fn("two", "valueTiles", params, sig)))
self.assertEqual(n, 0)
self.assertTrue(all(f["sqlSignatures"] for f in idl["functions"]))

def test_a_signature_no_claimant_fits_stays_on_each(self):
sig = [{"args": ["tbox", "text"], "ret": "tbox"}]
idl, n = attach_claims_fit(_idl(
_fn("one", "f", [("box", TB), ("a", "int")], sig),
_fn("two", "f", [("box", TB), ("a", "double")], sig)))
self.assertEqual(n, 0)

def test_an_internal_claimant_is_left_alone(self):
idl, n = attach_claims_fit(_idl(_kernel("tintbox_value_tiles", "int"),
_kernel("internal_tiles", "int64") | {"api": "internal"}))
fns = {f["name"]: f for f in idl["functions"]}
self.assertEqual(len(fns["internal_tiles"]["sqlSignatures"]), 3)
self.assertEqual(len(fns["tintbox_value_tiles"]["sqlSignatures"]), 3)
self.assertEqual(n, 0)


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