Skip to content

Commit eedc5a4

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

File tree

7 files changed

+793
-1
lines changed

7 files changed

+793
-1
lines changed

go.mod

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

3-
go 1.20
3+
go 1.22
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: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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+
case TextRaw:
176+
return string(v.Payload()) // TODO: escape stuff?
177+
case Text5:
178+
got, err := appendUnquote(nil, v.Payload())
179+
if err != nil {
180+
// TODO: add TextErr variant?
181+
panic(err)
182+
}
183+
return string(got)
184+
}
185+
panic("unreachable")
186+
}
187+
188+
// Int returns the integer value of v.
189+
// It panics if v is not an integer type or can't fit in an int64.
190+
// TODO(bradfitz): add IntOk for a non-panicking out-of-bounds version?
191+
func (v Value) Int() int64 {
192+
t := v.Type()
193+
if !t.CanInt() {
194+
panic("Int called on non-int Value")
195+
}
196+
switch t {
197+
case Int:
198+
n, err := strconv.ParseInt(string(v.Payload()), 10, 64)
199+
if err != nil {
200+
panic(err)
201+
}
202+
return n
203+
default:
204+
panic(fmt.Sprintf("TODO: handle %v", t))
205+
}
206+
}
207+
208+
// Float returns the float64 value of v.
209+
// It panics if v is not an integer type or can't fit in an float64.
210+
// TODO(bradfitz): add IntOk for a non-panicking out-of-bounds version?
211+
func (v Value) Float() float64 {
212+
t := v.Type()
213+
if !t.CanFloat() {
214+
panic("Float called on non-float Value")
215+
}
216+
switch t {
217+
case Float:
218+
n, err := strconv.ParseFloat(string(v.Payload()), 64)
219+
if err != nil {
220+
panic(err)
221+
}
222+
return n
223+
default:
224+
panic(fmt.Sprintf("TODO: handle %v", t))
225+
}
226+
}
227+
228+
// Type is a JSONB element type.
229+
type Type byte
230+
231+
const (
232+
Null Type = 0x0
233+
True Type = 0x1
234+
False Type = 0x2
235+
Int Type = 0x3
236+
Int5 Type = 0x4
237+
Float Type = 0x5
238+
Float5 Type = 0x6
239+
240+
// Text is a JSON string value that does not contain any escapes nor any
241+
// characters that need to be escaped for either SQL or JSON
242+
Text Type = 0x7
243+
// TextJ is a JSON string value that contains RFC 8259 character escapes
244+
// (such as "\n" or "\u0020"). Those escapes will need to be translated into
245+
// actual UTF8 if this element is extracted into SQL. The payload is the
246+
// UTF8 text representation of the escaped string value.
247+
TextJ Type = 0x8
248+
Text5 Type = 0x9
249+
TextRaw Type = 0xa
250+
251+
Array Type = 0xb
252+
Object Type = 0xc // pairs of key/value
253+
254+
Reserved13 Type = 0xd
255+
Reserved14 Type = 0xe
256+
Reserved15 Type = 0xf
257+
)
258+
259+
func (t Type) CanText() bool {
260+
return t >= Text && t <= TextRaw
261+
}
262+
263+
func (t Type) CanInt() bool {
264+
return t == Int || t == Int5
265+
}
266+
267+
func (t Type) CanBool() bool {
268+
return t == True || t == False
269+
}
270+
271+
func (t Type) CanFloat() bool {
272+
return t == Float || t == Float5
273+
}

0 commit comments

Comments
 (0)