Skip to content

Commit 2116946

Browse files
Swift Driver Additions (#262)
1 parent 15e1039 commit 2116946

File tree

9 files changed

+527
-54
lines changed

9 files changed

+527
-54
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212

1313
- Add `PowerSyncDatabase.inMemory` to create an in-memory SQLite database with PowerSync.
1414
This may be useful for testing.
15-
- The Supabase connector can now be subclassed to customize how rows are uploaded and how errors are handled.
15+
- The Supabase connector can now be subclassed to customize how rows are uploaded and how errors are
16+
handled.
1617
- Experimental support for sync streams.
18+
- [Swift] Added helpers for creating Swift SQLite connection pools.
1719

1820
## 1.6.1
1921

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
headers = sqlite3.h
22

3-
noStringConversion = sqlite3_prepare_v3
3+
noStringConversion = sqlite3_prepare_v3,sqlite3session_create

common/src/nativeMain/interop/sqlite3.h

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,49 @@
33

44
typedef struct sqlite3 sqlite3;
55
typedef struct sqlite3_stmt sqlite3_stmt;
6+
typedef struct sqlite3_session sqlite3_session;
7+
typedef struct sqlite3_changeset_iter sqlite3_changeset_iter;
68

79
int sqlite3_initialize();
810

911
int sqlite3_open_v2(char *filename, sqlite3 **ppDb, int flags,
1012
char *zVfs);
13+
1114
int sqlite3_close_v2(sqlite3 *db);
1215

1316
// Error handling
1417
int sqlite3_extended_result_codes(sqlite3 *db, int onoff);
18+
1519
int sqlite3_extended_errcode(sqlite3 *db);
20+
1621
char *sqlite3_errmsg(sqlite3 *db);
22+
1723
char *sqlite3_errstr(int code);
24+
1825
int sqlite3_error_offset(sqlite3 *db);
26+
1927
void sqlite3_free(void *ptr);
2028

2129
// Versions
2230
char *sqlite3_libversion();
31+
2332
char *sqlite3_sourceid();
33+
2434
int sqlite3_libversion_number();
2535

2636
// Database
2737
int sqlite3_get_autocommit(sqlite3 *db);
38+
2839
int sqlite3_db_config(sqlite3 *db, int op, ...);
40+
2941
int sqlite3_load_extension(
3042
sqlite3 *db, /* Load the extension into this database connection */
3143
const char *zFile, /* Name of the shared library containing extension */
3244
const char *zProc, /* Entry point. Derived from zFile if 0 */
3345
char **pzErrMsg /* Put error message here if not 0 */
3446
);
35-
int sqlite3_extended_result_codes(sqlite3*, int onoff);
47+
48+
int sqlite3_extended_result_codes(sqlite3 *, int onoff);
3649

3750
// Statements
3851
int sqlite3_prepare16_v3(
@@ -43,27 +56,81 @@ int sqlite3_prepare16_v3(
4356
sqlite3_stmt **ppStmt, /* OUT: Statement handle */
4457
const void **pzTail /* OUT: Pointer to unused portion of zSql */
4558
);
59+
4660
int sqlite3_finalize(sqlite3_stmt *pStmt);
61+
4762
int sqlite3_step(sqlite3_stmt *pStmt);
63+
4864
int sqlite3_reset(sqlite3_stmt *pStmt);
49-
int sqlite3_clear_bindings(sqlite3_stmt*);
65+
66+
int sqlite3_clear_bindings(sqlite3_stmt *);
5067

5168
int sqlite3_column_count(sqlite3_stmt *pStmt);
69+
5270
int sqlite3_bind_parameter_count(sqlite3_stmt *pStmt);
71+
5372
char *sqlite3_column_name(sqlite3_stmt *pStmt, int N);
5473

5574
int sqlite3_bind_blob64(sqlite3_stmt *pStmt, int index, void *data,
5675
uint64_t length, void *destructor);
76+
5777
int sqlite3_bind_double(sqlite3_stmt *pStmt, int index, double data);
78+
5879
int sqlite3_bind_int64(sqlite3_stmt *pStmt, int index, int64_t data);
80+
5981
int sqlite3_bind_null(sqlite3_stmt *pStmt, int index);
82+
6083
int sqlite3_bind_text16(sqlite3_stmt *pStmt, int index, char *data,
6184
int length, void *destructor);
6285

6386
void *sqlite3_column_blob(sqlite3_stmt *pStmt, int iCol);
87+
6488
double sqlite3_column_double(sqlite3_stmt *pStmt, int iCol);
89+
6590
int64_t sqlite3_column_int64(sqlite3_stmt *pStmt, int iCol);
91+
6692
void *sqlite3_column_text16(sqlite3_stmt *pStmt, int iCol);
93+
6794
int sqlite3_column_bytes(sqlite3_stmt *pStmt, int iCol);
95+
6896
int sqlite3_column_bytes16(sqlite3_stmt *pStmt, int iCol);
97+
6998
int sqlite3_column_type(sqlite3_stmt *pStmt, int iCol);
99+
100+
101+
int sqlite3session_create(
102+
sqlite3 *db, /* Database handle */
103+
const char *zDb, /* Name of db (e.g. "main") */
104+
sqlite3_session **ppSession /* OUT: New session object */
105+
);
106+
107+
int sqlite3session_attach(
108+
sqlite3_session *pSession, /* Session object */
109+
const char *zTab /* Table name or NULL for all */
110+
);
111+
112+
int sqlite3session_changeset(
113+
sqlite3_session *pSession, /* Session object */
114+
int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */
115+
void **ppChangeset /* OUT: Buffer containing changeset */
116+
);
117+
118+
int sqlite3changeset_start(
119+
sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */
120+
int nChangeset, /* Size of changeset blob in bytes */
121+
void *pChangeset /* Pointer to blob containing changeset */
122+
);
123+
124+
int sqlite3changeset_op(
125+
sqlite3_changeset_iter *pIter, /* Iterator object */
126+
const char **pzTab, /* OUT: Pointer to table name */
127+
int *pnCol, /* OUT: Number of columns in table */
128+
int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */
129+
int *pbIndirect /* OUT: True for an 'indirect' change */
130+
);
131+
132+
int sqlite3changeset_next(sqlite3_changeset_iter *pIter);
133+
134+
int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter);
135+
136+
void sqlite3session_delete(sqlite3_session *pSession);

internal/PowerSyncKotlin/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ kotlin {
4848
api(project(":core"))
4949
implementation(libs.ktor.client.logging)
5050
}
51+
52+
all {
53+
languageSettings {
54+
optIn("kotlinx.cinterop.ExperimentalForeignApi")
55+
optIn("com.powersync.ExperimentalPowerSyncAPI")
56+
}
57+
}
5158
}
5259
}
5360

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package com.powersync.pool
2+
3+
import androidx.sqlite.SQLiteStatement
4+
import com.powersync.db.driver.SQLiteConnectionLease
5+
import com.powersync.sqlite.Database
6+
7+
internal class RawConnectionLease(
8+
lease: SwiftLeaseAdapter,
9+
) : SQLiteConnectionLease {
10+
private var isCompleted = false
11+
12+
private var db = Database(lease.pointer)
13+
14+
private fun checkNotCompleted() {
15+
check(!isCompleted) { "Connection lease already closed" }
16+
}
17+
18+
override suspend fun isInTransaction(): Boolean = isInTransactionSync()
19+
20+
override fun isInTransactionSync(): Boolean {
21+
checkNotCompleted()
22+
return db.inTransaction()
23+
}
24+
25+
override suspend fun <R> usePrepared(
26+
sql: String,
27+
block: (SQLiteStatement) -> R,
28+
): R = usePreparedSync(sql, block)
29+
30+
override fun <R> usePreparedSync(
31+
sql: String,
32+
block: (SQLiteStatement) -> R,
33+
): R {
34+
checkNotCompleted()
35+
return db.prepare(sql).use(block)
36+
}
37+
}
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package com.powersync.pool
2+
3+
import cnames.structs.sqlite3
4+
import cnames.structs.sqlite3_changeset_iter
5+
import cnames.structs.sqlite3_session
6+
import com.powersync.PowerSyncException
7+
import com.powersync.PowerSyncResult
8+
import com.powersync.db.runWrapped
9+
import com.powersync.internal.sqlite3.sqlite3_free
10+
import com.powersync.internal.sqlite3.sqlite3changeset_finalize
11+
import com.powersync.internal.sqlite3.sqlite3changeset_next
12+
import com.powersync.internal.sqlite3.sqlite3changeset_op
13+
import com.powersync.internal.sqlite3.sqlite3changeset_start
14+
import com.powersync.internal.sqlite3.sqlite3session_attach
15+
import com.powersync.internal.sqlite3.sqlite3session_changeset
16+
import com.powersync.internal.sqlite3.sqlite3session_create
17+
import com.powersync.internal.sqlite3.sqlite3session_delete
18+
import kotlinx.cinterop.ByteVar
19+
import kotlinx.cinterop.COpaquePointerVar
20+
import kotlinx.cinterop.CPointer
21+
import kotlinx.cinterop.CPointerVar
22+
import kotlinx.cinterop.IntVar
23+
import kotlinx.cinterop.alloc
24+
import kotlinx.cinterop.memScoped
25+
import kotlinx.cinterop.ptr
26+
import kotlinx.cinterop.toKString
27+
import kotlinx.cinterop.value
28+
29+
public data class SessionResult(
30+
val blockResult: PowerSyncResult,
31+
val affectedTables: Set<String>,
32+
)
33+
34+
/**
35+
* We typically have a few options for table update hooks:
36+
* 1.) Registering a hook with SQLite
37+
* 2.) Using our Rust core to register update hooks
38+
* 3.) Receiving updates from an external API
39+
*
40+
* In some cases, particularly in the case of GRDB, none of these options are viable.
41+
* GRDB dynamically registers (and unregisters) its own update hooks and its update hook logic
42+
* does not report changes for operations made outside of its own APIs.
43+
*
44+
* 1.) We can't register our own hooks since GRDB might override it or our hook could conflict with GRDB's
45+
* 2.) We can't register hooks due to above
46+
* 3.) The GRDB APIs only report changes if made with their SQLite execution APIs. It's not trivial to implement [com.powersync.db.driver.SQLiteConnectionLease] with their APIs.
47+
*
48+
* This function provides an alternative method of obtaining table changes by using SQLite sessions.
49+
* https://www.sqlite.org/sessionintro.html
50+
*
51+
* We start a session, execute a block of code, and then extract the changeset from the session.
52+
* We then parse the changeset to extract the table names that were modified.
53+
* This approach is more heavyweight than using update hooks, but it works in scenarios where
54+
* update hooks are not currently feasible.
55+
*/
56+
@Throws(PowerSyncException::class)
57+
public fun withSession(
58+
db: CPointer<sqlite3>,
59+
block: () -> PowerSyncResult,
60+
): SessionResult =
61+
runWrapped {
62+
memScoped {
63+
val sessionPtr = alloc<CPointerVar<sqlite3_session>>()
64+
65+
val rc =
66+
sqlite3session_create(
67+
db,
68+
"main",
69+
sessionPtr.ptr,
70+
).checkResult("Could not create SQLite session")
71+
72+
val session =
73+
sessionPtr.value ?: throw PowerSyncException(
74+
"Could not create SQLite session",
75+
cause = Error(),
76+
)
77+
78+
try {
79+
// Attach all tables to track changes
80+
sqlite3session_attach(
81+
session,
82+
null,
83+
).checkResult("Could not attach all tables to session") // null means all tables
84+
85+
// Execute the block where changes happen
86+
val result = block()
87+
88+
// Get the changeset
89+
val changesetSizePtr = alloc<IntVar>()
90+
val changesetPtr = alloc<COpaquePointerVar>()
91+
92+
sqlite3session_changeset(
93+
session,
94+
changesetSizePtr.ptr,
95+
changesetPtr.ptr,
96+
).checkResult("Could not get changeset from session")
97+
98+
val changesetSize = changesetSizePtr.value
99+
val changeset = changesetPtr.value
100+
101+
if (changesetSize == 0 || changeset == null) {
102+
return@memScoped SessionResult(
103+
result,
104+
affectedTables = emptySet(),
105+
)
106+
}
107+
108+
// Parse the changeset to extract table names
109+
val changedTables = mutableSetOf<String>()
110+
val iterPtr = alloc<CPointerVar<sqlite3_changeset_iter>>()
111+
112+
sqlite3changeset_start(
113+
iterPtr.ptr,
114+
changesetSize,
115+
changeset,
116+
).checkResult("Could not start changeset iterator")
117+
118+
val iter = iterPtr.value
119+
120+
if (iter == null) {
121+
return@memScoped SessionResult(
122+
result,
123+
affectedTables = emptySet(),
124+
)
125+
}
126+
127+
try {
128+
// Iterate through all changes
129+
while (sqlite3changeset_next(iter) == 100) {
130+
val tableNamePtr = alloc<CPointerVar<ByteVar>>()
131+
val nColPtr = alloc<IntVar>()
132+
val opPtr = alloc<IntVar>()
133+
val indirectPtr = alloc<IntVar>()
134+
135+
val opRc =
136+
sqlite3changeset_op(
137+
iter,
138+
tableNamePtr.ptr,
139+
nColPtr.ptr,
140+
opPtr.ptr,
141+
indirectPtr.ptr,
142+
)
143+
144+
if (opRc == 0) {
145+
val tableNameCPtr = tableNamePtr.value
146+
if (tableNameCPtr != null) {
147+
val tableName = tableNameCPtr.toKString()
148+
changedTables.add(tableName)
149+
}
150+
}
151+
}
152+
} finally {
153+
sqlite3changeset_finalize(iter)
154+
// Free the changeset memory
155+
sqlite3_free(changeset)
156+
}
157+
158+
return@memScoped SessionResult(
159+
result,
160+
affectedTables = changedTables.toSet(),
161+
)
162+
} finally {
163+
// Clean up the session
164+
sqlite3session_delete(session)
165+
}
166+
}
167+
}
168+
169+
private fun Int.checkResult(message: String) {
170+
if (this != 0) {
171+
throw PowerSyncException("SQLite error code: $this", cause = Error(message))
172+
}
173+
}

0 commit comments

Comments
 (0)