Skip to content

Commit 3da0b82

Browse files
authored
SQLite: Coerce jsonb columns to json before returning to Go code (#3968)
* SQLite: Coerce jsonb columns to json before returning to Go code This one follows up the discussion in #3953 to try and make the `jsonb` data type in SQLite usable (see discussion there, but I believe that it's currently not). According the SQLite docs on JSONB [1], it's considered a format that's internal to the database itself, and no attempt should be made to parse it elsewhere: > JSONB is not intended as an external format to be used by > applications. JSONB is designed for internal use by SQLite only. > Programmers do not need to understand the JSONB format in order to use > it effectively. Applications should access JSONB only through the JSON > SQL functions, not by looking at individual bytes of the BLOB. Currently, when trying to use a `jsonb` column in SQLite, sqlc ends up returning the internal binary data, which ends up being unparsable in Go: riverdrivertest.go:3030: Error Trace: /Users/brandur/Documents/projects/river/internal/riverinternaltest/riverdrivertest/riverdrivertest.go:3030 Error: Not equal: expected: []byte{0x7b, 0x22, 0x66, 0x6f, 0x6f, 0x22, 0x3a, 0x20, 0x22, 0x62, 0x61, 0x72, 0x22, 0x7d} actual : []byte{0x8c, 0x37, 0x66, 0x6f, 0x6f, 0x37, 0x62, 0x61, 0x72} Diff: --- Expected +++ Actual @@ -1,3 +1,3 @@ -([]uint8) (len=14) { - 00000000 7b 22 66 6f 6f 22 3a 20 22 62 61 72 22 7d |{"foo": "bar"}| +([]uint8) (len=9) { + 00000000 8c 37 66 6f 6f 37 62 61 72 |.7foo7bar| } Test: TestDriverRiverSQLite/QueueCreateOrSetUpdatedAt/InsertsANewQueueWithDefaultUpdatedAt The fix is that we should make sure to coerce `jsonb` columns back to `json` before returning. That's what this pull request does, intercepting `SELECT *` and wrapping `jsonb` columns with a `json(...)` invocation. I also assign `json` and `jsonb` a `[]byte` data type by default so they don't end up as `any`, which isn't very useful. `[]byte` is consistent with the default for `pgx/v5`. [1] https://sqlite.org/jsonb.html * Make SQLite json/jsonb values `json.RawMessage` instead of `[]byte` This in response to code review feedback, it's a little more correct to make json/jsonb values `json.RawMessage` instead of `[]byte`. The former is a form of the latter, but better represents that the value is meant to be JSON. * Introduce `Selector` interface for generating column expressions This is response to code review feedback, add a new `Selector `interface whose job it is to provide an engine-agnostic way of generating output expressions for when selecting column values with `SELECT ...` or `RETURNING ...`. This is exclusively needed for SQLite for the time being, which uses it to wrap all output `jsonb` column values with a call to `json(...)` so that values are coerced to a publicly usable format before being returned. [1] #3968 (comment) * Make selectors internal to the `compiler` package This is based on original code review feedback that I'd misread initially. The selector interface doesn't need to be an outside package for any reason (it's used only internal to the compiler), and this lets us improve it somewhat by taking a full `*Column` struct rather than having to make it a `dataType string` (because `Column` is internal to `compiler` and it would otherwise introduce dependency cycles).
1 parent 71e66bf commit 3da0b82

File tree

17 files changed

+354
-0
lines changed

17 files changed

+354
-0
lines changed

internal/codegen/golang/sqlite_type.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ func sqliteType(req *plugin.GenerateRequest, options *opts.Options, col *plugin.
5656
}
5757
return "sql.NullTime"
5858

59+
case "json", "jsonb":
60+
return "json.RawMessage"
61+
5962
case "any":
6063
return "interface{}"
6164

internal/compiler/engine.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ type Compiler struct {
2323
result *Result
2424
analyzer analyzer.Analyzer
2525
client dbmanager.Client
26+
selector selector
2627

2728
schema []string
2829
}
@@ -39,12 +40,15 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
3940
case config.EngineSQLite:
4041
c.parser = sqlite.NewParser()
4142
c.catalog = sqlite.NewCatalog()
43+
c.selector = newSQLiteSelector()
4244
case config.EngineMySQL:
4345
c.parser = dolphin.NewParser()
4446
c.catalog = dolphin.NewCatalog()
47+
c.selector = newDefaultSelector()
4548
case config.EnginePostgreSQL:
4649
c.parser = postgresql.NewParser()
4750
c.catalog = postgresql.NewCatalog()
51+
c.selector = newDefaultSelector()
4852
if conf.Database != nil {
4953
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
5054
c.analyzer = analyzer.Cached(

internal/compiler/expand.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node)
149149
if counts[cname] > 1 {
150150
cname = tableName + "." + cname
151151
}
152+
153+
// This is important for SQLite in particular which needs to
154+
// wrap jsonb column values with `json(colname)` so they're in a
155+
// publicly usable format (i.e. not jsonb).
156+
cname = c.selector.ColumnExpr(cname, column)
152157
cols = append(cols, cname)
153158
}
154159
}

