Skip to content

Commit 58e9105

Browse files
authored
CSV type affinity (#102)
Use sqlite-createtable-parser compiled to Wasm to parse the CREATE TABLE statement.
1 parent 3719692 commit 58e9105

File tree

14 files changed

+371
-2
lines changed

14 files changed

+371
-2
lines changed

ext/csv/arg_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ func Test_uintArg(t *testing.T) {
4040
}
4141

4242
func Test_boolArg(t *testing.T) {
43+
t.Parallel()
44+
4345
tests := []struct {
4446
arg string
4547
key string
@@ -76,6 +78,8 @@ func Test_boolArg(t *testing.T) {
7678
}
7779

7880
func Test_runeArg(t *testing.T) {
81+
t.Parallel()
82+
7983
tests := []struct {
8084
arg string
8185
key string

ext/csv/csv.go

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"io"
1414
"io/fs"
15+
"strconv"
1516
"strings"
1617

1718
"github.com/ncruces/go-sqlite3"
@@ -93,6 +94,8 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) {
9394
}
9495
}
9596
schema = getSchema(header, columns, row)
97+
} else {
98+
table.typs = getColumnAffinities(schema)
9699
}
97100

98101
err = db.DeclareVTab(schema)
@@ -113,6 +116,7 @@ type table struct {
113116
fsys fs.FS
114117
name string
115118
data string
119+
typs []affinity
116120
comma rune
117121
header bool
118122
}
@@ -226,7 +230,40 @@ func (c *cursor) RowID() (int64, error) {
226230

227231
func (c *cursor) Column(ctx *sqlite3.Context, col int) error {
228232
if col < len(c.row) {
229-
ctx.ResultText(c.row[col])
233+
var typ affinity
234+
if col < len(c.table.typs) {
235+
typ = c.table.typs[col]
236+
}
237+
238+
txt := c.row[col]
239+
if typ == blob {
240+
ctx.ResultText(txt)
241+
return nil
242+
}
243+
if txt == "" {
244+
return nil
245+
}
246+
247+
switch typ {
248+
case numeric, integer:
249+
if strings.TrimLeft(txt, "+-0123456789") == "" {
250+
if i, err := strconv.ParseInt(txt, 10, 64); err == nil {
251+
ctx.ResultInt64(i)
252+
return nil
253+
}
254+
}
255+
fallthrough
256+
case real:
257+
if strings.TrimLeft(txt, "+-.0123456789Ee") == "" {
258+
if f, err := strconv.ParseFloat(txt, 64); err == nil {
259+
ctx.ResultFloat(f)
260+
return nil
261+
}
262+
}
263+
fallthrough
264+
case text:
265+
ctx.ResultText(c.row[col])
266+
}
230267
}
231268
return nil
232269
}

ext/csv/csv_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,50 @@ Robert "Griesemer" "gri"`
113113
}
114114
}
115115

116+
func TestAffinity(t *testing.T) {
117+
t.Parallel()
118+
119+
db, err := sqlite3.Open(":memory:")
120+
if err != nil {
121+
t.Fatal(err)
122+
}
123+
defer db.Close()
124+
125+
csv.Register(db)
126+
127+
const data = "01\n0.10\ne"
128+
err = db.Exec(`
129+
CREATE VIRTUAL TABLE temp.nums USING csv(
130+
data = ` + sqlite3.Quote(data) + `,
131+
schema = 'CREATE TABLE x(a numeric)'
132+
)`)
133+
if err != nil {
134+
t.Fatal(err)
135+
}
136+
137+
stmt, _, err := db.Prepare(`SELECT * FROM temp.nums`)
138+
if err != nil {
139+
t.Fatal(err)
140+
}
141+
defer stmt.Close()
142+
143+
if stmt.Step() {
144+
if got := stmt.ColumnText(0); got != "1" {
145+
t.Errorf("got %q want 1", got)
146+
}
147+
}
148+
if stmt.Step() {
149+
if got := stmt.ColumnText(0); got != "0.1" {
150+
t.Errorf("got %q want 0.1", got)
151+
}
152+
}
153+
if stmt.Step() {
154+
if got := stmt.ColumnText(0); got != "e" {
155+
t.Errorf("got %q want e", got)
156+
}
157+
}
158+
}
159+
116160
func TestRegister_errors(t *testing.T) {
117161
t.Parallel()
118162

ext/csv/types.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package csv
2+
3+
import (
4+
_ "embed"
5+
"strings"
6+
7+
"github.com/ncruces/go-sqlite3/util/vtabutil"
8+
)
9+
10+
type affinity byte
11+
12+
const (
13+
blob affinity = 0
14+
text affinity = 1
15+
numeric affinity = 2
16+
integer affinity = 3
17+
real affinity = 4
18+
)
19+
20+
func getColumnAffinities(schema string) []affinity {
21+
tab, err := vtabutil.Parse(schema)
22+
if err != nil {
23+
return nil
24+
}
25+
defer tab.Close()
26+
27+
types := make([]affinity, tab.NumColumns())
28+
for i := range types {
29+
col := tab.Column(i)
30+
types[i] = getAffinity(col.Type())
31+
}
32+
return types
33+
}
34+
35+
func getAffinity(declType string) affinity {
36+
// https://sqlite.org/datatype3.html#determination_of_column_affinity
37+
if declType == "" {
38+
return blob
39+
}
40+
name := strings.ToUpper(declType)
41+
if strings.Contains(name, "INT") {
42+
return integer
43+
}
44+
if strings.Contains(name, "CHAR") || strings.Contains(name, "CLOB") || strings.Contains(name, "TEXT") {
45+
return text
46+
}
47+
if strings.Contains(name, "BLOB") {
48+
return blob
49+
}
50+
if strings.Contains(name, "REAL") || strings.Contains(name, "FLOA") || strings.Contains(name, "DOUB") {
51+
return real
52+
}
53+
return numeric
54+
}

ext/csv/types_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package csv
2+
3+
import (
4+
_ "embed"
5+
"testing"
6+
)
7+
8+
func Test_getAffinity(t *testing.T) {
9+
tests := []struct {
10+
decl string
11+
want affinity
12+
}{
13+
{"", blob},
14+
{"INTEGER", integer},
15+
{"TINYINT", integer},
16+
{"TEXT", text},
17+
{"CHAR", text},
18+
{"CLOB", text},
19+
{"BLOB", blob},
20+
{"REAL", real},
21+
{"FLOAT", real},
22+
{"DOUBLE", real},
23+
{"NUMERIC", numeric},
24+
{"DECIMAL", numeric},
25+
{"BOOLEAN", numeric},
26+
{"DATETIME", numeric},
27+
}
28+
for _, tt := range tests {
29+
t.Run(tt.decl, func(t *testing.T) {
30+
if got := getAffinity(tt.decl); got != tt.want {
31+
t.Errorf("getAffinity() = %v, want %v", got, tt.want)
32+
}
33+
})
34+
}
35+
}

util/vtabutil/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Virtual Table utility functions
2+
3+
This package implements utilities mostly useful to virtual table implementations.
4+
5+
It also wraps a [parser](https://github.com/marcobambini/sqlite-createtable-parser)
6+
for the [`CREATE`](https://sqlite.org/lang_createtable.html) and
7+
[`ALTER TABLE`](https://sqlite.org/lang_altertable.html) commands,
8+
created by [Marco Bambini](https://github.com/marcobambini).

util/vtabutil/arg.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
// Package ioutil implements virtual table utility functions.
21
package vtabutil
32

43
import "strings"

util/vtabutil/parse.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package vtabutil
2+
3+
import (
4+
"context"
5+
"sync"
6+
7+
_ "embed"
8+
9+
"github.com/ncruces/go-sqlite3/internal/util"
10+
"github.com/tetratelabs/wazero"
11+
"github.com/tetratelabs/wazero/api"
12+
)
13+
14+
const (
15+
code = 4
16+
base = 8
17+
)
18+
19+
var (
20+
//go:embed parse/sql3parse_table.wasm
21+
binary []byte
22+
ctx context.Context
23+
once sync.Once
24+
runtime wazero.Runtime
25+
)
26+
27+
// Table holds metadata about a table.
28+
type Table struct {
29+
mod api.Module
30+
ptr uint32
31+
sql string
32+
}
33+
34+
// Parse parses a [CREATE] or [ALTER TABLE] command.
35+
//
36+
// [CREATE]: https://sqlite.org/lang_createtable.html
37+
// [ALTER TABLE]: https://sqlite.org/lang_altertable.html
38+
func Parse(sql string) (*Table, error) {
39+
once.Do(func() {
40+
ctx = context.Background()
41+
cfg := wazero.NewRuntimeConfigInterpreter().WithDebugInfoEnabled(false)
42+
runtime = wazero.NewRuntimeWithConfig(ctx, cfg)
43+
})
44+
45+
mod, err := runtime.InstantiateWithConfig(ctx, binary, wazero.NewModuleConfig().WithName(""))
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
if buf, ok := mod.Memory().Read(base, uint32(len(sql))); ok {
51+
copy(buf, sql)
52+
}
53+
r, err := mod.ExportedFunction("sql3parse_table").Call(ctx, base, uint64(len(sql)), code)
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
c, _ := mod.Memory().ReadUint32Le(code)
59+
if c == uint32(_MEMORY) {
60+
panic(util.OOMErr)
61+
}
62+
if c != uint32(_NONE) {
63+
return nil, ecode(c)
64+
}
65+
if r[0] == 0 {
66+
return nil, nil
67+
}
68+
return &Table{
69+
sql: sql,
70+
mod: mod,
71+
ptr: uint32(r[0]),
72+
}, nil
73+
}
74+
75+
// Close closes a table handle.
76+
func (t *Table) Close() error {
77+
mod := t.mod
78+
t.mod = nil
79+
return mod.Close(ctx)
80+
}
81+
82+
// NumColumns returns the number of columns of the table.
83+
func (t *Table) NumColumns() int {
84+
r, err := t.mod.ExportedFunction("sql3table_num_columns").Call(ctx, uint64(t.ptr))
85+
if err != nil {
86+
panic(err)
87+
}
88+
return int(int32(r[0]))
89+
}
90+
91+
// Column returns data for the ith column of the table.
92+
//
93+
// https://sqlite.org/lang_createtable.html#column_definitions
94+
func (t *Table) Column(i int) Column {
95+
r, err := t.mod.ExportedFunction("sql3table_get_column").Call(ctx, uint64(t.ptr), uint64(i))
96+
if err != nil {
97+
panic(err)
98+
}
99+
return Column{
100+
tab: t,
101+
ptr: uint32(r[0]),
102+
}
103+
}
104+
105+
// Column holds metadata about a column.
106+
type Column struct {
107+
tab *Table
108+
ptr uint32
109+
}
110+
111+
// Type returns the declared type of a column.
112+
//
113+
// https://sqlite.org/lang_createtable.html#column_data_types
114+
func (c Column) Type() string {
115+
r, err := c.tab.mod.ExportedFunction("sql3column_type").Call(ctx, uint64(c.ptr))
116+
if err != nil {
117+
panic(err)
118+
}
119+
if r[0] == 0 {
120+
return ""
121+
}
122+
off, _ := c.tab.mod.Memory().ReadUint32Le(uint32(r[0]) + 0)
123+
len, _ := c.tab.mod.Memory().ReadUint32Le(uint32(r[0]) + 4)
124+
return c.tab.sql[off-base : off+len-base]
125+
}
126+
127+
type ecode uint32
128+
129+
const (
130+
_NONE ecode = iota
131+
_MEMORY
132+
_SYNTAX
133+
_UNSUPPORTEDSQL
134+
)
135+
136+
func (e ecode) Error() string {
137+
switch e {
138+
case _SYNTAX:
139+
return "sql3parse: invalid syntax"
140+
case _UNSUPPORTEDSQL:
141+
return "sql3parse: unsupported SQL"
142+
default:
143+
panic(util.AssertErr())
144+
}
145+
}

util/vtabutil/parse/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
sql3parse_table.c
2+
sql3parse_table.h

0 commit comments

Comments
 (0)