Skip to content

Commit

Permalink
sql: implement array functions for JSON arrays
Browse files Browse the repository at this point in the history
My answer on
https://stackoverflow.com/questions/72347170/delete-element-from-jsonb-array-in-cockaroachdb/72351955#72351955
made me sad, and it's not the first time I've had trouble
manipulating a JSONB array.

There are a few different, non-mutually-exclusive ways we
could make this easier. We could allow aggregate(generator) functions
in scalar contexts. We could implement more JSON functions as needed.

This PR instead just overloads every function that takes a json[]
 argument (which is a datum type we don't allow in columns) to also
be able to take a json array argument, and alters the return type
accordingly. It's not particularly performance-optimized but we can
replace individual implementations if needed.

That said, this is an unauthorized spike, what do you think?

Release note (sql change): Built-in array functions array_append, array_prepend, array_cat, array_remove, array_replace, array_position, and array_positions may now be used with jsonb arrays.
  • Loading branch information
HonoreDB committed May 24, 2022
1 parent 0a34479 commit 2762b63
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 1 deletion.
14 changes: 14 additions & 0 deletions docs/generated/sql/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
</span></td></tr>
<tr><td><a name="array_append"></a><code>array_append(array: geometry[], elem: geometry) &rarr; geometry[]</code></td><td><span class="funcdesc"><p>Appends <code>elem</code> to <code>array</code>, returning the result.</p>
</span></td></tr>
<tr><td><a name="array_append"></a><code>array_append(array: jsonb, elem: jsonb) &rarr; jsonb</code></td><td><span class="funcdesc"><p>Appends <code>elem</code> to <code>array</code>, returning the result.</p>
</span></td></tr>
<tr><td><a name="array_append"></a><code>array_append(array: jsonb[], elem: jsonb) &rarr; jsonb[]</code></td><td><span class="funcdesc"><p>Appends <code>elem</code> to <code>array</code>, returning the result.</p>
</span></td></tr>
<tr><td><a name="array_append"></a><code>array_append(array: oid[], elem: oid) &rarr; oid[]</code></td><td><span class="funcdesc"><p>Appends <code>elem</code> to <code>array</code>, returning the result.</p>
Expand Down Expand Up @@ -81,6 +83,8 @@
</span></td></tr>
<tr><td><a name="array_cat"></a><code>array_cat(left: geometry[], right: geometry[]) &rarr; geometry[]</code></td><td><span class="funcdesc"><p>Appends two arrays.</p>
</span></td></tr>
<tr><td><a name="array_cat"></a><code>array_cat(left: jsonb, right: jsonb) &rarr; jsonb</code></td><td><span class="funcdesc"><p>Appends two arrays.</p>
</span></td></tr>
<tr><td><a name="array_cat"></a><code>array_cat(left: jsonb[], right: jsonb[]) &rarr; jsonb[]</code></td><td><span class="funcdesc"><p>Appends two arrays.</p>
</span></td></tr>
<tr><td><a name="array_cat"></a><code>array_cat(left: oid[], right: oid[]) &rarr; oid[]</code></td><td><span class="funcdesc"><p>Appends two arrays.</p>
Expand Down Expand Up @@ -129,6 +133,8 @@
</span></td></tr>
<tr><td><a name="array_position"></a><code>array_position(array: geometry[], elem: geometry) &rarr; <a href="int.html">int</a></code></td><td><span class="funcdesc"><p>Return the index of the first occurrence of <code>elem</code> in <code>array</code>.</p>
</span></td></tr>
<tr><td><a name="array_position"></a><code>array_position(array: jsonb, elem: jsonb) &rarr; <a href="int.html">int</a></code></td><td><span class="funcdesc"><p>Return the index of the first occurrence of <code>elem</code> in <code>array</code>.</p>
</span></td></tr>
<tr><td><a name="array_position"></a><code>array_position(array: jsonb[], elem: jsonb) &rarr; <a href="int.html">int</a></code></td><td><span class="funcdesc"><p>Return the index of the first occurrence of <code>elem</code> in <code>array</code>.</p>
</span></td></tr>
<tr><td><a name="array_position"></a><code>array_position(array: oid[], elem: oid) &rarr; <a href="int.html">int</a></code></td><td><span class="funcdesc"><p>Return the index of the first occurrence of <code>elem</code> in <code>array</code>.</p>
Expand Down Expand Up @@ -173,6 +179,8 @@
</span></td></tr>
<tr><td><a name="array_positions"></a><code>array_positions(array: geometry[], elem: geometry) &rarr; <a href="int.html">int</a>[]</code></td><td><span class="funcdesc"><p>Returns and array of indexes of all occurrences of <code>elem</code> in <code>array</code>.</p>
</span></td></tr>
<tr><td><a name="array_positions"></a><code>array_positions(array: jsonb, elem: jsonb) &rarr; <a href="int.html">int</a>[]</code></td><td><span class="funcdesc"><p>Returns and array of indexes of all occurrences of <code>elem</code> in <code>array</code>.</p>
</span></td></tr>
<tr><td><a name="array_positions"></a><code>array_positions(array: jsonb[], elem: jsonb) &rarr; <a href="int.html">int</a>[]</code></td><td><span class="funcdesc"><p>Returns and array of indexes of all occurrences of <code>elem</code> in <code>array</code>.</p>
</span></td></tr>
<tr><td><a name="array_positions"></a><code>array_positions(array: oid[], elem: oid) &rarr; <a href="int.html">int</a>[]</code></td><td><span class="funcdesc"><p>Returns and array of indexes of all occurrences of <code>elem</code> in <code>array</code>.</p>
Expand Down Expand Up @@ -217,6 +225,8 @@
</span></td></tr>
<tr><td><a name="array_prepend"></a><code>array_prepend(elem: geometry, array: geometry[]) &rarr; geometry[]</code></td><td><span class="funcdesc"><p>Prepends <code>elem</code> to <code>array</code>, returning the result.</p>
</span></td></tr>
<tr><td><a name="array_prepend"></a><code>array_prepend(elem: jsonb, array: jsonb) &rarr; jsonb</code></td><td><span class="funcdesc"><p>Prepends <code>elem</code> to <code>array</code>, returning the result.</p>
</span></td></tr>
<tr><td><a name="array_prepend"></a><code>array_prepend(elem: jsonb, array: jsonb[]) &rarr; jsonb[]</code></td><td><span class="funcdesc"><p>Prepends <code>elem</code> to <code>array</code>, returning the result.</p>
</span></td></tr>
<tr><td><a name="array_prepend"></a><code>array_prepend(elem: oid, array: oid[]) &rarr; oid[]</code></td><td><span class="funcdesc"><p>Prepends <code>elem</code> to <code>array</code>, returning the result.</p>
Expand Down Expand Up @@ -261,6 +271,8 @@
</span></td></tr>
<tr><td><a name="array_remove"></a><code>array_remove(array: geometry[], elem: geometry) &rarr; geometry[]</code></td><td><span class="funcdesc"><p>Remove from <code>array</code> all elements equal to <code>elem</code>.</p>
</span></td></tr>
<tr><td><a name="array_remove"></a><code>array_remove(array: jsonb, elem: jsonb) &rarr; jsonb</code></td><td><span class="funcdesc"><p>Remove from <code>array</code> all elements equal to <code>elem</code>.</p>
</span></td></tr>
<tr><td><a name="array_remove"></a><code>array_remove(array: jsonb[], elem: jsonb) &rarr; jsonb[]</code></td><td><span class="funcdesc"><p>Remove from <code>array</code> all elements equal to <code>elem</code>.</p>
</span></td></tr>
<tr><td><a name="array_remove"></a><code>array_remove(array: oid[], elem: oid) &rarr; oid[]</code></td><td><span class="funcdesc"><p>Remove from <code>array</code> all elements equal to <code>elem</code>.</p>
Expand Down Expand Up @@ -305,6 +317,8 @@
</span></td></tr>
<tr><td><a name="array_replace"></a><code>array_replace(array: geometry[], toreplace: geometry, replacewith: geometry) &rarr; geometry[]</code></td><td><span class="funcdesc"><p>Replace all occurrences of <code>toreplace</code> in <code>array</code> with <code>replacewith</code>.</p>
</span></td></tr>
<tr><td><a name="array_replace"></a><code>array_replace(array: jsonb, toreplace: jsonb, replacewith: jsonb) &rarr; jsonb</code></td><td><span class="funcdesc"><p>Replace all occurrences of <code>toreplace</code> in <code>array</code> with <code>replacewith</code>.</p>
</span></td></tr>
<tr><td><a name="array_replace"></a><code>array_replace(array: jsonb[], toreplace: jsonb, replacewith: jsonb) &rarr; jsonb[]</code></td><td><span class="funcdesc"><p>Replace all occurrences of <code>toreplace</code> in <code>array</code> with <code>replacewith</code>.</p>
</span></td></tr>
<tr><td><a name="array_replace"></a><code>array_replace(array: oid[], toreplace: oid, replacewith: oid) &rarr; oid[]</code></td><td><span class="funcdesc"><p>Replace all occurrences of <code>toreplace</code> in <code>array</code> with <code>replacewith</code>.</p>
Expand Down
5 changes: 5 additions & 0 deletions pkg/sql/logictest/testdata/logic_test/array
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,11 @@ SELECT array_remove(NULL::INT[], NULL::INT)
----
NULL

query T
SELECT array_remove('[1,2,3,2]'::jsonb, '2'::jsonb)
----
[1, 3]

# ARRAY_REPLACE function

query T
Expand Down
77 changes: 76 additions & 1 deletion pkg/sql/sem/builtins/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -8008,12 +8008,18 @@ var similarOverloads = []tree.Overload{
}

func arrayBuiltin(impl func(*types.T) tree.Overload) builtinDefinition {
overloads := make([]tree.Overload, 0, len(types.Scalar)+2)
overloads := make([]tree.Overload, 0, len(types.Scalar)+3)
for _, typ := range append(types.Scalar, types.AnyEnum) {
if ok, _ := types.IsValidArrayElementType(typ); ok {
overloads = append(overloads, impl(typ))
}
}

arrayOfJSONImpl := impl(types.Jsonb)
if _, ok := arrayOfJSONImpl.Types.(tree.ArgTypes); ok && arrayOfJSONImpl.Fn != nil {
overloads = append(overloads, makeJSONArrayOverload(arrayOfJSONImpl))
}

// Prevent usage in DistSQL because it cannot handle arrays of untyped tuples.
tupleOverload := impl(types.AnyTuple)
tupleOverload.DistsqlBlocklist = true
Expand All @@ -8024,6 +8030,75 @@ func arrayBuiltin(impl func(*types.T) tree.Overload) builtinDefinition {
}
}

// Convert a json[] function to a json_array function.
func makeJSONArrayOverload(arrayOfJSONImpl tree.Overload) tree.Overload {
isJSONArray := func(t *types.T) bool {
return t != nil && t.ArrayContents() == types.Jsonb
}
castToJSON := func(t *types.T) *types.T {
if isJSONArray(t) {
return types.Jsonb
}
return t
}
newImpl := arrayOfJSONImpl
newImpl.Types = make(tree.ArgTypes, len(arrayOfJSONImpl.Types.(tree.ArgTypes)))
argsToCast := make([]int, 0)
// TODO: Don't panic if it's not an ArgTypes
for i, t := range arrayOfJSONImpl.Types.(tree.ArgTypes) {
if isJSONArray(t.Typ) {
argsToCast = append(argsToCast, i)
newImpl.Types.(tree.ArgTypes)[i] = struct {
Name string
Typ *types.T
}{t.Name, types.Jsonb}
} else {
newImpl.Types.(tree.ArgTypes)[i] = t
}
}
newImpl.ReturnType = func(args []tree.TypedExpr) *types.T {
return castToJSON(arrayOfJSONImpl.ReturnType(args))
}
newImpl.Fn = func(ctx *eval.Context, args tree.Datums) (tree.Datum, error) {
newArgs := append(tree.Datums{}, args...)
for _, i := range argsToCast {
j := tree.MustBeDJSON(args[i])
if j.Type() != json.ArrayJSONType {
return nil, pgerror.Newf(pgcode.InvalidParameterValue,
"cannot call array function on non-array json value")
}
ary := tree.NewDArray(types.Jsonb)
for idx := 0; idx < j.Len(); idx++ {
json, err := j.FetchValIdx(idx)
if err != nil {
return nil, err
}
d, err := tree.MakeDJSON(json)
if err != nil {
return nil, err
}
if err := ary.Append(d); err != nil {
return nil, err
}
}
newArgs[i] = ary
}
ret, err := arrayOfJSONImpl.Fn.(eval.FnOverload)(ctx, newArgs)
if err == nil && isJSONArray(ret.ResolvedType()) {
dArray := tree.MustBeDArray(ret).Array
builder := json.NewArrayBuilder(len(dArray))
for _, elt := range dArray {
builder.Add(tree.MustBeDJSON(elt).JSON)
}
return tree.NewDJSON(builder.Build()), nil
}
return ret, err
}

return newImpl

}

func setProps(props tree.FunctionProperties, d builtinDefinition) builtinDefinition {
d.props = props
return d
Expand Down

0 comments on commit 2762b63

Please sign in to comment.