diff --git a/context.go b/context.go index 154c228..269bf52 100644 --- a/context.go +++ b/context.go @@ -1,7 +1,6 @@ package sqlite3 import ( - "encoding/json" "errors" "math" "time" @@ -173,21 +172,6 @@ func (ctx Context) ResultPointer(ptr any) { stk_t(ctx.handle), stk_t(valPtr)) } -// ResultJSON sets the result of the function to the JSON encoding of value. -// -// https://sqlite.org/c3ref/result_blob.html -func (ctx Context) ResultJSON(value any) { - err := json.NewEncoder(callbackWriter(func(p []byte) (int, error) { - ctx.ResultRawText(p[:len(p)-1]) // remove the newline - return 0, nil - })).Encode(value) - - if err != nil { - ctx.ResultError(err) - return // notest - } -} - // ResultValue sets the result of the function to a copy of [Value]. // // https://sqlite.org/c3ref/result_blob.html diff --git a/internal/util/json.go b/internal/util/json.go index 8462374..f582734 100644 --- a/internal/util/json.go +++ b/internal/util/json.go @@ -1,3 +1,5 @@ +//go:build !goexperiment.jsonv2 + package util import ( diff --git a/internal/util/json_v2.go b/internal/util/json_v2.go new file mode 100644 index 0000000..2fb0522 --- /dev/null +++ b/internal/util/json_v2.go @@ -0,0 +1,52 @@ +//go:build goexperiment.jsonv2 + +package util + +import ( + "encoding/json/v2" + "math" + "strconv" + "time" + "unsafe" +) + +type JSON struct{ Value any } + +func (j JSON) Scan(value any) error { + var buf []byte + + switch v := value.(type) { + case []byte: + buf = v + case string: + buf = unsafe.Slice(unsafe.StringData(v), len(v)) + case int64: + buf = strconv.AppendInt(nil, v, 10) + case float64: + buf = AppendNumber(nil, v) + case time.Time: + buf = append(buf, '"') + buf = v.AppendFormat(buf, time.RFC3339Nano) + buf = append(buf, '"') + case nil: + buf = []byte("null") + default: + panic(AssertErr()) + } + + return json.Unmarshal(buf, j.Value) +} + +func AppendNumber(dst []byte, f float64) []byte { + switch { + case math.IsNaN(f): + dst = append(dst, "null"...) + case math.IsInf(f, 1): + dst = append(dst, "9.0e999"...) + case math.IsInf(f, -1): + dst = append(dst, "-9.0e999"...) + default: + return strconv.AppendFloat(dst, f, 'g', -1, 64) + } + return dst +} diff --git a/json.go b/json.go index 2b762c0..78195f2 100644 --- a/json.go +++ b/json.go @@ -1,6 +1,13 @@ +//go:build !goexperiment.jsonv2 + package sqlite3 -import "github.com/ncruces/go-sqlite3/internal/util" +import ( + "encoding/json" + "strconv" + + "github.com/ncruces/go-sqlite3/internal/util" +) // JSON returns a value that can be used as an argument to // [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to @@ -10,3 +17,77 @@ import "github.com/ncruces/go-sqlite3/internal/util" func JSON(value any) any { return util.JSON{Value: value} } + +// ResultJSON sets the result of the function to the JSON encoding of value. +// +// https://sqlite.org/c3ref/result_blob.html +func (ctx Context) ResultJSON(value any) { + err := json.NewEncoder(callbackWriter(func(p []byte) (int, error) { + ctx.ResultRawText(p[:len(p)-1]) // remove the newline + return 0, nil + })).Encode(value) + + if err != nil { + ctx.ResultError(err) + return // notest + } +} + +// BindJSON binds the JSON encoding of value to the prepared statement. +// The leftmost SQL parameter has an index of 1. +// +// https://sqlite.org/c3ref/bind_blob.html +func (s *Stmt) BindJSON(param int, value any) error { + return json.NewEncoder(callbackWriter(func(p []byte) (int, error) { + return 0, s.BindRawText(param, p[:len(p)-1]) // remove the newline + })).Encode(value) +} + +// ColumnJSON parses the JSON-encoded value of the result column +// and stores it in the value pointed to by ptr. +// The leftmost column of the result set has the index 0. +// +// https://sqlite.org/c3ref/column_blob.html +func (s *Stmt) ColumnJSON(col int, ptr any) error { + var data []byte + switch s.ColumnType(col) { + case NULL: + data = []byte("null") + case TEXT: + data = s.ColumnRawText(col) + case BLOB: + data = s.ColumnRawBlob(col) + case INTEGER: + data = strconv.AppendInt(nil, s.ColumnInt64(col), 10) + case FLOAT: + data = util.AppendNumber(nil, s.ColumnFloat(col)) + default: + panic(util.AssertErr()) + } + return json.Unmarshal(data, ptr) +} + +// JSON parses a JSON-encoded value +// and stores the result in the value pointed to by ptr. +func (v Value) JSON(ptr any) error { + var data []byte + switch v.Type() { + case NULL: + data = []byte("null") + case TEXT: + data = v.RawText() + case BLOB: + data = v.RawBlob() + case INTEGER: + data = strconv.AppendInt(nil, v.Int64(), 10) + case FLOAT: + data = util.AppendNumber(nil, v.Float()) + default: + panic(util.AssertErr()) + } + return json.Unmarshal(data, ptr) +} + +type callbackWriter func(p []byte) (int, error) + +func (fn callbackWriter) Write(p []byte) (int, error) { return fn(p) } diff --git a/json_v2.go b/json_v2.go new file mode 100644 index 0000000..4b74bc7 --- /dev/null +++ b/json_v2.go @@ -0,0 +1,113 @@ +//go:build goexperiment.jsonv2 + +package sqlite3 + +import ( + "encoding/json/v2" + "strconv" + + "github.com/ncruces/go-sqlite3/internal/util" +) + +// JSON returns a value that can be used as an argument to +// [database/sql.DB.Exec], [database/sql.Row.Scan] and similar methods to +// store value as JSON, or decode JSON into value. +// JSON should NOT be used with [Stmt.BindJSON], [Stmt.ColumnJSON], +// [Value.JSON], or [Context.ResultJSON]. +func JSON(value any) any { + return util.JSON{Value: value} +} + +// ResultJSON sets the result of the function to the JSON encoding of value. +// +// https://sqlite.org/c3ref/result_blob.html +func (ctx Context) ResultJSON(value any) { + w := bytesWriter{sqlite: ctx.c.sqlite} + if err := json.MarshalWrite(&w, value); err != nil { + ctx.c.free(w.ptr) + ctx.ResultError(err) + return // notest + } + ctx.c.call("sqlite3_result_text_go", + stk_t(ctx.handle), stk_t(w.ptr), stk_t(len(w.buf))) +} + +// BindJSON binds the JSON encoding of value to the prepared statement. +// The leftmost SQL parameter has an index of 1. +// +// https://sqlite.org/c3ref/bind_blob.html +func (s *Stmt) BindJSON(param int, value any) error { + w := bytesWriter{sqlite: s.c.sqlite} + if err := json.MarshalWrite(&w, value); err != nil { + s.c.free(w.ptr) + return err + } + rc := res_t(s.c.call("sqlite3_bind_text_go", + stk_t(s.handle), stk_t(param), + stk_t(w.ptr), stk_t(len(w.buf)))) + return s.c.error(rc) +} + +// ColumnJSON parses the JSON-encoded value of the result column +// and stores it in the value pointed to by ptr. +// The leftmost column of the result set has the index 0. +// +// https://sqlite.org/c3ref/column_blob.html +func (s *Stmt) ColumnJSON(col int, ptr any) error { + var data []byte + switch s.ColumnType(col) { + case NULL: + data = []byte("null") + case TEXT: + data = s.ColumnRawText(col) + case BLOB: + data = s.ColumnRawBlob(col) + case INTEGER: + data = strconv.AppendInt(nil, s.ColumnInt64(col), 10) + case FLOAT: + data = util.AppendNumber(nil, s.ColumnFloat(col)) + default: + panic(util.AssertErr()) + } + return json.Unmarshal(data, ptr) +} + +// JSON parses a JSON-encoded value +// and stores the result in the value pointed to by ptr. +func (v Value) JSON(ptr any) error { + var data []byte + switch v.Type() { + case NULL: + data = []byte("null") + case TEXT: + data = v.RawText() + case BLOB: + data = v.RawBlob() + case INTEGER: + data = strconv.AppendInt(nil, v.Int64(), 10) + case FLOAT: + data = util.AppendNumber(nil, v.Float()) + default: + panic(util.AssertErr()) + } + return json.Unmarshal(data, ptr) +} + +type bytesWriter struct { + *sqlite + buf []byte + ptr ptr_t +} + +func (b *bytesWriter) Write(p []byte) (n int, err error) { + if len(p) > cap(b.buf)-len(b.buf) { + want := int64(len(b.buf)) + int64(len(p)) + grow := int64(cap(b.buf)) + grow += grow >> 1 + want = max(want, grow) + b.ptr = b.realloc(b.ptr, want) + b.buf = util.View(b.mod, b.ptr, want)[:len(b.buf)] + } + b.buf = append(b.buf, p...) + return len(p), nil +} diff --git a/stmt.go b/stmt.go index 706182f..e2523b6 100644 --- a/stmt.go +++ b/stmt.go @@ -1,9 +1,7 @@ package sqlite3 import ( - "encoding/json" "math" - "strconv" "time" "github.com/ncruces/go-sqlite3/internal/util" @@ -362,16 +360,6 @@ func (s *Stmt) BindPointer(param int, ptr any) error { return s.c.error(rc) } -// BindJSON binds the JSON encoding of value to the prepared statement. -// The leftmost SQL parameter has an index of 1. -// -// https://sqlite.org/c3ref/bind_blob.html -func (s *Stmt) BindJSON(param int, value any) error { - return json.NewEncoder(callbackWriter(func(p []byte) (int, error) { - return 0, s.BindRawText(param, p[:len(p)-1]) // remove the newline - })).Encode(value) -} - // BindValue binds a copy of value to the prepared statement. // The leftmost SQL parameter has an index of 1. // @@ -598,30 +586,6 @@ func (s *Stmt) columnRawBytes(col int, ptr ptr_t, nul int32) []byte { return util.View(s.c.mod, ptr, int64(n+nul))[:n] } -// ColumnJSON parses the JSON-encoded value of the result column -// and stores it in the value pointed to by ptr. -// The leftmost column of the result set has the index 0. -// -// https://sqlite.org/c3ref/column_blob.html -func (s *Stmt) ColumnJSON(col int, ptr any) error { - var data []byte - switch s.ColumnType(col) { - case NULL: - data = []byte("null") - case TEXT: - data = s.ColumnRawText(col) - case BLOB: - data = s.ColumnRawBlob(col) - case INTEGER: - data = strconv.AppendInt(nil, s.ColumnInt64(col), 10) - case FLOAT: - data = util.AppendNumber(nil, s.ColumnFloat(col)) - default: - panic(util.AssertErr()) - } - return json.Unmarshal(data, ptr) -} - // ColumnValue returns the unprotected value of the result column. // The leftmost column of the result set has the index 0. // @@ -748,7 +712,3 @@ func (s *Stmt) columns(count int64) ([]byte, ptr_t, error) { return util.View(s.c.mod, typePtr, count), dataPtr, nil } - -type callbackWriter func(p []byte) (int, error) - -func (fn callbackWriter) Write(p []byte) (int, error) { return fn(p) } diff --git a/tests/stmt_test.go b/tests/stmt_test.go index 92aa920..7ea9fbd 100644 --- a/tests/stmt_test.go +++ b/tests/stmt_test.go @@ -496,7 +496,7 @@ func TestStmt(t *testing.T) { if got := stmt.ColumnBlob(0, nil); string(got) != "true" { t.Errorf("got %q, want true", got) } - var got any = 1 + var got any if err := stmt.ColumnJSON(0, &got); err != nil { t.Error(err) } else if got != true { diff --git a/value.go b/value.go index 6806e9a..994743f 100644 --- a/value.go +++ b/value.go @@ -1,9 +1,7 @@ package sqlite3 import ( - "encoding/json" "math" - "strconv" "time" "github.com/ncruces/go-sqlite3/internal/util" @@ -162,27 +160,6 @@ func (v Value) Pointer() any { return util.GetHandle(v.c.ctx, ptr) } -// JSON parses a JSON-encoded value -// and stores the result in the value pointed to by ptr. -func (v Value) JSON(ptr any) error { - var data []byte - switch v.Type() { - case NULL: - data = []byte("null") - case TEXT: - data = v.RawText() - case BLOB: - data = v.RawBlob() - case INTEGER: - data = strconv.AppendInt(nil, v.Int64(), 10) - case FLOAT: - data = util.AppendNumber(nil, v.Float()) - default: - panic(util.AssertErr()) - } - return json.Unmarshal(data, ptr) -} - // NoChange returns true if and only if the value is unchanged // in a virtual table update operatiom. //