diff --git a/orga/changelog/fix-failed-collection-creation-leaves-meta-document.json b/orga/changelog/fix-failed-collection-creation-leaves-meta-document.json new file mode 100644 index 00000000000..1df8f41fc17 --- /dev/null +++ b/orga/changelog/fix-failed-collection-creation-leaves-meta-document.json @@ -0,0 +1,6 @@ +{ + "$schema": "./schema.json", + "text": "FIX `addCollections()` left the collection meta document and the storage instance behind when the creation of the collection failed after the storage instance was created (for example with `COL17`), so that adding the collection again with an adjusted schema threw `DB6`", + "links": [], + "priority": 4 +} diff --git a/src/rx-database.ts b/src/rx-database.ts index 2d948c7bb45..cb31db71136 100644 --- a/src/rx-database.ts +++ b/src/rx-database.ts @@ -55,6 +55,7 @@ import { } from './rx-error.ts'; import { createRxSchema, + getPreviousVersions, RxSchema } from './rx-schema.ts'; import { @@ -416,6 +417,7 @@ export class RxDatabaseBase< * so we can overlap these I/O operations to reduce time-to-first-insert. */ const collectionStorageInstancePromises: { [key: string]: Promise>; } = {}; + const storageInstanceCreationParamsByCollectionName: { [key: string]: RxStorageInstanceCreationParams; } = {}; Object.keys(collectionCreators).forEach((collectionName) => { const useArgs = useArgsByCollectionName[collectionName]; const storageInstanceCreationParams: RxStorageInstanceCreationParams = { @@ -429,6 +431,7 @@ export class RxDatabaseBase< devMode: overwritable.isDevMode() }; runPluginHooks('preCreateRxStorageInstance', storageInstanceCreationParams); + storageInstanceCreationParamsByCollectionName[collectionName] = storageInstanceCreationParams; const promise = createRxCollectionStorageInstance( this.asRxDatabase, storageInstanceCreationParams @@ -506,6 +509,7 @@ export class RxDatabaseBase< throw err; } + const conflictingMetaDocIds = new Set(putDocsResult.error.map(error => error.documentId)); const ret: { [key in keyof CreatedCollections]: RxCollection } = {} as any; await Promise.all( Object.keys(collectionCreators).map(async (collectionName) => { @@ -514,6 +518,44 @@ export class RxDatabaseBase< const collection = await createRxCollection({ ...useArgs, storageInstance + }).catch(async (err) => { + /** + * If the creation fails, the meta document and the storage instance + * that have been created by this call must not stay, + * so that the collection can be added again with another schema. + * When a meta document of a previous schema version exists, + * a migration might have written documents into the new storage instance. + * Then everything is kept so that the migration can be continued later. + * A database that is closing or closed keeps its data, + * and an error of the rollback must not hide the creation error. + */ + if (this.closePromise) { + throw err; + } + const jsonSchema = useArgs.schema.jsonSchema; + try { + const previousMetaDocs = await this.internalStore.findDocumentsById( + getPreviousVersions(jsonSchema).map(version => getPrimaryKeyOfInternalDocument( + collectionName + '-' + version, + INTERNAL_CONTEXT_COLLECTION + )), + false + ); + if ( + !conflictingMetaDocIds.has(getPrimaryKeyOfInternalDocument( + _collectionNamePrimary(collectionName, jsonSchema), + INTERNAL_CONTEXT_COLLECTION + )) && + previousMetaDocs.length === 0 + ) { + const newStorageInstance = await this.storage.createStorageInstance( + storageInstanceCreationParamsByCollectionName[collectionName] + ); + await newStorageInstance.remove(); + await this.removeCollectionDoc(collectionName, jsonSchema); + } + } catch { } + throw err; }); (ret as any)[collectionName] = collection; diff --git a/test/unit/rx-database.test.ts b/test/unit/rx-database.test.ts index db127a7c877..ae8acd29d01 100644 --- a/test/unit/rx-database.test.ts +++ b/test/unit/rx-database.test.ts @@ -441,6 +441,37 @@ describe('rx-database.test.ts', () => { await db2.close(); }); + it('should be able to add the collection with an adjusted schema after the first creation failed', async () => { + const db = await createRxDatabase({ + name: randomToken(10), + storage: config.storage.getStorage() + }); + await AsyncTestUtil.assertThrows( + () => db.addCollections({ + items: { + schema: schemas.human, + statics: { + insert: () => 'foobar' + } + } + }), + 'RxError', + 'COL17' + ); + + const collections = await db.addCollections({ + items: { + schema: { ...schemas.human, description: 'changed' }, + statics: { + foobar: () => 'foobar' + } + } + }); + await collections.items.insert(schemaObjects.humanData()); + const docs = await collections.items.find().exec(); + assert.strictEqual(docs.length, 1); + await db.remove(); + }); }); describe('negative', () => { it('broken schema (nostringIndex)', async () => {