From 58e91052bbb97f05d4cc69b27eef025ed83cb08e Mon Sep 17 00:00:00 2001 From: Nuno Cruces Date: Mon, 17 Jun 2024 23:44:37 +0100 Subject: [PATCH] CSV type affinity (#102) Use sqlite-createtable-parser compiled to Wasm to parse the CREATE TABLE statement. --- ext/csv/arg_test.go | 4 + ext/csv/csv.go | 39 +++++- ext/csv/csv_test.go | 44 +++++++ ext/csv/types.go | 54 +++++++++ ext/csv/types_test.go | 35 ++++++ util/vtabutil/README.md | 8 ++ util/vtabutil/arg.go | 1 - util/vtabutil/parse.go | 145 +++++++++++++++++++++++ util/vtabutil/parse/.gitignore | 2 + util/vtabutil/parse/build.sh | 28 +++++ util/vtabutil/parse/download.sh | 7 ++ util/vtabutil/parse/exports.txt | 4 + util/vtabutil/parse/sql3parse_table.wasm | Bin 0 -> 18116 bytes util/vtabutil/vtabutil.go | 2 + 14 files changed, 371 insertions(+), 2 deletions(-) create mode 100644 ext/csv/types.go create mode 100644 ext/csv/types_test.go create mode 100644 util/vtabutil/README.md create mode 100644 util/vtabutil/parse.go create mode 100644 util/vtabutil/parse/.gitignore create mode 100755 util/vtabutil/parse/build.sh create mode 100755 util/vtabutil/parse/download.sh create mode 100644 util/vtabutil/parse/exports.txt create mode 100755 util/vtabutil/parse/sql3parse_table.wasm create mode 100644 util/vtabutil/vtabutil.go diff --git a/ext/csv/arg_test.go b/ext/csv/arg_test.go index 50d4c47..6d009b8 100644 --- a/ext/csv/arg_test.go +++ b/ext/csv/arg_test.go @@ -40,6 +40,8 @@ func Test_uintArg(t *testing.T) { } func Test_boolArg(t *testing.T) { + t.Parallel() + tests := []struct { arg string key string @@ -76,6 +78,8 @@ func Test_boolArg(t *testing.T) { } func Test_runeArg(t *testing.T) { + t.Parallel() + tests := []struct { arg string key string diff --git a/ext/csv/csv.go b/ext/csv/csv.go index eb931c1..70517e1 100644 --- a/ext/csv/csv.go +++ b/ext/csv/csv.go @@ -12,6 +12,7 @@ import ( "fmt" "io" "io/fs" + "strconv" "strings" "github.com/ncruces/go-sqlite3" @@ -93,6 +94,8 @@ func RegisterFS(db *sqlite3.Conn, fsys fs.FS) { } } schema = getSchema(header, columns, row) + } else { + table.typs = getColumnAffinities(schema) } err = db.DeclareVTab(schema) @@ -113,6 +116,7 @@ type table struct { fsys fs.FS name string data string + typs []affinity comma rune header bool } @@ -226,7 +230,40 @@ func (c *cursor) RowID() (int64, error) { func (c *cursor) Column(ctx *sqlite3.Context, col int) error { if col < len(c.row) { - ctx.ResultText(c.row[col]) + var typ affinity + if col < len(c.table.typs) { + typ = c.table.typs[col] + } + + txt := c.row[col] + if typ == blob { + ctx.ResultText(txt) + return nil + } + if txt == "" { + return nil + } + + switch typ { + case numeric, integer: + if strings.TrimLeft(txt, "+-0123456789") == "" { + if i, err := strconv.ParseInt(txt, 10, 64); err == nil { + ctx.ResultInt64(i) + return nil + } + } + fallthrough + case real: + if strings.TrimLeft(txt, "+-.0123456789Ee") == "" { + if f, err := strconv.ParseFloat(txt, 64); err == nil { + ctx.ResultFloat(f) + return nil + } + } + fallthrough + case text: + ctx.ResultText(c.row[col]) + } } return nil } diff --git a/ext/csv/csv_test.go b/ext/csv/csv_test.go index 5f64343..5ae91e2 100644 --- a/ext/csv/csv_test.go +++ b/ext/csv/csv_test.go @@ -113,6 +113,50 @@ Robert "Griesemer" "gri"` } } +func TestAffinity(t *testing.T) { + t.Parallel() + + db, err := sqlite3.Open(":memory:") + if err != nil { + t.Fatal(err) + } + defer db.Close() + + csv.Register(db) + + const data = "01\n0.10\ne" + err = db.Exec(` + CREATE VIRTUAL TABLE temp.nums USING csv( + data = ` + sqlite3.Quote(data) + `, + schema = 'CREATE TABLE x(a numeric)' + )`) + if err != nil { + t.Fatal(err) + } + + stmt, _, err := db.Prepare(`SELECT * FROM temp.nums`) + if err != nil { + t.Fatal(err) + } + defer stmt.Close() + + if stmt.Step() { + if got := stmt.ColumnText(0); got != "1" { + t.Errorf("got %q want 1", got) + } + } + if stmt.Step() { + if got := stmt.ColumnText(0); got != "0.1" { + t.Errorf("got %q want 0.1", got) + } + } + if stmt.Step() { + if got := stmt.ColumnText(0); got != "e" { + t.Errorf("got %q want e", got) + } + } +} + func TestRegister_errors(t *testing.T) { t.Parallel() diff --git a/ext/csv/types.go b/ext/csv/types.go new file mode 100644 index 0000000..967b4e9 --- /dev/null +++ b/ext/csv/types.go @@ -0,0 +1,54 @@ +package csv + +import ( + _ "embed" + "strings" + + "github.com/ncruces/go-sqlite3/util/vtabutil" +) + +type affinity byte + +const ( + blob affinity = 0 + text affinity = 1 + numeric affinity = 2 + integer affinity = 3 + real affinity = 4 +) + +func getColumnAffinities(schema string) []affinity { + tab, err := vtabutil.Parse(schema) + if err != nil { + return nil + } + defer tab.Close() + + types := make([]affinity, tab.NumColumns()) + for i := range types { + col := tab.Column(i) + types[i] = getAffinity(col.Type()) + } + return types +} + +func getAffinity(declType string) affinity { + // https://sqlite.org/datatype3.html#determination_of_column_affinity + if declType == "" { + return blob + } + name := strings.ToUpper(declType) + if strings.Contains(name, "INT") { + return integer + } + if strings.Contains(name, "CHAR") || strings.Contains(name, "CLOB") || strings.Contains(name, "TEXT") { + return text + } + if strings.Contains(name, "BLOB") { + return blob + } + if strings.Contains(name, "REAL") || strings.Contains(name, "FLOA") || strings.Contains(name, "DOUB") { + return real + } + return numeric +} diff --git a/ext/csv/types_test.go b/ext/csv/types_test.go new file mode 100644 index 0000000..b88c351 --- /dev/null +++ b/ext/csv/types_test.go @@ -0,0 +1,35 @@ +package csv + +import ( + _ "embed" + "testing" +) + +func Test_getAffinity(t *testing.T) { + tests := []struct { + decl string + want affinity + }{ + {"", blob}, + {"INTEGER", integer}, + {"TINYINT", integer}, + {"TEXT", text}, + {"CHAR", text}, + {"CLOB", text}, + {"BLOB", blob}, + {"REAL", real}, + {"FLOAT", real}, + {"DOUBLE", real}, + {"NUMERIC", numeric}, + {"DECIMAL", numeric}, + {"BOOLEAN", numeric}, + {"DATETIME", numeric}, + } + for _, tt := range tests { + t.Run(tt.decl, func(t *testing.T) { + if got := getAffinity(tt.decl); got != tt.want { + t.Errorf("getAffinity() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/util/vtabutil/README.md b/util/vtabutil/README.md new file mode 100644 index 0000000..7653ae0 --- /dev/null +++ b/util/vtabutil/README.md @@ -0,0 +1,8 @@ +# Virtual Table utility functions + +This package implements utilities mostly useful to virtual table implementations. + +It also wraps a [parser](https://github.com/marcobambini/sqlite-createtable-parser) +for the [`CREATE`](https://sqlite.org/lang_createtable.html) and +[`ALTER TABLE`](https://sqlite.org/lang_altertable.html) commands, +created by [Marco Bambini](https://github.com/marcobambini). \ No newline at end of file diff --git a/util/vtabutil/arg.go b/util/vtabutil/arg.go index 15e2b74..7f8fac9 100644 --- a/util/vtabutil/arg.go +++ b/util/vtabutil/arg.go @@ -1,4 +1,3 @@ -// Package ioutil implements virtual table utility functions. package vtabutil import "strings" diff --git a/util/vtabutil/parse.go b/util/vtabutil/parse.go new file mode 100644 index 0000000..56154ca --- /dev/null +++ b/util/vtabutil/parse.go @@ -0,0 +1,145 @@ +package vtabutil + +import ( + "context" + "sync" + + _ "embed" + + "github.com/ncruces/go-sqlite3/internal/util" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" +) + +const ( + code = 4 + base = 8 +) + +var ( + //go:embed parse/sql3parse_table.wasm + binary []byte + ctx context.Context + once sync.Once + runtime wazero.Runtime +) + +// Table holds metadata about a table. +type Table struct { + mod api.Module + ptr uint32 + sql string +} + +// Parse parses a [CREATE] or [ALTER TABLE] command. +// +// [CREATE]: https://sqlite.org/lang_createtable.html +// [ALTER TABLE]: https://sqlite.org/lang_altertable.html +func Parse(sql string) (*Table, error) { + once.Do(func() { + ctx = context.Background() + cfg := wazero.NewRuntimeConfigInterpreter().WithDebugInfoEnabled(false) + runtime = wazero.NewRuntimeWithConfig(ctx, cfg) + }) + + mod, err := runtime.InstantiateWithConfig(ctx, binary, wazero.NewModuleConfig().WithName("")) + if err != nil { + return nil, err + } + + if buf, ok := mod.Memory().Read(base, uint32(len(sql))); ok { + copy(buf, sql) + } + r, err := mod.ExportedFunction("sql3parse_table").Call(ctx, base, uint64(len(sql)), code) + if err != nil { + return nil, err + } + + c, _ := mod.Memory().ReadUint32Le(code) + if c == uint32(_MEMORY) { + panic(util.OOMErr) + } + if c != uint32(_NONE) { + return nil, ecode(c) + } + if r[0] == 0 { + return nil, nil + } + return &Table{ + sql: sql, + mod: mod, + ptr: uint32(r[0]), + }, nil +} + +// Close closes a table handle. +func (t *Table) Close() error { + mod := t.mod + t.mod = nil + return mod.Close(ctx) +} + +// NumColumns returns the number of columns of the table. +func (t *Table) NumColumns() int { + r, err := t.mod.ExportedFunction("sql3table_num_columns").Call(ctx, uint64(t.ptr)) + if err != nil { + panic(err) + } + return int(int32(r[0])) +} + +// Column returns data for the ith column of the table. +// +// https://sqlite.org/lang_createtable.html#column_definitions +func (t *Table) Column(i int) Column { + r, err := t.mod.ExportedFunction("sql3table_get_column").Call(ctx, uint64(t.ptr), uint64(i)) + if err != nil { + panic(err) + } + return Column{ + tab: t, + ptr: uint32(r[0]), + } +} + +// Column holds metadata about a column. +type Column struct { + tab *Table + ptr uint32 +} + +// Type returns the declared type of a column. +// +// https://sqlite.org/lang_createtable.html#column_data_types +func (c Column) Type() string { + r, err := c.tab.mod.ExportedFunction("sql3column_type").Call(ctx, uint64(c.ptr)) + if err != nil { + panic(err) + } + if r[0] == 0 { + return "" + } + off, _ := c.tab.mod.Memory().ReadUint32Le(uint32(r[0]) + 0) + len, _ := c.tab.mod.Memory().ReadUint32Le(uint32(r[0]) + 4) + return c.tab.sql[off-base : off+len-base] +} + +type ecode uint32 + +const ( + _NONE ecode = iota + _MEMORY + _SYNTAX + _UNSUPPORTEDSQL +) + +func (e ecode) Error() string { + switch e { + case _SYNTAX: + return "sql3parse: invalid syntax" + case _UNSUPPORTEDSQL: + return "sql3parse: unsupported SQL" + default: + panic(util.AssertErr()) + } +} diff --git a/util/vtabutil/parse/.gitignore b/util/vtabutil/parse/.gitignore new file mode 100644 index 0000000..2b0defe --- /dev/null +++ b/util/vtabutil/parse/.gitignore @@ -0,0 +1,2 @@ +sql3parse_table.c +sql3parse_table.h \ No newline at end of file diff --git a/util/vtabutil/parse/build.sh b/util/vtabutil/parse/build.sh new file mode 100755 index 0000000..a21b01f --- /dev/null +++ b/util/vtabutil/parse/build.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd -P -- "$(dirname -- "$0")" + +ROOT=../../../ +BINARYEN="$ROOT/tools/binaryen-version_117/bin" +WASI_SDK="$ROOT/tools/wasi-sdk-22.0/bin" + +"$WASI_SDK/clang" --target=wasm32-wasi -std=c17 -flto -g0 -Oz \ + -Wall -Wextra -Wno-unused-parameter -Wno-unused-function \ + -o sql3parse_table.wasm sql3parse_table.c \ + -mexec-model=reactor \ + -msimd128 -mmutable-globals -mmultivalue \ + -mbulk-memory -mreference-types \ + -mnontrapping-fptoint -msign-ext \ + -fno-stack-protector -fno-stack-clash-protection \ + -Wl,--stack-first \ + -Wl,--import-undefined \ + $(awk '{print "-Wl,--export="$0}' exports.txt) + +trap 'rm -f sql3parse_table.tmp' EXIT +"$BINARYEN/wasm-ctor-eval" -g -c _initialize sql3parse_table.wasm -o sql3parse_table.tmp +"$BINARYEN/wasm-opt" -g --strip --strip-producers -c -Oz \ + sql3parse_table.tmp -o sql3parse_table.wasm \ + --enable-simd --enable-mutable-globals --enable-multivalue \ + --enable-bulk-memory --enable-reference-types \ + --enable-nontrapping-float-to-int --enable-sign-ext \ No newline at end of file diff --git a/util/vtabutil/parse/download.sh b/util/vtabutil/parse/download.sh new file mode 100755 index 0000000..4ef99a7 --- /dev/null +++ b/util/vtabutil/parse/download.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd -P -- "$(dirname -- "$0")" + +curl -#OL "https://github.com/ncruces/sqlite-createtable-parser/raw/master/sql3parse_table.c" +curl -#OL "https://github.com/ncruces/sqlite-createtable-parser/raw/master/sql3parse_table.h" \ No newline at end of file diff --git a/util/vtabutil/parse/exports.txt b/util/vtabutil/parse/exports.txt new file mode 100644 index 0000000..6bc38bc --- /dev/null +++ b/util/vtabutil/parse/exports.txt @@ -0,0 +1,4 @@ +sql3parse_table +sql3table_get_column +sql3table_num_columns +sql3column_type \ No newline at end of file diff --git a/util/vtabutil/parse/sql3parse_table.wasm b/util/vtabutil/parse/sql3parse_table.wasm new file mode 100755 index 0000000000000000000000000000000000000000..bf1efd6e381fcb2a7ce31e902a6e1abfea385894 GIT binary patch literal 18116 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 literal 0 HcmV?d00001 diff --git a/util/vtabutil/vtabutil.go b/util/vtabutil/vtabutil.go new file mode 100644 index 0000000..eb79778 --- /dev/null +++ b/util/vtabutil/vtabutil.go @@ -0,0 +1,2 @@ +// Package vtabutil implements virtual table utility functions. +package vtabutil