1- namespace PowerSync . Common . Tests . Client ;
1+ namespace PowerSync . Common . Tests . Client . Sync ;
2+
3+ using Microsoft . Data . Sqlite ;
24
3- using System . Diagnostics ;
45using Newtonsoft . Json ;
6+
57using PowerSync . Common . Client ;
6- using PowerSync . Common . Client . Sync . Bucket ;
78using PowerSync . Common . DB . Crud ;
9+ using PowerSync . Common . DB . Schema ;
10+ using PowerSync . Common . Tests . Utils ;
811
912public 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