@@ -106,16 +106,15 @@ VALUES(1, '$local', 5, 6, 7, 0, 0, 1, 0, 0, 0);
106106
107107 db.executeInTx ('select powersync_init()' );
108108
109- expect (db.select ('SELECT key, value FROM ps_kv ORDER BY key' ), containsAll ([
110- {'key' : 'last_seen_checkpoint_request_id' , 'value' : 6 },
111- {'key' : 'last_requested_checkpoint_request_id' , 'value' : 7 },
109+ expect (db.select ('SELECT key, value FROM ps_kv ORDER BY key' ), [
112110 {'key' : 'last_applied_checkpoint_request_id' , 'value' : 5 },
111+ {'key' : 'last_seen_checkpoint_request_id' , 'value' : 6 },
113112 {'key' : 'local_target_op' , 'value' : 7 },
114- ]));
113+ ]);
114+ expect (db.select (r"SELECT * FROM ps_buckets WHERE name = '$local'" ),
115+ isEmpty);
115116 expect (
116- db
117- .select ("PRAGMA table_info('ps_buckets')" )
118- .map ((row) => row['name' ]),
117+ db.select ("PRAGMA table_info('ps_buckets')" ).map ((row) => row['name' ]),
119118 isNot (contains ('target_op' )),
120119 );
121120 expect (
@@ -138,8 +137,8 @@ VALUES(1, '$local', 5, 6, 9223372036854775807, 0, 0, 1, 0, 0, 0);
138137 db.executeInTx ('select powersync_init()' );
139138
140139 // last_applied_op becomes the applied checkpoint id, but it must not seed the requested
141- // checkpoint counter. The sentinel target is preserved for blocking, but is not concrete
142- // enough to become last_requested_checkpoint_request_id.
140+ // checkpoint counter. The sentinel target is preserved for blocking, but target ops no longer
141+ // seed last_requested_checkpoint_request_id.
143142 expect (db.select ('SELECT key, value FROM ps_kv ORDER BY key' ), [
144143 {'key' : 'last_applied_checkpoint_request_id' , 'value' : 5 },
145144 {'key' : 'last_seen_checkpoint_request_id' , 'value' : 6 },
@@ -157,8 +156,8 @@ VALUES(1, '$local', 0, 0, 9223372036854775807, 0, 0, 1, 0, 0, 0);
157156
158157 db.executeInTx ('select powersync_init()' );
159158
160- // The max-op sentinel is valid local target state, but it is not a concrete checkpoint
161- // request id and must not seed last_requested_checkpoint_request_id.
159+ // The max-op sentinel is valid local target state, but target ops no longer seed
160+ // last_requested_checkpoint_request_id.
162161 expect (db.select ('SELECT key, value FROM ps_kv ORDER BY key' ), [
163162 {'key' : 'local_target_op' , 'value' : 9223372036854775807 },
164163 ]);
@@ -190,6 +189,42 @@ INSERT INTO ps_kv(key, value) VALUES
190189 );
191190 });
192191
192+ test ('re-upgrades after downgrade with checkpoint state' , () async {
193+ db.execute (fixtures.finalState);
194+ db.execute (r'''
195+ INSERT INTO ps_kv(key, value) VALUES
196+ ('last_requested_checkpoint_request_id', 7),
197+ ('last_seen_checkpoint_request_id', 6),
198+ ('last_applied_checkpoint_request_id', 5),
199+ ('local_target_op', 7);
200+ ''' );
201+
202+ db.executeInTx ('select powersync_test_migration(13)' );
203+
204+ // Simulate an older SDK advancing the restored $local row while downgraded.
205+ db.execute (r'''
206+ UPDATE ps_buckets
207+ SET last_op = 8, last_applied_op = 8, target_op = 9
208+ WHERE name = '$local'
209+ ''' );
210+
211+ db.executeInTx ('select powersync_init()' );
212+
213+ // The $local row is the source of truth on re-upgrade; the request counter is unrelated to
214+ // $local and survives the downgrade unchanged.
215+ expect (db.select ('SELECT key, value FROM ps_kv ORDER BY key' ), [
216+ {'key' : 'last_applied_checkpoint_request_id' , 'value' : 8 },
217+ {'key' : 'last_requested_checkpoint_request_id' , 'value' : 7 },
218+ {'key' : 'last_seen_checkpoint_request_id' , 'value' : 8 },
219+ {'key' : 'local_target_op' , 'value' : 9 },
220+ ]);
221+ expect (db.select (r"SELECT * FROM ps_buckets WHERE name = '$local'" ),
222+ isEmpty);
223+
224+ final schema = '${getSchema (db )}\n ${getMigrations (db )}' ;
225+ expect (schema, equals (fixtures.finalState.trim ()));
226+ });
227+
193228 test ('does not restore local bucket without local target on downgrade' ,
194229 () async {
195230 db.execute (fixtures.finalState);
0 commit comments