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
8 changes: 8 additions & 0 deletions meta/sql-columns.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@
"valuePercent": {
"note": "A ValueCount holds the count and the percent of a value; the rows of valuePercent read its value and its percent, as PostGIS's ST_ValuePercent does.",
"columns": {"value": {"field": "value"}, "percent": {"field": "percent"}}
},
"pixelAsPoints": {
"note": "A Pixel holds its column, its row, whether it is nodata, its value and its point; the rows of pixelAsPoints read its point, its value, its column and its row, as PostGIS's ST_PixelAsPoints does.",
"columns": {"geom": {"field": "geom"}, "val": {"field": "value"}, "x": {"field": "x"}, "y": {"field": "y"}}
},
"geoPoseFrames": {
"note": "A GeoPoseFrame holds the fields of one row of the geopose_frames table, each read by the column of its name.",
"columns": {"frame_id": {"field": "frame_id"}, "authority": {"field": "authority"}, "code": {"field": "code"}, "name": {"field": "name"}, "srid": {"field": "srid"}, "is_geographic": {"field": "is_geographic"}, "description": {"field": "description"}}
}
}
}
64 changes: 56 additions & 8 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,10 @@ def sql_statements(sql_src):
for stmt in _create_fn_stmts(text, bodies=True):
stmts.append(stmt)
argdecls, ret = stmt[1], stmt[2]
if ret:
table = _table_columns(ret)
if table:
vocab.update(t for _, t in table) # each column of RETURNS TABLE is a type
elif ret:
vocab.add(ret) # a RETURNS clause is always a type
for a in argdecls:
bt = _bare_type(a)
Expand All @@ -342,13 +345,25 @@ def sql_statements(sql_src):
return stmts, vocab, composites


def _table_columns(ret):
"""The `(name, type)` columns of a `RETURNS TABLE (...)` clause, or None for any other
return type."""
m = re.match(r"TABLE\s*\((.*)\)$", ret or "", re.I | re.S)
return [_column(c) for c in _split_top_commas(m.group(1)) if c.strip()] if m else None


