Skip to content

Commit 43faddb

Browse files
authored
Actually workaround the issues from AndroidX SQLite 2.6.1 (#123)
- https://issuetracker.google.com/issues/447613208
1 parent 535f58e commit 43faddb

File tree

4 files changed

+234
-46
lines changed

4 files changed

+234
-46
lines changed

library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteDriver.kt

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -482,23 +482,36 @@ public class AndroidxSqliteDriver(
482482
val driver = this
483483
val transacter = object : TransacterImpl(driver) {}
484484

485-
writerConnection.withDeferredForeignKeyChecks(
486-
transacter = transacter,
487-
configuration = configuration,
488-
) {
489-
transacter.transaction {
490-
when {
491-
isCreate -> schema.create(driver).value
492-
else -> schema.migrate(driver, currentVersion, schema.version, *migrationCallbacks).value
485+
try {
486+
// It's a little gross that we use writerConnection here after releasing it above
487+
// but ultimately it's the best way forward for now, since acquiring the writer connection
488+
// isn't re-entrant, and create/migrate will likely try to acquire the writer connection at some point.
489+
// There **should** only be one active thread throughout this process, so it **should** be safe...
490+
writerConnection.withForeignKeysDisabled(configuration) {
491+
transacter.transaction {
492+
when {
493+
isCreate -> schema.create(driver).value
494+
else -> schema.migrate(driver, currentVersion, schema.version, *migrationCallbacks).value
495+
}
496+
497+
if(configuration.isForeignKeyConstraintsCheckedAfterCreateOrUpdate) {
498+
writerConnection.reportForeignKeyViolations(
499+
configuration.maxMigrationForeignKeyConstraintViolationsToReport,
500+
)
501+
}
502+
503+
writerConnection.execSQL("PRAGMA user_version = ${schema.version}")
493504
}
494-
skipStatementsCache = configuration.cacheSize == 0
495-
when {
496-
isCreate -> onCreate()
497-
else -> onUpdate(currentVersion, schema.version)
498-
}
499-
writerConnection.prepare("PRAGMA user_version = ${schema.version}").use { it.step() }
500505
}
501506
}
507+
finally {
508+
skipStatementsCache = configuration.cacheSize == 0
509+
}
510+
511+
when {
512+
isCreate -> onCreate()
513+
else -> onUpdate(currentVersion, schema.version)
514+
}
502515
} else {
503516
skipStatementsCache = configuration.cacheSize == 0
504517
}
@@ -512,40 +525,27 @@ public class AndroidxSqliteDriver(
512525
}
513526
}
514527

515-
private inline fun SQLiteConnection.withDeferredForeignKeyChecks(
516-
transacter: Transacter,
528+
private inline fun SQLiteConnection.withForeignKeysDisabled(
517529
configuration: AndroidxSqliteConfiguration,
518530
crossinline block: () -> Unit,
519531
) {
520532
if(configuration.isForeignKeyConstraintsEnabled) {
521-
prepare("PRAGMA foreign_keys = OFF;").use(SQLiteStatement::step)
533+
execSQL("PRAGMA foreign_keys = OFF;")
522534
}
523535

524536
try {
525-
// AndroidSQLiteDriver requires foreign_key_check to be run
526-
// in the same transaction as the creation / migration.
527-
// BundledSQLiteDriver fails if one big transaction is used.
528-
// Both seem to be happy with this nested transaction ¯\_(ツ)_/¯
529-
transacter.transaction {
530-
block()
537+
block()
531538

532-
if(configuration.isForeignKeyConstraintsEnabled) {
533-
prepare("PRAGMA foreign_keys = ON;").use(SQLiteStatement::step)
534-
535-
if(configuration.isForeignKeyConstraintsCheckedAfterCreateOrUpdate) {
536-
reportForeignKeyViolations(
537-
configuration.maxMigrationForeignKeyConstraintViolationsToReport,
538-
)
539-
}
540-
}
539+
if(configuration.isForeignKeyConstraintsEnabled) {
540+
execSQL("PRAGMA foreign_keys = ON;")
541541
}
542542
} catch(e: Throwable) {
543543
// An exception happened during creation / migration.
544544
// We will try to re-enable foreign keys, and if that also fails,
545545
// we will add it as a suppressed exception to the original one.
546546
try {
547547
if(configuration.isForeignKeyConstraintsEnabled) {
548-
prepare("PRAGMA foreign_keys = ON;").use(SQLiteStatement::step)
548+
execSQL("PRAGMA foreign_keys = ON;")
549549
}
550550
} catch(fkException: Throwable) {
551551
e.addSuppressed(fkException)

library/src/commonMain/kotlin/com/eygraber/sqldelight/androidx/driver/ConnectionPool.kt

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.eygraber.sqldelight.androidx.driver
22

33
import androidx.sqlite.SQLiteConnection
4-
import androidx.sqlite.SQLiteStatement
4+
import androidx.sqlite.execSQL
55
import kotlinx.atomicfu.atomic
66
import kotlinx.coroutines.channels.Channel
77
import kotlinx.coroutines.runBlocking
@@ -136,7 +136,7 @@ internal class AndroidxDriverConnectionPool(
136136
private fun runPragmaOnAllConnections(sql: String) {
137137
val writer = acquireWriterConnection()
138138
try {
139-
writer.writePragma(sql)
139+
writer.execSQL(sql)
140140
} finally {
141141
releaseWriterConnection()
142142
}
@@ -148,7 +148,7 @@ internal class AndroidxDriverConnectionPool(
148148
try {
149149
// only apply the pragma to connections that were already created
150150
if(reader.isCreated) {
151-
reader.connection.value.writePragma(sql)
151+
reader.connection.value.execSQL(sql)
152152
}
153153
} finally {
154154
readerChannel.send(reader)
@@ -202,23 +202,27 @@ internal class PassthroughConnectionPool(
202202
)
203203

204204
val foreignKeys = if(isForeignKeyConstraintsEnabled) "ON" else "OFF"
205-
delegatedConnection.writePragma("PRAGMA foreign_keys = $foreignKeys;")
205+
delegatedConnection.execSQL("PRAGMA foreign_keys = $foreignKeys;")
206206
}
207207

208208
override fun setJournalMode(journalMode: SqliteJournalMode) {
209209
configuration = configuration.copy(
210210
journalMode = journalMode,
211211
)
212212

213-
delegatedConnection.writePragma("PRAGMA journal_mode = ${configuration.journalMode.value};")
213+
delegatedConnection.execSQL("PRAGMA journal_mode = ${configuration.journalMode.value};")
214+
215+
// this needs to come after PRAGMA journal_mode until https://issuetracker.google.com/issues/447613208 is fixed
216+
val foreignKeys = if(configuration.isForeignKeyConstraintsEnabled) "ON" else "OFF"
217+
delegatedConnection.execSQL("PRAGMA foreign_keys = $foreignKeys;")
214218
}
215219

216220
override fun setSync(sync: SqliteSync) {
217221
configuration = configuration.copy(
218222
sync = sync,
219223
)
220224

221-
delegatedConnection.writePragma("PRAGMA synchronous = ${configuration.sync.value};")
225+
delegatedConnection.execSQL("PRAGMA synchronous = ${configuration.sync.value};")
222226
}
223227

224228
override fun close() {
@@ -231,13 +235,11 @@ private fun SQLiteConnection.withConfiguration(
231235
): SQLiteConnection = this.apply {
232236
// copy the configuration for thread safety
233237
configuration.copy().apply {
238+
execSQL("PRAGMA journal_mode = ${journalMode.value};")
239+
execSQL("PRAGMA synchronous = ${sync.value};")
240+
241+
// this needs to come after PRAGMA journal_mode until https://issuetracker.google.com/issues/447613208 is fixed
234242
val foreignKeys = if(isForeignKeyConstraintsEnabled) "ON" else "OFF"
235-
writePragma("PRAGMA foreign_keys = $foreignKeys;")
236-
writePragma("PRAGMA journal_mode = ${journalMode.value};")
237-
writePragma("PRAGMA synchronous = ${sync.value};")
243+
execSQL("PRAGMA foreign_keys = $foreignKeys;")
238244
}
239245
}
240-
241-
private fun SQLiteConnection.writePragma(sql: String) {
242-
prepare(sql).use(SQLiteStatement::step)
243-
}

library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteCreationTest.kt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,71 @@ abstract class AndroidxSqliteCreationTest {
187187
}
188188
}
189189

190+
@Test
191+
fun `foreign keys are disabled during creation`() {
192+
val schema = getSchema {
193+
assertTrue {
194+
executeQuery(
195+
identifier = null,
196+
sql = "PRAGMA foreign_keys;",
197+
mapper = { cursor ->
198+
QueryResult.Value(
199+
when {
200+
cursor.next().value -> cursor.getLong(0)
201+
else -> 0L
202+
},
203+
)
204+
},
205+
parameters = 0,
206+
).value == 0L
207+
}
208+
}
209+
210+
val dbName = Random.nextULong().toHexString()
211+
212+
withDatabase(
213+
schema = schema,
214+
dbName = dbName,
215+
onCreate = {},
216+
onUpdate = { _, _ -> },
217+
onOpen = {},
218+
onConfigure = {},
219+
) {
220+
execute(null, "PRAGMA user_version;", 0, null)
221+
}
222+
}
223+
224+
@Test
225+
fun `foreign keys are re-enabled after successful creation`() {
226+
val schema = getSchema()
227+
val dbName = Random.nextULong().toHexString()
228+
229+
withDatabase(
230+
schema = schema,
231+
dbName = dbName,
232+
onCreate = {},
233+
onUpdate = { _, _ -> },
234+
onOpen = {},
235+
onConfigure = {},
236+
) {
237+
assertTrue {
238+
executeQuery(
239+
identifier = null,
240+
sql = "PRAGMA foreign_keys;",
241+
mapper = { cursor ->
242+
QueryResult.Value(
243+
when {
244+
cursor.next().value -> cursor.getLong(0)
245+
else -> 0L
246+
},
247+
)
248+
},
249+
parameters = 0,
250+
).value == 1L
251+
}
252+
}
253+
}
254+
190255
@Test
191256
fun `foreign key constraint violations during creation fail after the creation`() {
192257
val configuration = AndroidxSqliteConfiguration(

library/src/commonTest/kotlin/com/eygraber/sqldelight/androidx/driver/AndroidxSqliteMigrationTest.kt

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,127 @@ abstract class AndroidxSqliteMigrationTest {
217217
}
218218
}
219219

220+
@Test
221+
fun `foreign keys are disabled during migration`() {
222+
val schema = getSchema {
223+
assertTrue {
224+
executeQuery(
225+
identifier = null,
226+
sql = "PRAGMA foreign_keys;",
227+
mapper = { cursor ->
228+
QueryResult.Value(
229+
when {
230+
cursor.next().value -> cursor.getLong(0)
231+
else -> 0L
232+
},
233+
)
234+
},
235+
parameters = 0,
236+
).value == 0L
237+
}
238+
}
239+
240+
val dbName = Random.nextULong().toHexString()
241+
242+
// trigger creation
243+
withDatabase(
244+
schema = schema,
245+
dbName = dbName,
246+
onCreate = {},
247+
onUpdate = { _, _ -> },
248+
onOpen = {},
249+
onConfigure = {},
250+
deleteDbAfterRun = false,
251+
) {
252+
val result = executeQuery(
253+
identifier = null,
254+
sql = "SELECT COUNT(*) FROM post",
255+
mapper = { cursor ->
256+
if(cursor.next().value) {
257+
QueryResult.Value(cursor.getLong(0))
258+
} else {
259+
QueryResult.Value(null)
260+
}
261+
},
262+
parameters = 0,
263+
)
264+
265+
assertEquals(4, result.value)
266+
}
267+
268+
schema.version++
269+
withDatabase(
270+
schema = schema,
271+
dbName = dbName,
272+
onCreate = {},
273+
onUpdate = { _, _ -> },
274+
onOpen = {},
275+
onConfigure = {},
276+
deleteDbBeforeRun = false,
277+
) {
278+
execute(null, "PRAGMA user_version;", 0, null)
279+
}
280+
}
281+
282+
@Test
283+
fun `foreign keys are re-enabled after successful migration`() {
284+
val schema = getSchema()
285+
val dbName = Random.nextULong().toHexString()
286+
287+
// trigger creation
288+
withDatabase(
289+
schema = schema,
290+
dbName = dbName,
291+
onCreate = {},
292+
onUpdate = { _, _ -> },
293+
onOpen = {},
294+
onConfigure = {},
295+
deleteDbAfterRun = false,
296+
) {
297+
val result = executeQuery(
298+
identifier = null,
299+
sql = "SELECT COUNT(*) FROM post",
300+
mapper = { cursor ->
301+
if(cursor.next().value) {
302+
QueryResult.Value(cursor.getLong(0))
303+
} else {
304+
QueryResult.Value(null)
305+
}
306+
},
307+
parameters = 0,
308+
)
309+
310+
assertEquals(4, result.value)
311+
}
312+
313+
schema.version++
314+
withDatabase(
315+
schema = schema,
316+
dbName = dbName,
317+
onCreate = {},
318+
onUpdate = { _, _ -> },
319+
onOpen = {},
320+
onConfigure = {},
321+
deleteDbBeforeRun = false,
322+
) {
323+
assertTrue {
324+
executeQuery(
325+
identifier = null,
326+
sql = "PRAGMA foreign_keys;",
327+
mapper = { cursor ->
328+
QueryResult.Value(
329+
when {
330+
cursor.next().value -> cursor.getLong(0)
331+
else -> 0L
332+
},
333+
)
334+
},
335+
parameters = 0,
336+
).value == 1L
337+
}
338+
}
339+
}
340+
220341
@Test
221342
fun `foreign key constraint violations during migrations fail after the migration`() {
222343
val configuration = AndroidxSqliteConfiguration(

0 commit comments

Comments
 (0)