Skip to content

Commit e910fc4

Browse files
committed
Better loading method and API + fixes
1 parent 91a3f8f commit e910fc4

4 files changed

Lines changed: 82 additions & 119 deletions

File tree

PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private RequiredMDSQLiteOptions ResolveMDSQLiteOptions(MDSQLiteOptions? options)
6060
LockTimeoutMs = options?.LockTimeoutMs ?? defaults.LockTimeoutMs,
6161
EncryptionKey = options?.EncryptionKey ?? defaults.EncryptionKey,
6262
Extensions = options?.Extensions ?? defaults.Extensions,
63+
LoadPowerSyncExtension = options?.LoadPowerSyncExtension ?? defaults.LoadPowerSyncExtension,
6364
ReadPoolSize = options?.ReadPoolSize ?? defaults.ReadPoolSize,
6465
};
6566
}
@@ -105,7 +106,7 @@ private async Task Init()
105106
}
106107
return readConnection;
107108
};
108-
readPool = new MDSQLiteConnectionPool(resolvedOptions, readConnectionFactory);
109+
readPool = new MDSQLiteConnectionPool(resolvedOptions.ReadPoolSize, readConnectionFactory);
109110
await readPool.Init();
110111

111112
// Register TablesUpdated listener
@@ -144,12 +145,27 @@ private static SqliteConnection OpenDatabase(string dbFilename)
144145
protected virtual void LoadExtensions(SqliteConnection db)
145146
{
146147
db.EnableExtensions(true);
148+
if (resolvedOptions.LoadPowerSyncExtension)
149+
{
150+
LoadDefaultPowerSyncExtension(db);
151+
}
147152
foreach (var extension in resolvedOptions.Extensions)
148153
{
149154
db.LoadExtension(extension.Path, extension.EntryPoint);
150155
}
151156
}
152157

