Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
136e809
New unified driver interfaces
simolus3 Jul 22, 2025
435c7c5
Fix leaking statements
simolus3 Jul 24, 2025
522f4c6
Add raw connection API
simolus3 Jul 25, 2025
2359b48
Add changelog entry
simolus3 Jul 25, 2025
f4d98d5
Lease API that works better with Room
simolus3 Jul 25, 2025
eccb7f7
Notify updates from raw statements
simolus3 Jul 25, 2025
54829bc
Delete more driver stuff
simolus3 Aug 22, 2025
f532ba9
Fix deadlock in initialization
simolus3 Aug 22, 2025
c7adbab
Make addPowerSyncExtension public
simolus3 Aug 22, 2025
011b1da
Actually, use callbacks
simolus3 Aug 22, 2025
c87e079
merge main
simolus3 Sep 2, 2025
cb1373d
Add docs
simolus3 Sep 2, 2025
aa4e7bc
Fix lints
simolus3 Sep 2, 2025
03796e7
Add native sqlite driver
simolus3 Sep 5, 2025
a0682c2
Bring back static sqlite linking
simolus3 Sep 5, 2025
8f5f8cd
Fix linter errors
simolus3 Sep 5, 2025
f41b0a4
Fix Swift tests
simolus3 Sep 5, 2025
51521d8
Delete proguard rules
simolus3 Sep 5, 2025
fd04adc
grdb drivers
stevensJourney Sep 7, 2025
b32f7bf
wip: lease all connections
stevensJourney Sep 7, 2025
9008648
revert databasegroup changes.
stevensJourney Sep 8, 2025
d6697d9
Merge branch 'main' into grdb-drivers
stevensJourney Sep 16, 2025
a92930a
update after merging
stevensJourney Sep 16, 2025
ef4160c
revert test change
stevensJourney Sep 16, 2025
068d8ed
Merge remote-tracking branch 'origin/main' into grdb-drivers
stevensJourney Sep 19, 2025
c0bdde9
improve error handling
stevensJourney Sep 23, 2025
937d452
Use SQLite Session API for Swift updates.
stevensJourney Sep 25, 2025
59408b0
Code cleanup. Fix lint error.
stevensJourney Sep 25, 2025
07267c1
Merge remote-tracking branch 'origin/main' into grdb-drivers
stevensJourney Sep 25, 2025
587934c
cleanup APIs for sessions
stevensJourney Sep 29, 2025
5434a5f
Merge remote-tracking branch 'origin/main' into grdb-drivers
stevensJourney Oct 3, 2025
9f855e4
move Swift pool logic
stevensJourney Oct 3, 2025
11add5c
Add changelog entry
stevensJourney Oct 3, 2025
be8617e
Start moving code into common
simolus3 Oct 17, 2025
97e7c1b
Share common logic in test
simolus3 Oct 17, 2025
f16eedf
Well, compiling works
simolus3 Oct 17, 2025
eaaeaed
Fixing some tests
simolus3 Oct 17, 2025
13dd40e
Update readmes
simolus3 Oct 17, 2025
fbaa0da
Reformat
simolus3 Oct 20, 2025
f5cd4c7
Fix supabase test
simolus3 Oct 20, 2025
23cb1d9
Fix watchos tests
simolus3 Oct 20, 2025
55f0f28
Merge branch 'common-module' into grdb-drivers-sqlite-update
stevensJourney Oct 22, 2025
3f0d775
update connection pool
stevensJourney Oct 22, 2025
d26b9c6
refactor withSession for return type
stevensJourney Oct 22, 2025
81ef883
refactor withSession for return type
stevensJourney Oct 22, 2025
9641e3b
Helpers for Swift Strict Concurrency
stevensJourney Oct 22, 2025
a333ab5
Merge branch 'grdb-drivers-sqlite-update' into grdb-drivers
stevensJourney Oct 23, 2025
1f496d6
Merge branch 'main' into grdb-drivers
stevensJourney Oct 23, 2025
222e575
Linter fix
stevensJourney Oct 23, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

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

## 1.6.1

Expand Down
2 changes: 1 addition & 1 deletion common/src/nativeMain/interop/sqlite3.def
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
headers = sqlite3.h

noStringConversion = sqlite3_prepare_v3
noStringConversion = sqlite3_prepare_v3,sqlite3session_create
71 changes: 69 additions & 2 deletions common/src/nativeMain/interop/sqlite3.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,49 @@

typedef struct sqlite3 sqlite3;
typedef struct sqlite3_stmt sqlite3_stmt;
typedef struct sqlite3_session sqlite3_session;
typedef struct sqlite3_changeset_iter sqlite3_changeset_iter;

int sqlite3_initialize();

int sqlite3_open_v2(char *filename, sqlite3 **ppDb, int flags,
char *zVfs);

int sqlite3_close_v2(sqlite3 *db);

// Error handling
int sqlite3_extended_result_codes(sqlite3 *db, int onoff);

int sqlite3_extended_errcode(sqlite3 *db);

char *sqlite3_errmsg(sqlite3 *db);

