@@ -38,7 +38,11 @@ beforeEach(() => {
3838describe ( 'the D1 restore point a deploy takes before migrating' , ( ) => {
3939 it ( 'saves a restore point before applying pending migrations, and names how to restore it' , async ( ) => {
4040 const runner = recordingRunner ( {
41- 'd1.migrations.list' : [ pending ( [ '0001_add_column.sql' ] ) , pending ( [ ] ) ] ,
41+ 'd1.migrations.list' : [
42+ pending ( [ '0001_add_column.sql' ] ) ,
43+ pending ( [ '0001_add_column.sql' ] ) ,
44+ pending ( [ ] ) ,
45+ ] ,
4246 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
4347 } ) ;
4448
@@ -53,6 +57,7 @@ describe('the D1 restore point a deploy takes before migrating', () => {
5357 expect ( runner . calls ) . toEqual ( [
5458 'd1.migrations.list' ,
5559 'd1.time-travel.bookmark' ,
60+ 'd1.migrations.list' ,
5661 'd1.migrations.apply' ,
5762 'd1.migrations.list' ,
5863 ] ) ;
@@ -113,7 +118,7 @@ describe('the D1 restore point a deploy takes before migrating', () => {
113118
114119 it ( 'checks the uploader again after the restore point, before applying' , async ( ) => {
115120 const runner = recordingRunner ( {
116- 'd1.migrations.list' : [ pending ( [ '0001_add_column.sql' ] ) ] ,
121+ 'd1.migrations.list' : [ pending ( [ '0001_add_column.sql' ] ) , pending ( [ '0001_add_column.sql' ] ) ] ,
117122 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
118123 } ) ;
119124 const stillPinned = vi . fn ( ) . mockReturnValueOnce ( true ) . mockReturnValueOnce ( false ) ;
@@ -126,7 +131,11 @@ describe('the D1 restore point a deploy takes before migrating', () => {
126131 ) ;
127132
128133 expect ( applied ) . toBe ( false ) ;
129- expect ( runner . calls ) . toEqual ( [ 'd1.migrations.list' , 'd1.time-travel.bookmark' ] ) ;
134+ expect ( runner . calls ) . toEqual ( [
135+ 'd1.migrations.list' ,
136+ 'd1.time-travel.bookmark' ,
137+ 'd1.migrations.list' ,
138+ ] ) ;
130139 } ) ;
131140
132141 it . each ( [
@@ -137,7 +146,11 @@ describe('the D1 restore point a deploy takes before migrating', () => {
137146 { name : 'the uploader changed after the apply' , pinnedChecks : 2 } ,
138147 ] ) ( 'repeats the restore command when $name' , async ( { apply, verify, pinnedChecks } ) => {
139148 const runner = recordingRunner ( {
140- 'd1.migrations.list' : [ pending ( [ '0001_add_column.sql' ] ) , verify ?? pending ( [ ] ) ] ,
149+ 'd1.migrations.list' : [
150+ pending ( [ '0001_add_column.sql' ] ) ,
151+ pending ( [ '0001_add_column.sql' ] ) ,
152+ verify ?? pending ( [ ] ) ,
153+ ] ,
141154 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
142155 'd1.migrations.apply' : [ apply ?? clean ] ,
143156 } ) ;
@@ -156,7 +169,11 @@ describe('the D1 restore point a deploy takes before migrating', () => {
156169
157170 it ( 'names the platform in the restore command after a one-off Cloudflare deploy' , async ( ) => {
158171 const runner = recordingRunner ( {
159- 'd1.migrations.list' : [ pending ( [ '0001_add_column.sql' ] ) , pending ( [ ] ) ] ,
172+ 'd1.migrations.list' : [
173+ pending ( [ '0001_add_column.sql' ] ) ,
174+ pending ( [ '0001_add_column.sql' ] ) ,
175+ pending ( [ ] ) ,
176+ ] ,
160177 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
161178 } ) ;
162179
@@ -167,6 +184,65 @@ describe('the D1 restore point a deploy takes before migrating', () => {
167184 ) ;
168185 } ) ;
169186
187+ it ( 'names only the migrations still pending after the restore point' , async ( ) => {
188+ // An overlapping deploy applied 0001 between the first read and the bookmark.
189+ const runner = recordingRunner ( {
190+ 'd1.migrations.list' : [
191+ pending ( [ '0001_add_column.sql' , '0002_add_index.sql' ] ) ,
192+ pending ( [ '0002_add_index.sql' ] ) ,
193+ pending ( [ ] ) ,
194+ ] ,
195+ 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
196+ } ) ;
197+
198+ await applyAndVerifyRemoteD1Migrations ( 'app-db' , 'wrangler.json' , runner . run , ( ) => true ) ;
199+
200+ expect ( vi . mocked ( log . info ) ) . toHaveBeenCalledWith (
201+ expect . stringContaining ( 'Saved a D1 restore point before applying 1 migration.' ) ,
202+ ) ;
203+ } ) ;
204+
205+ it ( 'offers no restore point when an overlapping deploy applied every pending migration' , async ( ) => {
206+ const runner = recordingRunner ( {
207+ 'd1.migrations.list' : [ pending ( [ '0001_add_column.sql' ] ) , pending ( [ ] ) , pending ( [ ] ) ] ,
208+ 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
209+ 'd1.migrations.apply' : [ { status : 1 , stdout : '' , stderr : '' } ] ,
210+ } ) ;
211+
212+ const applied = await applyAndVerifyRemoteD1Migrations (
213+ 'app-db' ,
214+ 'wrangler.json' ,
215+ runner . run ,
216+ ( ) => true ,
217+ ) ;
218+
219+ // The bookmark is later than those migrations, so restoring it would not undo them.
220+ expect ( applied ) . toBe ( false ) ;
221+ expect ( vi . mocked ( log . info ) ) . not . toHaveBeenCalled ( ) ;
222+ expect ( errorText ( ) ) . not . toContain ( 'void db restore' ) ;
223+ } ) ;
224+
225+ it ( 'refuses to migrate when the pending list cannot be read after the restore point' , async ( ) => {
226+ const runner = recordingRunner ( {
227+ 'd1.migrations.list' : [
228+ pending ( [ '0001_add_column.sql' ] ) ,
229+ { status : 1 , stdout : '' , stderr : 'Authentication error' } ,
230+ ] ,
231+ 'd1.time-travel.bookmark' : [ { ...clean , data : { bookmark } } ] ,
232+ } ) ;
233+
234+ const applied = await applyAndVerifyRemoteD1Migrations (
235+ 'app-db' ,
236+ 'wrangler.json' ,
237+ runner . run ,
238+ ( ) => true ,
239+ ) ;
240+
241+ expect ( applied ) . toBe ( false ) ;
242+ expect ( runner . calls ) . not . toContain ( 'd1.migrations.apply' ) ;
243+ expect ( errorText ( ) ) . toContain ( 'Cloudflare: Authentication error' ) ;
244+ } ) ;
245+
170246 it ( 'saves no restore point when no migration is pending' , async ( ) => {
171247 const runner = recordingRunner ( { 'd1.migrations.list' : [ pending ( [ ] ) , pending ( [ ] ) ] } ) ;
172248
0 commit comments