def sql_signature(sqlname, argdecls, ret, retset, vocab, composites):
"""The SQL signature {sqlName, args, required, argDefaults, ret, retSet, columns} a
CREATE FUNCTION states, as #_wrapper_sql_sigs lists it."""
CREATE FUNCTION states, as #_wrapper_sql_sigs lists it. `RETURNS TABLE (...)` is
PostgreSQL's set of rows whose columns are its OUT arguments: a set of `record`, or of
the type of its one column, as `pixelAsPoints` returns its rows `(geom, val, x, y)`."""
table = _table_columns(ret)
if table:
ret, retset = ("record" if len(table) > 1 else table[0][1]), True
indecls = [a for a in argdecls if _is_in_arg(a)]
outcols = [_column(_OUTMODE.sub("", a.strip())) for a in argdecls
if _OUTMODE.match(a.strip())]
columns = outcols or composites.get(ret)
columns = table or outcols or composites.get(ret)
return {"sqlName": sqlname, "args": [_arg_type(a, vocab) for a in indecls],
"required": sum(1 for a in indecls if not re.search(r"\bDEFAULT\b", a, re.I)),
"argDefaults": [_arg_default(a) for a in indecls],
Expand Down Expand Up @@ -705,24 +720,34 @@ def attach_row_sources(idl, declared=None):
Runs once the object model and the type relations are attached, since a column
is matched to a C value by type. A row whose columns no C value and no
declaration feeds stops the catalog: guessing a column's source would hand a
binding a row PostgreSQL does not return."""
binding a row PostgreSQL does not return. Such a row of a signature passing a SQL
array where the function takes one element of it (#_loops_over_an_array) is the
wrapper's loop of calls, not a projection of the function, and the function gives the
signature up, as #attach_claims_fit gives up an unfit shared signature:
`dumpValues(raster, integer[], boolean)` calls `raster_dump_values` once per band of
its array, and each row's `nband` is a band of the argument."""
declared = declared_columns() if declared is None else declared
sqlc = _sql_ctypes(idl)
structs = {s["name"]: s for s in idl.get("structs") or ()}
unfed, n = [], 0
for f in idl["functions"]:
ar = (f.get("shape") or {}).get("arrayReturn")
struct = structs.get(_c_base(ar["element"]["c"])[0]) if ar else None
unfit = []
for s in f.get("sqlSignatures") or ():
if not s.get("columns"):
continue
columns = _column_sources(f, s, sqlc, declared, struct)
if columns is None:
if columns is None and _loops_over_an_array(f, s, sqlc):
unfit.append(s)
elif columns is None:
unfed.append(f"{f['name']}: {s.get('sqlName', f.get('sqlfn'))}"
f"({', '.join(s['args'])}) RETURNS {s['ret']}")
else:
s["columns"] = columns
n += 1
if unfit:
f["sqlSignatures"] = [s for s in f["sqlSignatures"] if s not in unfit]
if unfed:
raise ValueError(
"SQL rows with a column MEOS states no source for; state it in "
Expand All @@ -737,6 +762,19 @@ def _signature_fits(func, sig, sqlc):
two counts differ (an argument left to its default). The parameters `sig` binds, the
out-parameters and the length of each input array take no argument: a SQL array is
the C array together with its count (`shape.inputArrays`)."""
got = _signature_inputs(func, sig)
if got is None:
return None
inputs, types = got

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 _signature_inputs(func, sig):
"""(the C input parameters of `func`, the SQL types of the arguments of `sig`) as
#_signature_fits matches them, or None when the two counts differ."""
shape = func.get("shape") or {}
bound = sig.get("boundArgs") or shape.get("boundArgs") or {}
out = set(shape.get("outParams") or ())
Expand All @@ -750,10 +788,20 @@ def _signature_fits(func, sig, sqlc):
for a in sig["args"]:
t = re.sub(r"\s*\(.*\)$", "", a.strip().lower())
types.append(SQL_ALIASES.get(t, t))
return inputs, types

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 _loops_over_an_array(func, sig, sqlc):
"""Whether `sig` passes a SQL array where `func` takes one element of it, an argument
`X[]` at a C parameter of the type of an `X` (#_signature_inputs): the wrapper calls
the function once per element, as `dumpValues(raster, integer[], boolean)` calls
`raster_dump_values` once per band."""
got = _signature_inputs(func, sig)
if got is None:
return False
inputs, types = got
return any(t.endswith("[]") and _fits(t[:-2], *_c_base(p.get("cType")), sqlc)
and not _fits(t, *_c_base(p.get("cType")), sqlc) for t, p in zip(types, inputs))


def attach_claims_fit(idl):
Expand Down
63 changes: 63 additions & 0 deletions tests/test_sqlfn_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@
RETURNS SETOF integer
AS 'MODULE_PATHNAME', 'Set_unnest'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION pixelAsPoints(rast raster, band integer DEFAULT 1,
exclude_nodata_value boolean DEFAULT true)
RETURNS TABLE(geom geometry, val double precision, x integer, y integer)
AS 'MODULE_PATHNAME', 'Raster_pixel_as_points'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION bandNumbers(raster)
RETURNS TABLE (nband integer)
AS 'MODULE_PATHNAME', 'Raster_band_numbers'
LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION dumpAsPolygons(rast raster, band integer DEFAULT 1,
exclude_nodata_value boolean DEFAULT true)
RETURNS SETOF geomval
Expand Down Expand Up @@ -117,6 +126,20 @@ def test_a_composite_of_another_extension_gives_its_declared_members(self):
self.assertEqual((s["ret"], s["retSet"]), ("geomval", True))
self.assertEqual(s["columns"], [("geom", "geometry"), ("val", "double precision")])

def test_returns_table_is_a_set_of_records_of_its_columns(self):
"""As #test_out_arguments_are_columns_not_args: RETURNS TABLE is PostgreSQL's
set of rows whose columns are OUT arguments."""
s = _sigs()["Raster_pixel_as_points"][0]
self.assertEqual((s["ret"], s["retSet"]), ("record", True))
self.assertEqual(s["args"], ["raster", "integer", "boolean"])
self.assertEqual(s["columns"], [("geom", "geometry"), ("val", "double precision"),
("x", "integer"), ("y", "integer")])

def test_returns_table_of_one_column_is_a_set_of_its_type(self):
"""As #test_one_value_has_no_columns, for the one column of a RETURNS TABLE."""
s = _sigs()["Raster_band_numbers"][0]
self.assertEqual((s["ret"], s["retSet"], s["columns"]), ("integer", True, None))

def test_one_value_has_no_columns(self):
self.assertIsNone(_sigs()["Set_unnest"][0]["columns"])

Expand Down Expand Up @@ -331,8 +354,48 @@ def test_a_row_without_columns_is_left_alone(self):
self.assertEqual(n, 0)


def _dump_values(args):
"""raster_dump_values over the SQL signature `args` returning the rows (nband,
valarray) a wrapper loop over bands answers, and its one-band signature."""
return {
"name": "raster_dump_values",
"sqlfn": "dumpValues",
"params": [{"name": "rast", "cType": "const Raster *"},
{"name": "band", "cType": "int"},
{"name": "exclude_nodata", "cType": "bool"},
{"name": "nodata", "cType": "bool **"}, _count()],
"returnType": {"c": "double *"},
"shape": {"arrayReturn": {"lengthFrom": {"kind": "param", "name": "count"},
"element": {"c": "double"}},
"outputArrays": [{"param": "nodata"}], "outParams": ["nodata", "count"]},
"sqlSignatures": [
{"args": args, "ret": "record", "retSet": True,
"columns": _cols(("nband", "integer"), ("valarray", "double precision[]"))},
{"args": ["raster", "integer", "boolean"], "ret": "double precision[]"}]}


def _attach_raster(func):
idl = copy.deepcopy(CATALOG)
idl["objectModel"]["classes"]["Raster"] = {"cType": "Raster"}
idl["functions"] = [func]
idl, _ = attach_row_sources(idl, {})
return idl["functions"][0]["sqlSignatures"]


class UnfedRowTests(unittest.TestCase):

def test_an_unfed_row_of_an_unfit_signature_is_given_up(self):
"""As #attach_claims_fit gives up an unfit shared signature: dumpValues(raster,
integer[], boolean) loops over the bands of its array, so no C value feeds nband
and its arguments do not fit the one band the C function takes."""
sigs = _attach_raster(_dump_values(["raster", "integer[]", "boolean"]))
self.assertEqual([s["args"] for s in sigs], [["raster", "integer", "boolean"]])

def test_an_unfed_row_of_a_fitting_signature_still_stops_the_catalog(self):
"""As #test_a_column_no_c_value_fits_stops_the_catalog, for a signature that fits."""
with self.assertRaisesRegex(ValueError, "dumpValues"):
_attach_raster(_dump_values(["raster", "integer", "boolean"]))

def test_a_column_no_c_value_fits_stops_the_catalog(self):
func = copy.deepcopy(VALUE_TIME_SPLIT)
func["sqlSignatures"][0]["columns"][0]["type"] = "text"
Expand Down
Loading