Skip to content

Add support for the ReadOnly option in BeginTx#1372

Open
jmanero wants to merge 1 commit intomattn:masterfrom
jmanero:readonly-txopts
Open

Add support for the ReadOnly option in BeginTx#1372
jmanero wants to merge 1 commit intomattn:masterfrom
jmanero:readonly-txopts

Conversation

@jmanero
Copy link

@jmanero jmanero commented Jan 24, 2026

This change is similar to the modernc implementation, which ignores the transaction mode defined in the _txlock DSN parameter when the ReadOnly attribute in TxOptions values passed to BeginTx is true

While the resulting transactions are not actually read-only, this change achieves the result desired by #400 within the confines of the database/sql interface.

In WAL mode, this allows us to use DSN options like

_journal_mode=WAL&_busy_timeout=1000&_txlock=immediate

to enable blocking on concurrent calls to BeginTx(ctx, &sql.TxOptions{}) instead of polling for SQLITE_BUSY error codes, while making non-blocking read-only(ish) calls to BeginTx(ctx, &sql.TxOptions{ReadOnly: true}).

This change is similar to the modernc implementation, which ignores the
transaction mode defined in the `_txlock` DSN parameter when the
`ReadOnly` attribute in `TxOptions` values passed to `BeginTx` is `true`
@rittneje
Copy link
Collaborator

The fact that this doesn't actually make the transaction readonly seems very confusing.

I understand that having the side effect of doing BEGIN instead of BEGIN IMMEDIATE can be valuable. But what I can then see happening is an application has a bug where they accidentally write to the transaction anyway, which goes undetected in testing and will ultimately cause the very failure mode they are trying to avoid.

To that end, I think we need to leverage PRAGMA query_only in this case. Care must be taking to restore the original value of the pragma once the transaction completes.

@jmanero
Copy link
Author

jmanero commented Jan 24, 2026

Agreed. Also thinking through the unexpected behavior changes that this could cause to anything that's currently setting ReadOnly with no effect, this should probably have to be explicitly enabled by a new DSN parameter; e.g.

_tx_readonly

  • 0, off: Silently ignore TxOptions#ReadOnly default, current behavior
  • 1, weak: TxOptions#ReadOnly disables IMMEDIATE and EXCLUSIVE transaction locking defined by _txlock, but the connection can still be upgraded to read-write by executing a mutating operation
  • 2, strong: Disables IMMEDIATE and EXCLUSIVE transaction locking, plus PRAGMA query_only is set and unset around the transaction. Mutually exclusive with _query_only=1 DSN parameter

Would you be open to this ^ kind of change @rittneje?

@rittneje
Copy link
Collaborator

I'm not sure what the use case for the proposed "weak" mode is.

With regards to backwards compatibility, it seems unfortunate that people who explicitly set ReadOnly to leverage the new functionality instead have it silently ignored by default, just because someone might be misusing it today. It would seem better to honor ReadOnly correctly, and if it breaks an existing application, which must have been misusing it, the solution is to fix the application code. (See also #685.) But I would be interested in @mattn's perspective.

Whether ReadOnly should also overrule the _txlock by default is also an interesting question, as is possible the consumer wants BEGIN IMMEDIATE even for their readonly transactions. It is rather unfortunate that sql.TxOptions was not defined with any mechanism for conveying driver-specific options. (Maybe a good proposal to submit against the standard library.) I suppose we could always add a separate _readonly_txlock setting if there is a legitimate need.

@mattn
Copy link
Owner

mattn commented Jan 24, 2026

I'm not sure this change is necessary. If you need different transaction behaviors for read-only vs read-write operations, you should use separate connections:

// For writes: use immediate to prevent lock contention
writeDB, _ := sql.Open("sqlite3", "file.db?_journal_mode=WAL&_txlock=immediate")
writeDB.SetMaxOpenConns(1)

// For reads: use deferred or read-only mode
readDB, _ := sql.Open("sqlite3", "file.db?_journal_mode=WAL&_txlock=deferred")
// or even safer:
readDB, _ := sql.Open("sqlite3", "file.db?_journal_mode=WAL&mode=ro")

This approach:

  • Makes the intent explicit at the connection level
  • Guarantees true read-only behavior with mode=ro
  • Allows proper connection pool management (1 writer, multiple readers)

The problem you're trying to solve seems like an application design concern rather than something the driver should handle.

@rittneje
Copy link
Collaborator

rittneje commented Jan 24, 2026

@mattn I'd certainly agree that having two pools that can be independently configured is the best approach in general. However, I do still think this library ought to respect ReadOnly, even if it doesn't implicitly change the transaction mode. (Right now it violates the contract for BeginTx.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants