diff --git a/meta/sql-columns.json b/meta/sql-columns.json index 6d1dbf6..8f2afaa 100644 --- a/meta/sql-columns.json +++ b/meta/sql-columns.json @@ -61,6 +61,14 @@ "jsonbEachText": { "note": "The key and the value are both text: the key is the array jsonb_each_text returns, the value its out-parameter values.", "columns": {"key": {"from": "return"}, "value": {"from": "values"}} + }, + "valueCount": { + "note": "A ValueCount holds the count and the percent of a value; the rows of valueCount read its value and its count, as PostGIS's ST_ValueCount does.", + "columns": {"value": {"field": "value"}, "count": {"field": "count"}} + }, + "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"}} } } } diff --git a/meta/sql-columns.schema.json b/meta/sql-columns.schema.json index 233e417..f9a91d5 100644 --- a/meta/sql-columns.schema.json +++ b/meta/sql-columns.schema.json @@ -36,6 +36,11 @@ "type": "string", "pattern": "^(ordinal|return|[a-z_][a-z0-9_]*)$" }, + "field": { + "description": "The field of the struct an array return holds feeding the column. A row naming fields reads those only.", + "type": "string", + "pattern": "^[a-z_][a-z0-9_]*$" + }, "offset": { "description": "A constant the wrapper adds to the C value feeding the column.", "type": "integer" diff --git a/parser/sqlfn.py b/parser/sqlfn.py index 3489f7e..b1b7e84 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -621,21 +621,29 @@ def _column_sources(func, sig, sqlc, declared, struct): where two C values of the column's type fit it (`"from": "return"` or the name of an out-parameter, the key and the value of `jsonbEachText`); the column takes that value, and a statement naming no C value of the function stops the - catalog as a column nothing feeds does.""" + catalog as a column nothing feeds does. A statement naming the `field` of a + returned struct feeds its column from that field, and a row naming fields reads + those only: the rows of `valueCount` read the `value` and `count` of a + `ValueCount`, the rows of `valuePercent` its `value` and `percent`.""" stated = declared.get(sig["ret"]) or declared.get(sig.get("sqlName") or func.get("sqlfn")) or {} cols = sig["columns"] fed = [c for c in cols if stated.get(c["name"], {}).get("from") != "ordinal"] classes = {c for cs in sqlc.values() for c in cs} slots = _row_slots(func, sig.get("retSet", False), len(fed), struct, classes) + fields = {s["field"] for s in stated.values() if "field" in s} + if fields: + slots = [x for x in slots if x[0].get("field") in fields | {None}] used, out = set(), {} # A column stated with the C value feeding it takes that value: two C values of # one type leave the source of their columns to the statement, never to order. for c in fed: src = stated.get(c["name"], {}).get("from") - if src is None: + field = stated.get(c["name"], {}).get("field") + if src is None and field is None: continue named = [k for k, (slot, _, _) in enumerate(slots) - if slot["from"] == src and k not in used] + if src in (None, slot["from"]) and field in (None, slot.get("field")) + and k not in used] if len(named) != 1: return None used.add(named[0]) diff --git a/tests/test_sqlfn_rows.py b/tests/test_sqlfn_rows.py index 7dd9529..d68b619 100644 --- a/tests/test_sqlfn_rows.py +++ b/tests/test_sqlfn_rows.py @@ -139,6 +139,9 @@ def test_the_catalog_signature_lists_its_columns(self): {"name": "j", "cType": "int"}]}, {"name": "TBox", "fields": [{"name": "period", "cType": "Span"}, {"name": "span", "cType": "Span"}]}, + {"name": "ValueCount", "fields": [{"name": "value", "cType": "double"}, + {"name": "count", "cType": "uint32_t"}, + {"name": "percent", "cType": "double"}]}, ], } @@ -217,6 +220,18 @@ def _pairs(with_periods): PAIRS_OFFSET = {"i": {"offset": 1}, "j": {"offset": 1}} +def _value_count(sqlfn, column): + """A row of `sqlfn` reading the value of a `ValueCount` and one more field, built + as #_pairs builds the rows of a pair.""" + return {"name": "raster_value_count", "sqlfn": sqlfn, + "params": [{"name": "rast", "cType": "const Raster *"}, _count()], + "returnType": {"c": "ValueCount *"}, + "shape": {"arrayReturn": {"element": {"c": "ValueCount"}}, + "outParams": ["count"]}, + "sqlSignatures": [{"args": ["raster"], "ret": "record", "retSet": True, + "columns": _cols(("value", "float"), column)}]} + + class RowSourceTests(unittest.TestCase): def test_columns_match_their_source_by_type_not_by_position(self): @@ -351,6 +366,29 @@ def test_a_stated_source_feeds_its_column(self): self.assertEqual(cols, [{"name": "key", "type": "text", "from": "return"}, {"name": "value", "type": "text", "from": "values"}]) + def test_a_struct_field_no_column_reads_stops_the_catalog(self): + """As #test_a_c_value_feeding_no_column_stops_the_catalog, for a field.""" + with self.assertRaises(ValueError): + _attach(_value_count("valueCount", ("count", "integer"))) + + def test_a_row_naming_fields_reads_those_only(self): + """The rows of valueCount and valuePercent read two of the three fields of + a `ValueCount`, the second field the one each names.""" + for sqlfn, column in (("valueCount", ("count", "integer")), + ("valuePercent", ("percent", "float"))): + name = column[0] + cols, _ = _attach(_value_count(sqlfn, column), + {sqlfn: {"value": {"field": "value"}, + name: {"field": name}}}) + self.assertEqual([(c["name"], c["from"], c["field"]) for c in cols], + [("value", "return", "value"), (name, "return", name)]) + + def test_a_stated_field_naming_no_field_stops_the_catalog(self): + with self.assertRaises(ValueError): + _attach(_value_count("valueCount", ("count", "integer")), + {"valueCount": {"value": {"field": "value"}, + "count": {"field": "total"}}}) + class DeclaredColumnsTests(unittest.TestCase):