@@ -233,3 +233,311 @@ test("evolu_history unique index prevents duplicates", async () => {
233233 ) ;
234234 expect ( count . rows [ 0 ] . count ) . toBe ( 1 ) ;
235235} ) ;
236+
237+ test ( "timestamp ordering - newer mutations overwrite older ones" , async ( ) => {
238+ const [ , sqlite , db ] = await setupInitializedDbWorker ( ) ;
239+
240+ const recordId = testCreateId ( ) ;
241+
242+ // Create first mutation
243+ db . postMessage ( {
244+ type : "mutate" ,
245+ tabId : testCreateId ( ) ,
246+ changes : [
247+ {
248+ id : recordId ,
249+ table : "testTable" as Base64Url256 ,
250+ values : { [ "name" as Base64Url256 ] : "first_value" } ,
251+ } ,
252+ ] ,
253+ onCompleteIds : [ ] ,
254+ subscribedQueries : [ ] ,
255+ } ) ;
256+
257+ await wait ( 10 ) ;
258+
259+ // Create second mutation on same record (will have newer timestamp)
260+ db . postMessage ( {
261+ type : "mutate" ,
262+ tabId : testCreateId ( ) ,
263+ changes : [
264+ {
265+ id : recordId ,
266+ table : "testTable" as Base64Url256 ,
267+ values : { [ "name" as Base64Url256 ] : "second_value" } ,
268+ } ,
269+ ] ,
270+ onCompleteIds : [ ] ,
271+ subscribedQueries : [ ] ,
272+ } ) ;
273+
274+ await wait ( 10 ) ;
275+
276+ // Verify the app table has the latest value
277+ const finalResult = getOrThrow (
278+ sqlite . exec < { name : string } > ( sql `
279+ select name from testTable where id = ${ recordId } ;
280+ ` ) ,
281+ ) ;
282+ expect ( finalResult . rows [ 0 ] . name ) . toBe ( "second_value" ) ;
283+
284+ // Verify both mutations are stored in history
285+ const historyCount = getOrThrow (
286+ sqlite . exec < { count : number } > ( sql `
287+ select count(*) as count
288+ from evolu_history
289+ where
290+ "table" = 'testTable'
291+ and "id" = ${ idToBinaryId ( recordId ) }
292+ and "column" = 'name';
293+ ` ) ,
294+ ) ;
295+ expect ( historyCount . rows [ 0 ] . count ) . toBe ( 2 ) ;
296+ } ) ;
297+
298+ test ( "timestamp ordering - multiple columns update independently" , async ( ) => {
299+ const [ , sqlite , db ] = await setupInitializedDbWorker ( ) ;
300+
301+ const recordId = testCreateId ( ) ;
302+
303+ // Create first mutation that sets the name
304+ db . postMessage ( {
305+ type : "mutate" ,
306+ tabId : testCreateId ( ) ,
307+ changes : [
308+ {
309+ id : recordId ,
310+ table : "testTable" as Base64Url256 ,
311+ values : { [ "name" as Base64Url256 ] : "original_name" } ,
312+ } ,
313+ ] ,
314+ onCompleteIds : [ ] ,
315+ subscribedQueries : [ ] ,
316+ } ) ;
317+
318+ await wait ( 10 ) ;
319+
320+ // Update the same record with a different value for name
321+ db . postMessage ( {
322+ type : "mutate" ,
323+ tabId : testCreateId ( ) ,
324+ changes : [
325+ {
326+ id : recordId ,
327+ table : "testTable" as Base64Url256 ,
328+ values : { [ "name" as Base64Url256 ] : "updated_name" } ,
329+ } ,
330+ ] ,
331+ onCompleteIds : [ ] ,
332+ subscribedQueries : [ ] ,
333+ } ) ;
334+
335+ await wait ( 10 ) ;
336+
337+ // Verify the app table has the latest name value
338+ const finalResult = getOrThrow (
339+ sqlite . exec < { name : string } > ( sql `
340+ select name from testTable where id = ${ recordId } ;
341+ ` ) ,
342+ ) ;
343+ expect ( finalResult . rows [ 0 ] . name ) . toBe ( "updated_name" ) ;
344+
345+ // Verify we have two entries in history for the name column
346+ const nameHistoryCount = getOrThrow (
347+ sqlite . exec < { count : number } > ( sql `
348+ select count(*) as count
349+ from evolu_history
350+ where
351+ "table" = 'testTable'
352+ and "id" = ${ idToBinaryId ( recordId ) }
353+ and "column" = 'name';
354+ ` ) ,
355+ ) ;
356+ expect ( nameHistoryCount . rows [ 0 ] . count ) . toBe ( 2 ) ;
357+
358+ // Verify the values are stored in chronological order in history
359+ const historyValues = getOrThrow (
360+ sqlite . exec < { value : string } > ( sql `
361+ select value
362+ from evolu_history
363+ where
364+ "table" = 'testTable'
365+ and "id" = ${ idToBinaryId ( recordId ) }
366+ and "column" = 'name'
367+ order by timestamp;
368+ ` ) ,
369+ ) ;
370+ expect ( historyValues . rows [ 0 ] . value ) . toBe ( "original_name" ) ;
371+ expect ( historyValues . rows [ 1 ] . value ) . toBe ( "updated_name" ) ;
372+ } ) ;
373+
374+ test ( "timestamp ordering - concurrent mutations on different records" , async ( ) => {
375+ const [ , sqlite , db ] = await setupInitializedDbWorker ( ) ;
376+
377+ const recordId1 = testCreateId ( ) ;
378+ const recordId2 = testCreateId ( ) ;
379+
380+ // Create mutations on different records in quick succession
381+ db . postMessage ( {
382+ type : "mutate" ,
383+ tabId : testCreateId ( ) ,
384+ changes : [
385+ {
386+ id : recordId1 ,
387+ table : "testTable" as Base64Url256 ,
388+ values : { [ "name" as Base64Url256 ] : "record1_value" } ,
389+ } ,
390+ {
391+ id : recordId2 ,
392+ table : "testTable" as Base64Url256 ,
393+ values : { [ "name" as Base64Url256 ] : "record2_value" } ,
394+ } ,
395+ ] ,
396+ onCompleteIds : [ ] ,
397+ subscribedQueries : [ ] ,
398+ } ) ;
399+
400+ await wait ( 10 ) ;
401+
402+ // Verify both records exist with correct values
403+ const allRecords = getOrThrow (
404+ sqlite . exec < { id : string ; name : string } > ( sql `
405+ select id, name
406+ from testTable
407+ where id in (${ recordId1 } , ${ recordId2 } )
408+ order by id;
409+ ` ) ,
410+ ) ;
411+ expect ( allRecords . rows ) . toHaveLength ( 2 ) ;
412+
413+ const record1 = allRecords . rows . find ( ( r ) => r . id === recordId1 ) ;
414+ const record2 = allRecords . rows . find ( ( r ) => r . id === recordId2 ) ;
415+
416+ expect ( record1 ?. name ) . toBe ( "record1_value" ) ;
417+ expect ( record2 ?. name ) . toBe ( "record2_value" ) ;
418+
419+ // Verify both records have entries in history
420+ const totalHistoryCount = getOrThrow (
421+ sqlite . exec < { count : number } > ( sql `
422+ select count(*) as count
423+ from evolu_history
424+ where "table" = 'testTable' and "column" = 'name';
425+ ` ) ,
426+ ) ;
427+ expect ( totalHistoryCount . rows [ 0 ] . count ) . toBe ( 2 ) ;
428+ } ) ;
429+
430+ test ( "timestamp ordering - verify CRDT last-write-wins behavior" , async ( ) => {
431+ const [ , sqlite , db ] = await setupInitializedDbWorker ( ) ;
432+
433+ const recordId = testCreateId ( ) ;
434+
435+ // Create initial value
436+ db . postMessage ( {
437+ type : "mutate" ,
438+ tabId : testCreateId ( ) ,
439+ changes : [
440+ {
441+ id : recordId ,
442+ table : "testTable" as Base64Url256 ,
443+ values : { [ "name" as Base64Url256 ] : "initial" } ,
444+ } ,
445+ ] ,
446+ onCompleteIds : [ ] ,
447+ subscribedQueries : [ ] ,
448+ } ) ;
449+
450+ await wait ( 10 ) ;
451+
452+ // Update multiple times rapidly to ensure different timestamps
453+ db . postMessage ( {
454+ type : "mutate" ,
455+ tabId : testCreateId ( ) ,
456+ changes : [
457+ {
458+ id : recordId ,
459+ table : "testTable" as Base64Url256 ,
460+ values : { [ "name" as Base64Url256 ] : "second" } ,
461+ } ,
462+ ] ,
463+ onCompleteIds : [ ] ,
464+ subscribedQueries : [ ] ,
465+ } ) ;
466+
467+ await wait ( 5 ) ;
468+
469+ db . postMessage ( {
470+ type : "mutate" ,
471+ tabId : testCreateId ( ) ,
472+ changes : [
473+ {
474+ id : recordId ,
475+ table : "testTable" as Base64Url256 ,
476+ values : { [ "name" as Base64Url256 ] : "third" } ,
477+ } ,
478+ ] ,
479+ onCompleteIds : [ ] ,
480+ subscribedQueries : [ ] ,
481+ } ) ;
482+
483+ await wait ( 5 ) ;
484+
485+ db . postMessage ( {
486+ type : "mutate" ,
487+ tabId : testCreateId ( ) ,
488+ changes : [
489+ {
490+ id : recordId ,
491+ table : "testTable" as Base64Url256 ,
492+ values : { [ "name" as Base64Url256 ] : "final" } ,
493+ } ,
494+ ] ,
495+ onCompleteIds : [ ] ,
496+ subscribedQueries : [ ] ,
497+ } ) ;
498+
499+ await wait ( 10 ) ;
500+
501+ // Verify app table has the final value (last write wins)
502+ const appTableResult = getOrThrow (
503+ sqlite . exec < { name : string } > ( sql `
504+ select name from testTable where id = ${ recordId } ;
505+ ` ) ,
506+ ) ;
507+ expect ( appTableResult . rows [ 0 ] . name ) . toBe ( "final" ) ;
508+
509+ // Verify all mutations are preserved in history in timestamp order
510+ const historyResults = getOrThrow (
511+ sqlite . exec < { value : string } > ( sql `
512+ select value
513+ from evolu_history
514+ where
515+ "table" = 'testTable'
516+ and "id" = ${ idToBinaryId ( recordId ) }
517+ and "column" = 'name'
518+ order by timestamp;
519+ ` ) ,
520+ ) ;
521+
522+ expect ( historyResults . rows ) . toHaveLength ( 4 ) ;
523+ expect ( historyResults . rows [ 0 ] . value ) . toBe ( "initial" ) ;
524+ expect ( historyResults . rows [ 1 ] . value ) . toBe ( "second" ) ;
525+ expect ( historyResults . rows [ 2 ] . value ) . toBe ( "third" ) ;
526+ expect ( historyResults . rows [ 3 ] . value ) . toBe ( "final" ) ;
527+
528+ // Verify that the app table always reflects the value with the highest timestamp
529+ const timestampResults = getOrThrow (
530+ sqlite . exec < { value : string ; timestamp : Uint8Array } > ( sql `
531+ select value, timestamp
532+ from evolu_history
533+ where
534+ "table" = 'testTable'
535+ and "id" = ${ idToBinaryId ( recordId ) }
536+ and "column" = 'name'
537+ order by timestamp desc
538+ limit 1;
539+ ` ) ,
540+ ) ;
541+
542+ expect ( timestampResults . rows [ 0 ] . value ) . toBe ( "final" ) ;
543+ } ) ;
0 commit comments