From 9005c492edcdc8e37014347373fafa5d1b9efcc2 Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sun, 25 Oct 2026 22:24:35 +0100 Subject: [PATCH] State the columns of the composite types of another extension a SQL function returns meta/sql-columns.json states under composites the members of a composite type another extension creates, which the SQL the catalog reads does not: PostGIS's geomval (geom geometry, val double precision), created by postgis_raster, which MobilityDB requires when built with RASTER. sql_statements reads them through declared_composites beside the composites the SQL creates, so dumpAsPolygons(raster, integer, boolean) RETURNS SETOF geomval states its two columns, and a rows statement feeds them from the geom and the val of each GeomVal the function returns, as the rows of valueCount read the fields of each ValueCount. summarystats, the other PostGIS composite MobilityDB returns, is one value a binding reads from its structure, and stays without columns. Measured over MobilityDB master 1a7a1a0e11: the catalog differs from master's in raster_dump_as_polygons alone, whose signature states its columns. Over that catalog the JVM generators of JMEOS main answer the same surfaces as over master's, and refuse dumpAsPolygons as setret:column/return:geometry where they refused it as setret:column/return:geomval, and MobilityDuck main's generator writes the same files. tests/test_sqlfn_rows.py reads the declared members of geomval for a function whose SQL creates none, reads the composites by type, and refuses a member stated without a type; the suite passes, 589 tests and none skipped. --- meta/sql-columns.json | 10 ++++++++++ meta/sql-columns.schema.json | 27 +++++++++++++++++++++++++++ parser/sqlfn.py | 16 ++++++++++++++-- tests/test_sqlfn_rows.py | 27 ++++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 3 deletions(-) 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()