Users encountering ConstraintError making Dexie unusable
This is most likely something we are doing wrong with Dexie, however we've noticed a lot of instances of the following error being encountered by our users:
ConstraintError Failed to execute 'createObjectStore' on 'IDBDatabase': An object store with the specified name already exists.
related MDN docs
Initially we thought it was something mostly out of our control, like QuotaExceededError, and said we should probably just improve our error handling to stop spamming our error tracking service, although now we are having reports from users encountering this error where it breaks all data interactions.
As of writing, we don't know how to reproduce it. I've spent a bit of time searching around, however cannot find much useful info. I wanted to open this issue in case anyone here knows any Dexie-specific info regarding this error. Particularly what types of Dexie usages could lead to it being thrown.
Related: https://github.com/WorldBrain/Memex/issues/534
I've never heard anyone having this problem with Dexie before. To get a clue of a reason, could you point me to where in your code you declare your dexie instance (like a github url for example).
Thanks for the reply, David.
I've never heard anyone having this problem with Dexie before.
Yeah, searching around I couldn't find any info relating this error back to other uses of Dexie (sadly for us).
could you point me to where in your code you declare your dexie instance
Sure.
We currently have a class that extends the main Dexie class here:
https://github.com/WorldBrain/Memex/blob/d3f6503cc04d725cdbbd0941edabe58b3095fe52/src/search/storage/index.ts#L18
It gets instantiated here: https://github.com/WorldBrain/Memex/blob/d3f6503cc04d725cdbbd0941edabe58b3095fe52/src/search/index.ts#L25
Note that we have been building a bit of an abstraction around Dexie, which we call "StorageManager". This will explain the non-Dexie-like schema declarations in _initSchema method. They'll eventually get translated to Dexie schemas at another level and passed back to the Dexie instance via version().stores() (quite a confusing flow, but still a bit of a WIP until we refactor our Dexie code out).
I also understand the cause of this issue could very well be to do with the way we're handling this at our StorageManager->Dexie interaction layer, and we'll probably need to do a full audit of that code, although Dexie is still the only thing that interacts with IndexedDB in our codebase (AFAIK). Hence why I'm posting here in case anyone did have any ideas why this happens or how I could reproduce this error with just plain Dexie.
I suppose the error is that the dexieHistory does not represent a true picture of the real installed database. For example, if an earlier version (say version 2) installed a table called "tags" but calling this.version(2).stores({...}) omits tags for some reason (finalVersion is wrong or dexieHistory is incomplete), Dexie will believe tags has to be created now in version 3, since it was not listed earlier. However, when Dexie does try to create it, it will get that error, since it's already there.
It would be good to log finalVersion, finalSchema and migrations just before this call. To show what was actually installed before, it would also be good to log the current state of the DB schema before calling this.stores().version(). An example of reading the currently installed schema is found here. It basically opens the database dynamically (without specifying any version()) and then dump the schema as it was read from the db.
Here's an easier fiddle (to log the current installed schema) to use: https://jsfiddle.net/dfahlander/b8Levamm/
Thanks for the patience and insight, David! We currently can't reproduce this issue in our team, but we'll have to try and get in touch with an affected user to see if we can get them to paste and run some code in their installed extension to see what their derived Dexie schemas look like and the current schema state. Managed to derive a snippet based on the dump code you linked which will hopefully shed some more light.
Is there any significance to the ordering of the field entries in table schemas, apart from the PK? The only thing I've noticed so far after seeing the generated schemas of two affected users is some tables have differing entry orders for the same versions.
e.g.:
customLists: "++id, createdAt, isNestable, isDeletable, &name"
and
customLists: "++id, &name, createdAt, isNestable, isDeletable"
No, there's no significance of the order. Actually this only implies that those databases have been upgraded (for example if isDeletable or name index was added in a certain version). Dexie will not use any order when looking up an index. Neither should the native indexedDB API care about order. I doubt the order of indexes is specified in the W3C spec but a natural consequence of the order in which indexes was added.
I've encountered this error immediately upon using Dexie. My entire code is:
var db = new Dexie('jreodb')
db.version(1).stores({
episodes: "id++,title,tags,views_string,tags"
})
function testMax() {
db.episodes.orderBy("number").last().then(function(maxEpisodeObject){
console.log("Max episode object:")
console.log(maxEpisodeObject)
}).catch(function(error){
console.log("There was an error: " + error)
})
}
Seems to be happening upon call to Collection.last('number'). It's a huge bummer - I was really liking Dexie.js from a simplicity and API usability standpoint, but unfortunately this error keeps me from being able to even begin implementing the library in my project.
Pre-Post Edit: As you can see in my above code, I've included "tags" as an indexed column twice in my call to .stores(schema). Upon removing the second erroneous "tags" from the end of the schema string, the error goes away. I propose adding a check for duplicates in calls to "store" and throw an error upon multiple inclusions of the same indexed column name, considering that seems to be at least one cause of the mysterious ConstraintError being thrown.
@iway1 I get it - should give better error messages for this common mistake. Any PR is extremely welcome! Am I correct if you intentionally do orderBy() using a non-indexed property ("number") to trigger the strange ConstraintError? I would suppose that the constraint error happens in the implicit call to db.open() as it tries to create duplicate identical indexes though?
I get this error since about one week after not working on the project for about two weeks. I'm using Dexie 3.2.0 with Angular 10.2 (no discussions about the version, there are reasons!) following the https://dexie.org/docs/Typescript guide. I have three different databases, two for functional data of completely different parts of the app and therefore in seperated DBs. They still work fine. The third one is a database for recording application and performance logs, which floods the console with these variants of the error:
Unhandled rejection: OpenFailedError: ConstraintError Failed to execute 'createIndex' on 'IDBObjectStore': An index with the specified name already exists.
ConstraintError: Failed to execute 'createIndex' on 'IDBObjectStore': An index with the specified name already exists.
at push.Texg.Transaction.create (http://localhost:4200/vendor.js:161766:27)
at http://localhost:4200/vendor.js:162913:23
at http://localhost:4200/vendor.js:160250:23
at callListener (http://localhost:4200/vendor.js:159972:19)
at endMicroTickScope (http://localhost:4200/vendor.js:160044:25)
at physicalTick (http://localhost:4200/vendor.js:160027:30)
at ZoneDelegate.invoke (http://localhost:4200/polyfills.js:3239:30)
at Object.onInvoke (http://localhost:4200/vendor.js:220583:33)
From previous:
at DexiePromise.then (http://localhost:4200/vendor.js:159709:22)
at enterTransactionScope (http://localhost:4200/vendor.js:162901:35)
at http://localhost:4200/vendor.js:160250:23
at callListener (http://localhost:4200/vendor.js:159972:19)
at endMicroTickScope (http://localhost:4200/vendor.js:160044:25)
at physicalTick (http://localhost:4200/vendor.js:160027:30)
at ZoneDelegate.invoke (http://localhost:4200/polyfills.js:3239:30)
at Object.onInvoke (http://localhost:4200/vendor.js:220583:33)
From previous:
at Function.resolve (http://localhost:4200/vendor.js:159812:18)
at enterTransactionScope (http://localhost:4200/vendor.js:162901:25)
at http://localhost:4200/vendor.js:160250:23
at callListener (http://localhost:4200/vendor.js:159972:19)
at endMicroTickScope (http://localhost:4200/vendor.js:160044:25)
at physicalTick (http://localhost:4200/vendor.js:160027:30)
at ZoneDelegate.invoke (http://localhost:4200/polyfills.js:3239:30)
at Object.onInvoke (http://localhost:4200/vendor.js:220583:33)
From previous:
at push.Texg.Dexie._whenReady (http://localhost:4200/vendor.js:163747:99)
at push.Texg.Dexie._transaction (http://localhost:4200/vendor.js:163920:22)
at push.Texg.Dexie.transaction (http://localhost:4200/vendor.js:163862:34)
at AppLogDatabase.log (http://localhost:4200/main.js:6348:25)
at AppLogDatabase.info (http://localhost:4200/main.js:6234:21)
at OfflineService.<anonymous> (http://localhost:4200/main.js:11673:18)
at Generator.next (<anonymous>)
at http://localhost:4200/vendor.js:243139:71
Unhandled rejection: DatabaseClosedError: ConstraintError Failed to execute 'createIndex' on 'IDBObjectStore': An index with the specified name already exists.
ConstraintError: Failed to execute 'createIndex' on 'IDBObjectStore': An index with the specified name already exists.
at http://localhost:4200/vendor.js:163749:31
at executePromiseTask (http://localhost:4200/vendor.js:159886:9)
at new DexiePromise (http://localhost:4200/vendor.js:159700:5)
at push.Texg.Dexie._whenReady (http://localhost:4200/vendor.js:163747:99)
at push.Texg.Dexie._transaction (http://localhost:4200/vendor.js:163920:22)
at push.Texg.Dexie.transaction (http://localhost:4200/vendor.js:163862:34)
at AppLogDatabase.log (http://localhost:4200/main.js:6348:25)
at AppLogDatabase.info (http://localhost:4200/main.js:6234:21)
From previous:
at push.Texg.Dexie._whenReady (http://localhost:4200/vendor.js:163747:99)
at push.Texg.Dexie._transaction (http://localhost:4200/vendor.js:163920:22)
at push.Texg.Dexie.transaction (http://localhost:4200/vendor.js:163862:34)
at AppLogDatabase.log (http://localhost:4200/main.js:6348:25)
at AppLogDatabase.info (http://localhost:4200/main.js:6234:21)
at MapSubscriber.project (http://localhost:4200/main.js:36718:72)
at MapSubscriber._next (http://localhost:4200/vendor.js:235493:35)
at MapSubscriber.next (http://localhost:4200/vendor.js:90605:18)
Unhandled rejection: AbortError: Transaction aborted
at IDBTransaction.<anonymous> (http://localhost:4200/vendor.js:161782:43)
at IDBTransaction.__zone_symbol__ON_PROPERTYabort (http://localhost:4200/vendor.js:160095:23)
at IDBTransaction.wrapFn (http://localhost:4200/polyfills.js:4093:43)
at ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:3274:35)
at Object.onInvokeTask (http://localhost:4200/vendor.js:220571:33)
at ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:3273:40)
at Zone.runTask (http://localhost:4200/polyfills.js:3042:51)
at ZoneTask.invokeTask [as invoke] (http://localhost:4200/polyfills.js:3355:38)
From previous:
at new Transaction (http://localhost:4200/vendor.js:161904:28)
at Dexie._createTransaction (http://localhost:4200/vendor.js:163713:101)
at runUpgraders (http://localhost:4200/vendor.js:162403:20)
at IDBOpenDBRequest.<anonymous> (http://localhost:4200/vendor.js:162810:21)
at IDBOpenDBRequest.__zone_symbol__ON_PROPERTYupgradeneeded (http://localhost:4200/vendor.js:160095:23)
at IDBOpenDBRequest.wrapFn (http://localhost:4200/polyfills.js:4093:43)
at ZoneDelegate.invokeTask (http://localhost:4200/polyfills.js:3274:35)
at Object.onInvokeTask (http://localhost:4200/vendor.js:220571:33)
Variant 1 and 2 only differ in the call stack leading to the call of AppLogDatabase.log.
The error seems to happen on this.transaction('rw!', table, callback). I'm not able to catch the error, neither it inside callback nor wrapping the whole call in a try .. catch block.
Here is the schema definition:
protected initSchema() {
this.version(5).stores({
entries: '++id,[type+_time],[level+_time],[_timeFormatted+type],[_timeFormatted+level],[type+level],_time,_timeFormatted,[_time+type],[_timeFormatted+type]',
performance: '++id,[type+_time],[level+_time],[_timeFormatted+type],[_timeFormatted+level],[type+level],_time,_timeFormatted,[_time+type],[_timeFormatted+type]',
});
// Just informing Typescript what Dexie has already done...
this.entries = this.table('entries');
this.performance = this.table('performance');
this.entries.mapToClass(LogEntry);
}
Maybe it treats the indices [type+_time] and [_time+type] as equal?
Update after posting the comment
I've just realized that I've accidentally duplicated the index [_timeFormatted+type] in both tables. Maybe because the list of indices is quite long and when working with two code editor tabs side-by-side I couldn't see that the index is already there.
Suggestion
In my case the error message An index with the specified name already exists. was 100% correct, but still it was not easy to find out what's going on and took me several hours to find out. So improve the error message by adding relevant information to help isolating the cause:
An index with the specified name "[index-name]" already exists in [database-name].[table-name].
The entries table define [_timeFormatted+type] twice in the same table (So does also the performance table).
@dfahlander thanks, I noticed that after (or by) posting my comment and updated it accordingly. Furthermore I added a suggestion how to improve the error message.
Thanks for the improvement suggestion. When having many indexes on a table, they can be declared in a template string to get them one line at a time:
this.version(5).stores({
entries: `
++id,
[type+_time],
[level+_time],
[_timeFormatted+type],
[_timeFormatted+level],
[type+level],
_time,
_timeFormatted,
[_time+type],
[_timeFormatted+type]`,
...
});
When looking at your index list I see that you could remove another 2 indexes that are already included in some compound versions:
_timeis already included in[_time+type]_timeFormattedis already included in[_timeFormatted+type]
So the resulting list you'd need would be:
this.version(6).stores({
entries: `
++id,
[type+_time],
[level+_time],
[_timeFormatted+type],
[_timeFormatted+level],
[type+level],
[_time+type]`
});