Skip to content

Commit dae0d92

Browse files
authored
Re-enable foreign key support if needed when a migration fails (#114)
1 parent 2a2c83d commit dae0d92

File tree

3 files changed

+227
-28
lines changed

3 files changed

+227
-28
lines changed

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

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -483,40 +483,62 @@ private inline fun SQLiteConnection.withDeferredForeignKeyChecks(
483483
prepare("PRAGMA foreign_keys = OFF;").use(SQLiteStatement::step)
484484
}
485485

486-
block()
486+
try {
487+
block()
487488

488-
if(configuration.isForeignKeyConstraintsEnabled) {
489-
prepare("PRAGMA foreign_keys = ON;").use(SQLiteStatement::step)
490-
491-
if(configuration.isForeignKeyConstraintsCheckedAfterCreateOrUpdate) {
492-
prepare("PRAGMA foreign_key_check;").use { check ->
493-
val violations = mutableListOf<AndroidxSqliteDriver.ForeignKeyConstraintViolation>()
494-
var count = 0
495-
while(check.step() && count++ < configuration.maxMigrationForeignKeyConstraintViolationsToReport) {
496-
violations.add(
497-
AndroidxSqliteDriver.ForeignKeyConstraintViolation(
498-
referencingTable = check.getText(0),
499-
referencingRowId = check.getInt(1),
500-
referencedTable = check.getText(2),
501-
referencingConstraintIndex = check.getInt(3),
502-
),
503-
)
504-
}
489+
if(configuration.isForeignKeyConstraintsEnabled) {
490+
prepare("PRAGMA foreign_keys = ON;").use(SQLiteStatement::step)
491+
492+
if(configuration.isForeignKeyConstraintsCheckedAfterCreateOrUpdate) {
493+
reportForeignKeyViolations(
494+
configuration.maxMigrationForeignKeyConstraintViolationsToReport,
495+
)
496+
}
497+
}
498+
} catch(e: Throwable) {
499+
// An exception happened during creation / migration.
500+
// We will try to re-enable foreign keys, and if that also fails,
501+
// we will add it as a suppressed exception to the original one.
502+
try {
503+
if(configuration.isForeignKeyConstraintsEnabled) {
504+
prepare("PRAGMA foreign_keys = ON;").use(SQLiteStatement::step)
505+
}
506+
} catch(fkException: Throwable) {
507+
e.addSuppressed(fkException)
508+
}
509+
throw e
510+
}
511+
}
512+
513+
private fun SQLiteConnection.reportForeignKeyViolations(
514+
maxMigrationForeignKeyConstraintViolationsToReport: Int,
515+
) {
516+
prepare("PRAGMA foreign_key_check;").use { check ->
517+
val violations = mutableListOf<AndroidxSqliteDriver.ForeignKeyConstraintViolation>()
518+
var count = 0
519+
while(check.step() && count++ < maxMigrationForeignKeyConstraintViolationsToReport) {
520+
violations.add(
521+
AndroidxSqliteDriver.ForeignKeyConstraintViolation(
522+
referencingTable = check.getText(0),
523+
referencingRowId = check.getInt(1),
524+
referencedTable = check.getText(2),
525+
referencingConstraintIndex = check.getInt(3),
526+
),
527+
)
528+
}
505529

506-
if(violations.isNotEmpty()) {
507-
val unprintedViolationsCount = violations.size - 5
508-
val unprintedDisclaimer = if(unprintedViolationsCount > 0) " ($unprintedViolationsCount not shown)" else ""
530+
if(violations.isNotEmpty()) {
531+
val unprintedViolationsCount = violations.size - 5
532+
val unprintedDisclaimer = if(unprintedViolationsCount > 0) " ($unprintedViolationsCount not shown)" else ""
509533

510-
throw AndroidxSqliteDriver.ForeignKeyConstraintCheckException(
511-
violations = violations,
512-
message = """
534+
throw AndroidxSqliteDriver.ForeignKeyConstraintCheckException(
535+
violations = violations,
536+
message = """
513537
|The following foreign key constraints are violated$unprintedDisclaimer:
514538
|
515539
|${violations.take(5).joinToString(separator = "\n\n")}
516-
""".trimMargin(),
517-
)
518-
}
519-
}
540+
""".trimMargin(),
541+
)
520542
}
521543
}
522544
}

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlin.test.Test
1010
import kotlin.test.assertContentEquals
1111
import kotlin.test.assertEquals
1212
import kotlin.test.assertFailsWith
13+
import kotlin.test.assertTrue
1314

1415
abstract class AndroidxSqliteCreationTest {
1516
private fun getSchema(
@@ -429,4 +430,72 @@ abstract class AndroidxSqliteCreationTest {
429430
execute(null, "PRAGMA user_version;", 0, null)
430431
}
431432
}
433+
434+
@Test
435+
fun `exceptions thrown during creation are propagated to the caller`() {
436+
val schema = getSchema {
437+
throw RuntimeException("Test")
438+
}
439+
val dbName = Random.nextULong().toHexString()
440+
441+
withDatabase(
442+
schema = schema,
443+
dbName = dbName,
444+
onCreate = {},
445+
onUpdate = { _, _ -> },
446+
onOpen = {},
447+
onConfigure = {},
448+
configuration = AndroidxSqliteConfiguration(
449+
isForeignKeyConstraintsEnabled = true,
450+
isForeignKeyConstraintsCheckedAfterCreateOrUpdate = false,
451+
),
452+
) {
453+
val message = assertFailsWith<RuntimeException> {
454+
execute(null, "PRAGMA user_version;", 0, null)
455+
}.message
456+
457+
assertEquals("Test", message)
458+
}
459+
}
460+
461+
@Test
462+
fun `foreign keys are re-enabled after an exception is thrown during creation`() {
463+
val schema = getSchema {
464+
throw RuntimeException("Test")
465+
}
466+
val dbName = Random.nextULong().toHexString()
467+
468+
withDatabase(
469+
schema = schema,
470+
dbName = dbName,
471+
onCreate = {},
472+
onUpdate = { _, _ -> },
473+
onOpen = {},
474+
onConfigure = {},
475+
configuration = AndroidxSqliteConfiguration(
476+
isForeignKeyConstraintsEnabled = true,
477+
isForeignKeyConstraintsCheckedAfterCreateOrUpdate = false,
478+
),
479+
) {
480+
assertFailsWith<RuntimeException> {
481+
execute(null, "PRAGMA user_version;", 0, null)
482+
}
483+
484+
assertTrue {
485+
executeQuery(
486+
identifier = null,
487+
sql = "PRAGMA foreign_keys;",
488+
mapper = { cursor ->
489+
QueryResult.Value(
490+
when {
491+
cursor.next().value -> cursor.getLong(0)
492+
else -> 0L
493+
},
494+
)
495+
},
496+
parameters = 0,
497+
).value == 1L
498+
}
499+
}
500+
}
432501
}

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import kotlin.test.Test
1010
import kotlin.test.assertContentEquals
1111
import kotlin.test.assertEquals
1212
import kotlin.test.assertFailsWith
13+
import kotlin.test.assertTrue
1314

1415
abstract class AndroidxSqliteMigrationTest {
1516
private fun getSchema(
@@ -596,4 +597,111 @@ abstract class AndroidxSqliteMigrationTest {
596597
assertEquals(0, create)
597598
assertEquals(1, update)
598599
}
600+
601+
@Test
602+
fun `exceptions thrown during migration are propagated to the caller`() {
603+
val schema = getSchema {
604+
throw RuntimeException("Test")
605+
}
606+
val dbName = Random.nextULong().toHexString()
607+
608+
// trigger creation
609+
withDatabase(
610+
schema = schema,
611+
dbName = dbName,
612+
onCreate = {},
613+
onUpdate = { _, _ -> },
614+
onOpen = {},
615+
onConfigure = {},
616+
deleteDbAfterRun = false,
617+
configuration = AndroidxSqliteConfiguration(
618+
isForeignKeyConstraintsEnabled = true,
619+
isForeignKeyConstraintsCheckedAfterCreateOrUpdate = false,
620+
),
621+
) {
622+
execute(null, "PRAGMA user_version;", 0, null)
623+
}
624+
625+
schema.version++
626+
627+
withDatabase(
628+
schema = schema,
629+
dbName = dbName,
630+
onCreate = {},
631+
onUpdate = { _, _ -> },
632+
onOpen = {},
633+
onConfigure = {},
634+
deleteDbBeforeRun = false,
635+
configuration = AndroidxSqliteConfiguration(
636+
isForeignKeyConstraintsEnabled = true,
637+
isForeignKeyConstraintsCheckedAfterCreateOrUpdate = false,
638+
),
639+
) {
640+
val message = assertFailsWith<RuntimeException> {
641+
execute(null, "PRAGMA user_version;", 0, null)
642+
}.message
643+
assertEquals("Test", message)
644+
}
645+
}
646+
647+
@Test
648+
fun `foreign keys are re-enabled after an exception is thrown during migration`() {
649+
val schema = getSchema {
650+
throw RuntimeException("Test")
651+
}
652+
val dbName = Random.nextULong().toHexString()
653+
654+
// trigger creation
655+
withDatabase(
656+
schema = schema,
657+
dbName = dbName,
658+
onCreate = {},
659+
onUpdate = { _, _ -> },
660+
onOpen = {},
661+
onConfigure = {},
662+
deleteDbAfterRun = false,
663+
configuration = AndroidxSqliteConfiguration(
664+
isForeignKeyConstraintsEnabled = true,
665+
isForeignKeyConstraintsCheckedAfterCreateOrUpdate = false,
666+
),
667+
) {
668+
execute(null, "PRAGMA user_version;", 0, null)
669+
}
670+
671+
schema.version++
672+
673+
withDatabase(
674+
schema = schema,
675+
dbName = dbName,
676+
onCreate = {},
677+
onUpdate = { _, _ -> },
678+
onOpen = {},
679+
onConfigure = {},
680+
deleteDbBeforeRun = false,
681+
configuration = AndroidxSqliteConfiguration(
682+
isForeignKeyConstraintsEnabled = true,
683+
isForeignKeyConstraintsCheckedAfterCreateOrUpdate = false,
684+
),
685+
) {
686+
assertFailsWith<RuntimeException> {
687+
execute(null, "PRAGMA user_version;", 0, null)
688+
}
689+
690+
assertTrue {
691+
executeQuery(
692+
identifier = null,
693+
sql = "PRAGMA foreign_keys;",
694+
mapper = { cursor ->
695+
QueryResult.Value(
696+
when {
697+
cursor.next().value -> cursor.getLong(0)
698+
else -> 0L
699+
},
700+
)
701+
},
702+
parameters = 0,
703+
).value == 1L
704+
}
705+
}
706+
}
599707
}

0 commit comments

Comments
 (0)