Skip to content

Commit b4e093b

Browse files
committed
Added further CrudTests. Ensuring read connection is closed, clearing db connection from pool.
1 parent 8968574 commit b4e093b

File tree

7 files changed

+333
-158
lines changed

7 files changed

+333
-158
lines changed

PowerSync/PowerSync.Common/DB/Crud/CrudEntry.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public class CrudEntryJSON
3030
public class CrudEntryDataJSON
3131
{
3232
[JsonProperty("data")]
33-
public Dictionary<string, object> Data { get; set; } = new();
33+
public Dictionary<string, object>? Data { get; set; }
3434

3535
[JsonProperty("op")]
3636
public UpdateType Op { get; set; }
@@ -87,8 +87,6 @@ public static CrudEntry FromRow(CrudEntryJSON dbRow)
8787
);
8888
}
8989

90-
91-
9290
public override bool Equals(object? obj)
9391
{
9492
if (obj is not CrudEntry other) return false;

PowerSync/PowerSync.Common/MDSQLite/MDSQLiteAdapter.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public class MDSQLiteAdapter : EventStream<DBAdapterEvent>, IDBAdapter
3232
protected RequiredMDSQLiteOptions resolvedMDSQLiteOptions;
3333
private CancellationTokenSource? tablesUpdatedCts;
3434

35-
private static readonly AsyncLock writeMutex = new();
36-
private static readonly AsyncLock readMutex = new();
35+
private readonly AsyncLock writeMutex = new();
36+
private readonly AsyncLock readMutex = new();
3737

3838
public MDSQLiteAdapter(MDSQLiteAdapterOptions options)
3939
{
@@ -143,6 +143,7 @@ private void LoadExtension(SqliteConnection db)
143143
tablesUpdatedCts?.Cancel();
144144
base.Close();
145145
writeConnection?.Close();
146+
readConnection?.Close();
146147
}
147148

148149
public async Task<NonQueryResult> Execute(string query, object[]? parameters = null)

PowerSync/PowerSync.Common/MDSQLite/MDSQLiteConnection.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ public async Task<T> Get<T>(string sql, object[]? parameters = null)
213213
{
214214
base.Close();
215215
Db.Close();
216+
// https://stackoverflow.com/questions/8511901/system-data-sqlite-close-not-releasing-database-file
217+
SqliteConnection.ClearPool(Db);
216218
}
217219

218220
public async Task RefreshSchema()

Tests/PowerSync/PowerSync.Common.Tests/Client/Sync/CRUDTests.cs

Lines changed: 287 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,25 @@
1-
namespace PowerSync.Common.Tests.Client;
1+
namespace PowerSync.Common.Tests.Client.Sync;
2+
3+
using Microsoft.Data.Sqlite;
24

3-
using System.Diagnostics;
45
using Newtonsoft.Json;
6+
57
using PowerSync.Common.Client;
6-
using PowerSync.Common.Client.Sync.Bucket;
78
using PowerSync.Common.DB.Crud;
9+
using PowerSync.Common.DB.Schema;
10+
using PowerSync.Common.Tests.Utils;
811

912
public class CRUDTests : IAsyncLifetime
1013
{
1114
private PowerSyncDatabase db = default!;
15+
private readonly string testId = Guid.NewGuid().ToString();
16+
private readonly string dbName = "crud-test.db";
1217

1318
public async Task InitializeAsync()
1419
{
1520
db = new PowerSyncDatabase(new PowerSyncDatabaseOptions
1621
{
17-
Database = new SQLOpenOptions { DbFilename = "crudtest12xas.db" },
22+
Database = new SQLOpenOptions { DbFilename = dbName },
1823
Schema = TestSchema.appSchema,
1924
});
2025
await db.Init();
@@ -24,13 +29,12 @@ public async Task DisposeAsync()
2429
{
2530
await db.DisconnectAndClear();
2631
await db.Close();
32+
DatabaseUtils.CleanDb(dbName);
2733
}
2834

29-
[Fact(Skip = "Need to delete db file")]
30-
public async Task Insert_ShouldRecordCrudEntry()
35+
[Fact]
36+
public async Task Insert_RecordCrudEntryTest()
3137
{
32-
string testId = Guid.NewGuid().ToString();
33-
3438
var initialRows = await db.GetAll<object>("SELECT * FROM ps_crud");
3539
Assert.Empty(initialRows);
3640

@@ -59,5 +63,280 @@ public async Task Insert_ShouldRecordCrudEntry()
5963
Assert.True(tx.Crud.First().Equals(expectedCrudEntry));
6064
}
6165

66+
private record CountResult(int count);
67+
68+
[Fact]
69+
public async Task InsertOrReplaceTest()
70+
{
71+
await db.Execute("INSERT INTO assets(id, description) VALUES(?, ?)", [testId, "test"]);
72+
await db.Execute("DELETE FROM ps_crud WHERE 1");
73+
74+
// Replace existing entry
75+
await db.Execute("INSERT OR REPLACE INTO assets(id, description) VALUES(?, ?)", [testId, "test2"]);
76+
77+
var crudEntry = await db.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
78+
79+
Assert.Equal(
80+
JsonConvert.SerializeObject(new
81+
{
82+
op = "PUT",
83+
type = "assets",
84+
id = testId,
85+
data = new { description = "test2" }
86+
}),
87+
crudEntry.Data
88+
);
89+
90+
var assetCount = await db.Get<CountResult>("SELECT count(*) as count FROM assets");
91+
Assert.Equal(1, assetCount.count);
92+
93+
// Test uniqueness constraint
94+
var ex = await Assert.ThrowsAsync<SqliteException>(() =>
95+
db.Execute("INSERT INTO assets(id, description) VALUES(?, ?)", [testId, "test3"])
96+
);
97+
98+
Assert.Contains("UNIQUE constraint failed", ex.Message);
99+
}
100+
101+
[Fact]
102+
public async Task UpdateTest()
103+
{
104+
await db.Execute("INSERT INTO assets(id, description, make) VALUES(?, ?, ?)", [testId, "test", "test"]);
105+
await db.Execute("DELETE FROM ps_crud WHERE 1");
106+
107+
await db.Execute("UPDATE assets SET description = ? WHERE id = ?", ["test2", testId]);
108+
109+
var crudEntry = await db.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
110+
111+
Assert.Equal(
112+
JsonConvert.SerializeObject(new
113+
{
114+
op = "PATCH",
115+
type = "assets",
116+
id = testId,
117+
data = new { description = "test2" }
118+
}),
119+
crudEntry.Data
120+
);
121+
122+
var tx = await db.GetNextCrudTransaction();
123+
Assert.Equal(2, tx!.TransactionId);
124+
125+
var expectedCrudEntry = new CrudEntry(2, UpdateType.PATCH, "assets", testId, 2, new Dictionary<string, object>
126+
{
127+
{ "description", "test2" }
128+
});
129+
130+
Assert.True(tx.Crud.First().Equals(expectedCrudEntry));
131+
}
132+
133+
[Fact]
134+
public async Task DeleteTest()
135+
{
136+
await db.Execute("INSERT INTO assets(id, description, make) VALUES(?, ?, ?)", [testId, "test", "test"]);
137+
await db.Execute("DELETE FROM ps_crud WHERE 1");
138+
139+
await db.Execute("DELETE FROM assets WHERE id = ?", [testId]);
140+
141+
var crudEntry = await db.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
142+
143+
Assert.Equal(
144+
JsonConvert.SerializeObject(new
145+
{
146+
op = "DELETE",
147+
type = "assets",
148+
id = testId
149+
}),
150+
crudEntry.Data
151+
);
152+
153+
var tx = await db.GetNextCrudTransaction();
154+
Assert.Equal(2, tx!.TransactionId);
155+
156+
var expectedCrudEntry = new CrudEntry(2, UpdateType.DELETE, "assets", testId, 2);
157+
Assert.Equal(expectedCrudEntry, tx.Crud.First());
158+
}
159+
160+
[Fact]
161+
public async Task InsertOnlyTablesTest()
162+
{
163+
var logs = new Table(new Dictionary<string, ColumnType>
164+
{
165+
{ "level", ColumnType.TEXT },
166+
{ "content", ColumnType.TEXT },
167+
}, new TableOptions
168+
{
169+
InsertOnly = true
170+
});
171+
172+
Schema insertOnlySchema = new Schema(new Dictionary<string, Table>
173+
{
174+
{ "logs", logs },
175+
});
176+
177+
var uniqueDbName = $"test-{Guid.NewGuid()}.db";
178+
179+
var insertOnlyDb = new PowerSyncDatabase(new PowerSyncDatabaseOptions
180+
{
181+
Database = new SQLOpenOptions { DbFilename = uniqueDbName },
182+
Schema = insertOnlySchema,
183+
});
184+
185+
await insertOnlyDb.Init();
186+
187+
var initialCrudRows = await insertOnlyDb.GetAll<object>("SELECT * FROM ps_crud");
188+
Assert.Empty(initialCrudRows);
189+
190+
await insertOnlyDb.Execute("INSERT INTO logs(id, level, content) VALUES(?, ?, ?)", [testId, "INFO", "test log"]);
191+
192+
var crudEntry = await insertOnlyDb.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
193+
194+
Assert.Equal(
195+
JsonConvert.SerializeObject(new
196+
{
197+
op = "PUT",
198+
type = "logs",
199+
id = testId,
200+
data = new { content = "test log", level = "INFO" }
201+
}),
202+
crudEntry.Data
203+
);
204+
205+
var logRows = await insertOnlyDb.GetAll<object>("SELECT * FROM logs");
206+
Assert.Empty(logRows);
207+
208+
var tx = await insertOnlyDb.GetNextCrudTransaction();
209+
Assert.Equal(1, tx!.TransactionId);
210+
211+
var expectedCrudEntry = new CrudEntry(1, UpdateType.PUT, "logs", testId, 1, new Dictionary<string, object>
212+
{
213+
{ "content", "test log" },
214+
{ "level", "INFO" }
215+
});
216+
217+
Assert.True(tx.Crud.First().Equals(expectedCrudEntry));
218+
}
219+
220+
221+
private record QuantityResult(long quantity);
222+
223+
[Fact]
224+
public async Task BigNumbersIntegerTest()
225+
{
226+
long bigNumber = 1L << 62;
227+
await db.Execute("INSERT INTO assets(id, quantity) VALUES(?, ?)", [testId, bigNumber]);
228+
229+
var result = await db.Get<QuantityResult>("SELECT quantity FROM assets WHERE id = ?", [testId]);
230+
Assert.Equal(bigNumber, result.quantity);
62231

232+
var crudEntry = await db.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
233+
234+
Assert.Equal(
235+
JsonConvert.SerializeObject(new
236+
{
237+
op = "PUT",
238+
type = "assets",
239+
id = testId,
240+
data = new { quantity = bigNumber }
241+
}),
242+
crudEntry.Data
243+
);
244+
245+
var tx = await db.GetNextCrudTransaction();
246+
Assert.Equal(1, tx!.TransactionId);
247+
248+
var expectedCrudEntry = new CrudEntry(1, UpdateType.PUT, "assets", testId, 1, new Dictionary<string, object>
249+
{
250+
{ "quantity", bigNumber }
251+
});
252+
253+
Assert.True(tx.Crud.First().Equals(expectedCrudEntry));
254+
}
255+
256+
[Fact]
257+
public async Task BigNumbersTextTest()
258+
{
259+
long bigNumber = 1L << 62;
260+
await db.Execute("INSERT INTO assets(id, quantity) VALUES(?, ?)", [testId, bigNumber.ToString()]);
261+
262+
var result = await db.Get<QuantityResult>("SELECT quantity FROM assets WHERE id = ?", [testId]);
263+
Assert.Equal(bigNumber, result.quantity);
264+
265+
var crudEntry = await db.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
266+
267+
Assert.Equal(
268+
JsonConvert.SerializeObject(new
269+
{
270+
op = "PUT",
271+
type = "assets",
272+
id = testId,
273+
data = new { quantity = bigNumber.ToString() }
274+
}),
275+
crudEntry.Data
276+
);
277+
278+
await db.Execute("DELETE FROM ps_crud WHERE 1");
279+
280+
await db.Execute("UPDATE assets SET description = ?, quantity = CAST(quantity AS INTEGER) + 1 WHERE id = ?", [
281+
"updated",
282+
testId
283+
]);
284+
285+
crudEntry = await db.Get<CrudEntryJSON>("SELECT data FROM ps_crud ORDER BY id");
286+
287+
Assert.Equal(
288+
JsonConvert.SerializeObject(new
289+
{
290+
op = "PATCH",
291+
type = "assets",
292+
id = testId,
293+
data = new { description = "updated", quantity = bigNumber + 1 }
294+
}),
295+
crudEntry.Data
296+
);
297+
}
298+
299+
[Fact]
300+
public async Task TransactionGroupingTest()
301+
{
302+
var initialCrudRows = await db.GetAll<object>("SELECT * FROM ps_crud");
303+
Assert.Empty(initialCrudRows);
304+
305+
await db.WriteTransaction(async (tx) =>
306+
{
307+
await tx.Execute("INSERT INTO assets(id, description) VALUES(?, ?)", [testId, "test1"]);
308+
await tx.Execute("INSERT INTO assets(id, description) VALUES(?, ?)", ["test2", "test2"]);
309+
});
310+
311+
await db.WriteTransaction(async (tx) =>
312+
{
313+
await tx.Execute("UPDATE assets SET description = ? WHERE id = ?", ["updated", testId]);
314+
});
315+
316+
var tx1 = await db.GetNextCrudTransaction();
317+
Assert.Equal(1, tx1!.TransactionId);
318+
319+
var expectedCrudEntries = new[]
320+
{
321+
new CrudEntry(1, UpdateType.PUT, "assets", testId, 1, new Dictionary<string, object> { { "description", "test1" } }),
322+
new CrudEntry(2, UpdateType.PUT, "assets", "test2", 1, new Dictionary<string, object> { { "description", "test2" } })
323+
};
324+
325+
Assert.True(tx1.Crud.Select((entry, index) => entry.Equals(expectedCrudEntries[index])).All(result => result));
326+
await tx1.Complete();
327+
328+
var tx2 = await db.GetNextCrudTransaction();
329+
Assert.Equal(2, tx2!.TransactionId);
330+
331+
var expectedCrudEntry2 = new CrudEntry(3, UpdateType.PATCH, "assets", testId, 2, new Dictionary<string, object>
332+
{
333+
{ "description", "updated" }
334+
});
335+
336+
Assert.True(tx2.Crud.First().Equals(expectedCrudEntry2));
337+
await tx2.Complete();
338+
339+
var nextTx = await db.GetNextCrudTransaction();
340+
Assert.Null(nextTx);
341+
}
63342
}

0 commit comments

Comments
 (0)