Commit e7a39d2d authored by cznic's avatar cznic
Browse files

sqlite: document that a constructed Driver is not the registered one

Driver has been exported since 65f6d7e4 (2017), where "Make sqlite public"
renamed the unexported sqlite type as part of publishing the package. The
struct then held a connection counter and a mutex, so constructing one cost
nothing and lost nothing. a9227519 (2022) moved the function and collation
registries onto it and introduced the package-level instance, which is when
a constructed Driver started silently lacking things; 14082cad (2023) added
(*Driver).RegisterConnectionHook, which is only meaningful on an instance the
caller builds, and so made the export load-bearing.

The result is a type that is legitimately constructible for the private-hook
pattern yet carries none of what the package-level Register* functions
install. This documents that on the type and on the method, including the
consequence easiest to miss: a registered function replaces a SQLite built-in
of the same name, so a constructed Driver can evaluate upper(x) or date(x)
differently from a connection opened through sql.Open.

The half-global virtual table module behavior is documented as it stands
rather than changed. registerModules reads the package-level driver, so
modules do reach a constructed Driver while functions and collations do not.
Making that consistent breaks somebody in either direction - inheriting
silently changes query results for anyone whose registration shadows a
built-in, isolating turns a working CREATE VIRTUAL TABLE into "no such
module" - so it wants an issue of its own rather than a doc commit.

No behavior changes.