char *sqlite3_errstr(int code);

int sqlite3_error_offset(sqlite3 *db);

void sqlite3_free(void *ptr);

// Versions
char *sqlite3_libversion();

char *sqlite3_sourceid();

int sqlite3_libversion_number();

// Database
int sqlite3_get_autocommit(sqlite3 *db);

int sqlite3_db_config(sqlite3 *db, int op, ...);

int sqlite3_load_extension(
sqlite3 *db, /* Load the extension into this database connection */
const char *zFile, /* Name of the shared library containing extension */
const char *zProc, /* Entry point. Derived from zFile if 0 */
char **pzErrMsg /* Put error message here if not 0 */
);
int sqlite3_extended_result_codes(sqlite3*, int onoff);

int sqlite3_extended_result_codes(sqlite3 *, int onoff);

// Statements
int sqlite3_prepare16_v3(
Expand All @@ -43,27 +56,81 @@ int sqlite3_prepare16_v3(
sqlite3_stmt **ppStmt, /* OUT: Statement handle */
const void **pzTail /* OUT: Pointer to unused portion of zSql */
);

int sqlite3_finalize(sqlite3_stmt *pStmt);

int sqlite3_step(sqlite3_stmt *pStmt);

int sqlite3_reset(sqlite3_stmt *pStmt);
int sqlite3_clear_bindings(sqlite3_stmt*);

int sqlite3_clear_bindings(sqlite3_stmt *);

int sqlite3_column_count(sqlite3_stmt *pStmt);

int sqlite3_bind_parameter_count(sqlite3_stmt *pStmt);

char *sqlite3_column_name(sqlite3_stmt *pStmt, int N);

int sqlite3_bind_blob64(sqlite3_stmt *pStmt, int index, void *data,
uint64_t length, void *destructor);

int sqlite3_bind_double(sqlite3_stmt *pStmt, int index, double data);

int sqlite3_bind_int64(sqlite3_stmt *pStmt, int index, int64_t data);

int sqlite3_bind_null(sqlite3_stmt *pStmt, int index);

int sqlite3_bind_text16(sqlite3_stmt *pStmt, int index, char *data,
int length, void *destructor);

void *sqlite3_column_blob(sqlite3_stmt *pStmt, int iCol);

double sqlite3_column_double(sqlite3_stmt *pStmt, int iCol);

int64_t sqlite3_column_int64(sqlite3_stmt *pStmt, int iCol);

void *sqlite3_column_text16(sqlite3_stmt *pStmt, int iCol);

int sqlite3_column_bytes(sqlite3_stmt *pStmt, int iCol);

int sqlite3_column_bytes16(sqlite3_stmt *pStmt, int iCol);

int sqlite3_column_type(sqlite3_stmt *pStmt, int iCol);


int sqlite3session_create(
sqlite3 *db, /* Database handle */
const char *zDb, /* Name of db (e.g. "main") */
sqlite3_session **ppSession /* OUT: New session object */
);

int sqlite3session_attach(
sqlite3_session *pSession, /* Session object */
const char *zTab /* Table name or NULL for all */
);

int sqlite3session_changeset(
sqlite3_session *pSession, /* Session object */
int *pnChangeset, /* OUT: Size of buffer at *ppChangeset */
void **ppChangeset /* OUT: Buffer containing changeset */
);

int sqlite3changeset_start(
sqlite3_changeset_iter **pp, /* OUT: New changeset iterator handle */
int nChangeset, /* Size of changeset blob in bytes */
void *pChangeset /* Pointer to blob containing changeset */
);

int sqlite3changeset_op(
sqlite3_changeset_iter *pIter, /* Iterator object */
const char **pzTab, /* OUT: Pointer to table name */
int *pnCol, /* OUT: Number of columns in table */
int *pOp, /* OUT: SQLITE_INSERT, DELETE or UPDATE */
int *pbIndirect /* OUT: True for an 'indirect' change */
);

int sqlite3changeset_next(sqlite3_changeset_iter *pIter);

int sqlite3changeset_finalize(sqlite3_changeset_iter *pIter);

void sqlite3session_delete(sqlite3_session *pSession);
7 changes: 7 additions & 0 deletions internal/PowerSyncKotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ kotlin {
api(project(":core"))
implementation(libs.ktor.client.logging)
}

all {
languageSettings {
optIn("kotlinx.cinterop.ExperimentalForeignApi")
optIn("com.powersync.ExperimentalPowerSyncAPI")
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.powersync.pool

import androidx.sqlite.SQLiteStatement
import com.powersync.db.driver.SQLiteConnectionLease
import com.powersync.sqlite.Database

internal class RawConnectionLease(
lease: SwiftLeaseAdapter,
) : SQLiteConnectionLease {
private var isCompleted = false

private var db = Database(lease.pointer)

private fun checkNotCompleted() {
check(!isCompleted) { "Connection lease already closed" }
}

override suspend fun isInTransaction(): Boolean = isInTransactionSync()

override fun isInTransactionSync(): Boolean {
checkNotCompleted()
return db.inTransaction()
}

override suspend fun <R> usePrepared(
sql: String,
block: (SQLiteStatement) -> R,
): R = usePreparedSync(sql, block)

override fun <R> usePreparedSync(
sql: String,
block: (SQLiteStatement) -> R,
): R {
checkNotCompleted()
return db.prepare(sql).use(block)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package com.powersync.pool

import cnames.structs.sqlite3
import cnames.structs.sqlite3_changeset_iter
import cnames.structs.sqlite3_session
import com.powersync.PowerSyncException
import com.powersync.PowerSyncResult
import com.powersync.db.runWrapped
import com.powersync.internal.sqlite3.sqlite3_free
import com.powersync.internal.sqlite3.sqlite3changeset_finalize
import com.powersync.internal.sqlite3.sqlite3changeset_next
import com.powersync.internal.sqlite3.sqlite3changeset_op
import com.powersync.internal.sqlite3.sqlite3changeset_start
import com.powersync.internal.sqlite3.sqlite3session_attach
import com.powersync.internal.sqlite3.sqlite3session_changeset
import com.powersync.internal.sqlite3.sqlite3session_create
import com.powersync.internal.sqlite3.sqlite3session_delete
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.COpaquePointerVar
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.CPointerVar
import kotlinx.cinterop.IntVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.toKString
import kotlinx.cinterop.value

public data class SessionResult(
val blockResult: PowerSyncResult,
val affectedTables: Set<String>,
)

/**
* We typically have a few options for table update hooks:
* 1.) Registering a hook with SQLite
* 2.) Using our Rust core to register update hooks
* 3.) Receiving updates from an external API
*
* In some cases, particularly in the case of GRDB, none of these options are viable.
* GRDB dynamically registers (and unregisters) its own update hooks and its update hook logic
* does not report changes for operations made outside of its own APIs.
*
* 1.) We can't register our own hooks since GRDB might override it or our hook could conflict with GRDB's
* 2.) We can't register hooks due to above
* 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.
*
* This function provides an alternative method of obtaining table changes by using SQLite sessions.
* https://www.sqlite.org/sessionintro.html
*
* We start a session, execute a block of code, and then extract the changeset from the session.
* We then parse the changeset to extract the table names that were modified.
* This approach is more heavyweight than using update hooks, but it works in scenarios where
* update hooks are not currently feasible.
*/
@Throws(PowerSyncException::class)
public fun withSession(
db: CPointer<sqlite3>,
block: () -> PowerSyncResult,
): SessionResult =
runWrapped {
memScoped {
val sessionPtr = alloc<CPointerVar<sqlite3_session>>()

val rc =
sqlite3session_create(
db,
"main",
sessionPtr.ptr,
).checkResult("Could not create SQLite session")

val session =
sessionPtr.value ?: throw PowerSyncException(
"Could not create SQLite session",
cause = Error(),
)

try {
// Attach all tables to track changes
sqlite3session_attach(
session,
null,
).checkResult("Could not attach all tables to session") // null means all tables

// Execute the block where changes happen
val result = block()

// Get the changeset
val changesetSizePtr = alloc<IntVar>()
val changesetPtr = alloc<COpaquePointerVar>()

sqlite3session_changeset(
session,
changesetSizePtr.ptr,
changesetPtr.ptr,
).checkResult("Could not get changeset from session")

val changesetSize = changesetSizePtr.value
val changeset = changesetPtr.value

if (changesetSize == 0 || changeset == null) {
return@memScoped SessionResult(
result,
affectedTables = emptySet(),
)
}

// Parse the changeset to extract table names
val changedTables = mutableSetOf<String>()
val iterPtr = alloc<CPointerVar<sqlite3_changeset_iter>>()

sqlite3changeset_start(
iterPtr.ptr,
changesetSize,
changeset,
).checkResult("Could not start changeset iterator")

val iter = iterPtr.value

if (iter == null) {
return@memScoped SessionResult(
result,
affectedTables = emptySet(),
)
}

try {
// Iterate through all changes
while (sqlite3changeset_next(iter) == 100) {
val tableNamePtr = alloc<CPointerVar<ByteVar>>()
val nColPtr = alloc<IntVar>()
val opPtr = alloc<IntVar>()
val indirectPtr = alloc<IntVar>()

val opRc =
sqlite3changeset_op(
iter,
tableNamePtr.ptr,
nColPtr.ptr,
opPtr.ptr,
indirectPtr.ptr,
)

if (opRc == 0) {
val tableNameCPtr = tableNamePtr.value
if (tableNameCPtr != null) {
val tableName = tableNameCPtr.toKString()
changedTables.add(tableName)
}
}
}
} finally {
sqlite3changeset_finalize(iter)
// Free the changeset memory
sqlite3_free(changeset)
}

return@memScoped SessionResult(
result,
affectedTables = changedTables.toSet(),
)
} finally {
// Clean up the session
sqlite3session_delete(session)
}
}
}

private fun Int.checkResult(message: String) {
if (this != 0) {
throw PowerSyncException("SQLite error code: $this", cause = Error(message))
}
}
Loading