IndexedDB on iOS 8 - Broken Bad

This post is more than 2 years old.

Let me begin by saying that credit for this find goes to @jonnyknowsbest on Twitter and his SO post here: Primary Key issue on iOS8 implementation of IndexedDb. I did my research into this issue early this morning and I hope that I, and jonny, are both wrong. I'd love to be wrong about this. Unfortunately, I don't think that is the case.

So, as you know, iOS 8 finally brought IndexedDB to Mobile Safari. I may be biased, but I find features like this far more useful than CSS updates. Not to say that I don't appreciate them, but to me, deep data storage on the client is something that is more practical and useful to more people. Of course, I work for a company that is all about designers and not developers, so what do I know? ;)

Unfortunately, it seems as if Apple may have screwed up their implementation of IndexedDB - and screwed it up bad. Like real bad. If you read the SO post I linked to above, you will see that he was using assigned IDs and discovered that if you assigned the same ID to data in two datastores, then the data inserted in the first objectstore is removed. Let me restate that just to be obvious.

Imagine you have two object stores, people and beer. You want to add an object to both, and in both cases, you use a hard coded primary key of 1. When you do, no error is thrown, but the person object is deleted. Only beer remains. (Not the worst result...) Here is a full example showing this bug in action.

<!doctype html>
<html>
<head>
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
</head>
    
<body>

<script>
var db;

function indexedDBOk() {
	return "indexedDB" in window;
}

document.addEventListener("DOMContentLoaded", function() {

	//No support? Go in the corner and pout.
	if(!indexedDBOk()) return;

	var openRequest = indexedDB.open("ios8b",1);

	openRequest.onupgradeneeded = function(e) {
		var thisDB = e.target.result;

		console.log("running onupgradeneeded");

		if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {keyPath:"id"});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {keyPath:"uid"});
		}

	}

	openRequest.onsuccess = function(e) {
		console.log("running onsuccess");

		db = e.target.result;

		console.log("Current Object Stores");
		console.dir(db.objectStoreNames);

		//Listen for add clicks
		document.querySelector("#addButton").addEventListener("click", addPerson, false);
	}	

	openRequest.onerror = function(e) {
		//Do something for the error
	}


},false);


function addPerson(e) {
	console.log("About to add person and note");

	var id = Number(document.querySelector("#key").value);
	
	//Get a transaction
	//default for OS list is all, default for type is read
	var transaction = db.transaction(["people"],"readwrite");
	//Ask for the objectStore
	var store = transaction.objectStore("people");

	//Define a person
	var person = {
		name:"Ray",
		created:new Date().toString(),
		id:id
	}

	//Perform the add
	var request = store.add(person);

	request.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
	//Define a note
	var note = {
		note:"note",
		created:new Date().toString(),
		uid:id
	}

	var transaction2 = db.transaction(["notes"],"readwrite");
	//Ask for the objectStore
	var store2 = transaction2.objectStore("notes");

	//Perform the add
	var request2 = store2.add(note);

	request2.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request2.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
}
</script>

enter key: <input id="key"><br/>
<button id="addButton">Add Data</button>

</body>
</html>

This demo uses a simple form to ask you for a PK. When you click the button, it then adds a static person and note object using the value you gave for a PK. When you run this, no error is thrown. The success handler for both operations is run. But the data you created for the person is gone. This is horrible.

But wait! Who uses defined primary keys? Only nerds! I like auto incrementing keys, so why not just switch to that? Simple enough, right? I made a new demo, with a new database, and modified my objectstores:

		if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {autoIncrement:true});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {autoIncrement:true});
		}

And the same damn error occurs. I kid you not. Ok, fine iOS. So I then tried something else. According to the spec, you can create a transaction with multiple objectstores. I thought, maybe if I did that, iOS would handle the inserts better. So let's try this:

var transaction = db.transaction(["people","notes"],"readwrite");

But this threw an error: DOM IDBDatabase Exception 8: An operation failed because the requested database object could not be found.

Ok, so next I thought - what if we used autoIncrement and different key names. Maybe the key name being the same was confusing things:

		if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {autoIncrement:true,keyPath:"appleisshit"});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {autoIncrement:true,keyPath:"id"});
		}

Nope, same error. So... finally I gave up. I specified an ID number and prefixed it with a string.


