Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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: 4 additions & 0 deletions PowerSync/PowerSync.Common/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PowerSync.Common Changelog

## 0.1.3-dev.1

- Add support for loading custom SQLite extensions via `MDSQLiteOptions.Extensions`.

## 0.1.2

- Add support for MacCatalyst.
Expand Down
79 changes: 42 additions & 37 deletions PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private RequiredMDSQLiteOptions ResolveMDSQLiteOptions(MDSQLiteOptions? options)
LockTimeoutMs = options?.LockTimeoutMs ?? defaults.LockTimeoutMs,
EncryptionKey = options?.EncryptionKey ?? defaults.EncryptionKey,
Extensions = options?.Extensions ?? defaults.Extensions,
LoadPowerSyncExtension = options?.LoadPowerSyncExtension ?? defaults.LoadPowerSyncExtension,
ReadPoolSize = options?.ReadPoolSize ?? defaults.ReadPoolSize,
};
}
Expand Down Expand Up @@ -105,7 +106,7 @@ private async Task Init()
}
return readConnection;
};
readPool = new MDSQLiteConnectionPool(resolvedOptions, readConnectionFactory);
readPool = new MDSQLiteConnectionPool(resolvedOptions.ReadPoolSize, readConnectionFactory);
await readPool.Init();

// Register TablesUpdated listener
Expand All @@ -125,10 +126,25 @@ private async Task Init()
protected async Task<MDSQLiteConnection> OpenConnection(string dbFilename)
{
var db = OpenDatabase(dbFilename);
LoadExtension(db);
LoadExtensions(db);

var connection = new MDSQLiteConnection(new MDSQLiteConnectionOptions(db));
await connection.Execute("SELECT powersync_init()");
try
{
await connection.Execute("SELECT powersync_init()");
}
catch (SqliteException ex)
{
// SQLite will throw a very unhelpful "SQLite Error 1: 'The specified
// module could not be found.'" error if uncaught.
throw new SqliteException(
"Failed to initialize PowerSync: powersync_init() is not registered. " +
"Ensure the PowerSync core SQLite extension is loaded. Either set " +
"MDSQLiteOptions.LoadPowerSyncExtension to true (default), or supply " +
"a PowerSync-compatible extension via MDSQLiteOptions.Extensions.",
ex.SqliteErrorCode,
ex.SqliteExtendedErrorCode);
}

return connection;
}
Expand All @@ -141,11 +157,28 @@ private static SqliteConnection OpenDatabase(string dbFilename)
return connection;
}

protected virtual void LoadExtension(SqliteConnection db)
protected virtual void LoadExtensions(SqliteConnection db)
{
string extensionPath = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
db.EnableExtensions(true);
db.LoadExtension(extensionPath, "sqlite3_powersync_init");
if (resolvedOptions.LoadPowerSyncExtension)
{
LoadDefaultPowerSyncExtension(db);
}
foreach (var extension in resolvedOptions.Extensions)
{
db.LoadExtension(extension.Path, extension.EntryPoint);
}
}

/// <summary>
/// Loads the bundled PowerSync core SQLite extension. Override on
/// platform-specific adapters (e.g. MAUI iOS/Android) where the native library
/// lives outside the desktop runtime path.
/// </summary>
protected virtual void LoadDefaultPowerSyncExtension(SqliteConnection db)
{
var path = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
db.LoadExtension(path, "sqlite3_powersync_init");
}

public async Task Close()
Expand Down Expand Up @@ -301,18 +334,16 @@ await readPool.LeaseAll(async (connections) =>

class MDSQLiteConnectionPool
{
private readonly RequiredMDSQLiteOptions _options;
private readonly Channel<MDSQLiteConnection> _channel;
private readonly int _poolSize;
private readonly Func<Task<MDSQLiteConnection>> _connectionFactory;

private readonly Task _initialized;

public MDSQLiteConnectionPool(RequiredMDSQLiteOptions options, Func<Task<MDSQLiteConnection>> connectionFactory)
public MDSQLiteConnectionPool(int poolSize, Func<Task<MDSQLiteConnection>> connectionFactory)
{
_options = options;
_channel = Channel.CreateBounded<MDSQLiteConnection>(options.ReadPoolSize);
_poolSize = options.ReadPoolSize;
_channel = Channel.CreateBounded<MDSQLiteConnection>(poolSize);
_poolSize = poolSize;
_connectionFactory = connectionFactory;
_initialized = Initialize();
}
Expand Down Expand Up @@ -364,32 +395,6 @@ public async Task LeaseAll(Func<List<MDSQLiteConnection>, Task> callback)
}
}