internal/compiler/selector.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package compiler
2+
3+
// selector is an interface used by a compiler for generating expressions for
4+
// output columns in a `SELECT ...` or `RETURNING ...` statement.
5+
//
6+
// This interface is exclusively needed at the moment for SQLite, which must
7+
// wrap output `jsonb` columns with a `json(column_name)` invocation so that a
8+
// publicly consumable format (i.e. not jsonb) is returned.
9+
type selector interface {
10+
// ColumnExpr generates output to be used in a `SELECT ...` or `RETURNING
11+
// ...` statement based on input column name and metadata.
12+
ColumnExpr(name string, column *Column) string
13+
}
14+
15+
// defaultSelector is a selector implementation that does the simpliest possible
16+
// pass through when generating column expressions. Its use is suitable for all
17+
// database engines not requiring additional customization.
18+
type defaultSelector struct{}
19+
20+
func newDefaultSelector() *defaultSelector {
21+
return &defaultSelector{}
22+
}
23+
24+
func (s *defaultSelector) ColumnExpr(name string, column *Column) string {
25+
return name
26+
}
27+
28+
type sqliteSelector struct{}
29+
30+
func newSQLiteSelector() *sqliteSelector {
31+
return &sqliteSelector{}
32+
}
33+
34+
func (s *sqliteSelector) ColumnExpr(name string, column *Column) string {
35+
// Under SQLite, neither json nor jsonb are real data types, and rather just
36+
// of type blob, so database drivers just return whatever raw binary is
37+
// stored as values. This is a problem for jsonb, which is considered an
38+
// internal format to SQLite and no attempt should be made to parse it
39+
// outside of the database itself. For jsonb columns in SQLite, wrap values
40+
// in `json(col)` to coerce the internal binary format to JSON parsable by
41+
// the user-space application.
42+
if column.DataType == "jsonb" {
43+
return "json(" + name + ")"
44+
}
45+
return name
46+
}

internal/compiler/selector_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package compiler
2+
3+
import "testing"
4+
5+
func TestSelector(t *testing.T) {
6+
t.Parallel()
7+
8+
selectorExpectColumnExpr := func(t *testing.T, selector selector, expected, name string, column *Column) {
9+
if actual := selector.ColumnExpr(name, column); expected != actual {
10+
t.Errorf("Expected %v, got %v for data type %v", expected, actual, column.DataType)
11+
}
12+
}
13+
14+
t.Run("DefaultSelectorColumnExpr", func(t *testing.T) {
15+
t.Parallel()
16+
17+
selector := newDefaultSelector()
18+
19+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
20+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
21+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "jsonb"})
22+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
23+
})
24+
25+
t.Run("SQLiteSelectorColumnExpr", func(t *testing.T) {
26+
t.Parallel()
27+
28+
selector := newSQLiteSelector()
29+
30+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "integer"})
31+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "json"})
32+
selectorExpectColumnExpr(t, selector, "json(my_column)", "my_column", &Column{DataType: "jsonb"})
33+
selectorExpectColumnExpr(t, selector, "my_column", "my_column", &Column{DataType: "text"})
34+
})
35+
}

internal/endtoend/testdata/jsonb/pgx/go/db.go

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/jsonb/pgx/go/models.go

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/jsonb/pgx/go/query.sql.go

Lines changed: 50 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- name: InsertFoo :exec
2+
INSERT INTO foo (
3+
a,
4+
b,
5+
c,
6+
d
7+
) VALUES (
8+
@a,
9+
@b,
10+
@c,
11+
@d
12+
) RETURNING *;
13+
14+
-- name: SelectFoo :exec
15+
SELECT * FROM foo;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE foo (
2+
a json not null,
3+
b jsonb not null,
4+
c json,
5+
d jsonb
6+
);
7+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"version": "1",
3+
"packages": [
4+
{
5+
"path": "go",
6+
"engine": "postgresql",
7+
"sql_package": "pgx/v5",
8+
"name": "querytest",
9+
"schema": "schema.sql",
10+
"queries": "query.sql"
11+
}
12+
]
13+
}

internal/endtoend/testdata/jsonb/sqlite/go/db.go

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/jsonb/sqlite/go/models.go

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/endtoend/testdata/jsonb/sqlite/go/query.sql.go

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-- name: InsertFoo :exec
2+
INSERT INTO foo (
3+
a,
4+
b,
5+
c,
6+
d
7+
) VALUES (
8+
@a,
9+
@b,
10+
@c,
11+
@d
12+
) RETURNING *;
13+
14+
-- name: SelectFoo :exec
15+
SELECT * FROM foo;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CREATE TABLE foo (
2+
a json not null,
3+
b jsonb not null,
4+
c json,
5+
d jsonb
6+
);
7+

0 commit comments

Comments
 (0)