@tldraw/store 3.16.0-internal.a478398270c6 → 3.16.0-internal.f8b97f0c414f

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +13 -13
  2. package/dist-cjs/index.d.ts +10 -42
  3. package/dist-cjs/index.js +1 -2
  4. package/dist-cjs/index.js.map +2 -2
  5. package/dist-cjs/lib/RecordType.js +0 -16
  6. package/dist-cjs/lib/RecordType.js.map +2 -2
  7. package/dist-cjs/lib/RecordsDiff.js +3 -3
  8. package/dist-cjs/lib/RecordsDiff.js.map +2 -2
  9. package/dist-cjs/lib/Store.js +1 -20
  10. package/dist-cjs/lib/Store.js.map +2 -2
  11. package/dist-cjs/lib/StoreSchema.js +24 -8
  12. package/dist-cjs/lib/StoreSchema.js.map +3 -3
  13. package/dist-cjs/lib/migrate.js +57 -43
  14. package/dist-cjs/lib/migrate.js.map +2 -2
  15. package/dist-esm/index.d.mts +10 -42
  16. package/dist-esm/index.mjs +1 -3
  17. package/dist-esm/index.mjs.map +2 -2
  18. package/dist-esm/lib/RecordType.mjs +0 -16
  19. package/dist-esm/lib/RecordType.mjs.map +2 -2
  20. package/dist-esm/lib/RecordsDiff.mjs +3 -3
  21. package/dist-esm/lib/RecordsDiff.mjs.map +2 -2
  22. package/dist-esm/lib/Store.mjs +1 -20
  23. package/dist-esm/lib/Store.mjs.map +2 -2
  24. package/dist-esm/lib/StoreSchema.mjs +24 -8
  25. package/dist-esm/lib/StoreSchema.mjs.map +3 -3
  26. package/dist-esm/lib/migrate.mjs +57 -43
  27. package/dist-esm/lib/migrate.mjs.map +2 -2
  28. package/package.json +12 -19
  29. package/src/index.ts +0 -1
  30. package/src/lib/RecordType.ts +0 -17
  31. package/src/lib/RecordsDiff.ts +9 -3
  32. package/src/lib/Store.ts +1 -22
  33. package/src/lib/StoreSchema.ts +33 -8
  34. package/src/lib/migrate.ts +106 -57
  35. package/src/lib/test/AtomMap.test.ts +2 -1
  36. package/src/lib/test/dependsOn.test.ts +2 -2
  37. package/src/lib/test/migrationCaching.test.ts +209 -0
  38. package/src/lib/test/recordStore.test.ts +40 -37
  39. package/src/lib/test/sortMigrations.test.ts +36 -4
  40. package/src/lib/test/validateMigrations.test.ts +8 -8
  41. package/src/lib/test/defineMigrations.test.ts +0 -232
@@ -107,6 +107,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
107
107
 
108
108
  readonly migrations: Record<string, MigrationSequence> = {}
109
109
  readonly sortedMigrations: readonly Migration[]
110
+ private readonly migrationCache = new WeakMap<SerializedSchema, Result<Migration[], string>>()
110
111
 
111
112
  private constructor(
112
113
  public readonly types: {
@@ -158,10 +159,17 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
158
159
  }
159
160
  }
160
161
 
161
- // TODO: use a weakmap to store the result of this function
162
162
  public getMigrationsSince(persistedSchema: SerializedSchema): Result<Migration[], string> {
163
+ // Check cache first
164
+ const cached = this.migrationCache.get(persistedSchema)
165
+ if (cached) {
166
+ return cached
167
+ }
168
+
163
169
  const upgradeResult = upgradeSchema(persistedSchema)
164
170
  if (!upgradeResult.ok) {
171
+ // Cache the error result
172
+ this.migrationCache.set(persistedSchema, upgradeResult)
165
173
  return upgradeResult
166
174
  }
167
175
  const schema = upgradeResult.value
@@ -178,7 +186,10 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
178
186
  }
179
187
 
180
188
  if (sequenceIdsToInclude.size === 0) {
181
- return Result.ok([])
189
+ const result = Result.ok([])
190
+ // Cache the empty result
191
+ this.migrationCache.set(persistedSchema, result)
192
+ return result
182
193
  }