function addPerson(e) {
	console.log("About to add person and note");

	var id = document.querySelector("#key").value;
	
	//Get a transaction
	//default for OS list is all, default for type is read
	var transaction = db.transaction(["people"],"readwrite");
	//Ask for the objectStore
	var store = transaction.objectStore("people");

	//Define a person
	var person = {
		name:"Ray",
		created:new Date().toString(),
		id:"people/"+id
	}

	//Perform the add
	var request = store.add(person);

	request.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
	//Define a note
	var note = {
		note:"note",
		created:new Date().toString(),
		uid:"notes/"+id
	}

	var transaction2 = db.transaction(["notes"],"readwrite");
	//Ask for the objectStore
	var store2 = transaction2.objectStore("notes");

	//Perform the add
	var request2 = store2.add(note);

	request2.onerror = function(e) {
		console.log("Error",e.target.error.name);
		//some type of error handler
	}

	request2.onsuccess = function(e) {
		console.log("Woot! Did it");
	}
	
}

This worked. Of course, you still have the suck part of creating your own keys. You can, however, ask the objectstore for the size and simply increment yourself. I wrote up a new version that does this. This seems to work well and for now is what I'd recommend. It works fine in Chrome too so it isn't "harmful" to use this workaround.

<!doctype html>
<html>
<head>
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
</head>
    
<body>

<script>
var db;

function indexedDBOk() {
	return "indexedDB" in window;
}

document.addEventListener("DOMContentLoaded", function() {

	//No support? Go in the corner and pout.
	if(!indexedDBOk()) return;

	var openRequest = indexedDB.open("ios8_final3",1);

	openRequest.onupgradeneeded = function(e) {
		var thisDB = e.target.result;

		console.log("running onupgradeneeded");

		if(!thisDB.objectStoreNames.contains("people")) {
			thisDB.createObjectStore("people", {keyPath:"id"});
		}

		if(!thisDB.objectStoreNames.contains("notes")) {
			thisDB.createObjectStore("notes", {keyPath:"uid"});
		}

	}

	openRequest.onsuccess = function(e) {
		console.log("running onsuccess");

		db = e.target.result;

		console.log("Current Object Stores");
		console.dir(db.objectStoreNames);

		//Listen for add clicks
		document.querySelector("#addButton").addEventListener("click", addPerson, false);
	}	

	openRequest.onerror = function(e) {
		//Do something for the error
	}


},false);


function addPerson(e) {
	console.log("About to add person and note");


	//Define a person
	var person = {
		name:"Ray",
		created:new Date().toString(),
	}
	
	//Perform the add
	db.transaction(["people"],"readwrite").objectStore("people").count().onsuccess = function(event) {
		var total = event.target.result;
		console.log(total);
		person.id = "person/" + (total+1);
		
		var request = db.transaction(["people"],"readwrite").objectStore("people").add(person);
		
		request.onerror = function(e) {
			console.log("Error",e.target.error.name);
			//some type of error handler
		}

		request.onsuccess = function(e) {
			console.log("Woot! Did it");
		}

	}

	//Define a note
	var note = {
		note:"note",
		created:new Date().toString(),
	}

	db.transaction(["notes"],"readwrite").objectStore("notes").count().onsuccess = function(event) {
		var total = event.target.result;
		console.log(total);
		note.uid = "notes/" + (total+1);
		
		var request = db.transaction(["notes"],"readwrite").objectStore("notes").add(note);
		
		request.onerror = function(e) {
			console.log("Error",e.target.error.name);
			//some type of error handler
		}

		request.onsuccess = function(e) {
			console.log("Woot! Did it");
		}

	}
	
}
</script>

<button id="addButton">Add Data</button>

</body>
</html>

I hope this helps folks. As I said, maybe I'm being stupid and missing something obvious. I hope so. But considering that iOS 8 also broke file uploads (both "regular" and via XHR2), it isn't too surprising that this could be broken as well. I'm going to file a bug report now. If their reporting system supports sharing the URL, I'll do so in a comment.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Joseph Labrecque posted on 9/25/2014 at 5:45 PM

"I work for a company that is all about designers and not developers, so what do I know?" <= Oh no he didn't!!!

(honestly though - good to know)

Comment 2 by Raymond Camden posted on 9/25/2014 at 5:50 PM

I submitted it to bugreport.apple.com. The ID is 18454000. From what I can see though there is no way to share this publicly.

Comment 3 by Jonathan Smith posted on 9/25/2014 at 5:56 PM

I filed a bug report yesterday, I got id: 18440001 so assuming the bug numbers run sequentially, 13999 bug reports have been filed in just under 23 hours. Ouch!

Comment 4 by Raymond Camden posted on 9/25/2014 at 6:00 PM

Is that you, johnnyknowsbest?

Comment 5 by Jonathan Smith posted on 9/25/2014 at 6:20 PM

It is me indeed. If you want to stick a link to this blog article on SO, im more than happy to have this as the accepted answer.

