@@ -15,8 +15,10 @@ import app.cash.sqldelight.db.SqlDriver
1515import app.cash.sqldelight.db.SqlPreparedStatement
1616import app.cash.sqldelight.db.SqlSchema
1717import kotlinx.atomicfu.atomic
18+ import kotlinx.atomicfu.locks.ReentrantLock
1819import kotlinx.atomicfu.locks.SynchronizedObject
1920import kotlinx.atomicfu.locks.synchronized
21+ import kotlinx.atomicfu.locks.withLock
2022
2123internal expect class TransactionsThreadLocal () {
2224 internal fun get (): Transacter .Transaction ?
@@ -37,7 +39,17 @@ public class AndroidxSqliteDriver(
3739 private val schema : SqlSchema <QueryResult .Value <Unit >>,
3840 private val configuration : AndroidxSqliteConfiguration = AndroidxSqliteConfiguration (),
3941 private val migrateEmptySchema : Boolean = false ,
40- private val onConfigure : ConfigurableDatabase .() -> Unit = {},
42+ /* *
43+ * A callback to configure the database connection when it's first opened.
44+ *
45+ * This lambda is invoked on the first interaction with the database, immediately before the schema
46+ * is created or migrated. It provides an [AndroidxSqliteConfigurableDriver] as its receiver
47+ * to allow for safe configuration of connection properties like journal mode or foreign key
48+ * constraints.
49+ *
50+ * **Warning:** The [AndroidxSqliteConfigurableDriver] receiver is ephemeral and **must not** escape the callback.
51+ */
52+ private val onConfigure : AndroidxSqliteConfigurableDriver .() -> Unit = {},
4153 private val onCreate : AndroidxSqliteDriver .() -> Unit = {},
4254 private val onUpdate : AndroidxSqliteDriver .(Long , Long ) -> Unit = { _, _ -> },
4355 private val onOpen : AndroidxSqliteDriver .() -> Unit = {},
@@ -50,7 +62,17 @@ public class AndroidxSqliteDriver(
5062 schema: SqlSchema <QueryResult .Value <Unit >>,
5163 configuration: AndroidxSqliteConfiguration = AndroidxSqliteConfiguration (),
5264 migrateEmptySchema: Boolean = false ,
53- onConfigure: ConfigurableDatabase .() -> Unit = {},
65+ /* *
66+ * A callback to configure the database connection when it's first opened.
67+ *
68+ * This lambda is invoked on the first interaction with the database, immediately before the schema
69+ * is created or migrated. It provides an [AndroidxSqliteConfigurableDriver] as its receiver
70+ * to allow for safe configuration of connection properties like journal mode or foreign key
71+ * constraints.
72+ *
73+ * **Warning:** The [AndroidxSqliteConfigurableDriver] receiver is ephemeral and **must not** escape the callback.
74+ */
75+ onConfigure: AndroidxSqliteConfigurableDriver .() -> Unit = {},
5476 onCreate: SqlDriver .() -> Unit = {},
5577 onUpdate: SqlDriver .(Long , Long ) -> Unit = { _, _ -> },
5678 onOpen: SqlDriver .() -> Unit = {},
@@ -70,6 +92,8 @@ public class AndroidxSqliteDriver(
7092 migrationCallbacks = migrationCallbacks,
7193 )
7294
95+ public class ForeignKeyConstraintCheckException (message : String ) : Exception(message)
96+
7397 @Suppress(" NonBooleanPropertyPrefixedWithIs" )
7498 private val isFirstInteraction = atomic(true )
7599
@@ -102,24 +126,27 @@ public class AndroidxSqliteDriver(
102126 private val transactions = TransactionsThreadLocal ()
103127
104128 private val statementsCache = HashMap <SQLiteConnection , LruCache <Int , AndroidxStatement >>()
129+ private val statementsCacheLock = ReentrantLock ()
105130
106131 private fun getStatementCache (connection : SQLiteConnection ) =
107- when {
108- configuration.cacheSize > 0 ->
109- statementsCache.getOrPut(connection) {
110- object : LruCache <Int , AndroidxStatement >(configuration.cacheSize) {
111- override fun entryRemoved (
112- evicted : Boolean ,
113- key : Int ,
114- oldValue : AndroidxStatement ,
115- newValue : AndroidxStatement ? ,
116- ) {
117- if (evicted) oldValue.close()
132+ statementsCacheLock.withLock {
133+ when {
134+ configuration.cacheSize > 0 ->
135+ statementsCache.getOrPut(connection) {
136+ object : LruCache <Int , AndroidxStatement >(configuration.cacheSize) {
137+ override fun entryRemoved (
138+ evicted : Boolean ,
139+ key : Int ,
140+ oldValue : AndroidxStatement ,
141+ newValue : AndroidxStatement ? ,
142+ ) {
143+ if (evicted) oldValue.close()
144+ }
118145 }
119146 }
120- }
121147
122- else -> null
148+ else -> null
149+ }
123150 }
124151
125152 private var skipStatementsCache = true
@@ -132,7 +159,7 @@ public class AndroidxSqliteDriver(
132159 /* *
133160 * True if foreign key constraints are enabled.
134161 *
135- * This function will block until all connections have been updated.
162+ * This function will block until all created connections have been updated.
136163 *
137164 * An exception will be thrown if this is called from within a transaction.
138165 */
@@ -147,7 +174,7 @@ public class AndroidxSqliteDriver(
147174 /* *
148175 * Journal mode to use.
149176 *
150- * This function will block until all connections have been updated.
177+ * This function will block until all created connections have been updated.
151178 *
152179 * An exception will be thrown if this is called from within a transaction.
153180 */
@@ -162,7 +189,7 @@ public class AndroidxSqliteDriver(
162189 /* *
163190 * Synchronous mode to use.
164191 *
165- * This function will block until all connections have been updated.
192+ * This function will block until all created connections have been updated.
166193 *
167194 * An exception will be thrown if this is called from within a transaction.
168195 */
@@ -383,7 +410,7 @@ public class AndroidxSqliteDriver(
383410 if (isFirstInteraction.value && ! isNestedUnderCreateOrMigrate) {
384411 isNestedUnderCreateOrMigrate = true
385412
386- ConfigurableDatabase (this ).onConfigure()
413+ AndroidxSqliteConfigurableDriver (this ).onConfigure()
387414
388415 val writerConnection = connectionPool.acquireWriterConnection()
389416 val currentVersion = try {
@@ -397,21 +424,24 @@ public class AndroidxSqliteDriver(
397424 connectionPool.releaseWriterConnection()
398425 }
399426
400- if (currentVersion == 0L && ! migrateEmptySchema || currentVersion < schema.version) {
427+ val isCreate = currentVersion == 0L && ! migrateEmptySchema
428+ if (isCreate || currentVersion < schema.version) {
401429 val driver = this
402430 val transacter = object : TransacterImpl (driver) {}
403431
404- transacter.transaction {
405- when (currentVersion) {
406- 0L -> schema.create(driver).value
407- else -> schema.migrate(driver, currentVersion, schema.version, * migrationCallbacks).value
408- }
409- skipStatementsCache = configuration.cacheSize == 0
410- when (currentVersion) {
411- 0L -> onCreate()
412- else -> onUpdate(currentVersion, schema.version)
432+ writerConnection.withDeferredForeignKeyChecks(configuration) {
433+ transacter.transaction {
434+ when {
435+ isCreate -> schema.create(driver).value
436+ else -> schema.migrate(driver, currentVersion, schema.version, * migrationCallbacks).value
437+ }
438+ skipStatementsCache = configuration.cacheSize == 0
439+ when {
440+ isCreate -> onCreate()
441+ else -> onUpdate(currentVersion, schema.version)
442+ }
443+ writerConnection.prepare(" PRAGMA user_version = ${schema.version} " ).use { it.step() }
413444 }
414- writerConnection.prepare(" PRAGMA user_version = ${schema.version} " ).use { it.step() }
415445 }
416446 } else {
417447 skipStatementsCache = configuration.cacheSize == 0
@@ -426,6 +456,52 @@ public class AndroidxSqliteDriver(
426456 }
427457}
428458
459+ private inline fun SQLiteConnection.withDeferredForeignKeyChecks (
460+ configuration : AndroidxSqliteConfiguration ,
461+ block : () -> Unit ,
462+ ) {
463+ if (configuration.isForeignKeyConstraintsEnabled) {
464+ prepare(" PRAGMA foreign_keys = OFF;" ).use(SQLiteStatement ::step)
465+ }
466+
467+ block()
468+
469+ if (configuration.isForeignKeyConstraintsEnabled) {
470+ prepare(" PRAGMA foreign_keys = ON;" ).use(SQLiteStatement ::step)
471+
472+ if (configuration.isForeignKeyConstraintsCheckedAfterCreateOrUpdate) {
473+ prepare(" PRAGMA foreign_key_check;" ).use { check ->
474+ val violations = mutableListOf<String >()
475+ while (check.step()) {
476+ val referencingTable = check.getText(0 )
477+ val referencingRowId = check.getInt(1 )
478+ val referencedTable = check.getText(2 )
479+ val referencingConstraintIndex = check.getInt(3 )
480+
481+ violations.add(
482+ """
483+ |Constraint index: $referencingConstraintIndex
484+ |Referencing table: $referencingTable
485+ |Referencing rowId: $referencingRowId
486+ |Referenced table: $referencedTable
487+ """ .trimMargin(),
488+ )
489+ }
490+
491+ if (violations.isNotEmpty()) {
492+ throw AndroidxSqliteDriver .ForeignKeyConstraintCheckException (
493+ """
494+ |The following foreign key constraints are violated:
495+ |
496+ |${violations.joinToString(separator = " \n\n " )}
497+ """ .trimMargin(),
498+ )
499+ }
500+ }
501+ }
502+ }
503+ }
504+
429505internal interface AndroidxStatement : SqlPreparedStatement {
430506 fun execute (): Long
431507 fun <R > executeQuery (mapper : (SqlCursor ) -> QueryResult <R >): R
0 commit comments