From 91a3f8f7ee99f4c7d832b7967ff9709c17a5c137 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 12:34:49 +0200 Subject: [PATCH 1/7] Load custom SQLite extensions --- PowerSync/PowerSync.Common/CHANGELOG.md | 1 + .../MDSQLite/MDSQLiteAdapter.cs | 20 +++-- .../MDSQLite/MDSQLiteOptions.cs | 25 +++++- .../SQLite/MAUISQLiteAdapter.cs | 35 +++++++- .../MDSQLite/MDSQLiteAdapterTests.cs | 89 +++++++++++++++++++ 5 files changed, 157 insertions(+), 13 deletions(-) create mode 100644 Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs diff --git a/PowerSync/PowerSync.Common/CHANGELOG.md b/PowerSync/PowerSync.Common/CHANGELOG.md index f786650..af76335 100644 --- a/PowerSync/PowerSync.Common/CHANGELOG.md +++ b/PowerSync/PowerSync.Common/CHANGELOG.md @@ -6,6 +6,7 @@ - Add support for .NET 9.0. Supported targets now also include `net9.0`, `net9.0-android`, `net9.0-ios`, and `net9.0-maccatalyst`. - Update the PowerSync SQLite core extension to 0.4.13. - Add support for offline-first file attachments via `AttachmentQueue`. See `Attachments/README.md`. +- Add support for loading custom SQLite extensions via `MDSqliteOptions.Extensions`. ## 0.1.1 diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs index 0b83a2d..6ad1a2f 100644 --- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs +++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs @@ -125,7 +125,7 @@ private async Task Init() protected async Task OpenConnection(string dbFilename) { var db = OpenDatabase(dbFilename); - LoadExtension(db); + LoadExtensions(db); var connection = new MDSQLiteConnection(new MDSQLiteConnectionOptions(db)); await connection.Execute("SELECT powersync_init()"); @@ -141,11 +141,13 @@ 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"); + foreach (var extension in resolvedOptions.Extensions) + { + db.LoadExtension(extension.Path, extension.EntryPoint); + } } public async Task Close() @@ -367,7 +369,7 @@ public async Task LeaseAll(Func, Task> callback) private async Task OpenConnection(string dbFilename) { var db = OpenDatabase(dbFilename); - LoadExtension(db); + LoadExtensions(db); var connection = new MDSQLiteConnection(new MDSQLiteConnectionOptions(db)); await connection.Execute("SELECT powersync_init()"); @@ -383,11 +385,13 @@ private static SqliteConnection OpenDatabase(string dbFilename) return connection; } - private void LoadExtension(SqliteConnection db) + private void LoadExtensions(SqliteConnection db) { - string extensionPath = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); db.EnableExtensions(true); - db.LoadExtension(extensionPath, "sqlite3_powersync_init"); + foreach (var extension in _options.Extensions) + { + db.LoadExtension(extension.Path, extension.EntryPoint); + } } public async Task Close() diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs index 845b57c..69265b0 100644 --- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs +++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs @@ -1,5 +1,7 @@ namespace PowerSync.Common.MDSQLite; +using PowerSync.Common.Utils; + public sealed class TemporaryStorageOption { public static readonly TemporaryStorageOption MEMORY = new("memory"); @@ -55,10 +57,27 @@ public sealed class SqliteSynchronous public class SqliteExtension { + public static SqliteExtension DEFAULT_POWERSYNC_EXTENSION = new() + { + Path = TryResolveDefaultPowerSyncExtensionPath(), + EntryPoint = "sqlite3_powersync_init", + }; + public static SqliteExtension[] DEFAULT_POWERSYNC_EXTENSIONS = [DEFAULT_POWERSYNC_EXTENSION]; + public string Path { get; set; } = string.Empty; public string? EntryPoint { get; set; } + + // PowerSyncPathResolver only knows about desktop RIDs and throws on iOS/Android. + // Swallow that here so this static field can be referenced safely on mobile — + // platform-aware adapters (e.g. MAUI) intercept the sentinel before reading Path. + private static string TryResolveDefaultPowerSyncExtensionPath() + { + try { return PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); } + catch (PlatformNotSupportedException) { return string.Empty; } + } } + public class MDSQLiteOptions { /// @@ -99,7 +118,9 @@ public class MDSQLiteOptions public int? CacheSizeKb { get; set; } /// - /// Load extensions using the path and entryPoint. + /// Load SQLite extensions using the path and entryPoint. Defaults to + /// SqliteExtension.DEFAULT_POWERSYNC_EXTENSIONS. Remember to re-add + /// the DEFAULT_POWERSYNC_EXTENSION if modifying the list of extensions. /// public SqliteExtension[]? Extensions { get; set; } @@ -120,7 +141,7 @@ public class RequiredMDSQLiteOptions : MDSQLiteOptions TemporaryStorage = TemporaryStorageOption.MEMORY, LockTimeoutMs = 30000, EncryptionKey = null, - Extensions = [], + Extensions = SqliteExtension.DEFAULT_POWERSYNC_EXTENSIONS, ReadPoolSize = 5, }; diff --git a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs index 68a6b25..dcbd194 100644 --- a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs +++ b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs @@ -15,20 +15,49 @@ public MAUISQLiteAdapter(MDSQLiteAdapterOptions options) : base(options) { } - protected override void LoadExtension(SqliteConnection db) + protected override void LoadExtensions(SqliteConnection db) { db.EnableExtensions(true); + // The default PowerSync extension's path is resolved for desktop runtimes and + // does not apply on iOS/MacCatalyst/Android, where the native library lives + // in a platform-specific location. If the user didn't supply any extensions, + // load the platform-correct default. Otherwise honor their list — but still + // intercept the DEFAULT_POWERSYNC_EXTENSION sentinel so consumers can mix + // the bundled PowerSync extension with their own custom extensions. + var userExtensions = options.SqliteOptions?.Extensions; + if (userExtensions == null) + { + LoadDefaultPowerSyncExtension(db); + return; + } + + foreach (var extension in userExtensions) + { + if (ReferenceEquals(extension, SqliteExtension.DEFAULT_POWERSYNC_EXTENSION)) + { + LoadDefaultPowerSyncExtension(db); + } + else + { + db.LoadExtension(extension.Path, extension.EntryPoint); + } + } + } + + private static void LoadDefaultPowerSyncExtension(SqliteConnection db) + { #if IOS || MACCATALYST LoadExtensionApple(db); #elif ANDROID db.LoadExtension("libpowersync"); #else - base.LoadExtension(db); + var defaultExtension = SqliteExtension.DEFAULT_POWERSYNC_EXTENSION; + db.LoadExtension(defaultExtension.Path, defaultExtension.EntryPoint); #endif } - private void LoadExtensionApple(SqliteConnection db) + private static void LoadExtensionApple(SqliteConnection db) { #if IOS || MACCATALYST var bundlePath = Foundation.NSBundle.FromIdentifier("co.powersync.sqlitecore")?.BundlePath; diff --git a/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs new file mode 100644 index 0000000..e1f8ca2 --- /dev/null +++ b/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs @@ -0,0 +1,89 @@ +namespace PowerSync.Common.Tests.MDSQLite; + +using Microsoft.Data.Sqlite; + +using PowerSync.Common.Client; +using PowerSync.Common.MDSQLite; +using PowerSync.Common.Tests.Utils; + +/// +/// dotnet test -v n --framework net8.0 --filter "MDSQLiteAdapterTests" +/// +[Collection("MDSQLiteAdapterTests")] +public class MDSQLiteAdapterTests +{ + private class AssetResult + { + public string id { get; set; } = ""; + public string description { get; set; } = ""; + public string? make { get; set; } + } + + private static PowerSyncDatabase BuildDbWithExtensions(string dbFilename, SqliteExtension[] extensions) + { + return new PowerSyncDatabase(new PowerSyncDatabaseOptions + { + Database = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions + { + DbFilename = dbFilename, + SqliteOptions = new MDSQLiteOptions { Extensions = extensions }, + }), + Schema = TestSchema.AppSchema, + }); + } + + private static string CopyDefaultExtensionToTempPath() + { + var sourcePath = SqliteExtension.DEFAULT_POWERSYNC_EXTENSION.Path; + var tempPath = Path.Combine( + Path.GetTempPath(), + $"powersync-ext-copy-{Guid.NewGuid():N}{Path.GetExtension(sourcePath)}" + ); + File.Copy(sourcePath, tempPath, overwrite: true); + return tempPath; + } + + [Fact] + public async Task EmptyExtensionsArrayDoesNotLoadPowerSync() + { + var name = $"MDSQLiteAdapter-ext-empty-{Guid.NewGuid():N}.db"; + var db = BuildDbWithExtensions(name, []); + + try + { + // Without the PowerSync extension, `powersync_init()` is not a registered function. + await Assert.ThrowsAsync(async () => await db.Init()); + } + finally + { + try { await db.Close(); } catch { /* expected — init failed */ } + DatabaseUtils.CleanDb(name); + } + } + + [Fact] + public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath() + { + var name = $"MDSQLiteAdapter-ext-custom-{Guid.NewGuid():N}.db"; + var customPath = CopyDefaultExtensionToTempPath(); + var db = BuildDbWithExtensions(name, [ + new SqliteExtension { Path = customPath, EntryPoint = "sqlite3_powersync_init" }, + ]); + + try + { + await db.Init(); + + var id = await TestUtils.InsertRandomAsset(db); + var rows = await db.GetAll("SELECT id, description, make FROM assets"); + Assert.Single(rows); + Assert.Equal(id, rows[0].id); + } + finally + { + await db.Close(); + DatabaseUtils.CleanDb(name); + try { File.Delete(customPath); } catch { /* best-effort cleanup */ } + } + } +} From e910fc416e0517c79af8f2e2f8c42c16ad7faa30 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 13:02:04 +0200 Subject: [PATCH 2/7] Better loading method and API + fixes --- .../MDSQLite/MDSQLiteAdapter.cs | 54 ++++++--------- .../MDSQLite/MDSQLiteOptions.cs | 41 +++++------- .../SQLite/MAUISQLiteAdapter.cs | 41 +++--------- .../MDSQLite/MDSQLiteAdapterTests.cs | 65 ++++++++++--------- 4 files changed, 82 insertions(+), 119 deletions(-) diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs index 6ad1a2f..685e7f4 100644 --- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs +++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs @@ -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, }; } @@ -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 @@ -144,12 +145,27 @@ private static SqliteConnection OpenDatabase(string dbFilename) protected virtual void LoadExtensions(SqliteConnection db) { db.EnableExtensions(true); + if (resolvedOptions.LoadPowerSyncExtension) + { + LoadDefaultPowerSyncExtension(db); + } foreach (var extension in resolvedOptions.Extensions) { db.LoadExtension(extension.Path, extension.EntryPoint); } } + /// + /// 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. + /// + protected virtual void LoadDefaultPowerSyncExtension(SqliteConnection db) + { + var path = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); + db.LoadExtension(path, "sqlite3_powersync_init"); + } + public async Task Close() { tablesUpdatedCts?.Cancel(); @@ -303,18 +319,16 @@ await readPool.LeaseAll(async (connections) => class MDSQLiteConnectionPool { - private readonly RequiredMDSQLiteOptions _options; private readonly Channel _channel; private readonly int _poolSize; private readonly Func> _connectionFactory; private readonly Task _initialized; - public MDSQLiteConnectionPool(RequiredMDSQLiteOptions options, Func> connectionFactory) + public MDSQLiteConnectionPool(int poolSize, Func> connectionFactory) { - _options = options; - _channel = Channel.CreateBounded(options.ReadPoolSize); - _poolSize = options.ReadPoolSize; + _channel = Channel.CreateBounded(poolSize); + _poolSize = poolSize; _connectionFactory = connectionFactory; _initialized = Initialize(); } @@ -366,34 +380,6 @@ public async Task LeaseAll(Func, Task> callback) } } - private async Task OpenConnection(string dbFilename) - { - var db = OpenDatabase(dbFilename); - LoadExtensions(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 LoadExtensions(SqliteConnection db) - { - db.EnableExtensions(true); - foreach (var extension in _options.Extensions) - { - db.LoadExtension(extension.Path, extension.EntryPoint); - } - } - public async Task Close() { await LeaseAll((connections) => diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs index 69265b0..5ccf11b 100644 --- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs +++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs @@ -1,7 +1,5 @@ namespace PowerSync.Common.MDSQLite; -using PowerSync.Common.Utils; - public sealed class TemporaryStorageOption { public static readonly TemporaryStorageOption MEMORY = new("memory"); @@ -57,27 +55,10 @@ public sealed class SqliteSynchronous public class SqliteExtension { - public static SqliteExtension DEFAULT_POWERSYNC_EXTENSION = new() - { - Path = TryResolveDefaultPowerSyncExtensionPath(), - EntryPoint = "sqlite3_powersync_init", - }; - public static SqliteExtension[] DEFAULT_POWERSYNC_EXTENSIONS = [DEFAULT_POWERSYNC_EXTENSION]; - public string Path { get; set; } = string.Empty; public string? EntryPoint { get; set; } - - // PowerSyncPathResolver only knows about desktop RIDs and throws on iOS/Android. - // Swallow that here so this static field can be referenced safely on mobile — - // platform-aware adapters (e.g. MAUI) intercept the sentinel before reading Path. - private static string TryResolveDefaultPowerSyncExtensionPath() - { - try { return PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); } - catch (PlatformNotSupportedException) { return string.Empty; } - } } - public class MDSQLiteOptions { /// @@ -118,12 +99,23 @@ public class MDSQLiteOptions public int? CacheSizeKb { get; set; } /// - /// Load SQLite extensions using the path and entryPoint. Defaults to - /// SqliteExtension.DEFAULT_POWERSYNC_EXTENSIONS. Remember to re-add - /// the DEFAULT_POWERSYNC_EXTENSION if modifying the list of extensions. + /// 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 — do not include it + /// here. /// public SqliteExtension[]? Extensions { get; set; } + /// + /// 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. powersync_init()). Set to false only if you are + /// supplying an equivalent PowerSync-compatible extension via + /// . + /// + public bool? LoadPowerSyncExtension { get; set; } + /// /// The number of MDSQLiteConnection objects to create for the read pool. /// @@ -141,7 +133,8 @@ public class RequiredMDSQLiteOptions : MDSQLiteOptions TemporaryStorage = TemporaryStorageOption.MEMORY, LockTimeoutMs = 30000, EncryptionKey = null, - Extensions = SqliteExtension.DEFAULT_POWERSYNC_EXTENSIONS, + Extensions = [], + LoadPowerSyncExtension = true, ReadPoolSize = 5, }; @@ -161,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; } } diff --git a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs index dcbd194..0d6e399 100644 --- a/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs +++ b/PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs @@ -15,45 +15,20 @@ public MAUISQLiteAdapter(MDSQLiteAdapterOptions options) : base(options) { } - protected override void LoadExtensions(SqliteConnection db) - { - db.EnableExtensions(true); - - // The default PowerSync extension's path is resolved for desktop runtimes and - // does not apply on iOS/MacCatalyst/Android, where the native library lives - // in a platform-specific location. If the user didn't supply any extensions, - // load the platform-correct default. Otherwise honor their list — but still - // intercept the DEFAULT_POWERSYNC_EXTENSION sentinel so consumers can mix - // the bundled PowerSync extension with their own custom extensions. - var userExtensions = options.SqliteOptions?.Extensions; - if (userExtensions == null) - { - LoadDefaultPowerSyncExtension(db); - return; - } - - foreach (var extension in userExtensions) - { - if (ReferenceEquals(extension, SqliteExtension.DEFAULT_POWERSYNC_EXTENSION)) - { - LoadDefaultPowerSyncExtension(db); - } - else - { - db.LoadExtension(extension.Path, extension.EntryPoint); - } - } - } - - private static void LoadDefaultPowerSyncExtension(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) { #if IOS || MACCATALYST LoadExtensionApple(db); #elif ANDROID db.LoadExtension("libpowersync"); #else - var defaultExtension = SqliteExtension.DEFAULT_POWERSYNC_EXTENSION; - db.LoadExtension(defaultExtension.Path, defaultExtension.EntryPoint); + base.LoadDefaultPowerSyncExtension(db); #endif } diff --git a/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs index e1f8ca2..8789b7b 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs @@ -5,6 +5,7 @@ namespace PowerSync.Common.Tests.MDSQLite; using PowerSync.Common.Client; using PowerSync.Common.MDSQLite; using PowerSync.Common.Tests.Utils; +using PowerSync.Common.Utils; /// /// dotnet test -v n --framework net8.0 --filter "MDSQLiteAdapterTests" @@ -19,35 +20,23 @@ private class AssetResult public string? make { get; set; } } - private static PowerSyncDatabase BuildDbWithExtensions(string dbFilename, SqliteExtension[] extensions) + [Fact] + public async Task DisablingCoreExtensionPreventsPowerSyncFromLoading() { - return new PowerSyncDatabase(new PowerSyncDatabaseOptions + var dbName = $"MDSQLiteAdapter-{Guid.NewGuid():N}.db"; + var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions { Database = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions { - DbFilename = dbFilename, - SqliteOptions = new MDSQLiteOptions { Extensions = extensions }, + DbFilename = dbName, + SqliteOptions = new MDSQLiteOptions + { + LoadPowerSyncExtension = false, + Extensions = [], + }, }), Schema = TestSchema.AppSchema, }); - } - - private static string CopyDefaultExtensionToTempPath() - { - var sourcePath = SqliteExtension.DEFAULT_POWERSYNC_EXTENSION.Path; - var tempPath = Path.Combine( - Path.GetTempPath(), - $"powersync-ext-copy-{Guid.NewGuid():N}{Path.GetExtension(sourcePath)}" - ); - File.Copy(sourcePath, tempPath, overwrite: true); - return tempPath; - } - - [Fact] - public async Task EmptyExtensionsArrayDoesNotLoadPowerSync() - { - var name = $"MDSQLiteAdapter-ext-empty-{Guid.NewGuid():N}.db"; - var db = BuildDbWithExtensions(name, []); try { @@ -57,18 +46,36 @@ public async Task EmptyExtensionsArrayDoesNotLoadPowerSync() finally { try { await db.Close(); } catch { /* expected — init failed */ } - DatabaseUtils.CleanDb(name); + DatabaseUtils.CleanDb(dbName); } } [Fact] public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath() { - var name = $"MDSQLiteAdapter-ext-custom-{Guid.NewGuid():N}.db"; - var customPath = CopyDefaultExtensionToTempPath(); - var db = BuildDbWithExtensions(name, [ - new SqliteExtension { Path = customPath, EntryPoint = "sqlite3_powersync_init" }, - ]); + 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 { @@ -82,7 +89,7 @@ public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath() finally { await db.Close(); - DatabaseUtils.CleanDb(name); + DatabaseUtils.CleanDb(dbName); try { File.Delete(customPath); } catch { /* best-effort cleanup */ } } } From a5edfe225a46b858b9430f99ff4cfd63718bd041 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 13:29:50 +0200 Subject: [PATCH 3/7] Fix typo + rethrow unhelpful error --- PowerSync/PowerSync.Common/CHANGELOG.md | 2 +- .../MDSQLite/MDSQLiteAdapter.cs | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/PowerSync/PowerSync.Common/CHANGELOG.md b/PowerSync/PowerSync.Common/CHANGELOG.md index af76335..e524939 100644 --- a/PowerSync/PowerSync.Common/CHANGELOG.md +++ b/PowerSync/PowerSync.Common/CHANGELOG.md @@ -6,7 +6,7 @@ - Add support for .NET 9.0. Supported targets now also include `net9.0`, `net9.0-android`, `net9.0-ios`, and `net9.0-maccatalyst`. - Update the PowerSync SQLite core extension to 0.4.13. - Add support for offline-first file attachments via `AttachmentQueue`. See `Attachments/README.md`. -- Add support for loading custom SQLite extensions via `MDSqliteOptions.Extensions`. +- Add support for loading custom SQLite extensions via `MDSQLiteOptions.Extensions`. ## 0.1.1 diff --git a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs index 685e7f4..5ae03cf 100644 --- a/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs +++ b/PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs @@ -129,7 +129,22 @@ protected async Task OpenConnection(string dbFilename) 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; } From ce909524ee61e8392b7ec861462d7502ba5ca69f Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 13:35:49 +0200 Subject: [PATCH 4/7] Changelog fix --- PowerSync/PowerSync.Common/CHANGELOG.md | 5 ++++- PowerSync/PowerSync.Maui/CHANGELOG.md | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/PowerSync/PowerSync.Common/CHANGELOG.md b/PowerSync/PowerSync.Common/CHANGELOG.md index e524939..3e067d7 100644 --- a/PowerSync/PowerSync.Common/CHANGELOG.md +++ b/PowerSync/PowerSync.Common/CHANGELOG.md @@ -1,12 +1,15 @@ # PowerSync.Common Changelog +## 0.1.3 + +- Add support for loading custom SQLite extensions via `MDSQLiteOptions.Extensions`. + ## 0.1.2 - Add support for MacCatalyst. - Add support for .NET 9.0. Supported targets now also include `net9.0`, `net9.0-android`, `net9.0-ios`, and `net9.0-maccatalyst`. - Update the PowerSync SQLite core extension to 0.4.13. - Add support for offline-first file attachments via `AttachmentQueue`. See `Attachments/README.md`. -- Add support for loading custom SQLite extensions via `MDSQLiteOptions.Extensions`. ## 0.1.1 diff --git a/PowerSync/PowerSync.Maui/CHANGELOG.md b/PowerSync/PowerSync.Maui/CHANGELOG.md index 79b9c7b..9efe5e5 100644 --- a/PowerSync/PowerSync.Maui/CHANGELOG.md +++ b/PowerSync/PowerSync.Maui/CHANGELOG.md @@ -1,5 +1,9 @@ # PowerSync.Maui Changelog +## 0.1.3 + +- Upstream PowerSync.Common version bump (See Powersync.Common changelog 0.1.2 for more information) + ## 0.1.2 - Add support for MacCatalyst. From 7bd7cd3ec9fff78eaf5eeda9b54ca194a28871df Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 13:37:54 +0200 Subject: [PATCH 5/7] Yet another changelog fix --- PowerSync/PowerSync.Common/CHANGELOG.md | 2 +- PowerSync/PowerSync.Maui/CHANGELOG.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/PowerSync/PowerSync.Common/CHANGELOG.md b/PowerSync/PowerSync.Common/CHANGELOG.md index 3e067d7..2d28c50 100644 --- a/PowerSync/PowerSync.Common/CHANGELOG.md +++ b/PowerSync/PowerSync.Common/CHANGELOG.md @@ -1,6 +1,6 @@ # PowerSync.Common Changelog -## 0.1.3 +## 0.1.3 (unreleased) - Add support for loading custom SQLite extensions via `MDSQLiteOptions.Extensions`. diff --git a/PowerSync/PowerSync.Maui/CHANGELOG.md b/PowerSync/PowerSync.Maui/CHANGELOG.md index 9efe5e5..76f8a88 100644 --- a/PowerSync/PowerSync.Maui/CHANGELOG.md +++ b/PowerSync/PowerSync.Maui/CHANGELOG.md @@ -1,8 +1,8 @@ # PowerSync.Maui Changelog -## 0.1.3 +## 0.1.3 (unreleased) -- Upstream PowerSync.Common version bump (See Powersync.Common changelog 0.1.2 for more information) +- Upstream PowerSync.Common version bump (See Powersync.Common changelog 0.1.3 for more information) ## 0.1.2 From eb24d072deb2a7c62554a01b05a89f8f676ddfcf Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 13:40:49 +0200 Subject: [PATCH 6/7] Dev release --- PowerSync/PowerSync.Common/CHANGELOG.md | 2 +- PowerSync/PowerSync.Maui/CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PowerSync/PowerSync.Common/CHANGELOG.md b/PowerSync/PowerSync.Common/CHANGELOG.md index 2d28c50..7d8ec10 100644 --- a/PowerSync/PowerSync.Common/CHANGELOG.md +++ b/PowerSync/PowerSync.Common/CHANGELOG.md @@ -1,6 +1,6 @@ # PowerSync.Common Changelog -## 0.1.3 (unreleased) +## 0.1.3-dev.1 - Add support for loading custom SQLite extensions via `MDSQLiteOptions.Extensions`. diff --git a/PowerSync/PowerSync.Maui/CHANGELOG.md b/PowerSync/PowerSync.Maui/CHANGELOG.md index 76f8a88..26160a5 100644 --- a/PowerSync/PowerSync.Maui/CHANGELOG.md +++ b/PowerSync/PowerSync.Maui/CHANGELOG.md @@ -1,6 +1,6 @@ # PowerSync.Maui Changelog -## 0.1.3 (unreleased) +## 0.1.3-dev.1 - Upstream PowerSync.Common version bump (See Powersync.Common changelog 0.1.3 for more information) From 4515c64615b3a03d84d61455370a9c579f7c9f07 Mon Sep 17 00:00:00 2001 From: LucDeCaf Date: Tue, 19 May 2026 13:55:14 +0200 Subject: [PATCH 7/7] Flaky test --- Tests/PowerSync/PowerSync.Common.Tests/EventStreamTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Tests/PowerSync/PowerSync.Common.Tests/EventStreamTests.cs b/Tests/PowerSync/PowerSync.Common.Tests/EventStreamTests.cs index 657f40b..777c668 100644 --- a/Tests/PowerSync/PowerSync.Common.Tests/EventStreamTests.cs +++ b/Tests/PowerSync/PowerSync.Common.Tests/EventStreamTests.cs @@ -217,24 +217,25 @@ public async Task EventManager_ShouldNotReceiveEventsAfterDeregistering() var cts = new CancellationTokenSource(); var listener = stream.ListenAsync(cts.Token); + var tcs = new TaskCompletionSource(); 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()); Assert.False(manager.TryEmit("invalid")); - Assert.False(await sem.WaitAsync(100)); + Assert.False(await sem.WaitAsync(500)); // Cleanup cts.Cancel();