diff --git a/meta/sql-columns.json b/meta/sql-columns.json index 1a9634e..0677ba1 100644 --- a/meta/sql-columns.json +++ b/meta/sql-columns.json @@ -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.", diff --git a/meta/sql-columns.schema.json b/meta/sql-columns.schema.json index f870b29..f06b7f2 100644 --- a/meta/sql-columns.schema.json +++ b/meta/sql-columns.schema.json @@ -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.", diff --git a/parser/sqlfn.py b/parser/sqlfn.py index aca51c0..7cc6131 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -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 `` spellings read as MEOS's own (`int64_t` is `int64`), and the number of diff --git a/run.py b/run.py index d2b1a05..aae54fe 100644 --- a/run.py +++ b/run.py @@ -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) @@ -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 diff --git a/tests/test_sqlfn_rows.py b/tests/test_sqlfn_rows.py index 475f78c..4d28926 100644 --- a/tests/test_sqlfn_rows.py +++ b/tests/test_sqlfn_rows.py @@ -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] @@ -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()