Skip to content

Commit 2ca00fd

Browse files
committed
jsonb: add package for JSONB parsing
1 parent 99c0716 commit 2ca00fd

File tree

7 files changed

+750
-0
lines changed

7 files changed

+750
-0
lines changed

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
11
module github.com/tailscale/sqlite
22

33
go 1.20
4+
5+
require golang.org/x/tools v0.21.0
6+
7+
require (
8+
golang.org/x/mod v0.17.0 // indirect
9+
golang.org/x/sync v0.7.0 // indirect
10+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
2+
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
3+
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
4+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
5+
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
6+
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=

jsonb/jsonb.go

Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
// Package jsonb handles SQLite's JSONB format.
2+
//
3+
// See https://sqlite.org/draft/jsonb.html.
4+
package jsonb
5+
6+
//go:generate go run golang.org/x/tools/cmd/stringer -type=Type
7+
8+
import (
9+
"encoding/binary"
10+
"errors"
11+
"fmt"
12+
"math"
13+
"strconv"
14+
)
15+
16+
// Value is a JSONB value.
17+
//
18+
// The methods on Value report whether it's valid, its type, length,
19+
// and so on.
20+
type Value []byte
21+
22+
func (v Value) HeaderLen() int {
23+
if len(v) == 0 {
24+
return 0
25+
}
26+
switch v[0] >> 4 {
27+
default:
28+
return 1
29+
case 0xc:
30+
return 2
31+
case 0xd:
32+
return 3
33+
case 0xe:
34+
return 5
35+
case 0xf:
36+
return 9
37+
}
38+
}
39+
40+
func (v Value) Type() Type {
41+
if len(v) == 0 {
42+
panic("Type called on invalid Value")
43+
}
44+
return Type(v[0] & 0xf)
45+
}
46+
47+
func (v Value) PayloadLen() int {
48+
switch v.HeaderLen() {
49+
default:
50+
return 0
51+
case 1:
52+
return int(v[0] >> 4)
53+
case 2:
54+
return int(v[1])
55+
case 3:
56+
return int(binary.BigEndian.Uint16(v[1:]))
57+
case 5:
58+
n := binary.BigEndian.Uint32(v[1:])
59+
if int64(n) > math.MaxInt {
60+
return 0
61+
}
62+
return int(n)
63+
case 9:
64+
n := binary.BigEndian.Uint64(v[1:])
65+
if n > math.MaxInt {
66+
return 0
67+
}
68+
return int(n)
69+
}
70+
}
71+
72+
// Payload returns the payload of the element.
73+
//
74+
// Depending on v's element type, the payload may be a series of zero+
75+
// concatenated valid Value elements.
76+
func (v Value) Payload() []byte {
77+
return v[v.HeaderLen():][:v.PayloadLen()]
78+
}
79+
80+
// RangeArray calls f for each element in v, which must be an array. It returns
81+
// an error if v is not a valid array, or if f returns an error.
82+
func (v Value) RangeArray(f func(Value) error) error {
83+
if !v.Valid() {
84+
return fmt.Errorf("not valid")
85+
}
86+
if v.Type() != Array {
87+
return fmt.Errorf("got type %v; not an array", v.Type())
88+
}
89+
pay := v.Payload()
90+
for len(pay) > 0 {
91+
v, rest, ok := Cut(pay)
92+
pay = rest
93+
if !ok {
94+
return errors.New("malformed array payload")
95+
}
96+
if err := f(v); err != nil {
97+
return err
98+
}
99+
}
100+
return nil
101+
}
102+
103+
// RangeObject calls f for each pair in v, which must be an object. It returns
104+
// an error if v is not a valid object, or if f returns an error.
105+
func (v Value) RangeObject(f func(k, v Value) error) error {
106+
if !v.Valid() {
107+
return fmt.Errorf("not valid")
108+
}
109+
if v.Type() != Object {
110+
return fmt.Errorf("got type %v; not an object", v.Type())
111+
}
112+
pay := v.Payload()
113+
for len(pay) > 0 {
114+
key, rest, ok := Cut(pay)
115+
pay = rest
116+
if !ok {
117+
return errors.New("malformed array payload")
118+
}
119+
val, rest, ok := Cut(pay)
120+
pay = rest
121+
if !ok {
122+
return errors.New("malformed array payload")
123+
}
124+
if !key.Type().CanText() {
125+
return errors.New("object key is not text")
126+
}
127+
if err := f(key, val); err != nil {
128+
return err
129+
}
130+
}
131+
return nil
132+
}
133+
134+
// Cut returns the first valid JSONB element in v, the rest of v, and whether
135+
// the cut was successful. When ok is true, v is Valid.
136+
func Cut(b []byte) (v Value, rest []byte, ok bool) {
137+
if len(b) == 0 {
138+
return nil, nil, false
139+
}
140+
v = Value(b)
141+
hlen := v.HeaderLen()
142+
if hlen == 0 {
143+
return nil, nil, false
144+
}
145+
plen := v.PayloadLen()
146+
if len(v) < hlen+plen {
147+
return nil, nil, false
148+
}
149+
return v[:hlen+plen], b[hlen+plen:], true
150+
}
151+
152+
// Valid reports whether v contains a single valid JSONB value.
153+
func (v Value) Valid() bool {
154+
h := v.HeaderLen()
155+
p := v.PayloadLen()
156+
return h > 0 && len(v) == h+p
157+
}
158+
159+
// Text returns the unescaped text of v, which must be a text element.
160+
func (v Value) Text() string {
161+
t := v.Type()
162+
if !t.CanText() {
163+
panic("Text called on non-text Value")
164+
}
165+
switch t {
166+
case Text:
167+
return string(v.Payload())
168+
case TextJ:
169+
got, err := appendUnquote(nil, v.Payload())
170+
if err != nil {
171+
// TODO: add TextErr variant?
172+
panic(err)
173+
}
174+
return string(got)
175+
default:
176+
panic(fmt.Sprintf("TODO: handle %v", t))
177+
}
178+
}
179+
180+
// Int returns the integer value of v.
181+
// It panics if v is not an integer type or can't fit in an int64.
182+
// TODO(bradfitz): add IntOk for a non-panicking out-of-bounds version?
183+
func (v Value) Int() int64 {
184+
t := v.Type()
185+
if !t.CanInt() {
186+
panic("Int called on non-int Value")
187+
}
188+
switch t {
189+
case Int:
190+
n, err := strconv.ParseInt(string(v.Payload()), 10, 64)
191+
if err != nil {
192+
panic(err)
193+
}
194+
return n
195+
default:
196+
panic(fmt.Sprintf("TODO: handle %v", t))
197+
}
198+
}
199+
200+
// Float returns the float64 value of v.
201+
// It panics if v is not an integer type or can't fit in an float64.
202+
// TODO(bradfitz): add IntOk for a non-panicking out-of-bounds version?
203+
func (v Value) Float() float64 {
204+
t := v.Type()
205+
if !t.CanFloat() {
206+
panic("Float called on non-float Value")
207+
}
208+
switch t {
209+
case Float:
210+
n, err := strconv.ParseFloat(string(v.Payload()), 64)
211+
if err != nil {
212+
panic(err)
213+
}
214+
return n
215+
default:
216+
panic(fmt.Sprintf("TODO: handle %v", t))
217+
}
218+
}
219+
220+
// Type is a JSONB element type.
221+
type Type byte
222+
223+
const (
224+
Null Type = 0x0
225+
True Type = 0x1
226+
False Type = 0x2
227+
Int Type = 0x3
228+
Int5 Type = 0x4
229+
Float Type = 0x5
230+
Float5 Type = 0x6
231+
232+
// Text is a JSON string value that does not contain any escapes nor any
233+
// characters that need to be escaped for either SQL or JSON
234+
Text Type = 0x7
235+
// TextJ is a JSON string value that contains RFC 8259 character escapes
236+
// (such as "\n" or "\u0020"). Those escapes will need to be translated into
237+
// actual UTF8 if this element is extracted into SQL. The payload is the
238+
// UTF8 text representation of the escaped string value.
239+
TextJ Type = 0x8
240+
Text5 Type = 0x9
241+
TextRaw Type = 0xa
242+
243+
Array Type = 0xb
244+
Object Type = 0xc // pairs of key/value
245+
246+
Reserved13 Type = 0xd
247+
Reserved14 Type = 0xe
248+
Reserved15 Type = 0xf
249+
)
250+
251+
func (t Type) CanText() bool {
252+
return t >= Text && t <= TextRaw
253+
}
254+
255+
func (t Type) CanInt() bool {
256+
return t == Int || t == Int5
257+
}
258+
259+
func (t Type) CanBool() bool {
260+
return t == True || t == False
261+
}
262+
263+
func (t Type) CanFloat() bool {
264+
return t == Float || t == Float5
265+
}

0 commit comments

Comments
 (0)