Skip to content

Commit 1d939cc

Browse files
Concurrent Connections (#145)
1 parent 7d62a23 commit 1d939cc

File tree

56 files changed

+1519
-1174
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1519
-1174
lines changed

CHANGELOG.md

+15-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
11
# Changelog
22

3-
## 1.0.0-BETA29
4-
5-
* Added queing protection and warnings when connecting multiple PowerSync clients to the same database file.
6-
73
## 1.0.0-BETA28
84

95
* Update PowerSync SQLite core extension to 0.3.12.
6+
* Added queing protection and warnings when connecting multiple PowerSync clients to the same database file.
7+
* Improved concurrent SQLite connection support accross various platforms. All platforms now use a single write connection and multiple read connections for concurrent read queries.
8+
* Added the ability to open a SQLite database given a custom `dbDirectory` path. This is currently not supported on iOS due to internal driver restrictions.
9+
* Internaly improved the linking of SQLite for iOS.
10+
* The Android SQLite driver now uses the [Xerial JDBC library](https://github.com/xerial/sqlite-jdbc). This removes the requirement for users to add the jitpack Maven repository to their projects.
11+
```diff
12+
// settings.gradle.kts example
13+
repositories {
14+
google()
15+
- maven("https://jitpack.io") {
16+
- content { includeGroup("com.github.requery") }
17+
- }
18+
mavenCentral()
19+
}
20+
```
1021

1122
## 1.0.0-BETA27
1223

README.md

-5
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,9 @@ The PowerSync Kotlin Multiplatform SDK is currently in a beta release. It can be
4646
Current limitations:
4747

4848
- Integration with SQLDelight schema and API generation (ORM) is not yet supported.
49-
- Supports only a single database file.
5049

5150
Future work/ideas:
52-
53-
- Improved error handling.
5451
- Attachments helper package.
55-
- Management of DB connections on each platform natively.
56-
- Supporting additional targets (JVM, Wasm).
5752

5853
## Installation
5954

build.gradle.kts

-3
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@ allprojects {
2727
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
2828
// Repo for the backported Android IntelliJ Plugin by Jetbrains used in Ultimate
2929
maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-ide-plugin-dependencies/")
30-
maven("https://jitpack.io") {
31-
content { includeGroup("com.github.requery") }
32-
}
3330
}
3431

3532
configurations.configureEach {

core-tests-android/src/androidTest/java/com/powersync/AndroidDatabaseTest.kt

+129
Original file line numberDiff line numberDiff line change
@@ -93,4 +93,133 @@ class AndroidDatabaseTest {
9393
query.cancel()
9494
}
9595
}
96+
97+
@Test
98+
fun testConcurrentReads() =
99+
runTest {
100+
database.execute(
101+
"INSERT INTO users (id, name, email) VALUES (uuid(), ?, ?)",
102+
listOf(
103+
"steven",
104+
105+
),
106+
)
107+
108+
val pausedTransaction = CompletableDeferred<Unit>()
109+
val transactionItemCreated = CompletableDeferred<Unit>()
110+
// Start a long running writeTransaction
111+
val transactionJob =
112+
async {
113+
database.writeTransaction { tx ->
114+
// Create another user
115+
// External readers should not see this user while the transaction is open
116+
tx.execute(
117+
"INSERT INTO users (id, name, email) VALUES (uuid(), ?, ?)",
118+
listOf(
119+
"steven",
120+
121+
),
122+
)
123+
124+
transactionItemCreated.complete(Unit)
125+
126+
// Block this transaction until we free it
127+
runBlocking {
128+
pausedTransaction.await()
129+
}
130+
}
131+
}
132+
133+
// Make sure to wait for the item to have been created in the transaction
134+
transactionItemCreated.await()
135+
// Try and read while the write transaction is busy
136+
val result = database.getAll("SELECT * FROM users") { UserRow.from(it) }
137+
// The transaction is not commited yet, we should only read 1 user
138+
assertEquals(result.size, 1)
139+
140+
// Let the transaction complete
141+
pausedTransaction.complete(Unit)
142+
transactionJob.await()
143+
144+
val afterTx = database.getAll("SELECT * FROM users") { UserRow.from(it) }
145+
assertEquals(afterTx.size, 2)
146+
}
147+
148+
@Test
149+
fun transactionReads() =
150+
runTest {
151+
database.execute(
152+
"INSERT INTO users (id, name, email) VALUES (uuid(), ?, ?)",
153+
listOf(
154+
"steven",
155+
156+
),
157+
)
158+
159+
database.writeTransaction { tx ->
160+
val userCount =
161+
tx.getAll("SELECT COUNT(*) as count FROM users") { cursor -> cursor.getLong(0)!! }
162+
assertEquals(userCount[0], 1)
163+
164+
tx.execute(
165+
"INSERT INTO users (id, name, email) VALUES (uuid(), ?, ?)",
166+
listOf(
167+
"steven",
168+
169+
),
170+
)
171+
172+
// Getters inside the transaction should be able to see the latest update
173+
val userCount2 =
174+
tx.getAll("SELECT COUNT(*) as count FROM users") { cursor -> cursor.getLong(0)!! }
175+
assertEquals(userCount2[0], 2)
176+
}
177+
}
178+
179+
@Test
180+
fun openDBWithDirectory() =
181+
runTest {
182+
val tempDir =
183+
InstrumentationRegistry
184+
.getInstrumentation()
185+
.targetContext.cacheDir.canonicalPath
186+
val dbFilename = "testdb"
187+
188+
val db =
189+
PowerSyncDatabase(
190+
factory = DatabaseDriverFactory(InstrumentationRegistry.getInstrumentation().targetContext),
191+
schema = Schema(UserRow.table),
192+
dbDirectory = tempDir,
193+
dbFilename = dbFilename,
194+
)
195+
196+
val path = db.get("SELECT file FROM pragma_database_list;") { it.getString(0)!! }
197+
198+
assertEquals(path.contains(tempDir), true)
199+
200+
db.close()
201+
}
202+
203+
@Test
204+
fun readConnectionsReadOnly() =
205+
runTest {
206+
val exception =
207+
assertThrows(PowerSyncException::class.java) {
208+
// This version of assertThrows does not support suspending functions
209+
runBlocking {
210+
database.getOptional(
211+
"""
212+
INSERT INTO
213+
users (id, name, email)
214+
VALUES
215+
(uuid(), ?, ?)
216+
RETURNING *
217+
""".trimIndent(),
218+
parameters = listOf("steven", "[email protected]"),
219+
) {}
220+
}
221+
}
222+
// The exception messages differ slightly between drivers
223+
assertEquals(exception.message!!.contains("write a readonly database"), true)
224+
}
96225
}

core/.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -1 +1,5 @@
11
binaries/
2+
# Required for the JDBC SQLite driver, but should not be commiteed
3+
src/androidMain/jni
4+
5+
testdb-*

core/README.md

+20-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,31 @@
11
# PowerSync core module
22

3-
The PowerSync core module provides the core functionality for the PowerSync Kotlin Multiplatform SDK.
3+
The PowerSync core module provides the core functionality for the PowerSync Kotlin Multiplatform
4+
SDK.
45

56
## Structure
67

7-
This is a Kotlin Multiplatform project targeting Android, iOS platforms, with the following structure:
8+
This is a Kotlin Multiplatform project targeting Android, iOS platforms, with the following
9+
structure:
810

9-
- `commonMain` - Shared code for all targets, which includes the `PowerSyncBackendConnector` interface and `PowerSyncBuilder` for building a `PowerSync` instance. It also defines
11+
- `commonMain` - Shared code for all targets, which includes the `PowerSyncBackendConnector`
12+
interface and `PowerSyncBuilder` for building a `PowerSync` instance. It also defines
1013
the `DatabaseDriverFactory` class to be implemented in each platform.
11-
- `androidMain` - Android specific code, which includes a implementation of `DatabaseDriverFactory` class that creates an instance of `app.cash.sqldelight.driver.android.AndroidSqliteDriver` using
12-
a `io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory`. It also includes native SQLite bindings for Android.
13-
- `iosMain` - iOS specific code, which includes a implementation of `DatabaseDriverFactory` class that creates an instance of `app.cash.sqldelight.driver.native.NativeSqliteDriver` and also sets up native SQLite bindings for iOS.
14+
- `commonJava` - Common Java code including a Java SQLite driver using
15+
the [Xerial JDBC Driver](https://github.com/xerial/sqlite-jdbc). This is used by both the Android
16+
and JVM drivers.
17+
- `androidMain` - Android specific code, which includes an implementation of
18+
`DatabaseDriverFactory`.
19+
- `jvmMain` - JVM specific code which includes an implementation of `DatabaseDriverFactory`.
20+
- `iosMain` - iOS specific code, which includes am implementation of `DatabaseDriverFactory` class
21+
that creates an instance of `app.cash.sqldelight.driver.native.NativeSqliteDriver` and also sets
22+
up native SQLite bindings for iOS.
1423

1524
## Note on SQLDelight
1625

17-
The PowerSync core module, internally makes use of [SQLDelight](https://cashapp.github.io/sqldelight) for it database API and typesafe database query generation.
26+
The PowerSync core module, internally makes use
27+
of [SQLDelight](https://sqldelight.github.io/sqldelight/latest/) for it database API and typesafe database
28+
query generation.
1829

19-
The PowerSync core module does not currently support integrating with SQLDelight from client applications.
30+
The PowerSync core module does not currently support integrating with SQLDelight from client
31+
applications.

0 commit comments

Comments
 (0)