158+
/// <summary>
159+
/// Loads the bundled PowerSync core SQLite extension. Override on
160+
/// platform-specific adapters (e.g. MAUI iOS/Android) where the native library
161+
/// lives outside the desktop runtime path.
162+
/// </summary>
163+
protected virtual void LoadDefaultPowerSyncExtension(SqliteConnection db)
164+
{
165+
var path = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
166+
db.LoadExtension(path, "sqlite3_powersync_init");
167+
}
168+
153169
public async Task Close()
154170
{
155171
tablesUpdatedCts?.Cancel();
@@ -303,18 +319,16 @@ await readPool.LeaseAll(async (connections) =>
303319

304320
class MDSQLiteConnectionPool
305321
{
306-
private readonly RequiredMDSQLiteOptions _options;
307322
private readonly Channel<MDSQLiteConnection> _channel;
308323
private readonly int _poolSize;
309324
private readonly Func<Task<MDSQLiteConnection>> _connectionFactory;
310325

311326
private readonly Task _initialized;
312327

313-
public MDSQLiteConnectionPool(RequiredMDSQLiteOptions options, Func<Task<MDSQLiteConnection>> connectionFactory)
328+
public MDSQLiteConnectionPool(int poolSize, Func<Task<MDSQLiteConnection>> connectionFactory)
314329
{
315-
_options = options;
316-
_channel = Channel.CreateBounded<MDSQLiteConnection>(options.ReadPoolSize);
317-
_poolSize = options.ReadPoolSize;
330+
_channel = Channel.CreateBounded<MDSQLiteConnection>(poolSize);
331+
_poolSize = poolSize;
318332
_connectionFactory = connectionFactory;
319333
_initialized = Initialize();
320334
}
@@ -366,34 +380,6 @@ public async Task LeaseAll(Func<List<MDSQLiteConnection>, Task> callback)
366380
}
367381
}
368382

369-
private async Task<MDSQLiteConnection> OpenConnection(string dbFilename)
370-
{
371-
var db = OpenDatabase(dbFilename);
372-
LoadExtensions(db);
373-
374-
var connection = new MDSQLiteConnection(new MDSQLiteConnectionOptions(db));
375-
await connection.Execute("SELECT powersync_init()");
376-
377-
return connection;
378-
}
379-
380-
private static SqliteConnection OpenDatabase(string dbFilename)
381-
{
382-
string connectionString = $"Data Source={dbFilename};Pooling=False;";
383-
var connection = new SqliteConnection(connectionString);
384-
connection.Open();
385-
return connection;
386-
}
387-
388-
private void LoadExtensions(SqliteConnection db)
389-
{
390-
db.EnableExtensions(true);
391-
foreach (var extension in _options.Extensions)
392-
{
393-
db.LoadExtension(extension.Path, extension.EntryPoint);
394-
}
395-
}
396-
397383
public async Task Close()
398384
{
399385
await LeaseAll((connections) =>

PowerSync/PowerSync.Common/MDSQLite/MDSQLiteOptions.cs

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
namespace PowerSync.Common.MDSQLite;
22

3-
using PowerSync.Common.Utils;
4-
53
public sealed class TemporaryStorageOption
64
{
75
public static readonly TemporaryStorageOption MEMORY = new("memory");
@@ -57,27 +55,10 @@ public sealed class SqliteSynchronous
5755

5856
public class SqliteExtension
5957
{
60-
public static SqliteExtension DEFAULT_POWERSYNC_EXTENSION = new()
61-
{
62-
Path = TryResolveDefaultPowerSyncExtensionPath(),
63-
EntryPoint = "sqlite3_powersync_init",
64-
};
65-
public static SqliteExtension[] DEFAULT_POWERSYNC_EXTENSIONS = [DEFAULT_POWERSYNC_EXTENSION];
66-
6758
public string Path { get; set; } = string.Empty;
6859
public string? EntryPoint { get; set; }
69-
70-
// PowerSyncPathResolver only knows about desktop RIDs and throws on iOS/Android.
71-
// Swallow that here so this static field can be referenced safely on mobile —
72-
// platform-aware adapters (e.g. MAUI) intercept the sentinel before reading Path.
73-
private static string TryResolveDefaultPowerSyncExtensionPath()
74-
{
75-
try { return PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory); }
76-
catch (PlatformNotSupportedException) { return string.Empty; }
77-
}
7860
}
7961

80-
8162
public class MDSQLiteOptions
8263
{
8364
/// <summary>
@@ -118,12 +99,23 @@ public class MDSQLiteOptions
11899
public int? CacheSizeKb { get; set; }
119100

120101
/// <summary>
121-
/// Load SQLite extensions using the path and entryPoint. Defaults to
122-
/// SqliteExtension.DEFAULT_POWERSYNC_EXTENSIONS. Remember to re-add
123-
/// the DEFAULT_POWERSYNC_EXTENSION if modifying the list of extensions.
102+
/// Additional SQLite extensions to load on every connection, in order. Defaults
103+
/// to an empty list. The bundled PowerSync core extension is loaded separately
104+
/// and controlled by <see cref="LoadPowerSyncExtension"/> — do not include it
105+
/// here.
124106
/// </summary>
125107
public SqliteExtension[]? Extensions { get; set; }
126108

109+
/// <summary>
110+
/// Whether to load the bundled PowerSync core SQLite extension on every
111+
/// connection. Defaults to true and should remain true for normal use — the
112+
/// rest of the library relies on the SQL functions and virtual tables it
113+
/// registers (e.g. <c>powersync_init()</c>). Set to false only if you are
114+
/// supplying an equivalent PowerSync-compatible extension via
115+
/// <see cref="Extensions"/>.
116+
/// </summary>
117+
public bool? LoadPowerSyncExtension { get; set; }
118+
127119
/// <summary>
128120
/// The number of MDSQLiteConnection objects to create for the read pool.
129121
/// </summary>
@@ -141,7 +133,8 @@ public class RequiredMDSQLiteOptions : MDSQLiteOptions
141133
TemporaryStorage = TemporaryStorageOption.MEMORY,
142134
LockTimeoutMs = 30000,
143135
EncryptionKey = null,
144-
Extensions = SqliteExtension.DEFAULT_POWERSYNC_EXTENSIONS,
136+
Extensions = [],
137+
LoadPowerSyncExtension = true,
145138
ReadPoolSize = 5,
146139
};
147140

@@ -161,5 +154,7 @@ public class RequiredMDSQLiteOptions : MDSQLiteOptions
161154

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

157+
public new bool LoadPowerSyncExtension { get; set; }
158+
164159
public new int ReadPoolSize { get; set; }
165160
}

PowerSync/PowerSync.Maui/SQLite/MAUISQLiteAdapter.cs

Lines changed: 8 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -15,45 +15,20 @@ public MAUISQLiteAdapter(MDSQLiteAdapterOptions options) : base(options)
1515
{
1616
}
1717

18-
protected override void LoadExtensions(SqliteConnection db)
19-
{
20-
db.EnableExtensions(true);
21-
22-
// The default PowerSync extension's path is resolved for desktop runtimes and
23-
// does not apply on iOS/MacCatalyst/Android, where the native library lives
24-
// in a platform-specific location. If the user didn't supply any extensions,
25-
// load the platform-correct default. Otherwise honor their list — but still
26-
// intercept the DEFAULT_POWERSYNC_EXTENSION sentinel so consumers can mix
27-
// the bundled PowerSync extension with their own custom extensions.
28-
var userExtensions = options.SqliteOptions?.Extensions;
29-
if (userExtensions == null)
30-
{
31-
LoadDefaultPowerSyncExtension(db);
32-
return;
33-
}
34-
35-
foreach (var extension in userExtensions)
36-
{
37-
if (ReferenceEquals(extension, SqliteExtension.DEFAULT_POWERSYNC_EXTENSION))
38-
{
39-
LoadDefaultPowerSyncExtension(db);
40-
}
41-
else
42-
{
43-
db.LoadExtension(extension.Path, extension.EntryPoint);
44-
}
45-
}
46-
}
47-
48-
private static void LoadDefaultPowerSyncExtension(SqliteConnection db)
18+
// The bundled PowerSync extension lives in a platform-specific location on
19+
// iOS/MacCatalyst/Android — the desktop runtime path used by the base class
20+
// does not resolve to it. Override only the PowerSync-extension load hook;
21+
// user-supplied custom extensions still flow through MDSQLiteAdapter.LoadExtensions
22+
// unchanged, so consumers can freely combine the bundled extension (via the
23+
// LoadPowerSyncExtension flag) with their own.
24+
protected override void LoadDefaultPowerSyncExtension(SqliteConnection db)
4925
{
5026
#if IOS || MACCATALYST
5127
LoadExtensionApple(db);
5228
#elif ANDROID
5329
db.LoadExtension("libpowersync");
5430
#else
55-
var defaultExtension = SqliteExtension.DEFAULT_POWERSYNC_EXTENSION;
56-
db.LoadExtension(defaultExtension.Path, defaultExtension.EntryPoint);
31+
base.LoadDefaultPowerSyncExtension(db);
5732
#endif
5833
}
5934

Tests/PowerSync/PowerSync.Common.Tests/MDSQLite/MDSQLiteAdapterTests.cs

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ namespace PowerSync.Common.Tests.MDSQLite;
55
using PowerSync.Common.Client;
66
using PowerSync.Common.MDSQLite;
77
using PowerSync.Common.Tests.Utils;
8+
using PowerSync.Common.Utils;
89

910
/// <summary>
1011
/// dotnet test -v n --framework net8.0 --filter "MDSQLiteAdapterTests"
@@ -19,35 +20,23 @@ private class AssetResult
1920
public string? make { get; set; }
2021
}
2122

22-
private static PowerSyncDatabase BuildDbWithExtensions(string dbFilename, SqliteExtension[] extensions)
23+
[Fact]
24+
public async Task DisablingCoreExtensionPreventsPowerSyncFromLoading()
2325
{
24-
return new PowerSyncDatabase(new PowerSyncDatabaseOptions
26+
var dbName = $"MDSQLiteAdapter-{Guid.NewGuid():N}.db";
27+
var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
2528
{
2629
Database = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions
2730
{
28-
DbFilename = dbFilename,
29-
SqliteOptions = new MDSQLiteOptions { Extensions = extensions },
31+
DbFilename = dbName,
32+
SqliteOptions = new MDSQLiteOptions
33+
{
34+
LoadPowerSyncExtension = false,
35+
Extensions = [],
36+
},
3037
}),
3138
Schema = TestSchema.AppSchema,
3239
});
33-
}
34-
35-
private static string CopyDefaultExtensionToTempPath()
36-
{
37-
var sourcePath = SqliteExtension.DEFAULT_POWERSYNC_EXTENSION.Path;
38-
var tempPath = Path.Combine(
39-
Path.GetTempPath(),
40-
$"powersync-ext-copy-{Guid.NewGuid():N}{Path.GetExtension(sourcePath)}"
41-
);
42-
File.Copy(sourcePath, tempPath, overwrite: true);
43-
return tempPath;
44-
}
45-
46-
[Fact]
47-
public async Task EmptyExtensionsArrayDoesNotLoadPowerSync()
48-
{
49-
var name = $"MDSQLiteAdapter-ext-empty-{Guid.NewGuid():N}.db";
50-
var db = BuildDbWithExtensions(name, []);
5140

5241
try
5342
{
@@ -57,18 +46,36 @@ public async Task EmptyExtensionsArrayDoesNotLoadPowerSync()
5746
finally
5847
{
5948
try { await db.Close(); } catch { /* expected — init failed */ }
60-
DatabaseUtils.CleanDb(name);
49+
DatabaseUtils.CleanDb(dbName);
6150
}
6251
}
6352

6453
[Fact]
6554
public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath()
6655
{
67-
var name = $"MDSQLiteAdapter-ext-custom-{Guid.NewGuid():N}.db";
68-
var customPath = CopyDefaultExtensionToTempPath();
69-
var db = BuildDbWithExtensions(name, [
70-
new SqliteExtension { Path = customPath, EntryPoint = "sqlite3_powersync_init" },
71-
]);
56+
var dbName = $"MDSQLiteAdapter-{Guid.NewGuid():N}.db";
57+
var sourcePath = PowerSyncPathResolver.GetNativeLibraryPath(AppContext.BaseDirectory);
58+
var customPath = Path.Combine(
59+
Path.GetTempPath(),
60+
$"powersync-ext-copy-{Guid.NewGuid():N}{Path.GetExtension(sourcePath)}"
61+
);
62+
File.Copy(sourcePath, customPath, overwrite: true);
63+
64+
var db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
65+
{
66+
Database = new MDSQLiteDBOpenFactory(new MDSQLiteOpenFactoryOptions
67+
{
68+
DbFilename = dbName,
69+
SqliteOptions = new MDSQLiteOptions
70+
{
71+
LoadPowerSyncExtension = false,
72+
Extensions = [
73+
new SqliteExtension { Path = customPath, EntryPoint = "sqlite3_powersync_init" },
74+
],
75+
},
76+
}),
77+
Schema = TestSchema.AppSchema,
78+
});
7279

7380
try
7481
{
@@ -82,7 +89,7 @@ public async Task LoadsCustomPowerSyncExtensionFromOverriddenPath()
8289
finally
8390
{
8491
await db.Close();
85-
DatabaseUtils.CleanDb(name);
92+
DatabaseUtils.CleanDb(dbName);
8693
try { File.Delete(customPath); } catch { /* best-effort cleanup */ }
8794
}
8895
}

0 commit comments

Comments
 (0)