diff --git a/README.md b/README.md index 217c049..dc874d2 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ and uses [wazero](https://wazero.io/) to provide `cgo`-free SQLite bindings. - [x] [virtual tables](https://sqlite.org/vtab.html) - [x] [custom VFSes](https://sqlite.org/vfs.html) - [x] [online backup](https://sqlite.org/backup.html) -- [x] [JSON support](https://www.sqlite.org/json1.html) +- [x] [JSON support](https://sqlite.org/json1.html) - [x] [Unicode support](https://sqlite.org/src/dir/ext/icu) ### Caveats diff --git a/driver/driver.go b/driver/driver.go index f867c67..564786c 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -458,6 +458,16 @@ func (r *rows) Columns() []string { return columns } +func (r *rows) ColumnTypeDatabaseTypeName(index int) string { + decltype := r.Stmt.ColumnDeclType(index) + if len := len(decltype); len > 0 && decltype[len-1] == ')' { + if i := strings.LastIndexByte(decltype, '('); i >= 0 { + decltype = decltype[:i] + } + } + return strings.ToUpper(strings.TrimSpace(decltype)) +} + func (r *rows) Next(dest []driver.Value) error { old := r.Conn.SetInterrupt(r.ctx) defer r.Conn.SetInterrupt(old) diff --git a/embed/exports.txt b/embed/exports.txt index a98d79c..88310f0 100644 --- a/embed/exports.txt +++ b/embed/exports.txt @@ -1,95 +1,97 @@ free malloc malloc_destructor -sqlite3_errcode -sqlite3_errstr -sqlite3_errmsg -sqlite3_error_offset -sqlite3_open_v2 -sqlite3_close -sqlite3_close_v2 -sqlite3_prepare_v3 -sqlite3_finalize -sqlite3_reset -sqlite3_step -sqlite3_exec -sqlite3_interrupt -sqlite3_progress_handler_go -sqlite3_clear_bindings +sqlite3_aggregate_context +sqlite3_anycollseq_init +sqlite3_backup_finish +sqlite3_backup_init +sqlite3_backup_pagecount +sqlite3_backup_remaining +sqlite3_backup_step +sqlite3_bind_blob64 +sqlite3_bind_double +sqlite3_bind_int64 +sqlite3_bind_null sqlite3_bind_parameter_count sqlite3_bind_parameter_index sqlite3_bind_parameter_name -sqlite3_bind_null -sqlite3_bind_int64 -sqlite3_bind_double -sqlite3_bind_text64 -sqlite3_bind_blob64 -sqlite3_bind_zeroblob64 sqlite3_bind_pointer_go +sqlite3_bind_text64 sqlite3_bind_value -sqlite3_column_count -sqlite3_column_name -sqlite3_column_type -sqlite3_column_int64 -sqlite3_column_double -sqlite3_column_text +sqlite3_bind_zeroblob64 +sqlite3_blob_bytes +sqlite3_blob_close +sqlite3_blob_open +sqlite3_blob_read +sqlite3_blob_reopen +sqlite3_blob_write +sqlite3_changes64 +sqlite3_clear_bindings +sqlite3_close +sqlite3_close_v2 sqlite3_column_blob sqlite3_column_bytes +sqlite3_column_count +sqlite3_column_decltype +sqlite3_column_double +sqlite3_column_int64 +sqlite3_column_name +sqlite3_column_text +sqlite3_column_type sqlite3_column_value -sqlite3_blob_open -sqlite3_blob_close -sqlite3_blob_reopen -sqlite3_blob_bytes -sqlite3_blob_read -sqlite3_blob_write -sqlite3_backup_init -sqlite3_backup_step -sqlite3_backup_finish -sqlite3_backup_remaining -sqlite3_backup_pagecount -sqlite3_uri_parameter -sqlite3_uri_key -sqlite3_changes64 -sqlite3_last_insert_rowid -sqlite3_get_autocommit +sqlite3_create_aggregate_function_go sqlite3_create_collation_go sqlite3_create_function_go -sqlite3_create_aggregate_function_go -sqlite3_create_window_function_go sqlite3_create_module_go -sqlite3_overload_function -sqlite3_anycollseq_init -sqlite3_aggregate_context -sqlite3_user_data -sqlite3_set_auxdata_go +sqlite3_create_window_function_go +sqlite3_declare_vtab +sqlite3_errcode +sqlite3_errmsg +sqlite3_error_offset +sqlite3_errstr +sqlite3_exec +sqlite3_finalize +sqlite3_get_autocommit sqlite3_get_auxdata -sqlite3_value_type -sqlite3_value_int64 -sqlite3_value_double -sqlite3_value_text -sqlite3_value_blob -sqlite3_value_bytes -sqlite3_value_pointer_go -sqlite3_value_nochange -sqlite3_result_null -sqlite3_result_int64 -sqlite3_result_double -sqlite3_result_text64 +sqlite3_interrupt +sqlite3_last_insert_rowid +sqlite3_open_v2 +sqlite3_overload_function +sqlite3_prepare_v3 +sqlite3_progress_handler_go +sqlite3_reset sqlite3_result_blob64 -sqlite3_result_zeroblob64 -sqlite3_result_pointer_go -sqlite3_result_value +sqlite3_result_double sqlite3_result_error sqlite3_result_error_code sqlite3_result_error_nomem sqlite3_result_error_toobig -sqlite3_declare_vtab -sqlite3_vtab_config_go +sqlite3_result_int64 +sqlite3_result_null +sqlite3_result_pointer_go +sqlite3_result_text64 +sqlite3_result_value +sqlite3_result_zeroblob64 +sqlite3_set_auxdata_go +sqlite3_step +sqlite3_stmt_readonly +sqlite3_uri_key +sqlite3_uri_parameter +sqlite3_user_data +sqlite3_value_blob +sqlite3_value_bytes +sqlite3_value_double +sqlite3_value_int64 +sqlite3_value_nochange +sqlite3_value_pointer_go +sqlite3_value_text +sqlite3_value_type sqlite3_vtab_collation +sqlite3_vtab_config_go sqlite3_vtab_distinct sqlite3_vtab_in sqlite3_vtab_in_first sqlite3_vtab_in_next -sqlite3_vtab_rhs_value sqlite3_vtab_nochange -sqlite3_vtab_on_conflict \ No newline at end of file +sqlite3_vtab_on_conflict +sqlite3_vtab_rhs_value \ No newline at end of file diff --git a/embed/sqlite3.wasm b/embed/sqlite3.wasm index 54e9162..47c4d10 100755 Binary files a/embed/sqlite3.wasm and b/embed/sqlite3.wasm differ diff --git a/ext/statement/stmt.go b/ext/statement/stmt.go index 0dacc79..627f220 100644 --- a/ext/statement/stmt.go +++ b/ext/statement/stmt.go @@ -56,7 +56,9 @@ func (t *table) declare() error { if tail != "" { return fmt.Errorf("statement: multiple statements") } - // TODO: sqlite3_stmt_readonly + if !stmt.ReadOnly() { + return fmt.Errorf("statement: statement must be read only") + } t.inputs = stmt.BindCount() t.outputs = stmt.ColumnCount() @@ -68,14 +70,17 @@ func (t *table) declare() error { str.WriteString(sep) name := stmt.ColumnName(i) str.WriteString(sqlite3.QuoteIdentifier(name)) - // TODO: sqlite3_column_decltype + if typ := stmt.ColumnDeclType(i); typ != "" { + str.WriteByte(' ') + str.WriteString(typ) + } sep = "," } for i := 1; i <= t.inputs; i++ { str.WriteString(sep) name := stmt.BindName(i) if name == "" { - str.WriteByte('\'') + str.WriteString("'") str.WriteString(strconv.Itoa(i)) str.WriteString("' HIDDEN") } else { diff --git a/sqlite3/sqlite_cfg.h b/sqlite3/sqlite_cfg.h index e3e746a..28c02aa 100644 --- a/sqlite3/sqlite_cfg.h +++ b/sqlite3/sqlite_cfg.h @@ -37,11 +37,12 @@ #define SQLITE_DEFAULT_WAL_SYNCHRONOUS 1 #define SQLITE_LIKE_DOESNT_MATCH_BLOBS #define SQLITE_MAX_EXPR_DEPTH 0 -#define SQLITE_OMIT_DECLTYPE +#define SQLITE_USE_ALLOCA #define SQLITE_OMIT_DEPRECATED #define SQLITE_OMIT_SHARED_CACHE #define SQLITE_OMIT_AUTOINIT -#define SQLITE_USE_ALLOCA +// #define SQLITE_OMIT_DECLTYPE +// #define SQLITE_OMIT_PROGRESS_CALLBACK // Other Options diff --git a/stmt.go b/stmt.go index cb220e3..3f5da0d 100644 --- a/stmt.go +++ b/stmt.go @@ -90,6 +90,15 @@ func (s *Stmt) Exec() error { return s.Reset() } +// ReadOnly returns true if and only if the statement +// makes no direct changes to the content of the database file. +// +// https://sqlite.org/c3ref/stmt_readonly.html +func (s *Stmt) ReadOnly() bool { + r := s.c.call("sqlite3_stmt_readonly", uint64(s.handle)) + return r != 0 +} + // BindCount returns the number of SQL parameters in the prepared statement. // // https://sqlite.org/c3ref/bind_parameter_count.html @@ -344,6 +353,19 @@ func (s *Stmt) ColumnType(col int) Datatype { return Datatype(r) } +// ColumnDeclType returns the declared datatype of the result column. +// The leftmost column of the result set has the index 0. +// +// https://sqlite.org/c3ref/column_decltype.html +func (s *Stmt) ColumnDeclType(col int) string { + r := s.c.call("sqlite3_column_decltype", + uint64(s.handle), uint64(col)) + if r == 0 { + return "" + } + return util.ReadString(s.c.mod, uint32(r), _MAX_NAME) +} + // ColumnBool returns the value of the result column as a bool. // The leftmost column of the result set has the index 0. // SQLite does not have a separate boolean storage class. diff --git a/tests/driver_test.go b/tests/driver_test.go index d46e35d..2d57a07 100644 --- a/tests/driver_test.go +++ b/tests/driver_test.go @@ -72,6 +72,17 @@ func TestDriver(t *testing.T) { } defer rows.Close() + typs, err := rows.ColumnTypes() + if err != nil { + t.Fatal(err) + } + if got := typs[0].DatabaseTypeName(); got != "INT" { + t.Errorf("got %s, want INT", got) + } + if got := typs[1].DatabaseTypeName(); got != "VARCHAR" { + t.Errorf("got %s, want INT", got) + } + row := 0 ids := []int{0, 1, 2} names := []string{"go", "zig", "whatever"}