Repository navigation
Bug fixes for 2.6.0 - #131
Merged
Merged
Conversation
busyTimeout never took effect on Android: SQLite waited 2500 ms, as the framework sets it, for a lock that another connection held. On Android 9 and higher, it was given to OpenParams' deprecated setIdleConnectionTimeout, which means how long a connection may be idle before the framework closes it, and which SQLiteOpenHelper drops anyway, as the OpenParams.Builder it copies the parameters into doesn't copy that one, on every version from Android 9 to 16. It is now set with PRAGMA busy_timeout, when the database is configured. From Android 13 on, a database in WAL mode reads through several connections, so execPerConnectionSQL runs it on each of them, and on those opened later; it only runs a PRAGMA that returns a row, as this one does, from Android 13 on. Before, a database that SQLlin opens has a single connection, which runs it through rawQuery. The guides to getting started listed busyTimeout, and in Chinese journalMode, among the parameters that only work on Android 9 and higher; only inMemory and the lookaside parameters do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default lookasideSlotSize and lookasideSlotCount, 0, which the documentation said left SQLite's default configuration, disabled the lookaside memory allocator on Kotlin/Native and on Android 9 and higher, as sqlite3_db_config and setLookasideConfig take 0 to disable it, while the JVM, where sqlite-jdbc can't set it, and lower versions of Android kept SQLite's default configuration. Their default is now -1, in DatabaseConfiguration and DSLDBConfiguration, which leaves SQLite's default configuration on every platform, as the Android framework's own default does; 0 still disables the allocator. The Android driver only calls setLookasideConfig when both values are 0 or more, as it rejects negative ones. The examples of the guides show the new defaults, which they explain, and the guides name sqlite3_db_config() as what sqlite-jdbc doesn't support, rather than sqlite3_config(). The error of the Kotlin/Native driver had a stray '%'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F66 is the primitive that a reader which has to invalidate itself, as a
PagingSource does, needs: versioned { } returns the results of a
read-only scope with an opaque token of the versions of the tables it
read, captured under the lock that the read runs under, after the
tables are tracked, so that no change between the read and the capture
is missed. Tokens combine with +, taking the earlier version of a table,
and reject the tokens of another Database. awaitChange suspends until a
table changes, and onChange registers a one-shot callback, run after
the writing scope releases the lock, returning null when the token is
already stale, as it registers before it checks.
It comes from the design discussed with the MyMovie project, which
found that results, as observe gives, don't fit a PagingSource, and
that a version read without the read can't be sound, as a table only
has one once it is tracked. F13 now builds on it, with the callback
held weakly by the registry, so that a source that Pager abandons
doesn't stay registered, and the three traps of offset paging that the
project reported.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the JVM and Kotlin/Native, the connection is locked for each
statement rather than for a transaction, so a blocking database { }
scope on another thread can have its transaction open while the tracker
of observed queries creates its TEMP table and triggers. They are
rolled back with that transaction, while the tracker still counts their
tables as tracked: the next write failed with "no such table", and once
another table was observed, which created the TEMP table again, the
changes of the lost table were no longer counted. Android isn't
affected, as a transaction holds the primary connection there.
Before it reads the counts, the tracker now checks that the TEMP table
exists and holds a row for each tracked table, and that each tracked
table has its three triggers, in a transaction, as on Android a query
outside one may run on a reader connection. It creates again whatever
is gone, and counts a change of those tables above the version last
read, as their changes may have been missed. A failure to tell the
observed queries of a change is added to the failure of the statement
that made it, rather than taking its place.
The guide to observed queries, and the documentation of invoke and
observe, now say to write through suspendedScope while queries are
observed, as they run on another thread, and that a blocking scope
isn't serialized with them, as the guide to concurrency safety already
said of sharing a database between threads. The test stands for the
rollback by removing the row of a table from the TEMP table, and by
dropping it, through an entity of the TEMP table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The example called database suspendScope { }, which doesn't exist, in
a function that wasn't a suspend one, in English and in Chinese. It is
database suspendedScope { }, in a suspend function.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Transactions began DEFERRED on the JVM and Kotlin/Native, and EXCLUSIVE on Android. So on the JVM and Kotlin/Native, a transaction that read before it wrote failed at once with "database is locked" while another connection wrote, as SQLite doesn't wait for busyTimeout where waiting could deadlock, while on Android it waited. They now begin IMMEDIATE on every platform, taking the write lock at once, for which SQLite waits as long as busyTimeout. On Android, beginTransactionNonExclusive does it, which is what EXCLUSIVE did in WAL mode, and in DELETE mode lets other connections read until the transaction commits. On the JVM and Kotlin/Native, a read-only connection begins a deferred transaction, as it can't take the write lock. The JVM driver now begins and ends transactions with SQL, in auto-commit mode: sqlite-jdbc begins another transaction of its configured kind as soon as one is committed or rolled back, which an IMMEDIATE one couldn't do while another connection writes, so that a COMMIT that succeeded would fail. F27, choosing how a transaction begins, no longer says that the platforms differ. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SQLite rolls some transactions back itself: for a constraint with ON
CONFLICT ROLLBACK, which onConflict = OnConflict.ROLLBACK declares, for
a trigger's RAISE(ROLLBACK), and for some I/O errors. Ending such a
transaction then failed too, with "cannot rollback - no transaction is
active", and withTransaction threw that failure from its finally
block, in place of the one that made SQLite roll it back, on every
platform, and so did the DSL's transaction { }, which runs through it.
withTransaction now throws the block's failure, with the failure to end
the transaction added to it as a suppressed exception. It keeps ending
the transaction in a finally block, as a non-local return from the
block, which an inline function allows, has to end it too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug fixes for 2.6.0, one commit per fix, each with a test and a
CHANGELOG.mdentry, plus a roadmap item. More fixes of this cycle may be added to this PR.Fixes
lookasideSlotSizeandlookasideSlotCount, 0, which the documentation said left SQLite's default configuration, disabled SQLite's lookaside memory allocator on Kotlin/Native and on Android 9 and higher, while the JVM and lower versions of Android kept the default. Their default is now -1, which leaves SQLite's default configuration on every platform, inDatabaseConfigurationandDSLDBConfiguration; 0 still disables the allocator. (972867f)busyTimeoutnever took effect, so SQLite waited 2500 ms, as the framework sets it. On Android 9 and higher it was given to the deprecatedsetIdleConnectionTimeout, whichSQLiteOpenHelperdrops anyway. It is now set withPRAGMA busy_timeout: from Android 13 on throughexecPerConnectionSQL, on every connection, and below on the database's only connection. (cb25ba5)database { }scope on another thread could roll back the TEMP table and triggers that count the changes of observed queries, as the connection is locked per statement rather than per transaction. The next write then failed with "no such table", and once another table was observed, the lost table's changes were no longer counted. The tracker now checks for its TEMP objects before reading the counts, creates whatever is gone, and counts a change of those tables; a failure to tell observed queries is added to the statement's failure instead of replacing it. The docs now say to write throughsuspendedScopewhile queries are observed. (77bba18)database suspendScope { }, which doesn't exist, in a non-suspendfunction. (dd4d3ba)busyTimeout. They now begin IMMEDIATE on every platform; a read-only connection on the JVM and Kotlin/Native begins a deferred one. The JVM driver now begins and ends transactions with SQL, as sqlite-jdbc begins another transaction of its configured kind right after a commit, which an IMMEDIATE one couldn't do while another connection writes. (4a22c6f)ON CONFLICT ROLLBACK, a trigger'sRAISE(ROLLBACK)or some I/O errors, ending it failed with "cannot rollback - no transaction is active", andwithTransaction, and so the DSL'stransaction { }, threw that instead of the failure that caused the rollback, on every platform. The block's failure is now thrown, with the failure to end the transaction added as a suppressed exception. (ae4d7eb)None changes source compatibility.
Roadmap
versioned { },+,awaitChange,onChange), the primitive a self-invalidating reader such as aPagingSourceneeds, designed with the MyMovie project; F13 now builds on it. (caf0fa1)Testing
sqllin-driver:jvmTest(10 tests) andtestAndroidHostTeston Robolectric API 26 and 37 (21 tests) pass locally.sqllin-dsl-test:jvmTest(94 tests) andtestAndroidHostTest(188 tests) pass. The intermediate state after B3 was tested on its own too.testBusyTimeoutreturned 2500 on Android API 26 and 37,testLookasideConfigreturned (0, 0),testObserveRestoresTrackingmissed the insert after the tracker's row was gone,testImmediateTransactionlet another connection write during a transaction on the JVM, andtestRolledBackBySQLitegot "cannot rollback - no transaction is active" instead of the constraint or trigger failure on the JVM and on Android API 26 and 37.testLookaside. The development machine is an Intel Mac: the tests link formacosArm64andiosSimulatorArm64and compile forlinuxX64andmingwX64. CI on this PR is their first run.OpenParamswith therawQueryPRAGMA), which runs the samerawQuerycode as API 26, and a lost trigger without a lost row in B3, which the same check handles but which the DSL can't produce without a schema change.🤖 Generated with Claude Code