diff --git a/embed/build.sh b/embed/build.sh index abe5e60..8ea380e 100755 --- a/embed/build.sh +++ b/embed/build.sh @@ -7,7 +7,7 @@ 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 -O2 \ +"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -flto -g0 -O2 \ -Wall -Wextra -Wno-unused-parameter -Wno-unused-function \ -o sqlite3.wasm "$ROOT/sqlite3/main.c" \ -I"$ROOT/sqlite3" \ diff --git a/ext/csv/types.go b/ext/csv/types.go index e7697e5..ac4919a 100644 --- a/ext/csv/types.go +++ b/ext/csv/types.go @@ -1,7 +1,6 @@ package csv import ( - _ "embed" "strings" "github.com/ncruces/go-sqlite3/util/vtabutil" @@ -22,12 +21,10 @@ func getColumnAffinities(schema string) ([]affinity, error) { if err != nil { return nil, err } - defer tab.Close() - types := make([]affinity, tab.NumColumns()) - for i := range types { - col := tab.Column(i) - types[i] = getAffinity(col.Type()) + types := make([]affinity, len(tab.Columns)) + for i, col := range tab.Columns { + types[i] = getAffinity(col.Type) } return types, nil } diff --git a/ext/csv/types_test.go b/ext/csv/types_test.go index b88c351..ed7e7b5 100644 --- a/ext/csv/types_test.go +++ b/ext/csv/types_test.go @@ -1,9 +1,6 @@ package csv -import ( - _ "embed" - "testing" -) +import "testing" func Test_getAffinity(t *testing.T) { tests := []struct { diff --git a/util/vtabutil/const.go b/util/vtabutil/const.go new file mode 100644 index 0000000..291527f --- /dev/null +++ b/util/vtabutil/const.go @@ -0,0 +1,61 @@ +package vtabutil + +const ( + _NONE = iota + _MEMORY + _SYNTAX + _UNSUPPORTEDSQL +) + +type ConflictClause uint32 + +const ( + CONFLICT_NONE ConflictClause = iota + CONFLICT_ROLLBACK + CONFLICT_ABORT + CONFLICT_FAIL + CONFLICT_IGNORE + CONFLICT_REPLACE +) + +type OrderClause uint32 + +const ( + ORDER_NONE OrderClause = iota + ORDER_ASC + ORDER_DESC +) + +type FKAction uint32 + +const ( + FKACTION_NONE FKAction = iota + FKACTION_SETNULL + FKACTION_SETDEFAULT + FKACTION_CASCADE + FKACTION_RESTRICT + FKACTION_NOACTION +) + +type FKDefType uint32 + +const ( + DEFTYPE_NONE FKDefType = iota + DEFTYPE_DEFERRABLE + DEFTYPE_DEFERRABLE_INITIALLY_DEFERRED + DEFTYPE_DEFERRABLE_INITIALLY_IMMEDIATE + DEFTYPE_NOTDEFERRABLE + DEFTYPE_NOTDEFERRABLE_INITIALLY_DEFERRED + DEFTYPE_NOTDEFERRABLE_INITIALLY_IMMEDIATE +) + +type StatementType uint32 + +const ( + CREATE_UNKNOWN StatementType = iota + CREATE_TABLE + ALTER_RENAME_TABLE + ALTER_RENAME_COLUMN + ALTER_ADD_COLUMN + ALTER_DROP_COLUMN +) diff --git a/util/vtabutil/parse.go b/util/vtabutil/parse.go index 0b40772..63b2f8e 100644 --- a/util/vtabutil/parse.go +++ b/util/vtabutil/parse.go @@ -12,60 +12,50 @@ import ( ) const ( - _NONE = iota - _MEMORY - _SYNTAX - _UNSUPPORTEDSQL - - codeptr = 4 - baseptr = 8 + errp = 4 + sqlp = 8 ) var ( //go:embed parse/sql3parse_table.wasm - binary []byte - ctx context.Context - once sync.Once - runtime wazero.Runtime - module wazero.CompiledModule + binary []byte + once sync.Once + runtime wazero.Runtime + compiled wazero.CompiledModule ) -// 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, err error) { once.Do(func() { - ctx = context.Background() - cfg := wazero.NewRuntimeConfigInterpreter().WithDebugInfoEnabled(false) + ctx := context.Background() + cfg := wazero.NewRuntimeConfigInterpreter() runtime = wazero.NewRuntimeWithConfig(ctx, cfg) - module, err = runtime.CompileModule(ctx, binary) + compiled, err = runtime.CompileModule(ctx, binary) }) if err != nil { return nil, err } - mod, err := runtime.InstantiateModule(ctx, module, wazero.NewModuleConfig().WithName("")) + ctx := context.Background() + mod, err := runtime.InstantiateModule(ctx, compiled, wazero.NewModuleConfig().WithName("")) if err != nil { return nil, err } + defer mod.Close(ctx) - if buf, ok := mod.Memory().Read(baseptr, uint32(len(sql))); ok { + if buf, ok := mod.Memory().Read(sqlp, uint32(len(sql))); ok { copy(buf, sql) } - r, err := mod.ExportedFunction("sql3parse_table").Call(ctx, baseptr, uint64(len(sql)), codeptr) + + r, err := mod.ExportedFunction("sql3parse_table").Call(ctx, sqlp, uint64(len(sql)), errp) if err != nil { return nil, err } - c, _ := mod.Memory().ReadUint32Le(codeptr) + c, _ := mod.Memory().ReadUint32Le(errp) switch c { case _MEMORY: panic(util.OOMErr) @@ -74,68 +64,146 @@ func Parse(sql string) (_ *Table, err error) { case _UNSUPPORTEDSQL: return nil, util.ErrorString("sql3parse: unsupported SQL") } - if r[0] == 0 { - return nil, nil - } - return &Table{ - sql: sql, - mod: mod, - ptr: uint32(r[0]), - }, nil + + var tab Table + tab.load(mod, uint32(r[0]), sql) + return &tab, nil } -// Close closes a table handle. -func (t *Table) Close() error { - mod := t.mod - t.mod = nil - return mod.Close(ctx) +// Table holds metadata about a table. +type Table struct { + Name string + Schema string + Comment string + IsTemporary bool + IsIfNotExists bool + IsWithoutRowID bool + IsStrict bool + Columns []Column + Type StatementType + CurrentName string + NewName string } -// 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])) -} +func (t *Table) load(mod api.Module, ptr uint32, sql string) { + t.Name = loadString(mod, ptr+0, sql) + t.Schema = loadString(mod, ptr+8, sql) + t.Comment = loadString(mod, ptr+16, sql) -// 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]), - } -} + t.IsTemporary = loadBool(mod, ptr+24) + t.IsIfNotExists = loadBool(mod, ptr+25) + t.IsWithoutRowID = loadBool(mod, ptr+26) + t.IsStrict = loadBool(mod, ptr+27) -func (t *Table) string(ptr uint32) string { - if ptr == 0 { - return "" - } - off, _ := t.mod.Memory().ReadUint32Le(ptr + 0) - len, _ := t.mod.Memory().ReadUint32Le(ptr + 4) - return t.sql[off-baseptr : off+len-baseptr] + t.Columns = loadSlice(mod, ptr+28, func(ptr uint32, res *Column) { + p, _ := mod.Memory().ReadUint32Le(ptr) + res.load(mod, p, sql) + }) + + t.Type = loadEnum[StatementType](mod, ptr+44) + t.CurrentName = loadString(mod, ptr+48, sql) + t.NewName = loadString(mod, ptr+56, sql) } // Column holds metadata about a column. type Column struct { - tab *Table - ptr uint32 + Name string + Type string + Length string + ConstraintName string + Comment string + IsPrimaryKey bool + IsAutoIncrement bool + IsNotNull bool + IsUnique bool + PKOrder OrderClause + PKConflictClause ConflictClause + NotNullConflictClause ConflictClause + UniqueConflictClause ConflictClause + CheckExpr string + DefaultExpr string + CollateName string + ForeignKeyClause *ForeignKey } -// 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) +func (c *Column) load(mod api.Module, ptr uint32, sql string) { + c.Name = loadString(mod, ptr+0, sql) + c.Type = loadString(mod, ptr+8, sql) + c.Length = loadString(mod, ptr+16, sql) + c.ConstraintName = loadString(mod, ptr+24, sql) + c.Comment = loadString(mod, ptr+32, sql) + + c.IsPrimaryKey = loadBool(mod, ptr+40) + c.IsAutoIncrement = loadBool(mod, ptr+41) + c.IsNotNull = loadBool(mod, ptr+42) + c.IsUnique = loadBool(mod, ptr+43) + + c.PKOrder = loadEnum[OrderClause](mod, ptr+44) + c.PKConflictClause = loadEnum[ConflictClause](mod, ptr+48) + c.NotNullConflictClause = loadEnum[ConflictClause](mod, ptr+52) + c.UniqueConflictClause = loadEnum[ConflictClause](mod, ptr+56) + + c.CheckExpr = loadString(mod, ptr+60, sql) + c.DefaultExpr = loadString(mod, ptr+68, sql) + c.CollateName = loadString(mod, ptr+76, sql) + + if ptr, _ := mod.Memory().ReadUint32Le(ptr + 84); ptr != 0 { + c.ForeignKeyClause = &ForeignKey{} + c.ForeignKeyClause.load(mod, ptr, sql) } - return c.tab.string(uint32(r[0])) +} + +type ForeignKey struct { + Table string + Columns []string + OnDelete FKAction + OnUpdate FKAction + Match string + Deferrable FKDefType +} + +func (f *ForeignKey) load(mod api.Module, ptr uint32, sql string) { + f.Table = loadString(mod, ptr+0, sql) + + f.Columns = loadSlice(mod, ptr+8, func(ptr uint32, res *string) { + *res = loadString(mod, ptr, sql) + }) + + f.OnDelete = loadEnum[FKAction](mod, ptr+16) + f.OnUpdate = loadEnum[FKAction](mod, ptr+20) + f.Match = loadString(mod, ptr+24, sql) + f.Deferrable = loadEnum[FKDefType](mod, ptr+32) +} + +func loadString(mod api.Module, ptr uint32, sql string) string { + off, _ := mod.Memory().ReadUint32Le(ptr + 0) + if off == 0 { + return "" + } + len, _ := mod.Memory().ReadUint32Le(ptr + 4) + return sql[off-sqlp : off+len-sqlp] +} + +func loadSlice[T any](mod api.Module, ptr uint32, fn func(uint32, *T)) []T { + ref, _ := mod.Memory().ReadUint32Le(ptr + 4) + if ref == 0 { + return nil + } + len, _ := mod.Memory().ReadUint32Le(ptr + 0) + res := make([]T, len) + for i := range res { + fn(ref, &res[i]) + ref += 4 + } + return res +} + +func loadEnum[T ~uint32](mod api.Module, ptr uint32) T { + val, _ := mod.Memory().ReadUint32Le(ptr) + return T(val) +} + +func loadBool(mod api.Module, ptr uint32) bool { + val, _ := mod.Memory().ReadByte(ptr) + return val != 0 } diff --git a/util/vtabutil/parse/build.sh b/util/vtabutil/parse/build.sh index a21b01f..abb2162 100755 --- a/util/vtabutil/parse/build.sh +++ b/util/vtabutil/parse/build.sh @@ -7,9 +7,8 @@ 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 \ +"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -flto -g0 -Oz \ + -Wall -Wextra -o sql3parse_table.wasm main.c \ -mexec-model=reactor \ -msimd128 -mmutable-globals -mmultivalue \ -mbulk-memory -mreference-types \ @@ -17,11 +16,11 @@ WASI_SDK="$ROOT/tools/wasi-sdk-22.0/bin" -fno-stack-protector -fno-stack-clash-protection \ -Wl,--stack-first \ -Wl,--import-undefined \ - $(awk '{print "-Wl,--export="$0}' exports.txt) + -Wl,--export=sql3parse_table 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 \ +"$BINARYEN/wasm-ctor-eval" -c _initialize sql3parse_table.wasm -o sql3parse_table.tmp +"$BINARYEN/wasm-opt" --strip --strip-debug --strip-producers -c -Oz \ sql3parse_table.tmp -o sql3parse_table.wasm \ --enable-simd --enable-mutable-globals --enable-multivalue \ --enable-bulk-memory --enable-reference-types \ diff --git a/util/vtabutil/parse/exports.txt b/util/vtabutil/parse/exports.txt deleted file mode 100644 index 6bc38bc..0000000 --- a/util/vtabutil/parse/exports.txt +++ /dev/null @@ -1,4 +0,0 @@ -sql3parse_table -sql3table_get_column -sql3table_num_columns -sql3column_type \ No newline at end of file diff --git a/util/vtabutil/parse/main.c b/util/vtabutil/parse/main.c new file mode 100644 index 0000000..ede7edb --- /dev/null +++ b/util/vtabutil/parse/main.c @@ -0,0 +1,42 @@ +#include + +#include "sql3parse_table.c" + +static_assert(offsetof(sql3table, name) == 0, "Unexpected offset"); +static_assert(offsetof(sql3table, schema) == 8, "Unexpected offset"); +static_assert(offsetof(sql3table, comment) == 16, "Unexpected offset"); +static_assert(offsetof(sql3table, is_temporary) == 24, "Unexpected offset"); +static_assert(offsetof(sql3table, is_ifnotexists) == 25, "Unexpected offset"); +static_assert(offsetof(sql3table, is_withoutrowid) == 26, "Unexpected offset"); +static_assert(offsetof(sql3table, is_strict) == 27, "Unexpected offset"); +static_assert(offsetof(sql3table, num_columns) == 28, "Unexpected offset"); +static_assert(offsetof(sql3table, columns) == 32, "Unexpected offset"); +static_assert(offsetof(sql3table, type) == 44, "Unexpected offset"); +static_assert(offsetof(sql3table, current_name) == 48, "Unexpected offset"); +static_assert(offsetof(sql3table, new_name) == 56, "Unexpected offset"); + +static_assert(offsetof(sql3column, name) == 0, "Unexpected offset"); +static_assert(offsetof(sql3column, type) == 8, "Unexpected offset"); +static_assert(offsetof(sql3column, length) == 16, "Unexpected offset"); +static_assert(offsetof(sql3column, constraint_name) == 24, "Unexpected offset"); +static_assert(offsetof(sql3column, comment) == 32, "Unexpected offset"); +static_assert(offsetof(sql3column, is_primarykey) == 40, "Unexpected offset"); +static_assert(offsetof(sql3column, is_autoincrement) == 41, "Unexpected offset"); +static_assert(offsetof(sql3column, is_notnull) == 42, "Unexpected offset"); +static_assert(offsetof(sql3column, is_unique) == 43, "Unexpected offset"); +static_assert(offsetof(sql3column, pk_order) == 44, "Unexpected offset"); +static_assert(offsetof(sql3column, pk_conflictclause) == 48, "Unexpected offset"); +static_assert(offsetof(sql3column, notnull_conflictclause) == 52, "Unexpected offset"); +static_assert(offsetof(sql3column, unique_conflictclause) == 56, "Unexpected offset"); +static_assert(offsetof(sql3column, check_expr) == 60, "Unexpected offset"); +static_assert(offsetof(sql3column, default_expr) == 68, "Unexpected offset"); +static_assert(offsetof(sql3column, collate_name) == 76, "Unexpected offset"); +static_assert(offsetof(sql3column, foreignkey_clause) == 84, "Unexpected offset"); + +static_assert(offsetof(sql3foreignkey, table) == 0, "Unexpected offset"); +static_assert(offsetof(sql3foreignkey, num_columns) == 8, "Unexpected offset"); +static_assert(offsetof(sql3foreignkey, column_name) == 12, "Unexpected offset"); +static_assert(offsetof(sql3foreignkey, on_delete) == 16, "Unexpected offset"); +static_assert(offsetof(sql3foreignkey, on_update) == 20, "Unexpected offset"); +static_assert(offsetof(sql3foreignkey, match) == 24, "Unexpected offset"); +static_assert(offsetof(sql3foreignkey, deferrable) == 32, "Unexpected offset"); \ No newline at end of file diff --git a/util/vtabutil/parse/sql3parse_table.wasm b/util/vtabutil/parse/sql3parse_table.wasm index bf1efd6..be457df 100755 Binary files a/util/vtabutil/parse/sql3parse_table.wasm and b/util/vtabutil/parse/sql3parse_table.wasm differ diff --git a/util/vtabutil/parse_test.go b/util/vtabutil/parse_test.go new file mode 100644 index 0000000..0dd7776 --- /dev/null +++ b/util/vtabutil/parse_test.go @@ -0,0 +1,31 @@ +package vtabutil_test + +import ( + "testing" + + "github.com/ncruces/go-sqlite3/util/vtabutil" +) + +func TestParse(t *testing.T) { + tab, err := vtabutil.Parse(`CREATE TABLE child(x REFERENCES parent)`) + if err != nil { + t.Fatal(err) + } + + if got := tab.Name; got != "child" { + t.Errorf("got %s, want child", got) + } + if got := len(tab.Columns); got != 1 { + t.Errorf("got %d, want 1", got) + } + + col := tab.Columns[0] + if got := col.Name; got != "x" { + t.Errorf("got %s, want x", got) + } + + fk := col.ForeignKeyClause + if got := fk.Table; got != "parent" { + t.Errorf("got %s, want parent", got) + } +} diff --git a/vfs/tests/mptest/testdata/build.sh b/vfs/tests/mptest/testdata/build.sh index 598a19d..3ec27c3 100755 --- a/vfs/tests/mptest/testdata/build.sh +++ b/vfs/tests/mptest/testdata/build.sh @@ -7,7 +7,7 @@ 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 -O2 \ +"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -flto -g0 -O2 \ -o mptest.wasm main.c \ -I"$ROOT/sqlite3" \ -msimd128 -mmutable-globals \ diff --git a/vfs/tests/speedtest1/testdata/build.sh b/vfs/tests/speedtest1/testdata/build.sh index 061ae52..af15369 100755 --- a/vfs/tests/speedtest1/testdata/build.sh +++ b/vfs/tests/speedtest1/testdata/build.sh @@ -7,7 +7,7 @@ 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 -O2 \ +"$WASI_SDK/clang" --target=wasm32-wasi -std=c23 -flto -g0 -O2 \ -o speedtest1.wasm main.c \ -I"$ROOT/sqlite3" \ -msimd128 -mmutable-globals \