Skip to content

Commit df61201

Browse files
authored
Ensure that configuration changes propagate to all pooled connections (#36)
1 parent de01ec1 commit df61201

File tree

6 files changed

+206
-50
lines changed

6 files changed

+206
-50
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.eygraber.sqldelight.androidx.driver
2+
3+
/**
4+
* [sqlite.org journal_mode](https://www.sqlite.org/pragma.html#pragma_journal_mode)
5+
*/
6+
public enum class SqliteJournalMode(internal val value: String) {
7+
Delete("DELETE"),
8+
Truncate("TRUNCATE"),
9+
Persist("PERSIST"),
10+
Memory("MEMORY"),
11+
@Suppress("EnumNaming")
12+
WAL("WAL"),
13+
Off("OFF"),
14+
}
15+
16+
/**
17+
* [sqlite.org synchronous](https://www.sqlite.org/pragma.html#pragma_synchronous)
18+
*/
19+
public enum class SqliteSync(internal val value: String) {
20+
Off("OFF"),
21+
Normal("NORMAL"),
22+
Full("FULL"),
23+
Extra("EXTRA"),
24+
}
25+
26+
public class AndroidxSqliteConfiguration(
27+
/**
28+
* The maximum size of the prepared statement cache for each database connection.
29+
*
30+
* Default is 25.
31+
*/
32+
public val cacheSize: Int = 25,
33+
/**
34+
* True if foreign key constraints are enabled.
35+
*
36+
* Default is false.
37+
*/
38+
public var isForeignKeyConstraintsEnabled: Boolean = false,
39+
/**
40+
* Journal mode to use.
41+
*
42+
* Default is [SqliteJournalMode.WAL].
43+
*/
44+
public var journalMode: SqliteJournalMode = SqliteJournalMode.WAL,
45+
/**
46+
* Synchronous mode to use.
47+
*
48+
* Default is [SqliteSync.Full] unless [journalMode] is set to [SqliteJournalMode.WAL] in which case it is [SqliteSync.Normal].
49+
*/
50+
public var sync: SqliteSync = when(journalMode) {
51+
SqliteJournalMode.WAL -> SqliteSync.Normal
52+
SqliteJournalMode.Delete,
53+
SqliteJournalMode.Truncate,
54+
SqliteJournalMode.Persist,
55+
SqliteJournalMode.Memory,
56+
SqliteJournalMode.Off,
57+
-> SqliteSync.Full
58+
},
59+
/**
60+
* The max amount of read connections that will be kept in the [ConnectionPool].
61+
*
62+
* Defaults to 4 when [journalMode] is [SqliteJournalMode.WAL], otherwise 0 (since reads are blocked by writes).
63+
*
64+
* The default for [SqliteJournalMode.WAL] may be changed in the future to be based on how many CPUs are available.
65+
*/
66+
public val readerConnectionsCount: Int = when(journalMode) {
67+
SqliteJournalMode.WAL -> 4
68+
else -> 0
69+
},
70+
)

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

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ internal expect class TransactionsThreadLocal() {
2323
internal fun set(transaction: Transacter.Transaction?)
2424
}
2525

26-
internal const val DEFAULT_CACHE_SIZE = 20
27-
2826
/**
2927
* @param databaseType Specifies the type of the database file
3028
* (see [Sqlite open documentation](https://www.sqlite.org/c3ref/open.html)).
@@ -37,21 +35,19 @@ public class AndroidxSqliteDriver(
3735
createConnection: (String) -> SQLiteConnection,
3836
databaseType: AndroidxSqliteDatabaseType,
3937
private val schema: SqlSchema<QueryResult.Value<Unit>>,
40-
readerConnections: Int = 0,
41-
private val cacheSize: Int = DEFAULT_CACHE_SIZE,
38+
private val configuration: AndroidxSqliteConfiguration = AndroidxSqliteConfiguration(),
4239
private val migrateEmptySchema: Boolean = false,
4340
private val onConfigure: ConfigurableDatabase.() -> Unit = {},
44-
private val onCreate: SqlDriver.() -> Unit = {},
45-
private val onUpdate: SqlDriver.(Long, Long) -> Unit = { _, _ -> },
46-
private val onOpen: SqlDriver.() -> Unit = {},
41+
private val onCreate: AndroidxSqliteDriver.() -> Unit = {},
42+
private val onUpdate: AndroidxSqliteDriver.(Long, Long) -> Unit = { _, _ -> },
43+
private val onOpen: AndroidxSqliteDriver.() -> Unit = {},
4744
vararg migrationCallbacks: AfterVersion,
4845
) : SqlDriver {
4946
public constructor(
5047
driver: SQLiteDriver,
5148
databaseType: AndroidxSqliteDatabaseType,
5249
schema: SqlSchema<QueryResult.Value<Unit>>,
53-
readerConnections: Int = 0,
54-
cacheSize: Int = DEFAULT_CACHE_SIZE,
50+
configuration: AndroidxSqliteConfiguration = AndroidxSqliteConfiguration(),
5551
migrateEmptySchema: Boolean = false,
5652
onConfigure: ConfigurableDatabase.() -> Unit = {},
5753
onCreate: SqlDriver.() -> Unit = {},
@@ -62,8 +58,7 @@ public class AndroidxSqliteDriver(
6258
createConnection = driver::open,
6359
databaseType = databaseType,
6460
schema = schema,
65-
readerConnections = readerConnections,
66-
cacheSize = cacheSize,
61+
configuration = configuration,
6762
migrateEmptySchema = migrateEmptySchema,
6863
onConfigure = onConfigure,
6964
onCreate = onCreate,
@@ -83,11 +78,12 @@ public class AndroidxSqliteDriver(
8378
AndroidxSqliteDatabaseType.Memory -> ":memory:"
8479
AndroidxSqliteDatabaseType.Temporary -> ""
8580
},
86-
maxReaders = when(databaseType) {
87-
is AndroidxSqliteDatabaseType.File -> readerConnections
88-
AndroidxSqliteDatabaseType.Memory -> 0
89-
AndroidxSqliteDatabaseType.Temporary -> 0
81+
isFileBased = when(databaseType) {
82+
is AndroidxSqliteDatabaseType.File -> true
83+
AndroidxSqliteDatabaseType.Memory -> false
84+
AndroidxSqliteDatabaseType.Temporary -> false
9085
},
86+
configuration = configuration,
9187
)
9288
}
9389

@@ -99,7 +95,7 @@ public class AndroidxSqliteDriver(
9995
statementsCaches.getOrPut(
10096
connection,
10197
) {
102-
object : LruCache<Int, AndroidxStatement>(cacheSize) {
98+
object : LruCache<Int, AndroidxStatement>(configuration.cacheSize) {
10399
override fun entryRemoved(
104100
evicted: Boolean,
105101
key: Int,
@@ -118,6 +114,51 @@ public class AndroidxSqliteDriver(
118114

119115
private val migrationCallbacks = migrationCallbacks
120116

117+
/**
118+
* True if foreign key constraints are enabled.
119+
*
120+
* This function will block until all connections have been updated.
121+
*
122+
* An exception will be thrown if this is called from within a transaction.
123+
*/
124+
public fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) {
125+
check(currentTransaction() == null) {
126+
"setForeignKeyConstraintsEnabled cannot be called from within a transaction"
127+
}
128+
129+
connectionPool.setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled)
130+
}
131+
132+
/**
133+
* Journal mode to use.
134+
*
135+
* This function will block until all connections have been updated.
136+
*
137+
* An exception will be thrown if this is called from within a transaction.
138+
*/
139+
public fun setJournalMode(journalMode: SqliteJournalMode) {
140+
check(currentTransaction() == null) {
141+
"setJournalMode cannot be called from within a transaction"
142+
}
143+
144+
connectionPool.setJournalMode(journalMode)
145+
}
146+
147+
/**
148+
* Synchronous mode to use.
149+
*
150+
* This function will block until all connections have been updated.
151+
*
152+
* An exception will be thrown if this is called from within a transaction.
153+
*/
154+
public fun setSync(sync: SqliteSync) {
155+
check(currentTransaction() == null) {
156+
"setSync cannot be called from within a transaction"
157+
}
158+
159+
connectionPool.setSync(sync)
160+
}
161+
121162
override fun addListener(vararg queryKeys: String, listener: Query.Listener) {
122163
synchronized(listenersLock) {
123164
queryKeys.forEach {

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

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,19 @@ import app.cash.sqldelight.db.QueryResult
44
import app.cash.sqldelight.db.SqlCursor
55
import app.cash.sqldelight.db.SqlPreparedStatement
66

7-
public fun AndroidxSqliteDriver.enableForeignKeys() {
8-
execute(null, "PRAGMA foreign_keys = ON;", 0, null)
9-
}
10-
11-
public fun AndroidxSqliteDriver.disableForeignKeys() {
12-
execute(null, "PRAGMA foreign_keys = OFF;", 0, null)
13-
}
14-
15-
public fun AndroidxSqliteDriver.enableWAL() {
16-
execute(null, "PRAGMA journal_mode = WAL;", 0, null)
17-
}
18-
19-
public fun AndroidxSqliteDriver.disableWAL() {
20-
execute(null, "PRAGMA journal_mode = DELETE;", 0, null)
21-
}
22-
237
public class ConfigurableDatabase(
248
private val driver: AndroidxSqliteDriver,
259
) {
26-
public fun enableForeignKeys() {
27-
driver.enableForeignKeys()
28-
}
29-
30-
public fun disableForeignKeys() {
31-
driver.disableForeignKeys()
10+
public fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) {
11+
driver.setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled)
3212
}
3313

34-
public fun enableWAL() {
35-
driver.enableWAL()
14+
public fun setJournalMode(journalMode: SqliteJournalMode) {
15+
driver.setJournalMode(journalMode)
3616
}
3717

38-
public fun disableWAL() {
39-
driver.disableWAL()
18+
public fun setSync(sync: SqliteSync) {
19+
driver.setSync(sync)
4020
}
4121

4222
public fun executePragma(

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

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

33
import androidx.sqlite.SQLiteConnection
4+
import androidx.sqlite.SQLiteStatement
45
import kotlinx.coroutines.channels.Channel
56
import kotlinx.coroutines.runBlocking
67
import kotlinx.coroutines.sync.Mutex
@@ -9,13 +10,25 @@ import kotlinx.coroutines.sync.withLock
910
internal class ConnectionPool(
1011
private val createConnection: (String) -> SQLiteConnection,
1112
private val name: String,
12-
private val maxReaders: Int = 4,
13+
private val isFileBased: Boolean,
14+
private val configuration: AndroidxSqliteConfiguration,
1315
) {
14-
private val writerConnection: SQLiteConnection by lazy { createConnection(name) }
16+
private val writerConnection: SQLiteConnection by lazy {
17+
createConnection(name).withConfiguration()
18+
}
1519
private val writerMutex = Mutex()
1620

17-
private val readerChannel = Channel<SQLiteConnection>(capacity = maxReaders)
18-
private val readerConnections = List(maxReaders) { lazy { createConnection(name) } }
21+
private val maxReaderConnectionsCount = when {
22+
isFileBased -> configuration.readerConnectionsCount
23+
else -> 0
24+
}
25+
26+
private val readerChannel = Channel<SQLiteConnection>(capacity = maxReaderConnectionsCount)
27+
private val readerConnections = List(maxReaderConnectionsCount) {
28+
lazy {
29+
createConnection(name).withConfiguration()
30+
}
31+
}
1932

2033
/**
2134
* Acquires the writer connection, blocking if it's currently in use.
@@ -37,7 +50,7 @@ internal class ConnectionPool(
3750
* Acquires a reader connection, blocking if none are available.
3851
* @return A reader SQLiteConnection
3952
*/
40-
fun acquireReaderConnection() = when(maxReaders) {
53+
fun acquireReaderConnection() = when(maxReaderConnectionsCount) {
4154
0 -> acquireWriterConnection()
4255
else -> runBlocking {
4356
val firstUninitialized = readerConnections.firstOrNull { !it.isInitialized() }
@@ -49,13 +62,61 @@ internal class ConnectionPool(
4962
* Releases a reader connection back to the pool.
5063
* @param connection The SQLiteConnection to release
5164
*/
52-
fun releaseReaderConnection(connection: SQLiteConnection) = when(maxReaders) {
65+
fun releaseReaderConnection(connection: SQLiteConnection) = when(maxReaderConnectionsCount) {
5366
0 -> releaseWriterConnection()
5467
else -> runBlocking {
5568
readerChannel.send(connection)
5669
}
5770
}
5871

72+
fun setForeignKeyConstraintsEnabled(isForeignKeyConstraintsEnabled: Boolean) {
73+
configuration.isForeignKeyConstraintsEnabled = isForeignKeyConstraintsEnabled
74+
val foreignKeys = if(isForeignKeyConstraintsEnabled) "ON" else "OFF"
75+
runPragmaOnAllConnections("PRAGMA foreign_keys = $foreignKeys;")
76+
}
77+
78+
fun setJournalMode(journalMode: SqliteJournalMode) {
79+
configuration.journalMode = journalMode
80+
runPragmaOnAllConnections("PRAGMA journal_mode = ${configuration.journalMode.value};")
81+
}
82+
83+
fun setSync(sync: SqliteSync) {
84+
configuration.sync = sync
85+
runPragmaOnAllConnections("PRAGMA synchronous = ${configuration.sync.value};")
86+
}
87+
88+
private fun runPragmaOnAllConnections(sql: String) {
89+
val writer = acquireWriterConnection()
90+
try {
91+
writer.writePragma(sql)
92+
} finally {
93+
releaseWriterConnection()
94+
}
95+
96+
if(maxReaderConnectionsCount > 0) {
97+
runBlocking {
98+
val createdReadersCount = readerConnections.count { it.isInitialized() }
99+
val readers = List(createdReadersCount) {
100+
readerChannel.receive()
101+
}
102+
readers.forEach { reader ->
103+
try {
104+
reader.writePragma(sql)
105+
} finally {
106+
releaseReaderConnection(reader)
107+
}
108+
}
109+
}
110+
}
111+
}
112+
113+
private fun SQLiteConnection.withConfiguration(): SQLiteConnection = this.apply {
114+
val foreignKeys = if(configuration.isForeignKeyConstraintsEnabled) "ON" else "OFF"
115+
writePragma("PRAGMA foreign_keys = $foreignKeys;")
116+
writePragma("PRAGMA journal_mode = ${configuration.journalMode.value};")
117+
writePragma("PRAGMA synchronous = ${configuration.sync.value};")
118+
}
119+
59120
/**
60121
* Closes all connections in the pool.
61122
*/
@@ -71,3 +132,7 @@ internal class ConnectionPool(
71132
}
72133
}
73134
}
135+
136+
private fun SQLiteConnection.writePragma(sql: String) {
137+
prepare(sql).use(SQLiteStatement::step)
138+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ abstract class AndroidxSqliteConcurrencyTest {
4747
onCreate: SqlDriver.() -> Unit,
4848
onUpdate: SqlDriver.(Long, Long) -> Unit,
4949
onOpen: SqlDriver.() -> Unit,
50-
onConfigure: ConfigurableDatabase.() -> Unit = { enableWAL() },
50+
onConfigure: ConfigurableDatabase.() -> Unit = { setJournalMode(SqliteJournalMode.WAL) },
5151
): SqlDriver = AndroidxSqliteDriver(
5252
createConnection = androidxSqliteTestCreateConnection(),
5353
databaseType = AndroidxSqliteDatabaseType.File(dbName),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ abstract class AndroidxSqliteDriverTest {
7070
androidxSqliteTestDriver(),
7171
AndroidxSqliteDatabaseType.Memory,
7272
schema,
73-
cacheSize = 1,
73+
AndroidxSqliteConfiguration(cacheSize = 1),
7474
).use(block)
7575
}
7676

0 commit comments

Comments
 (0)