diff --git a/callback.go b/callback.go index d3056910..5781d42c 100644 --- a/callback.go +++ b/callback.go @@ -360,11 +360,11 @@ func callbackRetGeneric(ctx *C.sqlite3_context, v reflect.Value) error { } cb, err := callbackRet(v.Elem().Type()) - if err != nil { - return err - } + if err != nil { + return err + } - return cb(ctx, v.Elem()) + return cb(ctx, v.Elem()) } func callbackRet(typ reflect.Type) (callbackRetConverter, error) { @@ -409,3 +409,9 @@ func callbackSyntheticForTests(v reflect.Value, err error) callbackArgConverter return v, err } } + +// NULL is interpreted as an nil byte slice. +func IsNullCallbackArg(arg interface{}) bool { + val, ok := arg.([]byte) + return ok && val == nil +} diff --git a/sqlite3.go b/sqlite3.go index 5e4e2ff5..834dd20c 100644 --- a/sqlite3.go +++ b/sqlite3.go @@ -965,103 +965,104 @@ func (c *SQLiteConn) begin(ctx context.Context) (driver.Tx, error) { // The argument is may be either in parentheses or it may be separated from // the pragma name by an equal sign. The two syntaxes yield identical results. // In many pragmas, the argument is a boolean. The boolean can be one of: -// 1 yes true on -// 0 no false off +// +// 1 yes true on +// 0 no false off // // You can specify a DSN string using a URI as the filename. -// test.db -// file:test.db?cache=shared&mode=memory -// :memory: -// file::memory: // -// mode -// Access mode of the database. -// https://www.sqlite.org/c3ref/open.html -// Values: -// - ro -// - rw -// - rwc -// - memory +// test.db +// file:test.db?cache=shared&mode=memory +// :memory: +// file::memory: // -// cache -// SQLite Shared-Cache Mode -// https://www.sqlite.org/sharedcache.html -// Values: -// - shared -// - private +// mode +// Access mode of the database. +// https://www.sqlite.org/c3ref/open.html +// Values: +// - ro +// - rw +// - rwc +// - memory // -// immutable=Boolean -// The immutable parameter is a boolean query parameter that indicates -// that the database file is stored on read-only media. When immutable is set, -// SQLite assumes that the database file cannot be changed, -// even by a process with higher privilege, -// and so the database is opened read-only and all locking and change detection is disabled. -// Caution: Setting the immutable property on a database file that -// does in fact change can result in incorrect query results and/or SQLITE_CORRUPT errors. +// cache +// SQLite Shared-Cache Mode +// https://www.sqlite.org/sharedcache.html +// Values: +// - shared +// - private // -// go-sqlite3 adds the following query parameters to those used by SQLite: -// _loc=XXX -// Specify location of time format. It's possible to specify "auto". +// immutable=Boolean +// The immutable parameter is a boolean query parameter that indicates +// that the database file is stored on read-only media. When immutable is set, +// SQLite assumes that the database file cannot be changed, +// even by a process with higher privilege, +// and so the database is opened read-only and all locking and change detection is disabled. +// Caution: Setting the immutable property on a database file that +// does in fact change can result in incorrect query results and/or SQLITE_CORRUPT errors. // -// _mutex=XXX -// Specify mutex mode. XXX can be "no", "full". +// go-sqlite3 adds the following query parameters to those used by SQLite: // -// _txlock=XXX -// Specify locking behavior for transactions. XXX can be "immediate", -// "deferred", "exclusive". +// _loc=XXX +// Specify location of time format. It's possible to specify "auto". // -// _auto_vacuum=X | _vacuum=X -// 0 | none - Auto Vacuum disabled -// 1 | full - Auto Vacuum FULL -// 2 | incremental - Auto Vacuum Incremental +// _mutex=XXX +// Specify mutex mode. XXX can be "no", "full". // -// _busy_timeout=XXX"| _timeout=XXX -// Specify value for sqlite3_busy_timeout. +// _txlock=XXX +// Specify locking behavior for transactions. XXX can be "immediate", +// "deferred", "exclusive". // -// _case_sensitive_like=Boolean | _cslike=Boolean -// https://www.sqlite.org/pragma.html#pragma_case_sensitive_like -// Default or disabled the LIKE operation is case-insensitive. -// When enabling this options behaviour of LIKE will become case-sensitive. +// _auto_vacuum=X | _vacuum=X +// 0 | none - Auto Vacuum disabled +// 1 | full - Auto Vacuum FULL +// 2 | incremental - Auto Vacuum Incremental // -// _defer_foreign_keys=Boolean | _defer_fk=Boolean -// Defer Foreign Keys until outermost transaction is committed. +// _busy_timeout=XXX"| _timeout=XXX +// Specify value for sqlite3_busy_timeout. // -// _foreign_keys=Boolean | _fk=Boolean -// Enable or disable enforcement of foreign keys. +// _case_sensitive_like=Boolean | _cslike=Boolean +// https://www.sqlite.org/pragma.html#pragma_case_sensitive_like +// Default or disabled the LIKE operation is case-insensitive. +// When enabling this options behaviour of LIKE will become case-sensitive. // -// _ignore_check_constraints=Boolean -// This pragma enables or disables the enforcement of CHECK constraints. -// The default setting is off, meaning that CHECK constraints are enforced by default. +// _defer_foreign_keys=Boolean | _defer_fk=Boolean +// Defer Foreign Keys until outermost transaction is committed. // -// _journal_mode=MODE | _journal=MODE -// Set journal mode for the databases associated with the current connection. -// https://www.sqlite.org/pragma.html#pragma_journal_mode +// _foreign_keys=Boolean | _fk=Boolean +// Enable or disable enforcement of foreign keys. // -// _locking_mode=X | _locking=X -// Sets the database connection locking-mode. -// The locking-mode is either NORMAL or EXCLUSIVE. -// https://www.sqlite.org/pragma.html#pragma_locking_mode +// _ignore_check_constraints=Boolean +// This pragma enables or disables the enforcement of CHECK constraints. +// The default setting is off, meaning that CHECK constraints are enforced by default. // -// _query_only=Boolean -// The query_only pragma prevents all changes to database files when enabled. +// _journal_mode=MODE | _journal=MODE +// Set journal mode for the databases associated with the current connection. +// https://www.sqlite.org/pragma.html#pragma_journal_mode // -// _recursive_triggers=Boolean | _rt=Boolean -// Enable or disable recursive triggers. +// _locking_mode=X | _locking=X +// Sets the database connection locking-mode. +// The locking-mode is either NORMAL or EXCLUSIVE. +// https://www.sqlite.org/pragma.html#pragma_locking_mode // -// _secure_delete=Boolean|FAST -// When secure_delete is on, SQLite overwrites deleted content with zeros. -// https://www.sqlite.org/pragma.html#pragma_secure_delete +// _query_only=Boolean +// The query_only pragma prevents all changes to database files when enabled. // -// _synchronous=X | _sync=X -// Change the setting of the "synchronous" flag. -// https://www.sqlite.org/pragma.html#pragma_synchronous +// _recursive_triggers=Boolean | _rt=Boolean +// Enable or disable recursive triggers. // -// _writable_schema=Boolean -// When this pragma is on, the SQLITE_MASTER tables in which database -// can be changed using ordinary UPDATE, INSERT, and DELETE statements. -// Warning: misuse of this pragma can easily result in a corrupt database file. +// _secure_delete=Boolean|FAST +// When secure_delete is on, SQLite overwrites deleted content with zeros. +// https://www.sqlite.org/pragma.html#pragma_secure_delete // +// _synchronous=X | _sync=X +// Change the setting of the "synchronous" flag. +// https://www.sqlite.org/pragma.html#pragma_synchronous // +// _writable_schema=Boolean +// When this pragma is on, the SQLITE_MASTER tables in which database +// can be changed using ordinary UPDATE, INSERT, and DELETE statements. +// Warning: misuse of this pragma can easily result in a corrupt database file. func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) { if C.sqlite3_threadsafe() == 0 { return nil, errors.New("sqlite library was not compiled for thread-safe operation") diff --git a/sqlite3_test.go b/sqlite3_test.go index 326361ec..a8280011 100644 --- a/sqlite3_test.go +++ b/sqlite3_test.go @@ -1128,7 +1128,7 @@ func TestQueryer(t *testing.T) { if err != nil { t.Error("Failed to db.Query:", err) } - if id != n + 1 { + if id != n+1 { t.Error("Failed to db.Query: not matched results") } n = n + 1 @@ -1439,6 +1439,65 @@ func TestFunctionRegistration(t *testing.T) { } } +func TestNullCallbackArg(t *testing.T) { + sql.Register("sqlite3_NullCallbackArg", &SQLiteDriver{ + ConnectHook: func(conn *SQLiteConn) error { + return conn.RegisterFunc("isNullArg", IsNullCallbackArg, true) + }, + }) + db, err := sql.Open("sqlite3_NullCallbackArg", ":memory:") + if err != nil { + t.Fatal("Failed to open database:", err) + } + defer db.Close() + + _, err = db.Exec("CREATE TABLE test (id integer not null primary key, col_int int, col_float float, col_blob blob, col_bool bool)") + if err != nil { + t.Fatal("Failed to create table:", err) + } + + _, err = db.Exec("insert into test values (1, NULL, NULL, NULL, NULL), (2, ?, ?, ?, ?)", 1, 1.5, []byte("blob"), false) + if err != nil { + t.Fatal("Failed to insert records:", err) + } + _, err = db.Exec("insert into test values (3, NULL, ?, NULL, ?)", 1.5, true) + if err != nil { + t.Fatal("Failed to insert records:", err) + } + + tests := []struct { + id int64 + nullInt bool + nullFloat bool + nullBlob bool + nullBool bool + }{ + {1, true, true, true, true}, + {2, false, false, false, false}, + {3, true, false, true, false}, + } + + for _, test := range tests { + var retInt, retFloat, retBlob, retBool bool + err = db.QueryRow("select isNullArg(col_int), isNullArg(col_float), isNullArg(col_blob), isNullArg(col_bool) from test where id = $1", test.id).Scan(&retInt, &retFloat, &retBlob, &retBool) + if err != nil { + t.Fatal("Query failed:", err) + } + if retInt != test.nullInt { + t.Fatalf("isNullArg returned wrong value for col_int, got %v, want %v", retInt, test.nullInt) + } + if retFloat != test.nullFloat { + t.Fatalf("isNullArg returned wrong value for col_float, got %v, want %v", retFloat, test.nullFloat) + } + if retBlob != test.nullBlob { + t.Fatalf("isNullArg returned wrong value for col_blob, got %v, want %v", retBlob, test.nullBlob) + } + if retBool != test.nullBool { + t.Fatalf("isNullArg returned wrong value for col_bool, got %v, want %v", retBool, test.nullBlob) + } + } +} + type sumAggregator int64 func (s *sumAggregator) Step(x int64) { @@ -1497,28 +1556,28 @@ func TestAggregatorRegistration(t *testing.T) { } type mode struct { - counts map[interface{}]int - top interface{} - topCount int + counts map[interface{}]int + top interface{} + topCount int } func newMode() *mode { - return &mode{ - counts: map[interface{}]int{}, - } + return &mode{ + counts: map[interface{}]int{}, + } } func (m *mode) Step(x interface{}) { - m.counts[x]++ - c := m.counts[x] - if c > m.topCount { - m.top = x - m.topCount = c - } + m.counts[x]++ + c := m.counts[x] + if c > m.topCount { + m.top = x + m.topCount = c + } } func (m *mode) Done() interface{} { - return m.top + return m.top } func TestAggregatorRegistration_GenericReturn(t *testing.T) { @@ -1534,19 +1593,19 @@ func TestAggregatorRegistration_GenericReturn(t *testing.T) { defer db.Close() _, err = db.Exec("create table foo (department integer, profits integer)") - if err != nil { - t.Fatal("Failed to create table:", err) - } - _, err = db.Exec("insert into foo values (1, 10), (1, 20), (1, 45), (2, 42), (2, 115), (2, 20)") - if err != nil { - t.Fatal("Failed to insert records:", err) - } + if err != nil { + t.Fatal("Failed to create table:", err) + } + _, err = db.Exec("insert into foo values (1, 10), (1, 20), (1, 45), (2, 42), (2, 115), (2, 20)") + if err != nil { + t.Fatal("Failed to insert records:", err) + } var mode int - err = db.QueryRow("select mode(profits) from foo").Scan(&mode) - if err != nil { - t.Fatal("MODE query error:", err) - } + err = db.QueryRow("select mode(profits) from foo").Scan(&mode) + if err != nil { + t.Fatal("MODE query error:", err) + } if mode != 20 { t.Fatal("Got incorrect mode. Wanted 20, got: ", mode)