sqlite: let a caller-constructed Driver register its own functions, collations and modules

The additive half of #254: a caller-constructed Driver can now be filled in with its own functions, collations and virtual table modules, so it becomes the blank slate the issue describes rather than a trap. One narrow, loud exception to "nothing existing changes behavior" is called out below.

What is in it

registerFunction and registerCollation become methods on *Driver. The package-level functions keep targeting the package-level driver, now spelled explicitly instead of relying on the receiver named d shadowing the package variable of the same name, which is what let the two drift apart in the first place. newDriver is renamed defaultDriver: it read like a constructor but returns the singleton.

Driver gains RegisterFunction, RegisterScalarFunction, RegisterDeterministicScalarFunction, RegisterCollationUtf8 and RegisterModule, plus Must* variants of the first four. The per-Driver methods live in driver.go, so the Register/MustRegister pairing in sqlite.go stays intact, and they carry the dmesgs defers. The zero Driver stays usable: the maps are created on demand, since &Driver{} starts with nil maps where the package-level instance is built with make. A mutex on Driver makes concurrent registrations safe, so a *Driver can be handed out for several packages to fill from their init functions; the ordering requirement against Open stands and is documented.

The module ID handed to sqlite3_create_module_v2 as pAux is allocated per (Driver, name) pair -- your keyed fix, applied as posted, including the delete by key in the alloc-failure path. Keying by name alone let two Drivers registering the same module name share one dispatch entry, with the last registration winning process-wide and retroactively, on connections already open.

vtab.RegisterModule honours its db argument. A non-nil db registers on the driver backing it when that driver implements the new vtab.ModuleRegisterer; a nil db keeps targeting the driver this package registers as "sqlite". For a db opened on "sqlite" those are the same driver, so the outcome is unchanged. A db wrapped by some other driver falls through to the engine hook rather than failing. Both silent corners are documented now: the routing trusts (*sql.DB).Driver, so a connector whose Driver and Connect methods disagree parks the module on a driver that never opens a connection, and a hook installed with SetRegisterFunc is consulted only when the db argument does not resolve to a ModuleRegisterer.

The exception

vtab.RegisterModule(db, ...) where db was opened on a caller-constructed Driver: before, the db argument was discarded and the registration landed on the "sqlite" driver, reaching every connection in the process; now it lands on the constructed driver alone, and a sql.Open("sqlite") connection that used to resolve such a module gets no such module. The CHANGELOG entry and the commit message state the same exception.

What is deliberately not in it

The module inconsistency itself. A connection now receives the union of the modules on the package-level driver and those on the Driver that opened it, with the package-level set applied unconditionally, because that is what every connection has received since module support was added. The isolating change the issue warns about is not made here. A name registered in both resolves to the package-level implementation, which is now documented on Driver rather than only in an unexported function's comment.

That leaves the ground prepared for the resolution you describe: once callers have a way to put modules on their own Driver, isolating becomes a change with a deprecation period behind it instead of a silent surprise.

GOVERNANCE.md is out, per your call, and the entry you landed reads right.

Testing

Eight tests in driver_register_test.go: the new registrations, duplicate rejection, isolation between two constructed Drivers and from the package-level driver, the same-name case -- two Drivers, one module name, exercised on a connection pinned before the second driver opens one, which is the retroactive overwrite from your reproducer -- concurrent registrations, that globally registered modules still reach a constructed Driver, that the db argument selects the driver, and that argument validation runs before the driver lookup.

The deliberate-breakage set covers four properties now: the two behavior-preserving ones from the first round, the same-name case (reverting the moduleKey fix fails it at exactly your third line, reached:b on A's pinned connection), and the concurrency claim (removing the mutex trips the race detector).

Full suite green, gofmt and go vet clean on the changed files, the registration tests pass -race -count=10.

Updates #254

Edited by Ian Chechin

Merge request reports

Loading
Loading