Comment 6 by Raymond Camden posted on 9/25/2014 at 6:24 PM

SO doesn't like that because, apparently, links are "bad" and the entire Internet doesn't make sense. Seriously. (Ok, maybe I'm not being fair, and yes, links do die, but it pisses me off that SO takes an attitude like that. But - whatevs. ;) I just posted a summary though as an answer.

Comment 7 by Adam Tuttle posted on 9/26/2014 at 9:06 AM

SO etiquette requires pasting the relevant portion of the blog post into the answer, but last I checked, it is still fine to link to the source blog post, regardless of whether or not you wrote it.

Comment 8 by Raymond Camden posted on 9/26/2014 at 2:07 PM

Yeah Adam - sorry - I was being a bit of a prick there.

Comment 9 by Raymond Camden posted on 9/26/2014 at 6:14 PM

As an update, versionchange events may be broken too. In my test, it worked, but he had an open tab and expected to receive the event in tab 1 when tab 2 changed the db. That *should* work, absolutely, but is probably not something you have to worry about normally.

Bug: https://bugs.webkit.org/sho...

Comment 10 by Ray Nicholus posted on 9/26/2014 at 8:13 PM

I haven't gotten around to using IndexedDB in iOS yet, but I'm glad I came across your post to avoid some of the inevitable frustration when that happens.

iOS8 is a trainwreck from a quality standpoint. Luckily, the file uploads/File API issue you mentioned at the end of your post appears to be fixed in iOS 8.0.2 (released today).

https://github.com/Widen/fi...

Comment 11 by Raymond Camden posted on 9/26/2014 at 8:27 PM

Yeah I heard. Pretty good turn around there.

Comment 12 by Nolan Lawson posted on 9/26/2014 at 10:00 PM

The "DOM IDBDatabase Exception 8" bug has already been filed on WebKit twice:

* https://bugs.webkit.org/sho...
* https://bugs.webkit.org/sho...

However, the fact that you can't store the same ID in two object stores seems to be a new find. Did you report that one on the WebKit Bugzilla as well? I think they may respond faster than the general Apple issue tracker.

Comment 13 by Nolan Lawson posted on 9/26/2014 at 10:01 PM

Sorry, there should be an underscore between "show" and "bug" in those URLs I pasted.

Comment 14 by Raymond Camden posted on 9/26/2014 at 10:17 PM

That's not your fault, my blogware does some parsing on stuff and *should* ignore URLs but does not.

I will file it at bugs.webkit.org.

Comment 15 by Raymond Camden posted on 9/27/2014 at 12:17 AM

Bug files here: https://bugs.webkit.org/sho...

Going to try to fix the rendering bug too. (I mean in my comments.)

Comment 16 by Raymond Camden posted on 9/27/2014 at 12:31 AM

Fixed it. Had to do some fancy regex. Well, not fancy, but... meh - no one cares. ;)

Comment 17 by Brian Kierstead posted on 9/27/2014 at 1:11 AM

I've been fighting with this for days .. trying to figure out why my records keep dissapearing! Thanks very much for this post.

Comment 18 by John Smith posted on 9/27/2014 at 8:52 PM

IndexedDB is not working on the homescreen (as pinned website) at all!

Comment 19 by Raymond Camden posted on 9/27/2014 at 9:33 PM

Ugh. John, please file a bug report.

Comment 20 by PhistucK posted on 10/10/2014 at 7:23 PM

You can also file the same bug report at http://www.openradar.me/

Comment 21 by Brian Kierstead posted on 10/15/2014 at 5:07 PM

@Ray any idea when they might be fixing the primary key problem? I've been looking at the bug reports and trying to google up results but haven't found anything indicative. Have you? Anyone else?

Comment 22 by Raymond Camden posted on 10/15/2014 at 6:32 PM

Sorry, I haven't heard anything.

Comment 23 by John Smith posted on 10/20/2014 at 5:04 PM

Bug was reported (marked as duplicate).
iOS 8.1 comes today. We'll see (fat chance!) xD

Comment 24 by Hanko Panko posted on 10/29/2014 at 2:54 AM

Did ios8.1 solve anything here?
With me IndexedDB does not work.

Comment 25 by Raymond Camden posted on 10/29/2014 at 3:05 AM

I *think* I tested and it failed - but I don't remember and I'm out of country - anyone else in this thread?

Comment 26 by Peter S posted on 10/29/2014 at 12:18 PM

Would anyone know if the latest iOS update has fixed this issue? Cannot find any report on it, and fwiw, caniuse.com still points it out as broken with a reference to this blog post. Thanks,

Comment 27 by Chris Anderson posted on 11/7/2014 at 8:50 PM