183
194
 
184
195
  const allMigrationsToInclude = new Set<MigrationId>()
@@ -197,7 +208,10 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
197
208
  const idx = this.migrations[sequenceId].sequence.findIndex((m) => m.id === theirVersionId)
198
209
  // todo: better error handling
199
210
  if (idx === -1) {
200
- return Result.err('Incompatible schema?')
211
+ const result = Result.err('Incompatible schema?')
212
+ // Cache the error result
213
+ this.migrationCache.set(persistedSchema, result)
214
+ return result
201
215
  }
202
216
  for (const migration of this.migrations[sequenceId].sequence.slice(idx + 1)) {
203
217
  allMigrationsToInclude.add(migration.id)
@@ -205,7 +219,12 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
205
219
  }
206
220
 
207
221
  // collect any migrations
208
- return Result.ok(this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id)))
222
+ const result = Result.ok(
223
+ this.sortedMigrations.filter(({ id }) => allMigrationsToInclude.has(id))
224
+ )
225
+ // Cache the result
226
+ this.migrationCache.set(persistedSchema, result)
227
+ return result
209
228
  }
210
229
 
211
230
  migratePersistedRecord(
@@ -263,7 +282,10 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
263
282
  return { type: 'success', value: record }
264
283
  }
265
284
 
266
- migrateStoreSnapshot(snapshot: StoreSnapshot<R>): MigrationResult<SerializedStore<R>> {
285
+ migrateStoreSnapshot(
286
+ snapshot: StoreSnapshot<R>,
287
+ opts?: { mutateInputStore?: boolean }
288
+ ): MigrationResult<SerializedStore<R>> {
267
289
  let { store } = snapshot
268
290
  const migrations = this.getMigrationsSince(snapshot.schema)
269
291
  if (!migrations.ok) {
@@ -276,7 +298,9 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
276
298
  return { type: 'success', value: store }
277
299
  }
278
300
 
279
- store = structuredClone(store)
301
+ if (!opts?.mutateInputStore) {
302
+ store = structuredClone(store)
303
+ }
280
304
 
281
305
  try {
282
306
  for (const migration of migrationsToApply) {
@@ -286,13 +310,13 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
286
310
  if (!shouldApply) continue
287
311
  const result = migration.up!(record as any)
288
312
  if (result) {
289
- store[id as keyof typeof store] = structuredClone(result) as any
313
+ store[id as keyof typeof store] = result as any
290
314
  }
291
315
  }
292
316
  } else if (migration.scope === 'store') {
293
317
  const result = migration.up!(store)
294
318
  if (result) {
295
- store = structuredClone(result) as any
319
+ store = result as any
296
320
  }
297
321
  } else {
298
322
  exhaustiveSwitchError(migration)
@@ -325,6 +349,7 @@ export class StoreSchema<R extends UnknownRecord, P = unknown> {
325
349
 
326
350
  /**
327
351
  * @deprecated This is only here for legacy reasons, don't use it unless you have david's blessing!
352
+ * @internal
328
353
  */
329
354
  serializeEarliestVersion(): SerializedSchema {
330
355
  return {
@@ -2,45 +2,6 @@ import { assert, objectMapEntries } from '@tldraw/utils'
2
2
  import { UnknownRecord } from './BaseRecord'
3
3
  import { SerializedStore } from './Store'
4
4
 
5
- let didWarn = false
6
-
7
- /**
8
- * @public
9
- * @deprecated use `createShapePropsMigrationSequence` instead. See [the docs](https://tldraw.dev/docs/persistence#Updating-legacy-shape-migrations-defineMigrations) for how to migrate.
10
- */
11
- export function defineMigrations(opts: {
12
- firstVersion?: number
13
- currentVersion?: number
14
- migrators?: Record<number, LegacyMigration>
15
- subTypeKey?: string
16
- subTypeMigrations?: Record<string, LegacyBaseMigrationsInfo>
17
- }): LegacyMigrations {
18
- const { currentVersion, firstVersion, migrators = {}, subTypeKey, subTypeMigrations } = opts
19
- if (!didWarn) {
20
- console.warn(
21
- `The 'defineMigrations' function is deprecated and will be removed in a future release. Use the new migrations API instead. See the migration guide for more info: https://tldraw.dev/docs/persistence#Updating-legacy-shape-migrations-defineMigrations`
22
- )
23
- didWarn = true
24
- }
25
-
26
- // Some basic guards against impossible version combinations, some of which will be caught by TypeScript
27
- if (typeof currentVersion === 'number' && typeof firstVersion === 'number') {
28
- if ((currentVersion as number) === (firstVersion as number)) {
29
- throw Error(`Current version is equal to initial version.`)
30
- } else if (currentVersion < firstVersion) {
31
- throw Error(`Current version is lower than initial version.`)
32
- }
33
- }
34
-
35
- return {
36
- firstVersion: (firstVersion as number) ?? 0, // defaults
37
- currentVersion: (currentVersion as number) ?? 0, // defaults
38
- migrators,
39
- subTypeKey,
40
- subTypeMigrations,
41
- }
42
- }
43
-
44
5
  function squashDependsOn(sequence: Array<Migration | StandaloneDependsOn>): Migration[] {
45
6
  const result: Migration[] = []
46
7
  for (let i = sequence.length - 1; i >= 0; i--) {
@@ -199,38 +160,126 @@ export interface MigrationSequence {
199
160
  sequence: Migration[]
200
161
  }
201
162
 
163
+ /**
164
+ * Sorts migrations using a distance-minimizing topological sort.
165
+ *
166
+ * This function respects two types of dependencies:
167
+ * 1. Implicit sequence dependencies (foo/1 must come before foo/2)
168
+ * 2. Explicit dependencies via `dependsOn` property
169
+ *
170
+ * The algorithm minimizes the total distance between migrations and their explicit
171
+ * dependencies in the final ordering, while maintaining topological correctness.
172
+ * This means when migration A depends on migration B, A will be scheduled as close
173
+ * as possible to B (while respecting all constraints).
174
+ *
175
+ * Implementation uses Kahn's algorithm with priority scoring:
176
+ * - Builds dependency graph and calculates in-degrees
177
+ * - Uses priority queue that prioritizes migrations which unblock explicit dependencies
178
+ * - Processes migrations in urgency order while maintaining topological constraints
179
+ * - Detects cycles by ensuring all migrations are processed
180
+ *
181
+ * @param migrations - Array of migrations to sort
182
+ * @returns Sorted array of migrations in execution order
183
+ */
202
184
  export function sortMigrations(migrations: Migration[]): Migration[] {
203
- // we do a topological sort using dependsOn and implicit dependencies between migrations in the same sequence
204
- const byId = new Map(migrations.map((m) => [m.id, m]))
205
- const isProcessing = new Set<MigrationId>()
185
+ if (migrations.length === 0) return []
206
186
 
207
- const result: Migration[] = []
187
+ // Build dependency graph and calculate in-degrees
188
+ const byId = new Map(migrations.map((m) => [m.id, m]))
189
+ const dependents = new Map<MigrationId, Set<MigrationId>>() // who depends on this
190
+ const inDegree = new Map<MigrationId, number>()
191
+ const explicitDeps = new Map<MigrationId, Set<MigrationId>>() // explicit dependsOn relationships
208
192
 
209
- function process(m: Migration) {
210
- assert(!isProcessing.has(m.id), `Circular dependency in migrations: ${m.id}`)
211
- isProcessing.add(m.id)
193
+ // Initialize
194
+ for (const m of migrations) {
195
+ inDegree.set(m.id, 0)
196
+ dependents.set(m.id, new Set())
197
+ explicitDeps.set(m.id, new Set())
198
+ }
212
199
 
200
+ // Add implicit sequence dependencies and explicit dependencies
201
+ for (const m of migrations) {
213
202
  const { version, sequenceId } = parseMigrationId(m.id)
214
- const parent = byId.get(`${sequenceId}/${version - 1}`)
215
- if (parent) {
216
- process(parent)
203
+
204
+ // Implicit dependency on previous in sequence
205
+ const prevId = `${sequenceId}/${version - 1}` as MigrationId
206
+ if (byId.has(prevId)) {
207
+ dependents.get(prevId)!.add(m.id)
208
+ inDegree.set(m.id, inDegree.get(m.id)! + 1)
217
209
  }
218
210
 
211
+ // Explicit dependencies
219
212
  if (m.dependsOn) {
220
- for (const dep of m.dependsOn) {
221
- const depMigration = byId.get(dep)
222
- if (depMigration) {
223
- process(depMigration)
213
+ for (const depId of m.dependsOn) {
214
+ if (byId.has(depId)) {
215
+ dependents.get(depId)!.add(m.id)
216
+ explicitDeps.get(m.id)!.add(depId)
217
+ inDegree.set(m.id, inDegree.get(m.id)! + 1)
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ // Priority queue: migrations ready to process (in-degree 0)
224
+ const ready = migrations.filter((m) => inDegree.get(m.id) === 0)
225
+ const result: Migration[] = []
226
+ const processed = new Set<MigrationId>()
227
+
228
+ while (ready.length > 0) {
229
+ // Calculate urgency scores for ready migrations and pick the best one
230
+ let bestCandidate: Migration | undefined
231
+ let bestCandidateScore = -Infinity
232
+
233
+ for (const m of ready) {
234
+ let urgencyScore = 0
235
+
236
+ for (const depId of dependents.get(m.id) || []) {
237
+ if (!processed.has(depId)) {
238
+ // Priority 1: Count all unprocessed dependents (to break ties)
239
+ urgencyScore += 1
240
+
241
+ // Priority 2: If this migration is explicitly depended on by others, boost priority
242
+ if (explicitDeps.get(depId)!.has(m.id)) {
243
+ urgencyScore += 100
244
+ }
224
245
  }
225
246
  }
247
+
248
+ if (
249
+ urgencyScore > bestCandidateScore ||
250
+ // Tiebreaker: prefer lower sequence/version
251
+ (urgencyScore === bestCandidateScore && m.id.localeCompare(bestCandidate?.id ?? '') < 0)
252
+ ) {
253
+ bestCandidate = m
254
+ bestCandidateScore = urgencyScore
255
+ }
226
256
  }
227
257
 
228
- byId.delete(m.id)
229
- result.push(m)
258
+ const nextMigration = bestCandidate!
259
+ ready.splice(ready.indexOf(nextMigration), 1)
260
+
261
+ // Cycle detection - if we have processed everything and still have items left, there's a cycle
262
+ // This is handled by Kahn's algorithm naturally - if we finish with items unprocessed, there's a cycle
263
+
264
+ // Process this migration
265
+ result.push(nextMigration)
266
+ processed.add(nextMigration.id)
267
+
268
+ // Update in-degrees and add newly ready migrations
269
+ for (const depId of dependents.get(nextMigration.id) || []) {
270
+ if (!processed.has(depId)) {
271
+ inDegree.set(depId, inDegree.get(depId)! - 1)
272
+ if (inDegree.get(depId) === 0) {
273
+ ready.push(byId.get(depId)!)
274
+ }
275
+ }
276
+ }
230
277
  }
231
278
 
232
- for (const m of byId.values()) {
233
- process(m)
279
+ // Check for cycles - if we didn't process all migrations, there's a cycle
280
+ if (result.length !== migrations.length) {
281
+ const unprocessed = migrations.filter((m) => !processed.has(m.id))
282
+ assert(false, `Circular dependency in migrations: ${unprocessed[0].id}`)
234
283
  }
235
284
 
236
285
  return result
@@ -1,4 +1,5 @@
1
1
  import { react, transaction } from '@tldraw/state'
2
+ import { vi } from 'vitest'
2
3
  import { AtomMap } from '../AtomMap'
3
4
 
4
5
  describe('AtomMap', () => {
@@ -11,7 +12,7 @@ describe('AtomMap', () => {
11
12
  })
12
13
 
13
14
  function testReactor(name: string, fn: () => any) {
14
- const cb = jest.fn(fn)
15
+ const cb = vi.fn(fn)
15
16
  const cleanup = react(name, cb)
16
17
  cleanupFns.push(() => cleanup())
17
18
  return cb
@@ -26,7 +26,7 @@ describe('dependsOn', () => {
26
26
  }
27
27
  )
28
28
  }).toThrowErrorMatchingInlineSnapshot(
29
- `"Migration 'foo/1' depends on missing migration 'bar/1'"`
29
+ `[Error: Migration 'foo/1' depends on missing migration 'bar/1']`
30
30
  )
31
31
  })
32
32
 
@@ -108,7 +108,7 @@ describe('standalone dependsOn', () => {
108
108
  }
109
109
  )
110
110
  }).toThrowErrorMatchingInlineSnapshot(
111
- `"Migration 'foo/1' depends on missing migration 'bar/1'"`
111
+ `[Error: Migration 'foo/1' depends on missing migration 'bar/1']`
112
112
  )
113
113
  })
114
114
 
@@ -0,0 +1,209 @@
1
+ /* eslint-disable @typescript-eslint/no-deprecated */
2
+ import { assert } from '@tldraw/utils'
3
+ import {
4
+ BaseRecord,
5
+ Migration,
6
+ RecordId,
7
+ createMigrationIds,
8
+ createMigrationSequence,
9
+ createRecordType,
10
+ } from '../../'
11
+ import { StoreSchema } from '../StoreSchema'
12
+
13
+ interface TestRecord extends BaseRecord<'test', RecordId<TestRecord>> {
14
+ name: string
15
+ version: number
16
+ }
17
+
18
+ describe('StoreSchema migration caching', () => {
19
+ // Create migration IDs
20
+ const TestVersions = createMigrationIds('com.tldraw.test', {
21
+ AddVersion: 1,
22
+ UpdateVersion: 2,
23
+ })
24
+
25
+ // Create a simple schema with migrations
26
+ const createTestSchema = (version: number) => {
27
+ const TestRecordType = createRecordType<TestRecord>('test', {
28
+ scope: 'document',
29
+ })
30
+
31
+ const sequence: Migration[] = []
32
+
33
+ if (version > 1) {
34
+ sequence.push({
35
+ id: TestVersions.AddVersion,
36
+ scope: 'record',
37
+ up: (record: any) => {
38
+ // Mutate the record in place
39
+ record.version = 2
40
+ // Don't return anything
41
+ },
42
+ down: (record: any) => {
43
+ record.version = 1
44
+ // Don't return anything
45
+ },
46
+ })
47
+ }
48
+
49
+ if (version > 2) {
50
+ sequence.push({
51
+ id: TestVersions.UpdateVersion,
52
+ scope: 'record',
53
+ up: (record: any) => {
54
+ record.version = 3
55
+ // Don't return anything
56
+ },
57
+ down: (record: any) => {
58
+ record.version = 2
59
+ // Don't return anything
60
+ },
61
+ })
62
+ }
63
+
64
+ const schema = StoreSchema.create(
65
+ {
66
+ test: TestRecordType,
67
+ },
68
+ {
69
+ migrations: [createMigrationSequence({ sequenceId: 'com.tldraw.test', sequence })],
70
+ }
71
+ )
72
+
73
+ return schema
74
+ }
75
+
76
+ it('should cache migration results and return same array reference', () => {
77
+ const schema = createTestSchema(3)
78
+ const oldSchema = schema.serializeEarliestVersion()
79
+
80
+ // First call should create the migrations array
81
+ const migrations1 = schema.getMigrationsSince(oldSchema)
82
+ assert(migrations1.ok)
83
+ expect(migrations1.value).toHaveLength(2)
84
+
85
+ // Second call should return the same array reference (cached)
86
+ const migrations2 = schema.getMigrationsSince(oldSchema)
87
+ assert(migrations2.ok)
88
+ expect(migrations2.value).toBe(migrations1.value) // Same array reference
89
+
90
+ // Third call should also return the same array reference
91
+ const migrations3 = schema.getMigrationsSince(oldSchema)
92
+ assert(migrations3.ok)
93
+ expect(migrations3.value).toBe(migrations1.value)
94
+ })
95
+
96
+ it('should not cache when schema versions are different', () => {
97
+ const schema = createTestSchema(3)
98
+ const oldSchema = schema.serializeEarliestVersion()
99
+
100
+ // Call with original schema
101
+ const migrations1 = schema.getMigrationsSince(oldSchema)
102
+ expect(migrations1.ok).toBe(true)
103
+ if (!migrations1.ok) throw new Error('Expected migrations1 to be ok')
104
+
105
+ // Create a different schema version by using a schema with version 2
106
+ const schemaV2 = createTestSchema(2)
107
+ const schemaV2Serialized = schemaV2.serializeEarliestVersion()
108
+ const migrations2 = schema.getMigrationsSince(schemaV2Serialized)
109
+ expect(migrations2.ok).toBe(true)
110
+ if (!migrations2.ok) throw new Error('Expected migrations2 to be ok')
111
+
112
+ // Should be different arrays (no cache hit)
113
+ expect(migrations2.value).not.toBe(migrations1.value)
114
+ })
115
+
116
+ it('should handle mutateInputStore: true with migrators that return void', () => {
117
+ const schema = createTestSchema(3)
118
+ const oldSchema = schema.serializeEarliestVersion()
119
+
120
+ const store = {
121
+ test1: {
122
+ id: 'test1',
123
+ name: 'Test 1',
124
+ version: 1,
125
+ typeName: 'test',
126
+ },
127
+ test2: {
128
+ id: 'test2',
129
+ name: 'Test 2',
130
+ version: 1,
131
+ typeName: 'test',
132
+ },
133
+ }
134
+
135
+ // Test with mutateInputStore: true
136
+ const result1 = schema.migrateStoreSnapshot(
137
+ { store, schema: oldSchema },
138
+ { mutateInputStore: true }
139
+ )
140
+
141
+ assert(result1.type === 'success')
142
+ expect((result1.value as any).test1.version).toBe(3)
143
+ expect((result1.value as any).test2.version).toBe(3)
144
+
145
+ // The input store should be mutated in place
146
+ expect(result1.value).toBe(store)
147
+ })
148
+
149
+ it('should handle mutateInputStore: false with migrators that return void', () => {
150
+ const schema = createTestSchema(3)
151
+ const oldSchema = schema.serializeEarliestVersion()
152
+
153
+ const store = {
154
+ test1: {
155
+ id: 'test1',
156
+ name: 'Test 1',
157
+ version: 1,
158
+ typeName: 'test',
159
+ },
160
+ }
161
+
162
+ // Test with mutateInputStore: false (default)
163
+ const result = schema.migrateStoreSnapshot({ store, schema: oldSchema })
164
+
165
+ assert(result.type === 'success')
166
+ expect((result.value as any).test1.version).toBe(3)
167
+
168
+ // The input store should NOT be mutated
169
+ expect(store.test1.version).toBe(1)
170
+ })
171
+
172
+ it('should handle empty migration list caching', () => {
173
+ const schema = createTestSchema(1) // No migrations
174
+ const oldSchema = schema.serializeEarliestVersion()
175
+
176
+ // First call
177
+ const migrations1 = schema.getMigrationsSince(oldSchema)
178
+ assert(migrations1.ok)
179
+
180
+ expect(migrations1.value).toHaveLength(0)
181
+
182
+ // Second call should return same array reference
183
+ const migrations2 = schema.getMigrationsSince(oldSchema)
184
+ assert(migrations2.ok)
185
+ expect(migrations2.value).toBe(migrations1.value)
186
+ expect(migrations2.value).toHaveLength(0)
187
+ })
188
+
189
+ it('should handle incompatible schema caching', () => {
190
+ const schema = createTestSchema(3)
191
+ const incompatibleSchema = {
192
+ schemaVersion: 1 as const,
193
+ storeVersion: 1,
194
+ recordVersions: {
195
+ test: {
196
+ version: 999, // Much higher version than what we support
197
+ },
198
+ },
199
+ }
200
+
201
+ // First call should fail
202
+ const migrations1 = schema.getMigrationsSince(incompatibleSchema)
203
+ expect(migrations1.ok).toBe(false)
204
+
205
+ // Second call should also fail (but might be cached)
206
+ const migrations2 = schema.getMigrationsSince(incompatibleSchema)
207
+ expect(migrations2.ok).toBe(false)
208
+ })
209
+ })