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
8 changes: 8 additions & 0 deletions meta/sql-columns.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"}}
}
}
}
5 changes: 5 additions & 0 deletions meta/sql-columns.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
14 changes: 11 additions & 3 deletions parser/sqlfn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down
38 changes: 38 additions & 0 deletions tests/test_sqlfn_rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}]},
],
}

Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):

Expand Down
Loading