Thanks to this catalog of bugs, I was able to get pretty far on an IndexedDB project. Now I found another bug: https://bugs.webkit.org/sho...

When the elements of a multiEntry index array are themselves arrays, Safari seems to flatten them and concat across multiple rows.

Beware!

Comment 28 by Raymond Camden posted on 11/7/2014 at 8:51 PM

Ugh. It's like Apple hired the intern to add IDB support. The drunk intern.

Comment 29 by Brian Kierstead posted on 11/7/2014 at 9:35 PM

Hi Chris! Small world.

I was going to use the primary key work around to be able to go ahead with using indexedDB for our app, but it's really seeming like that will not be a great until they fix more of the problems.

Anyone using it in a production app?

Brian

Comment 30 by Hanko Panko posted on 11/7/2014 at 9:39 PM

We have web-based software running fine on Windows, Android and OSX. But IOS8.1 keeps loading the data forever. Not sure what is going on.

Did Apple acknowledge the "bugs", do we have any indication that they are working to repair them?

Comment 31 by Raymond Camden posted on 11/7/2014 at 10:02 PM

I believe Apple's bug reports are public - right?

Comment 32 by Chris Anderson posted on 11/7/2014 at 10:30 PM

Update to my last comment: it's actually worse than that, any arrays as keys are a recipe for crazy cursor behavior. Bug report updated.

Comment 33 by Hanko Panko posted on 11/19/2014 at 3:42 AM

I installed ios 8.1.1, but my IndexedDB data keep loading forever. Anybody doing better?

Comment 34 by Jay Meistrich posted on 11/26/2014 at 4:13 AM

I just ran into this bug. After an hour of debugging, I finally found this post. Turns out it's not my code. Thanks for the info.

Comment 35 by rbriank posted on 12/8/2014 at 10:45 PM

Is this fixed and released? https://bugs.webkit.org/sho...

I'm having a bit of trouble following what they mean.

