diff --git a/meta/sql-columns.json b/meta/sql-columns.json index 8f2afaa..4fc5c15 100644 --- a/meta/sql-columns.json +++ b/meta/sql-columns.json @@ -1,7 +1,17 @@ { "$schema": "./sql-columns.schema.json", "description": "The columns of rows a SQL function returns whose value MEOS does not state. parser/sqlfn.py feeds every column of a returned row from the C value of its type: the value or array the function returns, or an out-parameter. A column its wrapper computes rather than reads is declared here, keyed by the composite type the function returns, or by its SQL name when it returns record; a row with a column neither fed nor declared stops the catalog.", + "composites": { + "geomval": { + "note": "PostGIS's composite of a geometry and a value, created by postgis_raster, which MobilityDB requires when built with RASTER; dumpAsPolygons returns a set of them.", + "columns": [["geom", "geometry"], ["val", "double precision"]] + } + }, "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.", + "columns": {"geom": {"field": "geom"}, "val": {"field": "val"}} + }, "index_tbox": { "note": "The tiles of a temporal box: index numbers them from 1 in the order the MEOS array returns them.", "columns": {"index": {"from": "ordinal"}} diff --git a/meta/sql-columns.schema.json b/meta/sql-columns.schema.json index f9a91d5..800afff 100644 --- a/meta/sql-columns.schema.json +++ b/meta/sql-columns.schema.json @@ -9,6 +9,33 @@ "properties": { "$schema": { "type": "string" }, "description": { "type": "string" }, + "composites": { + "type": "object", + "description": "The composite types of another extension a SQL function returns, which the SQL the catalog reads does not create, keyed by type name.", + "additionalProperties": { + "type": "object", + "required": ["note", "columns"], + "additionalProperties": false, + "properties": { + "note": { + "type": "string", + "description": "The extension creating the type, and the functions returning it.", + "minLength": 1 + }, + "columns": { + "type": "array", + "description": "The (name, SQL type) of each member, in order.", + "minItems": 1, + "items": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { "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 2c90830..6193d94 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -321,8 +321,9 @@ def sql_statements(sql_src): """(statements, type vocabulary, composite types) of the CREATE FUNCTION statements under `sql_src`, each statement as #_create_fn_stmts yields it with its body. The vocabulary holds every RETURNS type and every single-token argument type, so an - argument's type is read against it (#_arg_type).""" - stmts, vocab, composites = [], set(), {} + argument's type is read against it (#_arg_type). The composite types are those the + SQL creates and those of another extension a function returns (#declared_composites).""" + stmts, vocab, composites = [], set(), dict(declared_composites()) sql_src = Path(sql_src) if not sql_src.exists(): return stmts, vocab, composites @@ -514,6 +515,17 @@ def declared_columns(path=_COLUMNS_META): return {key: entry["columns"] for key, entry in doc["rows"].items()} +def declared_composites(path=_COLUMNS_META): + """The composite types of another extension a SQL function returns, stated in + `meta/sql-columns.json` beside the rows #declared_columns reads, since the SQL the + catalog reads creates none of them; {typeName: [(column, type), ...]} as + #_composite_types yields the ones it creates: PostGIS's `geomval`, the rows of + `dumpAsPolygons`.""" + doc = json.loads(Path(path).read_text()) + return {key: [tuple(c) for c in entry["columns"]] + for key, entry in (doc.get("composites") or {}).items()} + + 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/tests/test_sqlfn_rows.py b/tests/test_sqlfn_rows.py index d68b619..55a9265 100644 --- a/tests/test_sqlfn_rows.py +++ b/tests/test_sqlfn_rows.py @@ -23,7 +23,7 @@ from pathlib import Path from parser.sqlfn import (_wrapper_sql_sigs, attach_row_sources, attach_sqlfn_map, - declared_columns) + declared_columns, declared_composites) ROOT = Path(__file__).resolve().parents[1] COLUMNS = ROOT / "meta" / "sql-columns.json" @@ -75,6 +75,11 @@ RETURNS SETOF integer AS 'MODULE_PATHNAME', 'Set_unnest' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; +CREATE FUNCTION dumpAsPolygons(rast raster, band integer DEFAULT 1, + exclude_nodata_value boolean DEFAULT true) + RETURNS SETOF geomval + AS 'MODULE_PATHNAME', 'Raster_dump_as_polygons' + LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; """ @@ -105,6 +110,13 @@ def test_a_composite_return_type_gives_its_members(self): self.assertEqual(s["columns"], [("index", "integer"), ("tile", "tbox")]) self.assertEqual(s["argDefaults"], [None, None, "'2000-01-03'"]) + def test_a_composite_of_another_extension_gives_its_declared_members(self): + """As #test_a_composite_return_type_gives_its_members, for a type the SQL does + not create: geomval's members are those meta/sql-columns.json states for it.""" + s = _sigs()["Raster_dump_as_polygons"][0] + self.assertEqual((s["ret"], s["retSet"]), ("geomval", True)) + self.assertEqual(s["columns"], [("geom", "geometry"), ("val", "double precision")]) + def test_one_value_has_no_columns(self): self.assertIsNone(_sigs()["Set_unnest"][0]["columns"]) @@ -409,6 +421,19 @@ def test_the_declarations_are_read_by_row(self): self.assertEqual(declared["index_tbox"], {"index": {"from": "ordinal"}}) self.assertEqual(declared["tDisjointPairs"], PAIRS_OFFSET) + def test_the_composites_are_read_by_type(self): + """As #test_the_declarations_are_read_by_row reads the rows.""" + self.assertEqual(declared_composites(COLUMNS)["geomval"], + [("geom", "geometry"), ("val", "double precision")]) + + def test_a_composite_member_without_a_type_is_refused(self): + """As #test_an_unknown_source_is_refused refuses a row's column.""" + import jsonschema + doc = json.loads(COLUMNS.read_text()) + doc["composites"]["geomval"]["columns"][0] = ["geom"] + with self.assertRaises(jsonschema.ValidationError): + jsonschema.validate(doc, json.loads(SCHEMA.read_text())) + if __name__ == "__main__": unittest.main()