From 850b9923f2cbf64c8600fc5526b36ae126a0a64e Mon Sep 17 00:00:00 2001 From: fengttt Date: Sun, 7 Jul 2024 21:37:10 -0700 Subject: [PATCH] Add jq and try_jq function. (#17217) Added jq and try_jq function. Approved by: @m-schen, @zhangxu19830126, @heni02, @XuPeng-SH --- go.mod | 6 +- go.sum | 15 +- pkg/container/nulls/nulls.go | 15 +- pkg/container/vector/functionTools.go | 17 + pkg/sql/plan/function/baseTemplate.go | 29 +- pkg/sql/plan/function/func_builtin_jq.go | 491 ++++++++++++++++++ pkg/sql/plan/function/function.go | 9 + pkg/sql/plan/function/function_id.go | 4 + pkg/sql/plan/function/list_builtIn.go | 40 ++ .../distributed/cases/function/func_jq.result | 306 +++++++++++ test/distributed/cases/function/func_jq.sql | 125 +++++ 11 files changed, 1024 insertions(+), 33 deletions(-) create mode 100644 pkg/sql/plan/function/func_builtin_jq.go create mode 100644 test/distributed/cases/function/func_jq.result create mode 100644 test/distributed/cases/function/func_jq.sql diff --git a/go.mod b/go.mod index 93053987210f..dca4fffe1937 100644 --- a/go.mod +++ b/go.mod @@ -38,6 +38,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/uuid v1.6.0 github.com/hashicorp/memberlist v0.3.1 + github.com/itchyny/gojq v0.12.16 github.com/jhump/protoreflect v1.15.2 github.com/lni/dragonboat/v4 v4.0.0-20220815145555-6f622e8bcbef github.com/lni/goutils v1.3.1-0.20220604063047-388d67b4dbc4 @@ -68,7 +69,7 @@ require ( go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20231006140011-7918f672742d golang.org/x/sync v0.6.0 - golang.org/x/sys v0.18.0 + golang.org/x/sys v0.20.0 gonum.org/v1/gonum v0.14.0 google.golang.org/grpc v1.62.1 google.golang.org/protobuf v1.33.0 @@ -87,6 +88,7 @@ require ( github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/gosimple/slug v1.13.1 // indirect github.com/gosimple/unidecode v1.0.1 // indirect + github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect @@ -98,7 +100,7 @@ require ( github.com/opencontainers/runtime-spec v1.0.2 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rs/xid v1.5.0 // indirect github.com/segmentio/encoding v0.3.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect diff --git a/go.sum b/go.sum index c4418ca76879..ef64afa2b2de 100644 --- a/go.sum +++ b/go.sum @@ -399,6 +399,10 @@ github.com/iris-contrib/i18n v0.0.0-20171121225848-987a633949d0/go.mod h1:pMCz62 github.com/iris-contrib/jade v1.1.3/go.mod h1:H/geBymxJhShH5kecoiOCSssPX7QWYH7UaeZTSWddIk= github.com/iris-contrib/pongo2 v0.0.1/go.mod h1:Ssh+00+3GAZqSQb30AvBRNxBx7rf0GqwkjqxNd0u65g= github.com/iris-contrib/schema v0.0.1/go.mod h1:urYA3uvUNG1TIIjOSCzHr9/LmbQo8LrOcOqfqxa4hXw= +github.com/itchyny/gojq v0.12.16 h1:yLfgLxhIr/6sJNVmYfQjTIv0jGctu6/DgDoivmxTr7g= +github.com/itchyny/gojq v0.12.16/go.mod h1:6abHbdC2uB9ogMS38XsErnfqJ94UlngIJGlRAIj4jTM= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/jhump/protoreflect v1.15.2 h1:7YppbATX94jEt9KLAc5hICx4h6Yt3SaavhQRsIUEHP0= github.com/jhump/protoreflect v1.15.2/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -489,8 +493,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= -github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -628,8 +632,9 @@ github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdO github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.0.1-alpha.1/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -956,8 +961,8 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/pkg/container/nulls/nulls.go b/pkg/container/nulls/nulls.go index 5295cd9fd421..13a2bc189194 100644 --- a/pkg/container/nulls/nulls.go +++ b/pkg/container/nulls/nulls.go @@ -329,18 +329,11 @@ func (nsp *Nulls) ReadNoCopy(data []byte) error { return nil } -// XXX This API is foundementally broken. Depends on empty or not -// this shit may or may not modify nsp. -func (nsp *Nulls) Or(m *Nulls) *Nulls { - if m.np.EmptyByFlag() { - return nsp - } - if nsp.np.EmptyByFlag() { - return m +// Or the m Nulls into nsp. +func (nsp *Nulls) Or(m *Nulls) { + if !m.np.EmptyByFlag() { + nsp.np.Or(&m.np) } - - nsp.np.Or(&m.np) - return nsp } func (nsp *Nulls) IsSame(m *Nulls) bool { diff --git a/pkg/container/vector/functionTools.go b/pkg/container/vector/functionTools.go index 5f39d5cd8302..c16db6bd6e4f 100644 --- a/pkg/container/vector/functionTools.go +++ b/pkg/container/vector/functionTools.go @@ -19,6 +19,7 @@ import ( "github.com/matrixorigin/matrixone/pkg/common/bitmap" "github.com/matrixorigin/matrixone/pkg/common/mpool" + "github.com/matrixorigin/matrixone/pkg/container/nulls" "github.com/matrixorigin/matrixone/pkg/container/types" ) @@ -457,6 +458,22 @@ func (fr *FunctionResult[T]) AppendMustNullForBytesResult() error { return appendOneFixed(fr.vec, v, true, fr.mp) } +func (fr *FunctionResult[T]) AddNullRange(start, end uint64) { + fr.vec.nsp.AddRange(start, end) +} + +func (fr *FunctionResult[T]) AddNullAt(idx uint64) { + fr.vec.nsp.Add(idx) +} + +func (fr *FunctionResult[T]) AddNulls(ns *nulls.Nulls) { + fr.vec.nsp.Or(ns) +} + +func (fr *FunctionResult[T]) GetNullAt(idx uint64) bool { + return fr.vec.nsp.Contains(idx) +} + func (fr *FunctionResult[T]) GetType() types.Type { return *fr.vec.GetType() } diff --git a/pkg/sql/plan/function/baseTemplate.go b/pkg/sql/plan/function/baseTemplate.go index 6462908b06f7..1b0d52e55af1 100644 --- a/pkg/sql/plan/function/baseTemplate.go +++ b/pkg/sql/plan/function/baseTemplate.go @@ -2353,33 +2353,32 @@ func opBinaryStrStrToFixedWithErrorCheck[Tr types.FixedSizeTExceptStrType]( p1 := vector.GenerateFunctionStrParameter(parameters[0]) p2 := vector.GenerateFunctionStrParameter(parameters[1]) rs := vector.MustFunctionResult[Tr](result) - rsVec := rs.GetResultVector() - rss := vector.MustFixedCol[Tr](rsVec) + rss := vector.MustFixedCol[Tr](rs.GetResultVector()) c1, c2 := parameters[0].IsConst(), parameters[1].IsConst() - rsNull := rsVec.GetNulls() rsAnyNull := false if selectList != nil { if selectList.IgnoreAllRow() { - nulls.AddRange(rsNull, 0, uint64(length)) + rs.AddNullRange(0, uint64(length)) return nil } if !selectList.ShouldEvalAllRow() { rsAnyNull = true for i := range selectList.SelectList { if selectList.Contains(uint64(i)) { - rsNull.Add(uint64(i)) + rs.AddNullAt(uint64(i)) } } } } + if c1 && c2 { v1, null1 := p1.GetStrValue(0) v2, null2 := p2.GetStrValue(0) ifNull := null1 || null2 if ifNull { - nulls.AddRange(rsNull, 0, uint64(length)) + rs.AddNullRange(0, uint64(length)) } else { r, err := fn(functionUtil.QuickBytesToStr(v1), functionUtil.QuickBytesToStr(v2)) if err != nil { @@ -2397,14 +2396,14 @@ func opBinaryStrStrToFixedWithErrorCheck[Tr types.FixedSizeTExceptStrType]( if c1 { v1, null1 := p1.GetStrValue(0) if null1 { - nulls.AddRange(rsNull, 0, uint64(length)) + rs.AddNullRange(0, uint64(length)) } else { x := functionUtil.QuickBytesToStr(v1) if p2.WithAnyNullValue() || rsAnyNull { - nulls.Or(rsNull, parameters[1].GetNulls(), rsNull) + rs.AddNulls(parameters[1].GetNulls()) rowCount := uint64(length) for i := uint64(0); i < rowCount; i++ { - if rsNull.Contains(i) { + if rs.GetNullAt(i) { continue } v2, _ := p2.GetStrValue(i) @@ -2430,14 +2429,14 @@ func opBinaryStrStrToFixedWithErrorCheck[Tr types.FixedSizeTExceptStrType]( if c2 { v2, null2 := p2.GetStrValue(0) if null2 { - nulls.AddRange(rsNull, 0, uint64(length)) + rs.AddNullRange(0, uint64(length)) } else { y := functionUtil.QuickBytesToStr(v2) if p1.WithAnyNullValue() || rsAnyNull { - nulls.Or(rsNull, parameters[0].GetNulls(), rsNull) + rs.AddNulls(parameters[0].GetNulls()) rowCount := uint64(length) for i := uint64(0); i < rowCount; i++ { - if rsNull.Contains(i) { + if rs.GetNullAt(i) { continue } v1, _ := p1.GetStrValue(i) @@ -2462,11 +2461,11 @@ func opBinaryStrStrToFixedWithErrorCheck[Tr types.FixedSizeTExceptStrType]( // basic case. if p1.WithAnyNullValue() || p2.WithAnyNullValue() || rsAnyNull { - nulls.Or(rsNull, parameters[0].GetNulls(), rsNull) - nulls.Or(rsNull, parameters[1].GetNulls(), rsNull) + rs.AddNulls(parameters[0].GetNulls()) + rs.AddNulls(parameters[1].GetNulls()) rowCount := uint64(length) for i := uint64(0); i < rowCount; i++ { - if rsNull.Contains(i) { + if rs.GetNullAt(i) { continue } v1, _ := p1.GetStrValue(i) diff --git a/pkg/sql/plan/function/func_builtin_jq.go b/pkg/sql/plan/function/func_builtin_jq.go new file mode 100644 index 000000000000..659d7dfe545d --- /dev/null +++ b/pkg/sql/plan/function/func_builtin_jq.go @@ -0,0 +1,491 @@ +// Copyright 2021 - 2022 Matrix Origin +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package function + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "math/big" + "sort" + "strconv" + "unicode/utf8" + + "github.com/itchyny/gojq" + "github.com/matrixorigin/matrixone/pkg/container/types" + "github.com/matrixorigin/matrixone/pkg/container/vector" + "github.com/matrixorigin/matrixone/pkg/vm/process" +) + +// jq: see https://github.com/itchyny/gojq +// +// jq(json, query): jq is a function that takes a json string and a jq query. +// It returns the result of the jq query on the json string. If either json +// or query is NULL, the result is NULL. +// +// try_jq: try_jq is the same as jq, but it will not return an error +// if either the json data or jq query has errors. Instead, it will +// return a NULL value. + +const ( + jqMapSizeLimit = 10 +) + +type opBuiltInJq struct { + jqCache map[string]*gojq.Code + enc encoder +} + +func newOpBuiltInJq() *opBuiltInJq { + var op opBuiltInJq + op.jqCache = make(map[string]*gojq.Code) + op.enc.intialize(false, 0) + return &op +} + +func (op *opBuiltInJq) jq(params []*vector.Vector, result vector.FunctionResultWrapper, + proc *process.Process, length int, selectList *FunctionSelectList) error { + return op.tryJqImpl(params, result, proc, length, selectList, false) +} + +func (op *opBuiltInJq) tryJq(params []*vector.Vector, result vector.FunctionResultWrapper, + proc *process.Process, length int, selectList *FunctionSelectList) error { + return op.tryJqImpl(params, result, proc, length, selectList, true) +} + +func (op *opBuiltInJq) tryJqImpl(params []*vector.Vector, result vector.FunctionResultWrapper, + proc *process.Process, length int, selectList *FunctionSelectList, + isTry bool) error { + p1 := vector.GenerateFunctionStrParameter(params[0]) + p2 := vector.GenerateFunctionStrParameter(params[1]) + rs := vector.MustFunctionResult[types.Varlena](result) + + // special case + if selectList.IgnoreAllRow() { + rs.AddNullRange(0, uint64(length)) + return nil + } + + c1, c2 := params[0].IsConst(), params[1].IsConst() + // if both parameters are constant, just eval + if c1 && c2 { + v1, null1 := p1.GetStrValue(0) + v2, null2 := p2.GetStrValue(0) + if null1 || null2 { + rs.AddNullRange(0, uint64(length)) + } else { + code, err := op.getJqCode(string(v2)) + if err == nil { + err = op.jqImpl(v1, code) + } + if err != nil { + if isTry { + rs.AddNullRange(0, uint64(length)) + return nil + } else { + return err + } + } + rs.AppendBytes(op.enc.bytes(), false) + op.enc.done() + } + return nil + } else if c1 { + // this is the strange version, we eval many jq again one piece + // of json string. + v1, null1 := p1.GetStrValue(0) + if null1 { + rs.AddNullRange(0, uint64(length)) + return nil + } else { + for i := uint64(0); i < uint64(length); i++ { + v2, null2 := p2.GetStrValue(i) + if null2 || selectList.Contains(i) { + rs.AppendBytes(nil, true) + } else { + code, err := op.getJqCode(string(v2)) + if err == nil { + err = op.jqImpl(v1, code) + } + if err != nil { + if isTry { + rs.AppendBytes(nil, true) + } else { + return err + } + } else { + rs.AppendBytes(op.enc.bytes(), false) + op.enc.done() + } + } + } + } + return nil + } else if c2 { + // this is the common case that need to be optimized. + v2, null2 := p2.GetStrValue(0) + if null2 { + rs.AddNullRange(0, uint64(length)) + return nil + } + code, err := op.getJqCode(string(v2)) + if err != nil { + if isTry { + rs.AddNullRange(0, uint64(length)) + return nil + } else { + return err + } + } + + for i := uint64(0); i < uint64(length); i++ { + v1, null1 := p1.GetStrValue(i) + if null1 || selectList.Contains(i) { + rs.AppendBytes(nil, true) + } else { + err = op.jqImpl(v1, code) + if err != nil { + if isTry { + rs.AppendBytes(nil, true) + } else { + return err + } + } else { + rs.AppendBytes(op.enc.bytes(), false) + op.enc.done() + } + } + } + } else { + // both are not constant, this is the less likely case in real life. + for i := uint64(0); i < uint64(length); i++ { + v1, null1 := p1.GetStrValue(i) + v2, null2 := p2.GetStrValue(i) + if null1 || null2 || selectList.Contains(i) { + rs.AppendBytes(nil, true) + } else { + code, err := op.getJqCode(string(v2)) + if err == nil { + err = op.jqImpl(v1, code) + } + + if err != nil { + if isTry { + rs.AppendBytes(nil, true) + // continue + } else { + return err + } + } else { + rs.AppendBytes(op.enc.bytes(), false) + op.enc.done() + } + } + } + } + return nil +} + +// run jq. The result is stored in the encoder bytes(). If succeeded, caller +// must call .done() to reset the encoder. +func (op *opBuiltInJq) jqImpl(jsonStr []byte, code *gojq.Code) error { + // first, turn jsonstr to any + var jv any + err := json.Unmarshal(jsonStr, &jv) + if err != nil { + return err + } + + iter := code.Run(jv) + for { + v, ok := iter.Next() + if !ok { + break + } + if verr, ok := v.(error); ok { + op.enc.done() + return verr + } + + if err := op.enc.encode(v); err != nil { + op.enc.done() + return err + } + } + return nil +} + +func (op *opBuiltInJq) getJqCode(jq string) (*gojq.Code, error) { + code, ok := op.jqCache[jq] + if ok { + return code, nil + } + + pq, err := gojq.Parse(jq) + if err != nil { + return nil, err + } + + code, err = gojq.Compile(pq) + if err != nil { + return nil, err + } + + // if we have cached too many, we need to remove some + if len(op.jqCache) == jqMapSizeLimit { + for key := range op.jqCache { + delete(op.jqCache, key) + // regexp folks has a interesting way of doing this, + // they break here, just remove one element. It + // depends on go map implementation to remove the right + // element. Not convinced it is the right thing to do. + // Here, we remove all elements. + } + } + op.jqCache[jq] = code + return code, nil +} + +// This is a simplified version of the encoder in gojq/cli/encode.go. +// It is used to encode the result of jq functions. +// We removed all the terminal color related code and we write to buffer w +// and do not flush until the encoding is done. +type encoder struct { + w *bytes.Buffer + tab bool + indent int + depth int + buf [64]byte +} + +func (e *encoder) intialize(tab bool, indent int) { + e.w = new(bytes.Buffer) + e.tab = tab + e.indent = indent +} + +func (e *encoder) bytes() []byte { + return e.w.Bytes() +} +func (e *encoder) done() { + e.w.Reset() + e.depth = 0 +} + +func (e *encoder) encode(v any) error { + switch v := v.(type) { + case nil: + e.w.Write([]byte("null")) + case bool: + if v { + e.w.Write([]byte("true")) + } else { + e.w.Write([]byte("false")) + } + case int: + e.w.Write(strconv.AppendInt(e.buf[:0], int64(v), 10)) + case float64: + e.encodeFloat64(v) + case *big.Int: + e.w.Write(v.Append(e.buf[:0], 10)) + case string: + e.encodeString(v) + case []any: + if err := e.encodeArray(v); err != nil { + return err + } + case map[string]any: + if err := e.encodeObject(v); err != nil { + return err + } + default: + panic(fmt.Sprintf("invalid type: %[1]T (%[1]v)", v)) + } + return nil +} + +// ref: floatEncoder in encoding/json +func (e *encoder) encodeFloat64(f float64) { + if math.IsNaN(f) { + e.w.Write([]byte("null")) + return + } + if f >= math.MaxFloat64 { + f = math.MaxFloat64 + } else if f <= -math.MaxFloat64 { + f = -math.MaxFloat64 + } + format := byte('f') + if x := math.Abs(f); x != 0 && x < 1e-6 || x >= 1e21 { + format = 'e' + } + buf := strconv.AppendFloat(e.buf[:0], f, format, -1, 64) + if format == 'e' { + // clean up e-09 to e-9 + if n := len(buf); n >= 4 && buf[n-4] == 'e' && buf[n-3] == '-' && buf[n-2] == '0' { + buf[n-2] = buf[n-1] + buf = buf[:n-1] + } + } + e.w.Write(buf) +} + +// ref: encodeState#string in encoding/json +func (e *encoder) encodeString(s string) { + e.w.WriteByte('"') + start := 0 + for i := 0; i < len(s); { + if b := s[i]; b < utf8.RuneSelf { + if ' ' <= b && b <= '~' && b != '"' && b != '\\' { + i++ + continue + } + if start < i { + e.w.WriteString(s[start:i]) + } + switch b { + case '"': + e.w.WriteString(`\"`) + case '\\': + e.w.WriteString(`\\`) + case '\b': + e.w.WriteString(`\b`) + case '\f': + e.w.WriteString(`\f`) + case '\n': + e.w.WriteString(`\n`) + case '\r': + e.w.WriteString(`\r`) + case '\t': + e.w.WriteString(`\t`) + default: + const hex = "0123456789abcdef" + e.w.WriteString(`\u00`) + e.w.WriteByte(hex[b>>4]) + e.w.WriteByte(hex[b&0xF]) + } + i++ + start = i + continue + } + c, size := utf8.DecodeRuneInString(s[i:]) + if c == utf8.RuneError && size == 1 { + if start < i { + e.w.WriteString(s[start:i]) + } + e.w.WriteString(`\ufffd`) + i += size + start = i + continue + } + i += size + } + if start < len(s) { + e.w.WriteString(s[start:]) + } + e.w.WriteByte('"') +} + +func (e *encoder) encodeArray(vs []any) error { + e.writeByte('[') + e.depth += e.indent + for i, v := range vs { + if i > 0 { + e.writeByte(',') + } + if e.indent != 0 { + e.writeIndent() + } + if err := e.encode(v); err != nil { + return err + } + } + e.depth -= e.indent + if len(vs) > 0 && e.indent != 0 { + e.writeIndent() + } + e.writeByte(']') + return nil +} + +func (e *encoder) encodeObject(vs map[string]any) error { + e.writeByte('{') + e.depth += e.indent + type keyVal struct { + key string + val any + } + kvs := make([]keyVal, len(vs)) + var i int + for k, v := range vs { + kvs[i] = keyVal{k, v} + i++ + } + sort.Slice(kvs, func(i, j int) bool { + return kvs[i].key < kvs[j].key + }) + for i, kv := range kvs { + if i > 0 { + e.writeByte(',') + } + if e.indent != 0 { + e.writeIndent() + } + e.encodeString(kv.key) + e.writeByte(':') + if e.indent != 0 { + e.w.WriteByte(' ') + } + if err := e.encode(kv.val); err != nil { + return err + } + } + e.depth -= e.indent + if len(vs) > 0 && e.indent != 0 { + e.writeIndent() + } + e.writeByte('}') + return nil +} + +func (e *encoder) writeIndent() { + e.w.WriteByte('\n') + if n := e.depth; n > 0 { + if e.tab { + e.writeIndentInternal(n, "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t") + } else { + e.writeIndentInternal(n, " ") + } + } +} + +func (e *encoder) writeIndentInternal(n int, spaces string) { + if l := len(spaces); n <= l { + e.w.WriteString(spaces[:n]) + } else { + e.w.WriteString(spaces) + for n -= l; n > 0; n, l = n-l, l*2 { + if n < l { + l = n + } + e.w.Write(e.w.Bytes()[e.w.Len()-l:]) + } + } +} + +func (e *encoder) writeByte(b byte) { + e.w.WriteByte(b) +} diff --git a/pkg/sql/plan/function/function.go b/pkg/sql/plan/function/function.go index 1ec2deb7af77..6357b1d45efb 100644 --- a/pkg/sql/plan/function/function.go +++ b/pkg/sql/plan/function/function.go @@ -497,13 +497,22 @@ type FunctionSelectList struct { } func (selectList *FunctionSelectList) ShouldEvalAllRow() bool { + if selectList == nil { + return true + } return !selectList.AnyNull } func (selectList *FunctionSelectList) IgnoreAllRow() bool { + if selectList == nil { + return false + } return selectList.AllNull } func (selectList *FunctionSelectList) Contains(row uint64) bool { + if selectList == nil { + return false + } return !selectList.SelectList[row] } diff --git a/pkg/sql/plan/function/function_id.go b/pkg/sql/plan/function/function_id.go index 2c97b31e16f4..7434c06311ee 100644 --- a/pkg/sql/plan/function/function_id.go +++ b/pkg/sql/plan/function/function_id.go @@ -281,6 +281,8 @@ const ( JSON_EXTRACT JSON_QUOTE JSON_UNQUOTE + JQ + TRY_JQ FORMAT SLEEP INSTR @@ -573,6 +575,8 @@ var functionIdRegister = map[string]int32{ "collation": COLLATION, "json_extract": JSON_EXTRACT, "json_quote": JSON_QUOTE, + "jq": JQ, + "try_jq": TRY_JQ, "enable_fault_injection": ENABLE_FAULT_INJECTION, "disable_fault_injection": DISABLE_FAULT_INJECTION, "dense_rank": DENSE_RANK, diff --git a/pkg/sql/plan/function/list_builtIn.go b/pkg/sql/plan/function/list_builtIn.go index c53fbd30a3bb..cf30077391aa 100644 --- a/pkg/sql/plan/function/list_builtIn.go +++ b/pkg/sql/plan/function/list_builtIn.go @@ -674,6 +674,46 @@ var supportedStringBuiltIns = []FuncNew{ }, }, + // function `jq` + { + functionId: JQ, + class: plan.Function_STRICT, + layout: STANDARD_FUNCTION, + checkFn: fixedTypeMatch, + Overloads: []overload{ + { + overloadId: 0, + args: []types.T{types.T_varchar, types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_varchar.ToType() + }, + newOp: func() executeLogicOfOverload { + return newOpBuiltInJq().jq + }, + }, + }, + }, + + // function `try_jq` + { + functionId: TRY_JQ, + class: plan.Function_STRICT, + layout: STANDARD_FUNCTION, + checkFn: fixedTypeMatch, + Overloads: []overload{ + { + overloadId: 0, + args: []types.T{types.T_varchar, types.T_varchar}, + retType: func(parameters []types.Type) types.Type { + return types.T_varchar.ToType() + }, + newOp: func() executeLogicOfOverload { + return newOpBuiltInJq().tryJq + }, + }, + }, + }, + // function `left` { functionId: LEFT, diff --git a/test/distributed/cases/function/func_jq.result b/test/distributed/cases/function/func_jq.result new file mode 100644 index 000000000000..ae52c92394a2 --- /dev/null +++ b/test/distributed/cases/function/func_jq.result @@ -0,0 +1,306 @@ +select jq('{"foo": 128}', '.foo'); +jq({"foo": 128}, .foo) +128 +select try_jq('{"foo": 128}', '.foo'); +try_jq({"foo": 128}, .foo) +128 +select jq('{"a": {"b": 42}}', '.a.b'); +jq({"a": {"b": 42}}, .a.b) +42 +select try_jq('{"a": {"b": 42}}', '.a.b'); +try_jq({"a": {"b": 42}}, .a.b) +42 +select jq(null, '.foo'); +jq(null, .foo) +null +select try_jq(null, '.foo'); +try_jq(null, .foo) +null +select jq('{"a": {"b": 42}}', null); +jq({"a": {"b": 42}}, null) +null +select try_jq('{"a": {"b": 42}}', null); +try_jq({"a": {"b": 42}}, null) +null +select jq('{"id": "sample", "10": {"b": 42}}', '{(.id): .["10"].b}'); +jq({"id": "sample", "10": {"b": 42}}, {(.id): .["10"].b}) +{"sample":42} +select jq('[{"id":1},{"id":2},{"id":3}]', '.[] | .id'); +jq([{"id":1},{"id":2},{"id":3}], .[] | .id) +123 +select jq('{"a":1, "b":2}', '.a += 1 | .b *= 2'); +jq({"a":1, "b":2}, .a += 1 | .b *= 2) +{"a":2,"b":4} +select jq('{"a":1} [2] 3', '. as {$a} ?// [$a] ?// $a | $a'); +invalid character '[' after top-level value +select jq('{"foo": 4722366482869645213696}', '.foo'); +jq({"foo": 4722366482869645213696}, .foo) +4.722366482869645e+21 +select jq('1', 'def fact($n): if $n < 1 then 1 else $n * fact($n - 1) end; fact(50)'); +jq(1, def fact($n): if $n < 1 then 1 else $n * fact($n - 1) end; fact(50)) +30414093201713378043612608166064768844377641568960512000000000000 +select jq('[1, 2, 3]', '.foo & .bar'); +unexpected token "&" +select try_jq('[1, 2, 3]', '.foo & .bar'); +try_jq([1, 2, 3], .foo & .bar) +null +select jq('{"foo": {bar: []} }', '.'); +invalid character 'b' looking for beginning of object key string +select try_jq('{"foo": {bar: []} }', '.'); +try_jq({"foo": {bar: []} }, .) +null +select jq($$ +{ +"a": 2 +}$$, '.a'); +jq(\n{\n"a": 2\n}, .a) +2 +select jq('', '.'); +unexpected end of JSON input +select try_jq('', '.'); +try_jq(, .) +null +select jq('1', ''); +missing query (try ".") +select try_jq('1', ''); +try_jq(1, ) +null +select jq('{"foo::bar": "zoo"}', '.["foo::bar"]'); +jq({"foo::bar": "zoo"}, .["foo::bar"]) +"zoo" +select jq('{"foo::bar": "zoo"}', '.foo::bar'); +unexpected token ":" +select try_jq('{"foo::bar": "zoo"}', '.foo::bar'); +try_jq({"foo::bar": "zoo"}, .foo::bar) +null +select jq('["a", "b", "c", "d", "e"]', '.[2:4]'); +jq(["a", "b", "c", "d", "e"], .[2:4]) +["c","d"] +select jq('["a", "b", "c", "d", "e"]', '.[:3]'); +jq(["a", "b", "c", "d", "e"], .[:3]) +["a","b","c"] +select jq('["a", "b", "c", "d", "e"]', '.[-2:]'); +jq(["a", "b", "c", "d", "e"], .[-2:]) +["d","e"] +select jq('["a", "b", "c", "d", "e"]', '.[]'); +jq(["a", "b", "c", "d", "e"], .[]) +"a""b""c""d""e" +select jq('[]', '.[]'); +jq([], .[]) + +select jq('{"foo": ["a", "b", "c", "d", "e"]}', '.foo[]'); +jq({"foo": ["a", "b", "c", "d", "e"]}, .foo[]) +"a""b""c""d""e" +select jq('{"a":1, "b":2}', '.[]'); +jq({"a":1, "b":2}, .[]) +12 +select jq('{"a":1, "b":2}', '.a, .b'); +jq({"a":1, "b":2}, .a, .b) +12 +select jq('["a", "b", "c", "d", "e"]', '.[4,2]'); +jq(["a", "b", "c", "d", "e"], .[4,2]) +"e""c" +select jq('{"a": 1, "b": [2, 3]}', '[.a, .b[]]'); +jq({"a": 1, "b": [2, 3]}, [.a, .b[]]) +[1,2,3] +select jq('[1, 2, 3]', '[ .[] | . * 2]'); +jq([1, 2, 3], [ .[] | . * 2]) +[2,4,6] +select jq('{"a":1, "b":2}', '{aa: .a, bb: .b}'); +jq({"a":1, "b":2}, {aa: .a, bb: .b}) +{"aa":1,"bb":2} +select jq('{"user":"stedolan","titles":["JQ Primer", "More JQ"]}', '{user, title: .titles[]}'); +jq({"user":"stedolan","titles":["JQ Primer", "More JQ"]}, {user, title: .titles[]}) +{"title":"JQ Primer","user":"stedolan"}{"title":"More JQ","user":"stedolan"} +select jq('[[{"a":1}]]', '.. | .a'); +expected an object but got: array ([[{"a":1}]]) +select jq('{"a":1, "b":2}', '.a + .b'); +jq({"a":1, "b":2}, .a + .b) +3 +select jq('{"a":1, "b":2}', '.a + null'); +jq({"a":1, "b":2}, .a + null) +1 +select jq('{"a":1, "b":2}', '. + {c: 3}'); +jq({"a":1, "b":2}, . + {c: 3}) +{"a":1,"b":2,"c":3} +select jq('{"a":1, "b":2}', '. + {a: 3, c: 3}'); +jq({"a":1, "b":2}, . + {a: 3, c: 3}) +{"a":3,"b":2,"c":3} +select jq('0', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'); +jq(0, if . == 0 then "zero" elif . == 1 then "one" else "many" end) +"zero" +select jq('1', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'); +jq(1, if . == 0 then "zero" elif . == 1 then "one" else "many" end) +"one" +select jq('2', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'); +jq(2, if . == 0 then "zero" elif . == 1 then "one" else "many" end) +"many" +select jq('[{}, true, {"a":1}]', '[.[]|try .a]'); +jq([{}, true, {"a":1}], [.[]|try .a]) +[null,1] +select jq('[{}, true, {"a":1}]', '[.[]|.a?]'); +jq([{}, true, {"a":1}], [.[]|.a?]) +[null,1] +select jq('[{}, true, {"a":1}]', '[.[]|try .a catch ". is not an object"]'); +jq([{}, true, {"a":1}], [.[]|try .a catch ". is not an object"]) +[null,". is not an object",1] +select jq('[1, 2, 3]', 'reduce .[] as $item (0; + $item)'); +jq([1, 2, 3], reduce .[] as $item (0; + $item)) +3 +select jq('[1, 2, 3]', 'foreach .[] as $item(0; . + $item; [$item, . * 2])'); +jq([1, 2, 3], foreach .[] as $item(0; . + $item; [$item, . * 2])) +[1,2][2,6][3,12] +create table jqt(id int, data varchar(255), jq varchar(255)); +insert into jqt values +(1, '{"foo": 128}', '.foo'), +(2, '{"foo": 128}', '.foo'), +(3, '{"a": {"b": 42}}', '.a.b'), +(4, '{"a": {"b": 42}}', '.a.b'), +(5, null, '.foo'), +(6, '{"a": {"b": 42}}', null), +(7, '{"id": "sample", "10": {"b": 42}}', '{(.id): .["10"].b}'), +(8, '[{"id":1},{"id":2},{"id":3}]', '.[] | .id'), +(9, '{"a":1, "b":2}', '.a += 1 | .b *= 2'), +(10, '{"a":1} [2] 3', '. as {$a} ?// [$a] ?// $a | $a'), +(11, '{"foo": 4722366482869645213696}', '.foo'), +(12, '1', 'def fact($n): if $n < 1 then 1 else $n * fact($n - 1) end; fact(50)') +; +insert into jqt values +(100, '[1, 2, 3]', '.foo & .bar'), +(101, '[1, 2, 3]', '.foo & .bar'), +(102, '{"foo": {bar: []} }', '.'), +(103, '{"foo": {bar: []} }', '.'); +insert into jqt values +(200, '{"a":1, "b":2}', '.a + .b'), +(201, '{"a":1, "b":2}', '.a + null'), +(202, '{"a":1, "b":2}', '. + {c: 3}'), +(203, '{"a":1, "b":2}', '. + {a: 3, c: 3}'), +(204, '0', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'), +(205, '1', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'), +(206, '2', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'), +(207, '[{}, true, {"a":1}]', '[.[]|try .a]'), +(208, '[{}, true, {"a":1}]', '[.[]|.a?]'), +(209, '[{}, true, {"a":1}]', '[.[]|try .a catch ". is not an object"]') +; +select count(*) from jqt; +count(*) +26 +select id, jq(data, '.') from jqt; +invalid character '[' after top-level value +select id, jq(data, '.') from jqt where id < 100; +invalid character '[' after top-level value +select id, try_jq(data, '.') from jqt; +id try_jq(data, .) +1 {"foo":128} +2 {"foo":128} +3 {"a":{"b":42}} +4 {"a":{"b":42}} +5 null +6 {"a":{"b":42}} +7 {"10":{"b":42},"id":"sample"} +8 [{"id":1},{"id":2},{"id":3}] +9 {"a":1,"b":2} +10 null +11 {"foo":4.722366482869645e+21} +12 1 +100 [1,2,3] +101 [1,2,3] +102 null +103 null +200 {"a":1,"b":2} +201 {"a":1,"b":2} +202 {"a":1,"b":2} +203 {"a":1,"b":2} +204 0 +205 1 +206 2 +207 [{},true,{"a":1}] +208 [{},true,{"a":1}] +209 [{},true,{"a":1}] +select id, jq(null, jq) from jqt; +id jq(null, jq) +1 null +2 null +3 null +4 null +5 null +6 null +7 null +8 null +9 null +10 null +11 null +12 null +100 null +101 null +102 null +103 null +200 null +201 null +202 null +203 null +204 null +205 null +206 null +207 null +208 null +209 null +select id, jq(data, null) from jqt; +id jq(data, null) +1 null +2 null +3 null +4 null +5 null +6 null +7 null +8 null +9 null +10 null +11 null +12 null +100 null +101 null +102 null +103 null +200 null +201 null +202 null +203 null +204 null +205 null +206 null +207 null +208 null +209 null +select id, jq(data, jq) from jqt; +invalid character '[' after top-level value +select id, try_jq(data, jq) from jqt; +id try_jq(data, jq) +1 128 +2 128 +3 42 +4 42 +5 null +6 null +7 {"sample":42} +8 123 +9 {"a":2,"b":4} +10 null +11 4.722366482869645e+21 +12 30414093201713378043612608166064768844377641568960512000000000000 +100 null +101 null +102 null +103 null +200 3 +201 1 +202 {"a":1,"b":2,"c":3} +203 {"a":3,"b":2,"c":3} +204 "zero" +205 "one" +206 "many" +207 [null,1] +208 [null,1] +209 [null,". is not an object",1] +drop table jqt; diff --git a/test/distributed/cases/function/func_jq.sql b/test/distributed/cases/function/func_jq.sql new file mode 100644 index 000000000000..538ec4eb1198 --- /dev/null +++ b/test/distributed/cases/function/func_jq.sql @@ -0,0 +1,125 @@ +-- +-- jq test +-- + +select jq('{"foo": 128}', '.foo'); +select try_jq('{"foo": 128}', '.foo'); +select jq('{"a": {"b": 42}}', '.a.b'); +select try_jq('{"a": {"b": 42}}', '.a.b'); + +select jq(null, '.foo'); +select try_jq(null, '.foo'); +select jq('{"a": {"b": 42}}', null); +select try_jq('{"a": {"b": 42}}', null); + +select jq('{"id": "sample", "10": {"b": 42}}', '{(.id): .["10"].b}'); +select jq('[{"id":1},{"id":2},{"id":3}]', '.[] | .id'); +select jq('{"a":1, "b":2}', '.a += 1 | .b *= 2'); +select jq('{"a":1} [2] 3', '. as {$a} ?// [$a] ?// $a | $a'); +select jq('{"foo": 4722366482869645213696}', '.foo'); +select jq('1', 'def fact($n): if $n < 1 then 1 else $n * fact($n - 1) end; fact(50)'); + +select jq('[1, 2, 3]', '.foo & .bar'); +select try_jq('[1, 2, 3]', '.foo & .bar'); + +select jq('{"foo": {bar: []} }', '.'); +select try_jq('{"foo": {bar: []} }', '.'); + +select jq($$ + { + "a": 2 + }$$, '.a'); + +select jq('', '.'); +select try_jq('', '.'); +select jq('1', ''); +select try_jq('1', ''); + +select jq('{"foo::bar": "zoo"}', '.["foo::bar"]'); +select jq('{"foo::bar": "zoo"}', '.foo::bar'); +select try_jq('{"foo::bar": "zoo"}', '.foo::bar'); + +select jq('["a", "b", "c", "d", "e"]', '.[2:4]'); +select jq('["a", "b", "c", "d", "e"]', '.[:3]'); +select jq('["a", "b", "c", "d", "e"]', '.[-2:]'); + +select jq('["a", "b", "c", "d", "e"]', '.[]'); +select jq('[]', '.[]'); +select jq('{"foo": ["a", "b", "c", "d", "e"]}', '.foo[]'); +select jq('{"a":1, "b":2}', '.[]'); + +select jq('{"a":1, "b":2}', '.a, .b'); +select jq('["a", "b", "c", "d", "e"]', '.[4,2]'); +select jq('{"a": 1, "b": [2, 3]}', '[.a, .b[]]'); +select jq('[1, 2, 3]', '[ .[] | . * 2]'); + +select jq('{"a":1, "b":2}', '{aa: .a, bb: .b}'); +select jq('{"user":"stedolan","titles":["JQ Primer", "More JQ"]}', '{user, title: .titles[]}'); +select jq('[[{"a":1}]]', '.. | .a'); + +-- expressions +select jq('{"a":1, "b":2}', '.a + .b'); +select jq('{"a":1, "b":2}', '.a + null'); +select jq('{"a":1, "b":2}', '. + {c: 3}'); +select jq('{"a":1, "b":2}', '. + {a: 3, c: 3}'); +select jq('0', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'); +select jq('1', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'); +select jq('2', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'); +select jq('[{}, true, {"a":1}]', '[.[]|try .a]'); +select jq('[{}, true, {"a":1}]', '[.[]|.a?]'); +select jq('[{}, true, {"a":1}]', '[.[]|try .a catch ". is not an object"]'); + +-- advanced +select jq('[1, 2, 3]', 'reduce .[] as $item (0; + $item)'); +select jq('[1, 2, 3]', 'foreach .[] as $item(0; . + $item; [$item, . * 2])'); + +-- enough, move on to tables. +create table jqt(id int, data varchar(255), jq varchar(255)); +insert into jqt values +(1, '{"foo": 128}', '.foo'), +(2, '{"foo": 128}', '.foo'), +(3, '{"a": {"b": 42}}', '.a.b'), +(4, '{"a": {"b": 42}}', '.a.b'), +(5, null, '.foo'), +(6, '{"a": {"b": 42}}', null), +(7, '{"id": "sample", "10": {"b": 42}}', '{(.id): .["10"].b}'), +(8, '[{"id":1},{"id":2},{"id":3}]', '.[] | .id'), +(9, '{"a":1, "b":2}', '.a += 1 | .b *= 2'), +(10, '{"a":1} [2] 3', '. as {$a} ?// [$a] ?// $a | $a'), +(11, '{"foo": 4722366482869645213696}', '.foo'), +(12, '1', 'def fact($n): if $n < 1 then 1 else $n * fact($n - 1) end; fact(50)') +; + + +insert into jqt values +(100, '[1, 2, 3]', '.foo & .bar'), +(101, '[1, 2, 3]', '.foo & .bar'), +(102, '{"foo": {bar: []} }', '.'), +(103, '{"foo": {bar: []} }', '.'); + +insert into jqt values +(200, '{"a":1, "b":2}', '.a + .b'), +(201, '{"a":1, "b":2}', '.a + null'), +(202, '{"a":1, "b":2}', '. + {c: 3}'), +(203, '{"a":1, "b":2}', '. + {a: 3, c: 3}'), +(204, '0', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'), +(205, '1', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'), +(206, '2', 'if . == 0 then "zero" elif . == 1 then "one" else "many" end'), +(207, '[{}, true, {"a":1}]', '[.[]|try .a]'), +(208, '[{}, true, {"a":1}]', '[.[]|.a?]'), +(209, '[{}, true, {"a":1}]', '[.[]|try .a catch ". is not an object"]') +; + +select count(*) from jqt; +select id, jq(data, '.') from jqt; +select id, jq(data, '.') from jqt where id < 100; +select id, try_jq(data, '.') from jqt; + +select id, jq(null, jq) from jqt; +select id, jq(data, null) from jqt; + +select id, jq(data, jq) from jqt; +select id, try_jq(data, jq) from jqt; + +drop table jqt; +