Comment 36 (In reply to #35) by Raymond Camden posted on 12/8/2014 at 10:56 PM

My take is that it is fixed but not yet released.

Comment 37 by Nolan Lawson posted on 1/9/2015 at 7:46 PM

For anyone wondering about other storage bugs in iOS 8:

Homescreen apps
* WebSQL doesn't respond after standby: https://bugs.webkit.org/sho... / https://bugs.webkit.org/sho...

UIWebView
* IndexedDB set to null, so unshimmable: https://bugs.webkit.org/sho...

WKWebView
* WebSQL disallowed (DOM Exception 18): https://bugs.webkit.org/sho...

Comment 38 (In reply to #37) by Raymond Camden posted on 1/10/2015 at 8:13 PM

Jesus. Well, thanks for sharing.

Comment 39 (In reply to #37) by Nolan Lawson posted on 1/14/2015 at 5:14 PM

New one: using WebSQL in Chrome for iOS intermittently throws a DOM Exception 18 or DOM Exception 11: https://bugs.webkit.org/sho...

Sorry for flooding your blog with these, but currently this blog post is the most comprehensive resource for storage bugs in iOS 8. :)

Comment 40 (In reply to #39) by Raymond Camden posted on 1/14/2015 at 5:53 PM

I absolutely do not mind and *please* continue to share.

Comment 41 (In reply to #39) by Steve Husting posted on 1/14/2015 at 6:07 PM

But your PouchDB works in iOS and Android? Thanks!

Comment 42 (In reply to #41) by Raymond Camden posted on 1/14/2015 at 6:31 PM

Err, is that a question? A statement? From what I know of PouchDB, it is a JavaScript library that enables storage and db syncing. It does not, obviously can not, modify the browser to support anything the browser can't support.

Comment 43 (In reply to #41) by rbriank posted on 1/21/2015 at 3:43 PM

Looks like it does! http://pouchdb.com/2014/09/...

Comment 44 (In reply to #43) by Raymond Camden posted on 1/21/2015 at 3:49 PM

Well, it looks like it falls back to WebSQL. That's not a fix. ;)

Comment 45 (In reply to #44) by rbriank posted on 1/21/2015 at 3:53 PM

A wink is the same as nod to a blind horse. :D

Comment 46 (In reply to #45) by Raymond Camden posted on 1/21/2015 at 3:54 PM

Heh, fair enough.

Comment 47 by steve posted on 2/3/2015 at 11:32 AM

indexeddb still broken in IOS 8.1.3
Another workround which seems to work is to use a separate db for each table.
Has anyone else tried this ?

Comment 48 (In reply to #2) by Tim posted on 2/19/2015 at 12:00 AM

There's no way to share Apple's Radar bug reports directly, but some enterprising souls built OpenRadar <http: openradar.appspot.com=""> where developers can voluntarily post public copies of their Radar bug reports.

Comment 49 (In reply to #47) by Mike Gibson posted on 3/6/2015 at 8:22 PM

Here is a workaround I created and have been using. It's kind of complicated but it uses the table name and original key to create a new unique key for every object you save, avoiding this bug.

http://rebrandsoft.blogspot...

Comment 50 by Raymond Camden posted on 3/10/2015 at 10:19 AM

Still broken in iOS 8.2.

Comment 51 (In reply to #50) by JP Richardson posted on 3/11/2015 at 6:12 PM

Damn, I came here to ask that specific question :( Hopefully Apple will fix this madness soon.

Comment 52 (In reply to #50) by Nolan Lawson posted on 3/11/2015 at 8:23 PM

Still broken in 8.3 beta too.

Comment 53 by taylorbuley posted on 3/27/2015 at 4:03 AM

Word to the wise: use `oncomplete` handlers when dealing with iOS8. Apparently FF and Chrome have a different approach to committing transactions than in iOS8 WebKit. Using `complete` events seems the only way to appease all parties. But even then, I've been having a good deal of trouble with creating indexes. I've documented my issues for those facing an obscure iOS8-only error message such as "DOM IDBDatabase Exception 8: An operation failed because the requested database object could not be found." https://bugs.webkit.org/sho...

Comment 54 (In reply to #52) by JP Richardson posted on 4/8/2015 at 5:48 PM

How about 8.3 release that just came out today?

Comment 55 (In reply to #54) by Raymond Camden posted on 4/8/2015 at 7:39 PM

A user on Twitter, @nolanlawson, reported that it is NOT fixed.

Comment 56 by jokeyrhyme posted on 5/8/2015 at 7:53 AM

Still broken in WebKit nightly r183930, Safari 8.0.5 (10600.5.17) on OS X 10.10.4, and in Mobile Safari on iOS 8.4 (beta?). :S
Your work arounds do still work though, encouragingly.

Comment 57 by Andreas Linnert posted on 5/13/2015 at 1:42 PM

Is this an issue on iOS only? What about OSX? Why do they use a different implementation?

Comment 58 (In reply to #57) by Raymond Camden posted on 5/13/2015 at 1:56 PM

It is both desktop Safari and mobile Safari. I don't really think about desktop Safari as I only use it to watch Apple keynotes.

Comment 59 by Raymond Camden posted on 6/8/2015 at 11:56 PM

From my quick testing, it looks like iOS9 has the fix.

Comment 60 (In reply to #59) by Raymond Camden posted on 6/9/2015 at 1:36 AM

But - see Nolan's testing here - more stuff broken: https://gist.github.com/nol...

Comment 61 by JP Richardson posted on 7/2/2015 at 10:16 AM

I'm sure that I can predict the answer... but does iOS 8.4 fix IndexedDB?

Comment 62 (In reply to #61) by Raymond Camden posted on 7/2/2015 at 1:30 PM

I didn't test 8.4, just 9. ANyone else?

Comment 63 (In reply to #50) by Kenji Tayama posted on 7/6/2015 at 5:57 AM

I did some researching to find out when bugfixes will be available on iOS.

For the bug "IndexedDB is deleting data when a PK is shared amongst two objectStores",
the fix's changeset was on 2014-10-30.
http://trac.webkit.org/chan...

WebKit version 600.1.4 is used though iOS beta 5 to iOS 9.2 beta 2(on going), and the corresponding WebKit changeset was on 2014-08-01.
http://trac.webkit.org/chan...

Looks like no luck for this fix on iOS 8 :(

Here is where I found these information.
https://github.com/uupaa/We...

Unfortunately it is in Japanase(my native language), but has many useful information such as:
- It is unlikely that Mobile Safari gets updates in minor iOS updates.
- The version of Mobile Safari for iOS 9 would probably be fixed somewhere around August 2015.

The site also has a list of expected changesets relating to IndexedDB for iOS 9
https://github.com/uupaa/We...

Hope this information helps someone.

Comment 64 (In reply to #63) by Raymond Camden posted on 7/6/2015 at 12:54 PM

Thanks for sharing this, Kenji.

Comment 65 (In reply to #63) by Nolan Lawson posted on 7/6/2015 at 1:17 PM

I've also been trying to keep track of the status of all iOS IndexedDB/WebSQL bugs here: https://gist.github.com/nol...

Comment 66 (In reply to #65) by Kenji Tayama posted on 7/7/2015 at 2:50 AM

Thanks! And shocked to see so many bugs still in iOS 9...