Co-Authored-By: default avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
parent 2c7e3eb3
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@

 - 2026-07-30 v1.56.0:
     - Add `NewConnector`, returning a `database/sql/driver.Connector` for use with `sql.OpenDB`. It opens the same connections `sql.Open("sqlite", dsn)` does, from the same registered driver, so every function, collation, connection hook and virtual table module registered through this package applies to them. It exists for callers that need to interpose on the physical connections `database/sql` opens — tracing, metrics, connection-scoped setup — which `sql.Open` gives no access to: such a caller can embed the returned `Connector`, override `Connect`, and pass its own wrapper to `sql.OpenDB`. Previously the only way to reach the registered driver was the `db, _ := sql.Open("sqlite", ""); drv := db.Driver(); db.Close()` idiom, which works only because `sql.Open` does not connect and this driver does not implement `driver.DriverContext`; and the only way to get a wrapper into a `*sql.DB` was `sql.Register`, which is process-global, panics on a name it has already seen, and cannot be undone, so a library had to invent a unique driver name per configuration. `sql.OpenDB` registers nothing. Constructing a `&sqlite.Driver{}` is not an alternative — its fields are unexported, so it carries none of the registrations. Only the syntax of the DSN query string is checked by `NewConnector`; parameter values continue to be validated when the connection is opened, so an unknown parameter or an out-of-range value is reported by `Connect` rather than at construction. Nothing about the existing `sql.Open` path changes: `*Driver` deliberately still does not implement `driver.DriverContext`, so `sql.Open` remains lazy and DSN errors continue to surface where they always have. A runnable sample is in `examples/connector`. Resolves [GitLab issue #253](https://gitlab.com/cznic/sqlite/-/issues/253), thanks Alessandro Segala (@ItalyPaleAle)!
     - Document that a caller-constructed `sqlite.Driver` is not the driver this package registers as `"sqlite"`. Its fields are unexported, so it starts with no functions, collations or connection hooks and the only way to give it any is its own `RegisterConnectionHook` method; the package-level `Register*` functions always apply to the registered driver. Connections such a `Driver` opens therefore run without the package-level functions and collations — and because a registered function silently replaces a SQLite built-in of the same name, a `Driver` you construct can evaluate `upper(x)`, `date(x)` and the like differently from one opened through `sql.Open`. Virtual table modules are the one exception: they are held process-globally and reach every `Driver`. Constructing one remains supported for the private-hook pattern — a driver registered under a name of its own with `sql.Register` so its connection hooks apply only to its own connections — and is otherwise best avoided in favour of `sql.Open` or `NewConnector`. Documentation only; no behavior changes.

 - 2026-07-20 v1.55.0:
     - Add `github.com/mattn/go-sqlite3`-compatible shorthand DSN query parameters to ease migration from that driver: `_busy_timeout`/`_timeout`, `_foreign_keys`/`_fk`, `_journal_mode`/`_journal`, `_synchronous`/`_sync`, `_auto_vacuum`/`_vacuum`, and `_query_only`, each setting the correspondingly named PRAGMA. Values are validated against the same set `mattn/go-sqlite3` accepts (case-insensitive) and an unrecognized value fails the connection with an error, so a typo such as `_synchronous=fu1l` or `_foreign_keys=yes_please` is reported rather than silently downgrading durability or dropping foreign-key enforcement. The keys are applied in a fixed order independent of their order in the DSN — `_busy_timeout` and `_auto_vacuum` before any `_pragma` values (`auto_vacuum` must be set before the database is first written), the rest after, and `_query_only` last — and where a key and its alias are both supplied the alias wins, matching `mattn/go-sqlite3`; selection is by presence rather than by value, so supplying the alias empty (`_foreign_keys=on&_fk=`) suppresses the PRAGMA rather than deferring to the primary key, again matching that driver. Behavior change to note: prior releases ignored these keys entirely, so a DSN carried over from a `mattn/go-sqlite3` setup changes in two ways. A recognized key that previously did nothing now takes effect — `_foreign_keys=on` begins enforcing constraints against data that may already violate them, `_journal_mode=wal` persistently converts the database file, and `_query_only=1` makes the connection read-only. And a value outside the accepted set now fails the connection with an error where the same DSN previously opened successfully — for example a duration-style `_busy_timeout=5s` or `_timeout=5000ms`, neither of which is the integer that key requires. Review such DSNs before upgrading. `_pragma` is unchanged and no pre-existing parameter changes meaning, though see the following entry for a change in when all of them are validated.
+29 −1
Original line number Diff line number Diff line
@@ -13,7 +13,30 @@ import (

// Driver implements database/sql/driver.Driver.
//
// Registration functions and methods must be called before the first call to Open.
// Registration functions and methods must be called before the first call to
// Open.
//
// Most code has no use for this type. sql.Open("sqlite", dsn) and
// [NewConnector] both go through the driver this package registers as
// "sqlite", which carries everything registered with [RegisterFunction],
// [RegisterScalarFunction], [RegisterDeterministicScalarFunction],
// [RegisterCollationUtf8], [RegisterConnectionHook] and
// [vtab.RegisterModule].
//
// A Driver a caller constructs is not equivalent to that one. Its fields are
// unexported, so it starts out with no functions, collations or connection
// hooks, and the only way to give it any is [Driver.RegisterConnectionHook];
// the package-level registration functions always apply to the registered
// driver, never to a constructed one. Connections it opens therefore run
// without the package-level functions and collations -- and where such a
// registration overrides a SQLite built-in of the same name, they run with
// SQLite's built-in in force instead. Virtual table modules are the one
// exception: they are held process-globally and reach every Driver.
//
// Constructing one is supported for the private-hook pattern: a driver
// registered under a name of its own with sql.Register, so that its connection
// hooks apply to its own connections rather than to every connection in the
// process. Prefer sql.Open or NewConnector for anything else.
type Driver struct {
	// user defined functions that are added to every new connection on Open
	udfs map[string]*userDefinedFunction
@@ -197,6 +220,11 @@ func (d *Driver) Open(name string) (conn driver.Conn, err error) {

// RegisterConnectionHook registers a function to be called after each connection
// is opened. This is called after all the connection has been set up.
//
// The hook applies only to connections opened by d. To register one on the
// driver this package registers as "sqlite", and so on the connections
// sql.Open and [NewConnector] hand out, use the package-level
// [RegisterConnectionHook].
func (d *Driver) RegisterConnectionHook(fn ConnectionHookFn) {
	d.connectionHooks = append(d.connectionHooks, fn)
}