From 6dde80c986c6ec7dc7c02069bc0368b1b38e5b2e Mon Sep 17 00:00:00 2001 From: Esteban Zimanyi Date: Sun, 4 Oct 2026 12:31:09 +0200 Subject: [PATCH] State the C value feeding a row column two C values of its type fit meta/sql-columns.json states, beside the ordinal and the offset a wrapper computes, the C value feeding a column where the types leave it open: "from" is "return" or the name of an out-parameter, and the column takes that value. A statement naming no C value of the function stops the catalog, as a column nothing feeds does. jsonbEachText states its key from the returned array and its value from the out-parameter values. Witness: jsonb_each_text returns its keys and fills its values, both text, and jsonbEachText returns rows (key text, value text). Over a MobilityDB tree deploying jsonbEachText, run.py stops with "SQL rows with a column MEOS states no source for ... jsonb_each_text: jsonbEachText(jsonb) RETURNS record", since both C values fit both columns. With this change the catalog reads key from return and value from values. Why: matching a column by type refuses to choose between two C values of one type, and it must, since the SQL column order and the C parameter order need not agree; the source is then a fact only the function's author knows, and meta/sql-columns.json is where the catalog reads such facts. A statement that only skipped its column, as the ordinal does, left both C values unfed. Measured: over MobilityDB 453039dd6c the catalog run.py derives with this change is byte-identical to the one it derives without it. Over the MobilityDB branch meos/host-base-sqlfn it derives, where it stopped before, and jsonbEachText carries the columns above. tests/test_sqlfn_rows.py states that two C values of one type need a statement, that a statement feeds its column, and that a statement naming no C value stops the catalog; the suite passes 487 tests with none skipped, above the floor of 469. --- meta/sql-columns.json | 4 ++++ meta/sql-columns.schema.json | 5 +++-- parser/sqlfn.py | 22 +++++++++++++++++++-- tests/test_sqlfn_rows.py | 38 +++++++++++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 5 deletions(-) diff --git a/meta/sql-columns.json b/meta/sql-columns.json index cb2decb..6d1dbf6 100644 --- a/meta/sql-columns.json +++ b/meta/sql-columns.json @@ -57,6 +57,10 @@ "tTouchesPairs": { "note": "i and j are positions in the SQL arrays, from 1; MEOS returns the C indices, from 0.", "columns": {"i": {"offset": 1}, "j": {"offset": 1}} + }, + "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"}} } } } diff --git a/meta/sql-columns.schema.json b/meta/sql-columns.schema.json index cef752f..233e417 100644 --- a/meta/sql-columns.schema.json +++ b/meta/sql-columns.schema.json @@ -32,8 +32,9 @@ "additionalProperties": false, "properties": { "from": { - "description": "ordinal: the column numbers the rows from 1, as PostgreSQL's WITH ORDINALITY.", - "const": "ordinal" + "description": "ordinal: the column numbers the rows from 1, as PostgreSQL's WITH ORDINALITY. return, or the name of an out-parameter: the C value feeding the column, where two C values of the column's type leave its source to this statement.", + "type": "string", + "pattern": "^(ordinal|return|[a-z_][a-z0-9_]*)$" }, "offset": { "description": "A constant the wrapper adds to the C value feeding the column.", diff --git a/parser/sqlfn.py b/parser/sqlfn.py index a8188bb..686081a 100644 --- a/parser/sqlfn.py +++ b/parser/sqlfn.py @@ -589,14 +589,32 @@ def _column_sources(func, sig, sqlc, declared, struct): `meta/sql-columns.json` states what only the wrapper does: a column numbering the rows from 1, as PostgreSQL's WITH ORDINALITY (`"from": "ordinal"`, the index of a tile), and a constant the wrapper adds (`"offset": 1`, turning a C array - index into a SQL array position).""" + index into a SQL array position). It also states the C value feeding a column + 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.""" 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 "from" not in stated.get(c["name"], {})] + 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) 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: + continue + named = [k for k, (slot, _, _) in enumerate(slots) + if slot["from"] == src and k not in used] + if len(named) != 1: + return None + used.add(named[0]) + out[c["name"]] = dict(slots[named[0]][0]) + for c in fed: + if c["name"] in out: + continue fits = [k for k, (_, base, stars) in enumerate(slots) if k not in used and _fits(c["type"], base, stars, sqlc)] if not fits or len({slots[k][0]["from"] for k in fits}) > 1: diff --git a/tests/test_sqlfn_rows.py b/tests/test_sqlfn_rows.py index 042341d..7dd9529 100644 --- a/tests/test_sqlfn_rows.py +++ b/tests/test_sqlfn_rows.py @@ -176,6 +176,21 @@ def _attach(func, declared=None): ("tnumber", "tbigint"))}], } +# A row whose two columns are fed by two C values of one type, the returned keys +# and the out-parameter values, shaped as #VALUE_TIME_SPLIT is. +EACH_TEXT = { + "name": "jsonb_each_text", + "sqlfn": "jsonbEachText", + "params": [{"name": "jb", "cType": "const Jsonb *"}, + {"name": "values", "cType": "text **"}, + _count()], + "returnType": {"c": "text **"}, + "shape": {"arrayReturn": {"element": {"c": "text *"}}, + "outParams": ["values", "count"]}, + "sqlSignatures": [{"args": ["jsonb"], "ret": "record", "retSet": True, + "columns": _cols(("key", "text"), ("value", "text"))}], +} + def _pairs(with_periods): params = [{"name": "arr1", "cType": "const Temporal **"}, @@ -315,6 +330,27 @@ def test_an_undeclared_offset_is_not_invented(self): cols, _ = _attach(_pairs(False)) self.assertNotIn("offset", cols[0]) + def test_two_c_values_of_one_type_need_a_statement(self): + """As #test_a_column_two_c_values_fit_stops_the_catalog, over EACH_TEXT.""" + with self.assertRaises(ValueError): + _attach(copy.deepcopy(EACH_TEXT)) + + def test_a_stated_source_naming_no_c_value_stops_the_catalog(self): + """As #test_a_column_no_c_value_fits_stops_the_catalog, for a statement.""" + with self.assertRaises(ValueError): + _attach(copy.deepcopy(EACH_TEXT), + {"jsonbEachText": {"key": {"from": "return"}, + "value": {"from": "vals"}}}) + + def test_a_stated_source_feeds_its_column(self): + """As #test_an_ordinal_column_is_declared_and_a_class_is_one_value states + an ordinal, a statement names the C value of each column of EACH_TEXT.""" + cols, _ = _attach(copy.deepcopy(EACH_TEXT), + {"jsonbEachText": {"key": {"from": "return"}, + "value": {"from": "values"}}}) + self.assertEqual(cols, [{"name": "key", "type": "text", "from": "return"}, + {"name": "value", "type": "text", "from": "values"}]) + class DeclaredColumnsTests(unittest.TestCase): @@ -326,7 +362,7 @@ def test_the_declarations_validate(self): def test_an_unknown_source_is_refused(self): import jsonschema doc = json.loads(COLUMNS.read_text()) - doc["rows"]["index_tbox"]["columns"]["index"] = {"from": "position"} + doc["rows"]["index_tbox"]["columns"]["index"] = {"from": "with ordinality"} with self.assertRaises(jsonschema.ValidationError): jsonschema.validate(doc, json.loads(SCHEMA.read_text()))