From 9f4b4ab44617215c5685e3c7cec0df4c3063754c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Esteban=20Zim=C3=A1nyi?= Date: Mon, 26 Oct 2026 13:19:39 +0100 Subject: [PATCH] Generate the path functions of a plain jsonb The path functions read the path as a list of text over a plain jsonb as they do over a temporal and a set: jsonbSet, jsonbInsert, jsonbDeletePath, jsonbDeleteArray and jsonbExtractPath take the jsonb, the path as LIST(VARCHAR) and the arguments MobilityDB declares after it, and answer a jsonb, NULL where MEOS answers none. The text-array shape admits a base value as its operand, reads it with the base value's own blob reader, answers through its own {Base}ToBlobN and registers over its own type, as the base-value scalars do. Witness: the five functions answer {"a": [9, 2]}, {"a": [1, 9, 2]}, {"a": {"c": 2}}, {"b": 2} and [1, 2] on the arrays and objects of test/sql/parity/027_tjsonb.test, jsonbExtractPath of a key the object does not hold answers NULL, and jsonbSet over three rows answers NULL for the row whose value is NULL and each other row its own value. The generator adds these five registrations and removes none. --- src/generated/generated_temporal_udfs_12.cpp | 31 +++++ src/generated/generated_temporal_udfs_15.cpp | 128 +++++++++++++++++++ test/sql/parity/027_tjsonb.test | 26 ++++ tools/codegen_duck_udfs.py | 40 ++++-- 4 files changed, 215 insertions(+), 10 deletions(-) diff --git a/src/generated/generated_temporal_udfs_12.cpp b/src/generated/generated_temporal_udfs_12.cpp index 7dc9757a..44a98a6a 100644 --- a/src/generated/generated_temporal_udfs_12.cpp +++ b/src/generated/generated_temporal_udfs_12.cpp @@ -1974,6 +1974,36 @@ static void Gen_jsonb_array_length(DataChunk &args, ExpressionState &, Vector &r }); } +static void Gen_jsonb_extract_path(DataChunk &args, ExpressionState &, Vector &result) { + EnsureMeosThreadInitialized(); + auto &lst = args.data[1]; lst.Flatten(args.size()); + auto &child = ListVector::GetEntry(lst); + child.Flatten(ListVector::GetListSize(lst)); + const idx_t rows = args.size(); + UnifiedVectorFormat u[2]; + for (idx_t c = 0; c < 2; c++) args.data[c].ToUnifiedFormat(rows, u[c]); + result.SetVectorType(VectorType::FLAT_VECTOR); + auto out = FlatVector::GetData(result); + auto &mask = FlatVector::Validity(result); + for (idx_t idx = 0; idx < rows; idx++) { + idx_t ri[2]; + bool valid = true; + for (idx_t c = 0; c < 2; c++) { + ri[c] = u[c].sel->get_index(idx); + valid = valid && u[c].validity.RowIsValid(ri[c]); + } + if (!valid) { mask.SetInvalid(idx); continue; } + out[idx] = [&]() -> string_t { + Jsonb *t = BlobToJsonb(UnifiedVectorFormat::GetData(u[0])[ri[0]]); + int n = 0; + text **elems = ListToTextArr(child, UnifiedVectorFormat::GetData(u[1])[ri[1]], &n); + Jsonb *r = jsonb_extract_path(t, elems, n); + free(t); FreeTextArr(elems, n); + return JsonbToBlobN(result, r, mask, idx); + }(); + } +} + static void Gen_jsonb_hash(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); UnaryExecutor::Execute(args.data[0], result, args.size(), @@ -4882,6 +4912,7 @@ void RegisterGenerated_meos_h3_vertex(ExtensionLoader &loader) { void RegisterGenerated_meos_json_base_accessor(ExtensionLoader &loader) { RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbArrayLength", {TJsonbTypes::jsonb()}, LogicalType::INTEGER, Gen_jsonb_array_length)); + RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbExtractPath", {TJsonbTypes::jsonb(), LogicalType::LIST(LogicalType::VARCHAR)}, TJsonbTypes::jsonb(), Gen_jsonb_extract_path)); RegisterSerializedScalarFunction(loader, ScalarFunction("hash", {TJsonbTypes::jsonb()}, LogicalType::UINTEGER, Gen_jsonb_hash)); RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbContained", {TJsonbTypes::jsonb(), TJsonbTypes::jsonb()}, LogicalType::BOOLEAN, Gen_jsonb_contained)); RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbContains", {TJsonbTypes::jsonb(), TJsonbTypes::jsonb()}, LogicalType::BOOLEAN, Gen_jsonb_contains)); diff --git a/src/generated/generated_temporal_udfs_15.cpp b/src/generated/generated_temporal_udfs_15.cpp index ab41acaa..cb6317e2 100644 --- a/src/generated/generated_temporal_udfs_15.cpp +++ b/src/generated/generated_temporal_udfs_15.cpp @@ -1390,6 +1390,130 @@ static void Gen_h3index_get_resolution(DataChunk &args, ExpressionState &, Vecto // ===== @ingroup meos_json_base_transf ===== +static void Gen_jsonb_delete_array(DataChunk &args, ExpressionState &, Vector &result) { + EnsureMeosThreadInitialized(); + auto &lst = args.data[1]; lst.Flatten(args.size()); + auto &child = ListVector::GetEntry(lst); + child.Flatten(ListVector::GetListSize(lst)); + const idx_t rows = args.size(); + UnifiedVectorFormat u[2]; + for (idx_t c = 0; c < 2; c++) args.data[c].ToUnifiedFormat(rows, u[c]); + result.SetVectorType(VectorType::FLAT_VECTOR); + auto out = FlatVector::GetData(result); + auto &mask = FlatVector::Validity(result); + for (idx_t idx = 0; idx < rows; idx++) { + idx_t ri[2]; + bool valid = true; + for (idx_t c = 0; c < 2; c++) { + ri[c] = u[c].sel->get_index(idx); + valid = valid && u[c].validity.RowIsValid(ri[c]); + } + if (!valid) { mask.SetInvalid(idx); continue; } + out[idx] = [&]() -> string_t { + Jsonb *t = BlobToJsonb(UnifiedVectorFormat::GetData(u[0])[ri[0]]); + int n = 0; + text **elems = ListToTextArr(child, UnifiedVectorFormat::GetData(u[1])[ri[1]], &n); + Jsonb *r = jsonb_delete_array(t, elems, n); + free(t); FreeTextArr(elems, n); + return JsonbToBlobN(result, r, mask, idx); + }(); + } +} + +static void Gen_jsonb_delete_path(DataChunk &args, ExpressionState &, Vector &result) { + EnsureMeosThreadInitialized(); + auto &lst = args.data[1]; lst.Flatten(args.size()); + auto &child = ListVector::GetEntry(lst); + child.Flatten(ListVector::GetListSize(lst)); + const idx_t rows = args.size(); + UnifiedVectorFormat u[2]; + for (idx_t c = 0; c < 2; c++) args.data[c].ToUnifiedFormat(rows, u[c]); + result.SetVectorType(VectorType::FLAT_VECTOR); + auto out = FlatVector::GetData(result); + auto &mask = FlatVector::Validity(result); + for (idx_t idx = 0; idx < rows; idx++) { + idx_t ri[2]; + bool valid = true; + for (idx_t c = 0; c < 2; c++) { + ri[c] = u[c].sel->get_index(idx); + valid = valid && u[c].validity.RowIsValid(ri[c]); + } + if (!valid) { mask.SetInvalid(idx); continue; } + out[idx] = [&]() -> string_t { + Jsonb *t = BlobToJsonb(UnifiedVectorFormat::GetData(u[0])[ri[0]]); + int n = 0; + text **elems = ListToTextArr(child, UnifiedVectorFormat::GetData(u[1])[ri[1]], &n); + Jsonb *r = jsonb_delete_path(t, elems, n); + free(t); FreeTextArr(elems, n); + return JsonbToBlobN(result, r, mask, idx); + }(); + } +} + +static void Gen_jsonb_insert(DataChunk &args, ExpressionState &, Vector &result) { + EnsureMeosThreadInitialized(); + auto &lst = args.data[1]; lst.Flatten(args.size()); + auto &child = ListVector::GetEntry(lst); + child.Flatten(ListVector::GetListSize(lst)); + const idx_t rows = args.size(); + UnifiedVectorFormat u[4]; + for (idx_t c = 0; c < 4; c++) args.data[c].ToUnifiedFormat(rows, u[c]); + result.SetVectorType(VectorType::FLAT_VECTOR); + auto out = FlatVector::GetData(result); + auto &mask = FlatVector::Validity(result); + for (idx_t idx = 0; idx < rows; idx++) { + idx_t ri[4]; + bool valid = true; + for (idx_t c = 0; c < 4; c++) { + ri[c] = u[c].sel->get_index(idx); + valid = valid && u[c].validity.RowIsValid(ri[c]); + } + if (!valid) { mask.SetInvalid(idx); continue; } + out[idx] = [&]() -> string_t { + Jsonb *t = BlobToJsonb(UnifiedVectorFormat::GetData(u[0])[ri[0]]); + int n = 0; + text **elems = ListToTextArr(child, UnifiedVectorFormat::GetData(u[1])[ri[1]], &n); + Jsonb *c2 = BlobToJsonb(UnifiedVectorFormat::GetData(u[2])[ri[2]]); + bool c3 = UnifiedVectorFormat::GetData(u[3])[ri[3]]; + Jsonb *r = jsonb_insert(t, elems, n, c2, c3); + free(t); FreeTextArr(elems, n); free(c2); + return JsonbToBlobN(result, r, mask, idx); + }(); + } +} + +static void Gen_jsonb_set(DataChunk &args, ExpressionState &, Vector &result) { + EnsureMeosThreadInitialized(); + auto &lst = args.data[1]; lst.Flatten(args.size()); + auto &child = ListVector::GetEntry(lst); + child.Flatten(ListVector::GetListSize(lst)); + const idx_t rows = args.size(); + UnifiedVectorFormat u[4]; + for (idx_t c = 0; c < 4; c++) args.data[c].ToUnifiedFormat(rows, u[c]); + result.SetVectorType(VectorType::FLAT_VECTOR); + auto out = FlatVector::GetData(result); + auto &mask = FlatVector::Validity(result); + for (idx_t idx = 0; idx < rows; idx++) { + idx_t ri[4]; + bool valid = true; + for (idx_t c = 0; c < 4; c++) { + ri[c] = u[c].sel->get_index(idx); + valid = valid && u[c].validity.RowIsValid(ri[c]); + } + if (!valid) { mask.SetInvalid(idx); continue; } + out[idx] = [&]() -> string_t { + Jsonb *t = BlobToJsonb(UnifiedVectorFormat::GetData(u[0])[ri[0]]); + int n = 0; + text **elems = ListToTextArr(child, UnifiedVectorFormat::GetData(u[1])[ri[1]], &n); + Jsonb *c2 = BlobToJsonb(UnifiedVectorFormat::GetData(u[2])[ri[2]]); + bool c3 = UnifiedVectorFormat::GetData(u[3])[ri[3]]; + Jsonb *r = jsonb_set(t, elems, n, c2, c3); + free(t); FreeTextArr(elems, n); free(c2); + return JsonbToBlobN(result, r, mask, idx); + }(); + } +} + static void Gen_jsonb_concat(DataChunk &args, ExpressionState &, Vector &result) { EnsureMeosThreadInitialized(); BinaryExecutor::ExecuteWithNulls(args.data[0], args.data[1], result, args.size(), @@ -4775,6 +4899,10 @@ void RegisterGenerated_meos_h3_base_inspection(ExtensionLoader &loader) { } void RegisterGenerated_meos_json_base_transf(ExtensionLoader &loader) { + RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbDeleteArray", {TJsonbTypes::jsonb(), LogicalType::LIST(LogicalType::VARCHAR)}, TJsonbTypes::jsonb(), Gen_jsonb_delete_array)); + RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbDeletePath", {TJsonbTypes::jsonb(), LogicalType::LIST(LogicalType::VARCHAR)}, TJsonbTypes::jsonb(), Gen_jsonb_delete_path)); + RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbInsert", {TJsonbTypes::jsonb(), LogicalType::LIST(LogicalType::VARCHAR), TJsonbTypes::jsonb(), LogicalType::BOOLEAN}, TJsonbTypes::jsonb(), Gen_jsonb_insert)); + RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbSet", {TJsonbTypes::jsonb(), LogicalType::LIST(LogicalType::VARCHAR), TJsonbTypes::jsonb(), LogicalType::BOOLEAN}, TJsonbTypes::jsonb(), Gen_jsonb_set)); RegisterSerializedScalarFunction(loader, ScalarFunction("jsonbConcat", {TJsonbTypes::jsonb(), TJsonbTypes::jsonb()}, TJsonbTypes::jsonb(), Gen_jsonb_concat)); } diff --git a/test/sql/parity/027_tjsonb.test b/test/sql/parity/027_tjsonb.test index dadcb323..380db66d 100644 --- a/test/sql/parity/027_tjsonb.test +++ b/test/sql/parity/027_tjsonb.test @@ -236,6 +236,32 @@ FROM (VALUES (1, '7', false), (2, '8', true), (3, NULL, false)) t(i, v, after) O 2 "{\"a\": [1, 2, 8]}"@2000-01-01 00:00:00+01 3 NULL +# The path functions of a plain jsonb take the path as a list of text, as their temporal +# twins do, and answer a jsonb: set and insert into an array, delete a key by its path and a +# list of keys, and extract the value at a path, NULL where the path names nothing. +query IIIII +SELECT jsonbSet(jsonb '{"a":[1,2]}', ['a','0'], jsonb '9', true)::VARCHAR, + jsonbInsert(jsonb '{"a":[1,2]}', ['a','1'], jsonb '9', false)::VARCHAR, + jsonbDeletePath(jsonb '{"a":{"b":1,"c":2}}', ['a','b'])::VARCHAR, + jsonbDeleteArray(jsonb '{"a":1,"b":2,"c":3}', ['a','c'])::VARCHAR, + jsonbExtractPath(jsonb '{"a":{"b":[1,2]}}', ['a','b'])::VARCHAR; +---- +{"a": [9, 2]} {"a": [1, 9, 2]} {"a": {"c": 2}} {"b": 2} [1, 2] + +query I +SELECT jsonbExtractPath(jsonb '{"a":1}', ['b']) IS NULL; +---- +true + +# Each row sets its own value, and a row whose value is NULL answers NULL. +query II +SELECT i, jsonbSet(jsonb '{"a":1}', ['a'], v::jsonb, true)::VARCHAR +FROM (VALUES (1, NULL), (2, '7'), (3, '8')) t(i, v) ORDER BY i; +---- +1 NULL +2 {"a": 7} +3 {"a": 8} + # A NULL value in the first row answers NULL there and leaves the next row its own value. query II SELECT i, tjsonbInsert(TJSONB('"{\"a\":[1,2]}"@2000-01-01'), ['a','1'], v::jsonb, false)::VARCHAR diff --git a/tools/codegen_duck_udfs.py b/tools/codegen_duck_udfs.py index d603ce9b..5b062317 100644 --- a/tools/codegen_duck_udfs.py +++ b/tools/codegen_duck_udfs.py @@ -2366,21 +2366,26 @@ def emit_body_bound_tail(f, kind, vary, tail, sig): return _close_body(L) def shape_text_array(f): - """(Temporal *, text **elems, int count, ) -> Temporal *. + """(Temporal *, text **elems, int count, ) -> Temporal *, + and the same over a set or a base value (Set *, Jsonb *). A `text **` and the `int` that follows it are ONE SQL argument — MobilityDB declares `text[]` (`sqlSignatures.args`), so DuckDB registers LIST(VARCHAR) and the count is supplied by the list's own length rather than exposed.""" ins, out = classify(f) if out is not None or len(ins) < 3: return None - # The path array reads the same over a SET as over a temporal — MobilityDB declares - # `jsonbsetExtractPath(jsonbset, text[], text)` beside `tjsonbExtractPath(tjsonb, ...)` — + # The path array reads the same over a SET and a base value as over a temporal — + # MobilityDB declares `jsonbsetExtractPath(jsonbset, text[], text)` and + # `jsonbSet(jsonb, text[], jsonb, boolean)` beside `tjsonbExtractPath(tjsonb, ...)` — # so the operand family is what varies, not the shape. A set scopes through # set_reg_scope (`jsonbset_*` -> SetTypes::jsonbset()), which is a different map from the # temporal reg_scope: reg_scope answers None for EVERY `jsonbset_*` name, including the # ones the set shapes already emit, so reading it for a set operand rejects the family. + # A base value scopes to its own type (PTR_IN), as #shape_baseval_scalar registers it. ob = base(ins[0]["canonical"]) - if ob not in ("Temporal", "Set") or not norm(ins[0]["canonical"]).endswith("*"): + if ob not in ("Temporal", "Set") and ob not in BASEVAL_PTR_IN: + return None + if not norm(ins[0]["canonical"]).endswith("*"): return None # `base()` answers the sentinel for a double pointer, so the test reads the normalised # spelling: a `text **` is the path/keys array MobilityDB declares as `text[]`. @@ -2401,13 +2406,19 @@ def shape_text_array(f): bound = ((f.get("shape") or {}).get("boundArgs") or {}) tail = _build_tail(ins[3:], bound) if tail is None: return None - if (set_reg_scope if ob == "Set" else reg_scope)(f["name"]) is None: return None + if ob in ("Temporal", "Set") and \ + (set_reg_scope if ob == "Set" else reg_scope)(f["name"]) is None: + return None rb = base(f["returnType"]["canonical"]); rn = norm(f["returnType"]["canonical"]) # The operand family and the result family are the same one: a set path operation answers a - # set (`jsonbset_extract_path` -> `Set *`), a temporal one a temporal. Accepting a crossed - # pair would emit a body marshalling the result through the wrong `*ToBlobN`. + # set (`jsonbset_extract_path` -> `Set *`), a temporal one a temporal, a base value its own + # type (`jsonb_set` -> `Jsonb *`). Accepting a crossed pair would emit a body marshalling the + # result through the wrong `*ToBlobN`. if not (rb == ob and rn.endswith("*")): return None + if ob in BASEVAL_PTR_IN and ob not in PTR_RET: return None sig = ["LogicalType::LIST(LogicalType::VARCHAR)"] + _tail_sig(tail) + if ob in BASEVAL_PTR_IN: + return ("base:" + ob, PTR_RET[ob][0], tail, sig) # `MD_SET` is the set counterpart of the `MD_TEMPORAL` sentinel: the registration resolves it # per registered accessor, so the result reads as `jsonbset` rather than the raw BLOB the # value travels in. Registering the storage type instead builds green and passes the suite @@ -2422,9 +2433,15 @@ def emit_body_text_array(f, kind, tail, sig): " auto &child = ListVector::GetEntry(lst);", " child.Flatten(ListVector::GetListSize(lst));"] # The operand and result marshal through the family the shape resolved: a set reads with - # BlobToSet and answers through SetToBlobN, a temporal with BlobToTemporal/TemporalToBlobN. - ctype, blob_in, blob_out = (("Set", "BlobToSet", "SetToBlobN") if kind == "set" - else ("Temporal", "BlobToTemporal", "TemporalToBlobN")) + # BlobToSet and answers through SetToBlobN, a temporal with BlobToTemporal/TemporalToBlobN, + # a base value with its own PTR_IN reader and {Base}ToBlobN (BlobToJsonb/JsonbToBlobN). + if kind.startswith("base:"): + ctype = kind.split(":", 1)[1] + blob_in = PTR_IN[ctype][1].split("(", 1)[0] + blob_out = PTR_RET[ctype][1].split("(", 1)[0] + "N" + else: + ctype, blob_in, blob_out = (("Set", "BlobToSet", "SetToBlobN") if kind == "set" + else ("Temporal", "BlobToTemporal", "TemporalToBlobN")) _row_frame_open(L, len(sig) + 1, "string_t") L += [f" {ctype} *t = {blob_in}({_row_get('string_t', 0)});", " int n = 0;", @@ -7774,8 +7791,11 @@ def gen_cpp(fns, out_path, declared=None, aliases=None): # `scope, accs` above reads reg_scope, which resolves TEMPORAL scope and answers # None for every set name; the set operand takes the set map the shape already # cleared it on, the way the set emit paths below read set_reg_scope. + # A base value registers over its own type alone. if kind == "set": scope, accs = set_reg_scope(fn) + elif kind.startswith("base:"): + scope, accs = "types", [PTR_IN[kind.split(":", 1)[1]][0]] bodies.append(emit_body_text_array(f, kind, tail, sig)) _rest = "".join(", %s" % x for x in sig[1:]) argsig = "{type, %s%s}" % (sig[0], _rest)