private async Task<MDSQLiteConnection> OpenConnection(string dbFilename)
{
var db = OpenDatabase(dbFilename);
LoadExtension(db);

var connection = new MDSQLiteConnection(new MDSQLiteConnectionOptions(db));
await connection.Execute("SELECT powersync_init()");

return connection;
}

private static SqliteConnection OpenDatabase(string dbFilename)
{
string connectionString = $"Data Source={dbFilename};Pooling=False;";
var connection = new SqliteConnection(connectionString);
connection.Open();
return connection;
}

private void LoadExtension(SqliteConnection db)
{
string extensionPath = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
db.EnableExtensions(true);
db.LoadExtension(extensionPath, "sqlite3_powersync_init");
}

public async Task Close()
{
await LeaseAll((connections) =>
Expand Down
18 changes: 17 additions & 1 deletion PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,23 @@ public class MDSQLiteOptions
public int? CacheSizeKb { get; set; }

/// <summary>
/// Load extensions using the path and entryPoint.
/// Additional SQLite extensions to load on every connection, in order. Defaults
/// to an empty list. The bundled PowerSync core extension is loaded separately
/// and controlled by <see cref="LoadPowerSyncExtension"/> — do not include it
/// here.
/// </summary>
public SqliteExtension[]? Extensions { get; set; }

/// <summary>
/// Whether to load the bundled PowerSync core SQLite extension on every
/// connection. Defaults to true and should remain true for normal use — the
/// rest of the library relies on the SQL functions and virtual tables it
/// registers (e.g. <c>powersync_init()</c>). Set to false only if you are
/// supplying an equivalent PowerSync-compatible extension via
/// <see cref="Extensions"/>.
/// </summary>
public bool? LoadPowerSyncExtension { get; set; }

/// <summary>
/// The number of MDSQLiteConnection objects to create for the read pool.
/// </summary>
Expand All @@ -121,6 +134,7 @@ public class RequiredMDSQLiteOptions : MDSQLiteOptions
LockTimeoutMs = 30000,
EncryptionKey = null,
Extensions = [],
LoadPowerSyncExtension = true,
ReadPoolSize = 5,
};

Expand All @@ -140,5 +154,7 @@ public class RequiredMDSQLiteOptions : MDSQLiteOptions

public new SqliteExtension[] Extensions { get; set; } = null!;

public new bool LoadPowerSyncExtension { get; set; }

public new int ReadPoolSize { get; set; }
}
4 changes: 4 additions & 0 deletions PowerSync/PowerSync.Maui/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# PowerSync.Maui Changelog

## 0.1.3-dev.1

- Upstream PowerSync.Common version bump (See Powersync.Common changelog 0.1.3 for more information)

## 0.1.2

- Add support for MacCatalyst.
Expand Down
14 changes: 9 additions & 5 deletions PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,24 @@ public MAUISQLiteAdapter(MDSQLiteAdapterOptions options) : base(options)
{
}

protected override void LoadExtension(SqliteConnection db)
// The bundled PowerSync extension lives in a platform-specific location on
// iOS/MacCatalyst/Android — the desktop runtime path used by the base class
// does not resolve to it. Override only the PowerSync-extension load hook;
// user-supplied custom extensions still flow through MDSQLiteAdapter.LoadExtensions
// unchanged, so consumers can freely combine the bundled extension (via the
// LoadPowerSyncExtension flag) with their own.
protected override void LoadDefaultPowerSyncExtension(SqliteConnection db)
{
db.EnableExtensions(true);

#if IOS || MACCATALYST
LoadExtensionApple(db);
#elif ANDROID
db.LoadExtension("libpowersync");
#else
base.LoadExtension(db);
base.LoadDefaultPowerSyncExtension(db);
#endif
}

