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
9 changes: 9 additions & 0 deletions meta/sql-columns.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@
"rowLength": {"call": "raster_width", "args": ["rast"]}
}
},
"inputs": {
"setValues": {
"note": "Raster_set_values reads its values, a one- or two-dimensional array, row by row into the values of raster_set_values, true in nodata for a NULL element, its rows and columns the height and width of the block, as PostGIS's ST_SetValues does; it reads its flags, an array of their own size, into noset, a NULL flag false.",
"args": {
"4": {"values": "values", "nulls": "nodata", "rows": "height", "columns": "width", "count": "count"},
"5": {"values": "noset", "rows": "nosetheight", "columns": "nosetwidth", "nullElement": "false"}
}
}
},
"rows": {
"geomval": {
"note": "A GeomVal holds a polygon and the value of the pixels it covers; the rows of geomval read its geom and its val, as PostGIS's ST_DumpAsPolygons does.",
Expand Down
68 changes: 68 additions & 0 deletions meta/sql-columns.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,74 @@
}
}
},
"inputs": {
"type": "object",
"description": "The C parameters a wrapper fills from a two-dimensional SQL array it takes, keyed by SQL name.",
"additionalProperties": {
"type": "object",
"required": [
"note",
"args"
],
"additionalProperties": false,
"properties": {
"note": {
"type": "string",
"description": "What the wrapper reads of the arrays, and why MEOS does not state it.",
"minLength": 1
},
"args": {
"type": "object",
"description": "Keyed by the position of the argument, from 0.",
"propertyNames": {
"pattern": "^[0-9]+$"
},
"minProperties": 1,
"additionalProperties": {
"type": "object",
"required": [
"values",
"rows",
"columns"
],
"additionalProperties": false,
"properties": {
"values": {
"description": "The C array of the elements, row by row.",
"type": "string",
"pattern": "^[a-z_][a-z0-9_]*$"
},
"rows": {
"description": "The parameter of the number of rows.",
"type": "string",
"pattern": "^[a-z_][a-z0-9_]*$"
},
"columns": {
"description": "The parameter of the number of columns.",
"type": "string",
"pattern": "^[a-z_][a-z0-9_]*$"
},
"count": {
"description": "The parameter of the number of elements.",
"type": "string",
"pattern": "^[a-z_][a-z0-9_]*$"
},
"nulls": {
"description": "The C array true for a NULL element.",
"type": "string",
"pattern": "^[a-z_][a-z0-9_]*$"
},
"nullElement": {
"description": "The C value a NULL element passes, where no nulls array is stated.",
"type": "string",
"minLength": 1
}
}
}
}
}
}
},
"rows": {
"type": "object",
"description": "Keyed by the composite type a function returns, or by its SQL name when it returns record.",
Expand Down
50 changes: 50 additions & 0 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,56 @@ def attach_array_shapes(idl, declared=None):
return idl, n


def declared_inputs(path=_COLUMNS_META):
"""The C parameters a wrapper fills from a two-dimensional SQL array it takes, stated in
`meta/sql-columns.json` beside the layouts #declared_arrays reads, keyed by SQL name and
then by the position of the argument: {sqlName: {position: {"values": ..., "rows": ...,
"columns": ..., ...}}}."""
doc = json.loads(Path(path).read_text())
return {key: {int(k): dict(v) for k, v in entry["args"].items()}
for key, entry in (doc.get("inputs") or {}).items()}


def attach_input_shapes(idl, declared=None):
"""(idl, number of signatures stating the layout of an array they take) once each SQL
signature taking a two-dimensional array whose layout only its wrapper reads states it
as `inputShape`, as #attach_array_shapes states the layout of an array a signature
returns: for the position of each such argument, `values`, the C array of its elements
row by row, `rows` and `columns`, the parameters of its size, `count`, the parameter of
its number of elements, `nulls`, the C array true for its NULL elements, or
`nullElement`, the value a NULL element passes where none is stated.
`setValues(raster, integer, integer, integer, float[], boolean[], boolean)` passes its
values as `values`, `nodata`, `height`, `width` and `count` of raster_set_values, and
its flags as `noset`, `nosetheight` and `nosetwidth`, a NULL flag false.

A statement naming a parameter the function does not take, an argument that is no
array, or a statement no signature takes, stops the catalog: a binding reading it would
pass the function arguments PostgreSQL does not."""
declared = declared_inputs() if declared is None else declared
bad, used, n = [], set(), 0
for f in idl["functions"]:
params = {p["name"] for p in f.get("params") or ()}
for s in f.get("sqlSignatures") or ():
key = s.get("sqlName") or f.get("sqlfn")
st = declared.get(key)
if not st:
continue
used.add(key)
args = list(s.get("args") or ())
named = {v for a in st.values() for k, v in a.items() if k != "nullElement"}
if not named <= params or any(k >= len(args) or not args[k].endswith("[]")
for k in st):
bad.append(f"{f['name']}: {key}({', '.join(args)})")
continue
s["inputShape"] = {str(k): dict(v) for k, v in sorted(st.items())}
n += 1
bad += [f"{key}: no SQL signature taking an array" for key in declared if key not in used]
if bad:
raise ValueError("SQL array inputs meta/sql-columns.json states that no function "
"takes:\n " + "\n ".join(sorted(set(bad))))
return idl, n


def _c_base(ctype):
"""A C type without `const`, `struct` and its pointer levels, with the
`<stdint.h>` spellings read as MEOS's own (`int64_t` is `int64`), and the number of
Expand Down
6 changes: 5 additions & 1 deletion run.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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_array_shapes, attach_claims_fit,
attach_array_shapes, attach_claims_fit, attach_input_shapes,
attach_sqlaggfn_map, lint_container_family_csqlfn,
lint_ea_sqlfn, lint_positional_sqlfn,
lint_sqlfn_case_collisions, state_deployed_sqlfn)
Expand Down Expand Up @@ -429,6 +429,10 @@ def main():
if SQL_SRC.exists():
idl, narrays = attach_array_shapes(idl)
print(f" SQL arrays stating their layout: {narrays}", file=sys.stderr)
# and the C parameters a wrapper fills from a two-dimensional array it takes
idl, ninputs = attach_input_shapes(idl)
print(f" SQL signatures stating the layout of an array they take: {ninputs}",
file=sys.stderr)

# State each SQL function composed over the functions of another type, its casts,
# call and restore named by MEOS function, read from the deployed SQL bodies and
Expand Down
60 changes: 59 additions & 1 deletion tests/test_sqlfn_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
from pathlib import Path

from parser.sqlfn import (_wrapper_sql_sigs, attach_row_sources, attach_sqlfn_map,
attach_array_shapes, declared_arrays, declared_columns,
attach_array_shapes, attach_input_shapes, declared_arrays,
declared_columns, declared_inputs,
declared_composites)

ROOT = Path(__file__).resolve().parents[1]
Expand Down Expand Up @@ -559,5 +560,62 @@ def test_a_layout_no_signature_takes_stops_the_catalog(self):
_array_idl(layout)


SET_VALUES_INPUTS = {"setValues": {
4: {"values": "values", "nulls": "nodata", "rows": "height", "columns": "width",
"count": "count"},
5: {"values": "noset", "rows": "nosetheight", "columns": "nosetwidth",
"nullElement": "false"}}}


def _input_idl(inputs=None, args=None):
"""raster_set_values over the signature of setValues, through #attach_input_shapes with
the statement `inputs`."""
names = ["rast", "band", "x", "y", "width", "height", "values", "nodata", "noset",
"nosetwidth", "nosetheight", "count", "keepnodata"]
func = {"name": "raster_set_values", "sqlfn": "setValues",
"params": [{"name": n} for n in names],
"sqlSignatures": [{"args": args or ["raster", "integer", "integer", "integer",
"float[]", "boolean[]", "boolean"],
"ret": "raster"}]}
idl, n = attach_input_shapes({"functions": [func]},
SET_VALUES_INPUTS if inputs is None else inputs)
return idl["functions"][0]["sqlSignatures"][0], n


class InputShapeTests(unittest.TestCase):
"""#attach_input_shapes, the arrays a signature takes as #ArrayShapeTests the array a
signature returns."""

def test_each_array_states_the_parameters_it_fills(self):
sig, n = _input_idl()
self.assertEqual(n, 1)
self.assertEqual(sig["inputShape"], {
"4": SET_VALUES_INPUTS["setValues"][4], "5": SET_VALUES_INPUTS["setValues"][5]})

def test_a_parameter_the_function_lacks_stops_the_catalog(self):
"""As #test_a_null_mask_naming_no_output_array_stops_the_catalog."""
inputs = {"setValues": {**SET_VALUES_INPUTS["setValues"],
5: dict(SET_VALUES_INPUTS["setValues"][5], rows="nosetrows")}}
with self.assertRaisesRegex(ValueError, "setValues"):
_input_idl(inputs)

def test_an_argument_that_is_no_array_stops_the_catalog(self):
with self.assertRaisesRegex(ValueError, "setValues"):
_input_idl(args=["raster", "integer", "integer", "integer", "float",
"boolean[]", "boolean"])

def test_a_statement_no_signature_takes_stops_the_catalog(self):
"""As #test_a_layout_no_signature_takes_stops_the_catalog."""
inputs = dict(SET_VALUES_INPUTS, reclass={0: {"values": "values", "rows": "height",
"columns": "width"}})
with self.assertRaisesRegex(ValueError, "reclass"):
_input_idl(inputs)

def test_the_inputs_are_read_by_sql_name(self):
"""As #test_the_array_layouts_are_read_by_sql_name."""
self.assertEqual(declared_inputs(COLUMNS)["setValues"],
SET_VALUES_INPUTS["setValues"])


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