Skip to content

Commit 0555230

Browse files
committed
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)
1 parent 967f653 commit 0555230

File tree

6 files changed

+97
-12
lines changed

6 files changed

+97
-12
lines changed

internal/compiler/engine.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
1414
"github.com/sqlc-dev/sqlc/internal/opts"
1515
"github.com/sqlc-dev/sqlc/internal/sql/catalog"
16+
"github.com/sqlc-dev/sqlc/internal/sql/selector"
1617
)
1718

1819
type Compiler struct {
@@ -23,6 +24,7 @@ type Compiler struct {
2324
result *Result
2425
analyzer analyzer.Analyzer
2526
client dbmanager.Client
27+
selector selector.Selector
2628

2729
schema []string
2830
}
@@ -39,12 +41,15 @@ func NewCompiler(conf config.SQL, combo config.CombinedSettings) (*Compiler, err
3941
case config.EngineSQLite:
4042
c.parser = sqlite.NewParser()
4143
c.catalog = sqlite.NewCatalog()
44+
c.selector = sqlite.NewSelector()
4245
case config.EngineMySQL:
4346
c.parser = dolphin.NewParser()
4447
c.catalog = dolphin.NewCatalog()
48+
c.selector = selector.NewDefaultSelector()
4549
case config.EnginePostgreSQL:
4650
c.parser = postgresql.NewParser()
4751
c.catalog = postgresql.NewCatalog()
52+
c.selector = selector.NewDefaultSelector()
4853
if conf.Database != nil {
4954
if conf.Analyzer.Database == nil || *conf.Analyzer.Database {
5055
c.analyzer = analyzer.Cached(

internal/compiler/expand.go

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"strings"
77

88
"github.com/sqlc-dev/sqlc/internal/config"
9-
"github.com/sqlc-dev/sqlc/internal/engine/sqlite"
109
"github.com/sqlc-dev/sqlc/internal/source"
1110
"github.com/sqlc-dev/sqlc/internal/sql/ast"
1211
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
@@ -150,17 +149,11 @@ func (c *Compiler) expandStmt(qc *QueryCatalog, raw *ast.RawStmt, node ast.Node)
150149
if counts[cname] > 1 {
151150
cname = tableName + "." + cname
152151
}
153-
// Under SQLite, neither json nor jsonb are real data types, and
154-
// rather just of type blob, so database drivers just return
155-
// whatever raw binary is stored as values. This is a problem
156-
// for jsonb, which is considered an internal format to SQLite
157-
// and no attempt should be made to parse it outside of the
158-
// database itself. For jsonb columns in SQLite, wrap returned
159-
// columns in `json(col)` to coerce the internal binary format
160-
// to JSON parsable by the user-space application.
161-
if _, ok := c.parser.(*sqlite.Parser); ok && column.DataType == "jsonb" {
162-
cname = "json(" + cname + ")"
163-
}
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.DataType)
164157
cols = append(cols, cname)
165158
}
166159
}

internal/engine/sqlite/selector.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package sqlite
2+
3+
type Selector struct{}
4+
5+
func NewSelector() *Selector {
6+
return &Selector{}
7+
}
8+
9+
func (s *Selector) ColumnExpr(name string, dataType string) string {
10+
// Under SQLite, neither json nor jsonb are real data types, and rather just
11+
// of type blob, so database drivers just return whatever raw binary is
12+
// stored as values. This is a problem for jsonb, which is considered an
13+
// internal format to SQLite and no attempt should be made to parse it
14+
// outside of the database itself. For jsonb columns in SQLite, wrap values
15+
// in `json(col)` to coerce the internal binary format to JSON parsable by
16+
// the user-space application.
17+
if dataType == "jsonb" {
18+
return "json(" + name + ")"
19+
}
20+
return name
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package sqlite
2+
3+
import "testing"
4+
5+
func TestSelectorColumnExpr(t *testing.T) {
6+
t.Parallel()
7+
8+
selector := NewSelector()
9+
10+
expectExpr := func(expected, name, dataType string) {
11+
if actual := selector.ColumnExpr(name, dataType); expected != actual {
12+
t.Errorf("Expected %v, got %v for data type %v", expected, actual, dataType)
13+
}
14+
}
15+
16+
expectExpr("my_column", "my_column", "integer")
17+
expectExpr("my_column", "my_column", "json")
18+
expectExpr("json(my_column)", "my_column", "jsonb")
19+
expectExpr("my_column", "my_column", "text")
20+
}

internal/sql/selector/selector.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package selector
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, dataType string) 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, dataType string) string {
25+
return name
26+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package selector
2+
3+
import "testing"
4+
5+
func TestDefaultSelectorColumnExpr(t *testing.T) {
6+
t.Parallel()
7+
8+
selector := NewDefaultSelector()
9+
10+
expectExpr := func(expected, name, dataType string) {
11+
if actual := selector.ColumnExpr(name, dataType); expected != actual {
12+
t.Errorf("Expected %v, got %v for data type %v", expected, actual, dataType)
13+
}
14+
}
15+
16+
expectExpr("my_column", "my_column", "integer")
17+
expectExpr("my_column", "my_column", "json")
18+
expectExpr("my_column", "my_column", "jsonb")
19+
expectExpr("my_column", "my_column", "text")
20+
}

0 commit comments

Comments
 (0)