private void LoadExtensionApple(SqliteConnection db)
private static void LoadExtensionApple(SqliteConnection db)
{
#if IOS || MACCATALYST
var bundlePath = Foundation.NSBundle.FromIdentifier("co.powersync.sqlitecore")?.BundlePath;
Expand Down
7 changes: 4 additions & 3 deletions Tests/PowerSync/PowerSync.Common.Tests/EventStreamTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,24 +217,25 @@ public async Task EventManager_ShouldNotReceiveEventsAfterDeregistering()

var cts = new CancellationTokenSource();
var listener = stream.ListenAsync(cts.Token);
var tcs = new TaskCompletionSource<bool>();
var sem = new SemaphoreSlim(0);
int eventCount = 0;

_ = Task.Run(async () =>
{
sem.Release();
tcs.SetResult(true);
await foreach (var evt in listener)
{
eventCount++;
sem.Release();
}
}, cts.Token);
Assert.True(await sem.WaitAsync(100));
await tcs.Task;

Assert.True(manager.Deregister<string>());

Assert.False(manager.TryEmit("invalid"));
Assert.False(await sem.WaitAsync(100));
Assert.False(await sem.WaitAsync(500));

// Cleanup
cts.Cancel();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
namespace PowerSync.Common.Tests.MDSQLite;

using Microsoft.Data.Sqlite;

using PowerSync.Common.Client;
using PowerSync.Common.MDSQLite;
using PowerSync.Common.Tests.Utils;
using PowerSync.Common.Utils;

/// <summary>
/// dotnet test -v n --framework net8.0 --filter "MDSQLiteAdapterTests"
/// </summary>
[Collection("MDSQLiteAdapterTests")]
public class MDSQLiteAdapterTests
{
private class AssetResult
{
public string id { get; set; } = "";
public string description { get; set; } = "";
public string? make { get; set; }
}

[Fact]
public async Task DisablingCoreExtensionPreventsPowerSyncFromLoading()
{
var dbName = $"MDSQLiteAdapter-{Guid.NewGuid():N}.db";
var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
{
Database = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions
{
DbFilename = dbName,
SqliteOptions = new MDSQLiteOptions
{
LoadPowerSyncExtension = false,
Extensions = [],
},
}),
Schema = TestSchema.AppSchema,
});

try
{
// Without the PowerSync extension, `powersync_init()` is not a registered function.
await Assert.ThrowsAsync<SqliteException>(async () => await db.Init());
}
finally
{
try { await db.Close(); } catch { /* expected — init failed */ }
DatabaseUtils.CleanDb(dbName);
}
}

[Fact]
public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath()
{
var dbName = $"MDSQLiteAdapter-{Guid.NewGuid():N}.db";
var sourcePath = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
var customPath = Path.Combine(
Path.GetTempPath(),
$"powersync-ext-copy-{Guid.NewGuid():N}{Path.GetExtension(sourcePath)}"
);
File.Copy(sourcePath, customPath, overwrite: true);

var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
{
Database = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions
{
DbFilename = dbName,
SqliteOptions = new MDSQLiteOptions
{
LoadPowerSyncExtension = false,
Extensions = [
new SqliteExtension { Path = customPath, EntryPoint = "sqlite3_powersync_init" },
],
},
}),
Schema = TestSchema.AppSchema,
});

try
{
await db.Init();

var id = await TestUtils.InsertRandomAsset(db);
var rows = await db.GetAll<AssetResult>("SELECT id, description, make FROM assets");
Assert.Single(rows);
Assert.Equal(id, rows[0].id);
}
finally
{
await db.Close();
DatabaseUtils.CleanDb(dbName);
try { File.Delete(customPath); } catch { /* best-effort cleanup */ }
}
}
}
Loading