Bug 692630: Support multi-entry indexes. r=bent
authorJonas Sicking <jonas@sicking.cc>
Sun, 04 Dec 2011 09:39:01 -0800
changeset 81995 0de28e40259f83d5e29613f6a7946ff3147e81c7
parent 81994 b6fb93ef1aee8e10c518720aaecd1b88535d45f9
child 81996 b3a301279d1c3ccd4d94c57089eeafaafdaf3a83
push idunknown
push userunknown
push dateunknown
reviewersbent
bugs692630
milestone11.0a1
Bug 692630: Support multi-entry indexes. r=bent
dom/base/nsDOMClassInfo.cpp
dom/base/nsDOMClassInfo.h
dom/indexedDB/DatabaseInfo.cpp
dom/indexedDB/DatabaseInfo.h
dom/indexedDB/IDBFactory.cpp
dom/indexedDB/IDBIndex.cpp
dom/indexedDB/IDBIndex.h
dom/indexedDB/IDBObjectStore.cpp
dom/indexedDB/IDBObjectStore.h
dom/indexedDB/IDBTransaction.cpp
dom/indexedDB/IndexedDatabase.h
dom/indexedDB/OpenDatabaseHelper.cpp
dom/indexedDB/nsIIDBIndex.idl
dom/indexedDB/test/Makefile.in
dom/indexedDB/test/test_multientry.html
--- a/dom/base/nsDOMClassInfo.cpp
+++ b/dom/base/nsDOMClassInfo.cpp
@@ -1636,16 +1636,17 @@ jsid nsDOMClassInfo::sNodePrincipal_id  
 jsid nsDOMClassInfo::sDocumentURIObject_id=JSID_VOID;
 jsid nsDOMClassInfo::sJava_id            = JSID_VOID;
 jsid nsDOMClassInfo::sPackages_id        = JSID_VOID;
 jsid nsDOMClassInfo::sWrappedJSObject_id = JSID_VOID;
 jsid nsDOMClassInfo::sURL_id             = JSID_VOID;
 jsid nsDOMClassInfo::sKeyPath_id         = JSID_VOID;
 jsid nsDOMClassInfo::sAutoIncrement_id   = JSID_VOID;
 jsid nsDOMClassInfo::sUnique_id          = JSID_VOID;
+jsid nsDOMClassInfo::sMultiEntry_id      = JSID_VOID;
 jsid nsDOMClassInfo::sOnload_id          = JSID_VOID;
 jsid nsDOMClassInfo::sOnerror_id         = JSID_VOID;
 
 static const JSClass *sObjectClass = nsnull;
 
 /**
  * Set our JSClass pointer for the Object class
  */
@@ -1899,16 +1900,17 @@ nsDOMClassInfo::DefineStaticJSVals(JSCon
   SET_JSID_TO_STRING(sDocumentURIObject_id,cx,"documentURIObject");
   SET_JSID_TO_STRING(sJava_id,            cx, "java");
   SET_JSID_TO_STRING(sPackages_id,        cx, "Packages");
   SET_JSID_TO_STRING(sWrappedJSObject_id, cx, "wrappedJSObject");
   SET_JSID_TO_STRING(sURL_id,             cx, "URL");
   SET_JSID_TO_STRING(sKeyPath_id,         cx, "keyPath");
   SET_JSID_TO_STRING(sAutoIncrement_id,   cx, "autoIncrement");
   SET_JSID_TO_STRING(sUnique_id,          cx, "unique");
+  SET_JSID_TO_STRING(sMultiEntry_id,      cx, "multiEntry");
   SET_JSID_TO_STRING(sOnload_id,          cx, "onload");
   SET_JSID_TO_STRING(sOnerror_id,         cx, "onerror");
 
   return NS_OK;
 }
 
 static nsresult
 CreateExceptionFromResult(JSContext *cx, nsresult aResult)
@@ -4897,16 +4899,17 @@ nsDOMClassInfo::ShutDown()
   sNodePrincipal_id   = JSID_VOID;
   sDocumentURIObject_id=JSID_VOID;
   sJava_id            = JSID_VOID;
   sPackages_id        = JSID_VOID;
   sWrappedJSObject_id = JSID_VOID;
   sKeyPath_id         = JSID_VOID;
   sAutoIncrement_id   = JSID_VOID;
   sUnique_id          = JSID_VOID;
+  sMultiEntry_id      = JSID_VOID;
   sOnload_id          = JSID_VOID;
   sOnerror_id         = JSID_VOID;
 
   NS_IF_RELEASE(sXPConnect);
   NS_IF_RELEASE(sSecMan);
   sIsInitialized = false;
 }
 
--- a/dom/base/nsDOMClassInfo.h
+++ b/dom/base/nsDOMClassInfo.h
@@ -290,16 +290,17 @@ public:
   static jsid sDocumentURIObject_id;
   static jsid sJava_id;
   static jsid sPackages_id;
   static jsid sWrappedJSObject_id;
   static jsid sURL_id;
   static jsid sKeyPath_id;
   static jsid sAutoIncrement_id;
   static jsid sUnique_id;
+  static jsid sMultiEntry_id;
   static jsid sOnload_id;
   static jsid sOnerror_id;
 
 protected:
   static JSPropertyOp sXPCNativeWrapperGetPropertyOp;
   static JSPropertyOp sXrayWrapperPropertyHolderGetPropertyOp;
 };
 
--- a/dom/indexedDB/DatabaseInfo.cpp
+++ b/dom/indexedDB/DatabaseInfo.cpp
@@ -92,27 +92,29 @@ DatabaseInfo::~DatabaseInfo()
   }
 }
 
 #ifdef NS_BUILD_REFCNT_LOGGING
 
 IndexInfo::IndexInfo()
 : id(LL_MININT),
   unique(false),
-  autoIncrement(false)
+  autoIncrement(false),
+  multiEntry(false)
 {
   MOZ_COUNT_CTOR(IndexInfo);
 }
 
 IndexInfo::IndexInfo(const IndexInfo& aOther)
 : id(aOther.id),
   name(aOther.name),
   keyPath(aOther.keyPath),
   unique(aOther.unique),
-  autoIncrement(aOther.autoIncrement)
+  autoIncrement(aOther.autoIncrement),
+  multiEntry(aOther.multiEntry)
 {
   MOZ_COUNT_CTOR(IndexInfo);
 }
 
 IndexInfo::~IndexInfo()
 {
   MOZ_COUNT_DTOR(IndexInfo);
 }
--- a/dom/indexedDB/DatabaseInfo.h
+++ b/dom/indexedDB/DatabaseInfo.h
@@ -116,16 +116,17 @@ struct IndexInfo
   : id(LL_MININT), unique(false), autoIncrement(false) { }
 #endif
 
   PRInt64 id;
   nsString name;
   nsString keyPath;
   bool unique;
   bool autoIncrement;
+  bool multiEntry;
 };
 
 struct ObjectStoreInfo
 {
 #ifdef NS_BUILD_REFCNT_LOGGING
   ObjectStoreInfo();
   ObjectStoreInfo(ObjectStoreInfo& aOther);
   ~ObjectStoreInfo();
@@ -144,15 +145,16 @@ struct ObjectStoreInfo
 
 struct IndexUpdateInfo
 {
 #ifdef NS_BUILD_REFCNT_LOGGING
   IndexUpdateInfo();
   ~IndexUpdateInfo();
 #endif
 
-  IndexInfo info;
+  PRInt64 indexId;
+  bool indexUnique;
   Key value;
 };
 
 END_INDEXEDDB_NAMESPACE
 
 #endif // mozilla_dom_indexeddb_databaseinfo_h__
--- a/dom/indexedDB/IDBFactory.cpp
+++ b/dom/indexedDB/IDBFactory.cpp
@@ -268,17 +268,17 @@ IDBFactory::LoadDatabaseInformation(mozI
 
     mapEntry->id = info->id;
     mapEntry->info = info;
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Load index information
   rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
-    "SELECT object_store_id, id, name, key_path, unique_index, "
+    "SELECT object_store_id, id, name, key_path, unique_index, multientry, "
            "object_store_autoincrement "
     "FROM object_store_index"
   ), getter_AddRefs(stmt));
   NS_ENSURE_SUCCESS(rv, rv);
 
   while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) {
     PRInt64 objectStoreId = stmt->AsInt64(0);
 
@@ -302,17 +302,18 @@ IDBFactory::LoadDatabaseInformation(mozI
 
     rv = stmt->GetString(2, indexInfo->name);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = stmt->GetString(3, indexInfo->keyPath);
     NS_ENSURE_SUCCESS(rv, rv);
 
     indexInfo->unique = !!stmt->AsInt32(4);
-    indexInfo->autoIncrement = !!stmt->AsInt32(5);
+    indexInfo->multiEntry = !!stmt->AsInt32(5);
+    indexInfo->autoIncrement = !!stmt->AsInt32(6);
   }
   NS_ENSURE_SUCCESS(rv, rv);
 
   // Load version information.
   rv = aConnection->CreateStatement(NS_LITERAL_CSTRING(
     "SELECT version "
     "FROM database"
   ), getter_AddRefs(stmt));
--- a/dom/indexedDB/IDBIndex.cpp
+++ b/dom/indexedDB/IDBIndex.cpp
@@ -313,16 +313,17 @@ IDBIndex::Create(IDBObjectStore* aObject
   index->mScriptContext = database->ScriptContext();
   index->mOwner = database->Owner();
 
   index->mObjectStore = aObjectStore;
   index->mId = aIndexInfo->id;
   index->mName = aIndexInfo->name;
   index->mKeyPath = aIndexInfo->keyPath;
   index->mUnique = aIndexInfo->unique;
+  index->mMultiEntry = aIndexInfo->multiEntry;
   index->mAutoIncrement = aIndexInfo->autoIncrement;
 
   return index.forget();
 }
 
 IDBIndex::IDBIndex()
 : mId(LL_MININT),
   mUnique(false),
@@ -392,16 +393,25 @@ IDBIndex::GetUnique(bool* aUnique)
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
 
   *aUnique = mUnique;
   return NS_OK;
 }
 
 NS_IMETHODIMP
+IDBIndex::GetMultiEntry(bool* aMultiEntry)
+{
+  NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
+
+  *aMultiEntry = mMultiEntry;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 IDBIndex::GetObjectStore(nsIIDBObjectStore** aObjectStore)
 {
   NS_ASSERTION(NS_IsMainThread(), "Wrong thread!");
 
   nsCOMPtr<nsIIDBObjectStore> objectStore(mObjectStore);
   objectStore.forget(aObjectStore);
   return NS_OK;
 }
--- a/dom/indexedDB/IDBIndex.h
+++ b/dom/indexedDB/IDBIndex.h
@@ -82,16 +82,21 @@ public:
     return mName;
   }
 
   bool IsUnique() const
   {
     return mUnique;
   }
 
+  bool IsMultiEntry() const
+  {
+    return mMultiEntry;
+  }
+
   bool IsAutoIncrement() const
   {
     return mAutoIncrement;
   }
 
   const nsString& KeyPath() const
   {
     return mKeyPath;
@@ -105,14 +110,15 @@ private:
 
   nsCOMPtr<nsIScriptContext> mScriptContext;
   nsCOMPtr<nsPIDOMWindow> mOwner;
 
   PRInt64 mId;
   nsString mName;
   nsString mKeyPath;
   bool mUnique;
+  bool mMultiEntry;
   bool mAutoIncrement;
 };
 
 END_INDEXEDDB_NAMESPACE
 
 #endif // mozilla_dom_indexeddb_idbindex_h__
--- a/dom/indexedDB/IDBObjectStore.cpp
+++ b/dom/indexedDB/IDBObjectStore.cpp
@@ -92,19 +92,16 @@ public:
 
   void ReleaseMainThreadObjects()
   {
     mObjectStore = nsnull;
     IDBObjectStore::ClearStructuredCloneBuffer(mCloneBuffer);
     AsyncConnectionHelper::ReleaseMainThreadObjects();
   }
 
-  nsresult UpdateIndexes(mozIStorageConnection* aConnection,
-                         PRInt64 aObjectDataId);
-
 private:
   // In-params.
   nsRefPtr<IDBObjectStore> mObjectStore;
 
   // These may change in the autoincrement case.
   JSAutoStructuredCloneBuffer mCloneBuffer;
   Key mKey;
   const bool mOverwrite;
@@ -417,20 +414,20 @@ IgnoreWhitespace(PRUnichar c)
 {
   return false;
 }
 
 typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;
 
 inline
 nsresult
-GetKeyFromValue(JSContext* aCx,
-                jsval aVal,
-                const nsAString& aKeyPath,
-                Key& aKey)
+GetJSValFromKeyPath(JSContext* aCx,
+                    jsval aVal,
+                    const nsAString& aKeyPath,
+                    jsval& aKey)
 {
   NS_ASSERTION(aCx, "Null pointer!");
   // aVal can be primitive iff the key path is empty.
   NS_ASSERTION(IDBObjectStore::IsValidKeyPath(aCx, aKeyPath),
                "This will explode!");
 
   KeyPathTokenizer tokenizer(aKeyPath, '.');
 
@@ -447,18 +444,33 @@ GetKeyFromValue(JSContext* aCx,
       intermediate = JSVAL_VOID;
       break;
     }
 
     JSBool ok = JS_GetUCProperty(aCx, JSVAL_TO_OBJECT(intermediate),
                                  keyPathChars, keyPathLen, &intermediate);
     NS_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
   }
-
-  if (NS_FAILED(aKey.SetFromJSVal(aCx, intermediate))) {
+  
+  aKey = intermediate;
+  return NS_OK;
+}
+
+inline
+nsresult
+GetKeyFromValue(JSContext* aCx,
+                jsval aVal,
+                const nsAString& aKeyPath,
+                Key& aKey)
+{
+  jsval key;
+  nsresult rv = GetJSValFromKeyPath(aCx, aVal, aKeyPath, key);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (NS_FAILED(aKey.SetFromJSVal(aCx, key))) {
     aKey.Unset();
   }
 
   return NS_OK;
 }
 
 inline
 already_AddRefed<IDBRequest>
@@ -566,112 +578,89 @@ IDBObjectStore::IsValidKeyPath(JSContext
     return false;
   }
 
   return true;
 }
 
 // static
 nsresult
-IDBObjectStore::GetKeyPathValueFromStructuredData(const PRUint8* aData,
-                                                  PRUint32 aDataLength,
-                                                  const nsAString& aKeyPath,
-                                                  JSContext* aCx,
-                                                  Key& aValue)
+IDBObjectStore::AppendIndexUpdateInfo(PRInt64 aIndexID,
+                                      const nsAString& aKeyPath,
+                                      bool aUnique,
+                                      bool aMultiEntry,
+                                      JSContext* aCx,
+                                      jsval aObject,
+                                      nsTArray<IndexUpdateInfo>& aUpdateInfoArray)
 {
-  NS_ASSERTION(aData, "Null pointer!");
-  NS_ASSERTION(aDataLength, "Empty data!");
-  NS_ASSERTION(aCx, "Null pointer!");
-
-  JSAutoRequest ar(aCx);
-
-  jsval clone;
-  if (!JS_ReadStructuredClone(aCx, reinterpret_cast<const uint64*>(aData),
-                              aDataLength, JS_STRUCTURED_CLONE_VERSION,
-                              &clone, NULL, NULL)) {
-    return NS_ERROR_DOM_DATA_CLONE_ERR;
-  }
-
-  if (JSVAL_IS_PRIMITIVE(clone) && !aKeyPath.IsEmpty()) {
-    // This isn't an object, so just leave the key unset.
-    aValue.Unset();
-    return NS_OK;
-  }
-
-  nsresult rv = GetKeyFromValue(aCx, clone, aKeyPath, aValue);
+  jsval key;
+  nsresult rv = GetJSValFromKeyPath(aCx, aObject, aKeyPath, key);
   NS_ENSURE_SUCCESS(rv, rv);
 
-  return NS_OK;
-}
-
-/* static */
-nsresult
-IDBObjectStore::GetIndexUpdateInfo(ObjectStoreInfo* aObjectStoreInfo,
-                                   JSContext* aCx,
-                                   jsval aObject,
-                                   nsTArray<IndexUpdateInfo>& aUpdateInfoArray)
-{
-  JSObject* cloneObj = nsnull;
-
-  PRUint32 count = aObjectStoreInfo->indexes.Length();
-  if (count) {
-    if (!aUpdateInfoArray.SetCapacity(count)) {
-      NS_ERROR("Out of memory!");
-      return NS_ERROR_OUT_OF_MEMORY;
+  if (aMultiEntry && !JSVAL_IS_PRIMITIVE(key) &&
+      JS_IsArrayObject(aCx, JSVAL_TO_OBJECT(key))) {
+    JSObject* array = JSVAL_TO_OBJECT(key);
+    jsuint arrayLength;
+    if (!JS_GetArrayLength(aCx, array, &arrayLength)) {
+      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
     }
 
-    for (PRUint32 indexesIndex = 0; indexesIndex < count; indexesIndex++) {
-      const IndexInfo& indexInfo = aObjectStoreInfo->indexes[indexesIndex];
+    for (jsuint arrayIndex = 0; arrayIndex < arrayLength; arrayIndex++) {
+      jsval arrayItem;
+      if (!JS_GetElement(aCx, array, arrayIndex, &arrayItem)) {
+        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+      }
 
       Key value;
-      nsresult rv = GetKeyFromValue(aCx, aObject, indexInfo.keyPath, value);
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      if (value.IsUnset()) {
+      if (NS_FAILED(value.SetFromJSVal(aCx, arrayItem)) ||
+          value.IsUnset()) {
         // Not a value we can do anything with, ignore it.
         continue;
       }
 
       IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
-      updateInfo->info = indexInfo;
+      updateInfo->indexId = aIndexID;
+      updateInfo->indexUnique = aUnique;
       updateInfo->value = value;
     }
   }
   else {
-    aUpdateInfoArray.Clear();
+    Key value;
+    if (NS_FAILED(value.SetFromJSVal(aCx, key)) ||
+        value.IsUnset()) {
+      // Not a value we can do anything with, ignore it.
+      return NS_OK;
+    }
+
+    IndexUpdateInfo* updateInfo = aUpdateInfoArray.AppendElement();
+    updateInfo->indexId = aIndexID;
+    updateInfo->indexUnique = aUnique;
+    updateInfo->value = value;
   }
 
   return NS_OK;
 }
 
-/* static */
+// static
 nsresult
 IDBObjectStore::UpdateIndexes(IDBTransaction* aTransaction,
                               PRInt64 aObjectStoreId,
                               const Key& aObjectStoreKey,
                               bool aAutoIncrement,
                               bool aOverwrite,
                               PRInt64 aObjectDataId,
                               const nsTArray<IndexUpdateInfo>& aUpdateInfoArray)
 {
-#ifdef DEBUG
-  if (aAutoIncrement) {
-    NS_ASSERTION(aObjectDataId != LL_MININT, "Bad objectData id!");
-  }
-  else {
-    NS_ASSERTION(aObjectDataId == LL_MININT, "Bad objectData id!");
-  }
-#endif
-
-  PRUint32 indexCount = aUpdateInfoArray.Length();
+  NS_ASSERTION(!aAutoIncrement || aObjectDataId != LL_MININT,
+               "Bad objectData id!");
 
   nsCOMPtr<mozIStorageStatement> stmt;
   nsresult rv;
 
-  if (!aAutoIncrement) {
+  if (aObjectDataId == LL_MININT) {
     stmt = aTransaction->GetCachedStatement(
       "SELECT id "
       "FROM object_data "
       "WHERE object_store_id = :osid "
       "AND key_value = :key_value"
     );
     NS_ENSURE_TRUE(stmt, NS_ERROR_FAILURE);
 
@@ -723,43 +712,59 @@ IDBObjectStore::UpdateIndexes(IDBTransac
 
     rv = stmt->BindInt64ByName(objectDataId, aObjectDataId);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = stmt->Execute();
     NS_ENSURE_SUCCESS(rv, rv);
   }
 
-  for (PRUint32 indexIndex = 0; indexIndex < indexCount; indexIndex++) {
-    const IndexUpdateInfo& updateInfo = aUpdateInfoArray[indexIndex];
-
-    NS_ASSERTION(updateInfo.info.autoIncrement == aAutoIncrement, "Huh?!");
+  PRUint32 infoCount = aUpdateInfoArray.Length();
+  for (PRUint32 i = 0; i < infoCount; i++) {
+    const IndexUpdateInfo& updateInfo = aUpdateInfoArray[i];
 
     // Insert new values.
     stmt = aTransaction->IndexDataInsertStatement(aAutoIncrement,
-                                                  updateInfo.info.unique);
+                                                  updateInfo.indexUnique);
     NS_ENSURE_TRUE(stmt, NS_ERROR_FAILURE);
 
     mozStorageStatementScoper scoper4(stmt);
 
-    rv = stmt->BindInt64ByName(indexId, updateInfo.info.id);
+    rv = stmt->BindInt64ByName(indexId, updateInfo.indexId);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = stmt->BindInt64ByName(objectDataId, aObjectDataId);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    if (!updateInfo.info.autoIncrement) {
+    if (!aAutoIncrement) {
       rv = aObjectStoreKey.BindToStatement(stmt, objectDataKey);
       NS_ENSURE_SUCCESS(rv, rv);
     }
 
     rv = updateInfo.value.BindToStatement(stmt, value);
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = stmt->Execute();
+    if (rv == NS_ERROR_STORAGE_CONSTRAINT && updateInfo.indexUnique) {
+      // If we're inserting multiple entries for the same unique index, then
+      // we might have failed to insert due to colliding with another entry for
+      // the same index in which case we should ignore it.
+      
+      for (PRInt32 j = (PRInt32)i - 1;
+           j >= 0 && aUpdateInfoArray[j].indexId == updateInfo.indexId;
+           --j) {
+        if (updateInfo.value == aUpdateInfoArray[j].value) {
+          // We found a key with the same value for the same index. So we
+          // must have had a collision with a value we just inserted.
+          rv = NS_OK;
+          break;
+        }
+      }
+    }
+
     if (NS_FAILED(rv)) {
       return rv;
     }
   }
 
   return NS_OK;
 }
 
@@ -924,18 +929,27 @@ IDBObjectStore::GetAddInfo(JSContext* aC
   }
 
   // Figure out indexes and the index values to update here.
   ObjectStoreInfo* info;
   if (!mTransaction->Database()->Info()->GetObjectStore(mName, &info)) {
     NS_ERROR("This should never fail!");
   }
 
-  rv = GetIndexUpdateInfo(info, aCx, aValue, aUpdateInfoArray);
-  NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+  PRUint32 count = info->indexes.Length();
+  aUpdateInfoArray.SetCapacity(count); // Pretty good estimate
+  for (PRUint32 indexesIndex = 0; indexesIndex < count; indexesIndex++) {
+    const IndexInfo& indexInfo = info->indexes[indexesIndex];
+
+    rv = AppendIndexUpdateInfo(indexInfo.id, indexInfo.keyPath,
+                               indexInfo.unique, indexInfo.multiEntry,
+                               aCx, aValue, aUpdateInfoArray);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+  }
 
   const jschar* keyPathChars =
     reinterpret_cast<const jschar*>(mKeyPath.get());
   const size_t keyPathLen = mKeyPath.Length();
   JSBool ok = JS_FALSE;
 
   if (HasKeyPath() && aKey.IsUnset()) {
     NS_ASSERTION(mAutoIncrement, "Should have bailed earlier!");
@@ -1360,16 +1374,17 @@ IDBObjectStore::CreateIndex(const nsAStr
 
   if (found) {
     return NS_ERROR_DOM_INDEXEDDB_CONSTRAINT_ERR;
   }
 
   NS_ASSERTION(mTransaction->IsOpen(), "Impossible!");
 
   bool unique = false;
+  bool multiEntry = false;
 
   // Get optional arguments.
   if (!JSVAL_IS_VOID(aOptions) && !JSVAL_IS_NULL(aOptions)) {
     if (JSVAL_IS_PRIMITIVE(aOptions)) {
       // XXX Update spec for a real code here
       return NS_ERROR_DOM_TYPE_ERR;
     }
 
@@ -1383,30 +1398,42 @@ IDBObjectStore::CreateIndex(const nsAStr
     }
 
     JSBool boolVal;
     if (!JS_ValueToBoolean(aCx, val, &boolVal)) {
       NS_WARNING("JS_ValueToBoolean failed!");
       return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
     }
     unique = !!boolVal;
+
+    if (!JS_GetPropertyById(aCx, options, nsDOMClassInfo::sMultiEntry_id, &val)) {
+      NS_WARNING("JS_GetPropertyById failed!");
+      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+    }
+
+    if (!JS_ValueToBoolean(aCx, val, &boolVal)) {
+      NS_WARNING("JS_ValueToBoolean failed!");
+      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
+    }
+    multiEntry = !!boolVal;
   }
 
   DatabaseInfo* databaseInfo = mTransaction->Database()->Info();
 
   IndexInfo* indexInfo = info->indexes.AppendElement();
   if (!indexInfo) {
     NS_WARNING("Out of memory!");
     return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
   }
 
   indexInfo->id = databaseInfo->nextIndexId++;
   indexInfo->name = aName;
   indexInfo->keyPath = aKeyPath;
   indexInfo->unique = unique;
+  indexInfo->multiEntry = multiEntry;
   indexInfo->autoIncrement = mAutoIncrement;
 
   // Don't leave this in the list if we fail below!
   AutoRemoveIndex autoRemove(mTransaction->Database(), mName, aName);
 
 #ifdef DEBUG
   for (PRUint32 index = 0; index < mCreatedIndexes.Length(); index++) {
     if (mCreatedIndexes[index]->Name() == aName) {
@@ -2148,18 +2175,19 @@ CreateIndexHelper::DestroyTLSEntry(void*
 
 nsresult
 CreateIndexHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
 {
   // Insert the data into the database.
   nsCOMPtr<mozIStorageStatement> stmt =
     mTransaction->GetCachedStatement(
     "INSERT INTO object_store_index (id, name, key_path, unique_index, "
-      "object_store_id, object_store_autoincrement) "
-    "VALUES (:id, :name, :key_path, :unique, :osid, :os_auto_increment)"
+      "multientry, object_store_id, object_store_autoincrement) "
+    "VALUES (:id, :name, :key_path, :unique, :multientry, :osid, "
+      ":os_auto_increment)"
   );
   NS_ENSURE_TRUE(stmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   mozStorageStatementScoper scoper(stmt);
 
   nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"),
                                       mIndex->Id());
   NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
@@ -2170,16 +2198,20 @@ CreateIndexHelper::DoDatabaseWork(mozISt
   rv = stmt->BindStringByName(NS_LITERAL_CSTRING("key_path"),
                               mIndex->KeyPath());
   NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("unique"),
                              mIndex->IsUnique() ? 1 : 0);
   NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
+  rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("multientry"),
+                             mIndex->IsMultiEntry() ? 1 : 0);
+  NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+
   rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"),
                              mIndex->ObjectStore()->Id());
   NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("os_auto_increment"),
                              mIndex->IsAutoIncrement() ? 1 : 0);
   NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
@@ -2226,79 +2258,79 @@ CreateIndexHelper::InsertDataFromObjectS
   NS_ENSURE_TRUE(stmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   mozStorageStatementScoper scoper(stmt);
 
   nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("osid"),
                                       mIndex->ObjectStore()->Id());
   NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
+  NS_ENSURE_TRUE(sTLSIndex != BAD_TLS_INDEX, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+
   bool hasResult;
-  while (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) {
-    nsCOMPtr<mozIStorageStatement> insertStmt =
-      mTransaction->IndexDataInsertStatement(mIndex->IsAutoIncrement(),
-                                             mIndex->IsUnique());
-    NS_ENSURE_TRUE(insertStmt, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
-
-    mozStorageStatementScoper scoper2(insertStmt);
-
-    rv = insertStmt->BindInt64ByName(NS_LITERAL_CSTRING("index_id"),
-                                     mIndex->Id());
-    NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
-
-    rv = insertStmt->BindInt64ByName(NS_LITERAL_CSTRING("object_data_id"),
-                                     stmt->AsInt64(0));
-    NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
-
-    if (!mIndex->IsAutoIncrement()) {
-      NS_NAMED_LITERAL_CSTRING(objectDataKey, "object_data_key");
-
-      Key key;
-      rv = key.SetFromStatement(stmt, 2);
-      NS_ENSURE_SUCCESS(rv, rv);
-
-      rv =
-        key.BindToStatement(insertStmt, NS_LITERAL_CSTRING("object_data_key"));
-      NS_ENSURE_SUCCESS(rv, rv);
-    }
-
+  rv = stmt->ExecuteStep(&hasResult);
+  NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+  if (!hasResult) {
+    // Bail early if we have no data to avoid creating the below runtime
+    return NS_OK;
+  }
+
+  ThreadLocalJSRuntime* tlsEntry =
+    reinterpret_cast<ThreadLocalJSRuntime*>(PR_GetThreadPrivate(sTLSIndex));
+
+  if (!tlsEntry) {
+    tlsEntry = ThreadLocalJSRuntime::Create();
+    NS_ENSURE_TRUE(tlsEntry, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
+
+    PR_SetThreadPrivate(sTLSIndex, tlsEntry);
+  }
+
+  JSContext* cx = tlsEntry->Context();
+  JSAutoRequest ar(cx);
+
+  do {
     const PRUint8* data;
     PRUint32 dataLength;
     rv = stmt->GetSharedBlob(1, &dataLength, &data);
     NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
-    NS_ENSURE_TRUE(sTLSIndex != BAD_TLS_INDEX, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
-
-    ThreadLocalJSRuntime* tlsEntry =
-      reinterpret_cast<ThreadLocalJSRuntime*>(PR_GetThreadPrivate(sTLSIndex));
-
-    if (!tlsEntry) {
-      tlsEntry = ThreadLocalJSRuntime::Create();
-      NS_ENSURE_TRUE(tlsEntry, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
-
-      PR_SetThreadPrivate(sTLSIndex, tlsEntry);
+    jsval clone;
+    if (!JS_ReadStructuredClone(cx, reinterpret_cast<const uint64*>(data),
+                                dataLength, JS_STRUCTURED_CLONE_VERSION,
+                                &clone, NULL, NULL)) {
+      return NS_ERROR_DOM_DATA_CLONE_ERR;
     }
 
+    nsTArray<IndexUpdateInfo> updateInfo;
+    rv = IDBObjectStore::AppendIndexUpdateInfo(mIndex->Id(),
+                                               mIndex->KeyPath(),
+                                               mIndex->IsUnique(),
+                                               mIndex->IsMultiEntry(),
+                                               tlsEntry->Context(),
+                                               clone, updateInfo);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    PRInt64 objectDataID = stmt->AsInt64(0);
+
     Key key;
-    rv = IDBObjectStore::GetKeyPathValueFromStructuredData(data, dataLength,
-                                                           mIndex->KeyPath(),
-                                                           tlsEntry->Context(),
-                                                           key);
+    if (!mIndex->IsAutoIncrement()) {
+      rv = key.SetFromStatement(stmt, 2);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+    else {
+      key.SetFromInteger(objectDataID);
+    }
+
+    rv = IDBObjectStore::UpdateIndexes(mTransaction, mIndex->Id(),
+                                       key, mIndex->IsAutoIncrement(),
+                                       false, objectDataID, updateInfo);
     NS_ENSURE_SUCCESS(rv, rv);
 
-    if (key.IsUnset()) {
-      continue;
-    }
-
-    rv = key.BindToStatement(insertStmt, NS_LITERAL_CSTRING("value"));
-    NS_ENSURE_SUCCESS(rv, rv);
-
-    rv = insertStmt->Execute();
-    NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
-  }
+  } while (NS_SUCCEEDED(rv = stmt->ExecuteStep(&hasResult)) && hasResult);
+  NS_ENSURE_SUCCESS(rv, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
 
   return NS_OK;
 }
 
 nsresult
 DeleteIndexHelper::DoDatabaseWork(mozIStorageConnection* aConnection)
 {
   NS_PRECONDITION(!NS_IsMainThread(), "Wrong thread!");
--- a/dom/indexedDB/IDBObjectStore.h
+++ b/dom/indexedDB/IDBObjectStore.h
@@ -71,27 +71,23 @@ public:
   static already_AddRefed<IDBObjectStore>
   Create(IDBTransaction* aTransaction,
          const ObjectStoreInfo* aInfo);
 
   static bool
   IsValidKeyPath(JSContext* aCx, const nsAString& aKeyPath);
 
   static nsresult
-  GetKeyPathValueFromStructuredData(const PRUint8* aData,
-                                    PRUint32 aDataLength,
-                                    const nsAString& aKeyPath,
-                                    JSContext* aCx,
-                                    Key& aValue);
-
-  static nsresult
-  GetIndexUpdateInfo(ObjectStoreInfo* aObjectStoreInfo,
-                     JSContext* aCx,
-                     jsval aObject,
-                     nsTArray<IndexUpdateInfo>& aUpdateInfoArray);
+  AppendIndexUpdateInfo(PRInt64 aIndexID,
+                        const nsAString& aKeyPath,
+                        bool aUnique,
+                        bool aMultiEntry,
+                        JSContext* aCx,
+                        jsval aObject,
+                        nsTArray<IndexUpdateInfo>& aUpdateInfoArray);
 
   static nsresult
   UpdateIndexes(IDBTransaction* aTransaction,
                 PRInt64 aObjectStoreId,
                 const Key& aObjectStoreKey,
                 bool aAutoIncrement,
                 bool aOverwrite,
                 PRInt64 aObjectDataId,
--- a/dom/indexedDB/IDBTransaction.cpp
+++ b/dom/indexedDB/IDBTransaction.cpp
@@ -401,30 +401,30 @@ IDBTransaction::IndexDataInsertStatement
     if (aUnique) {
       return GetCachedStatement(
         "INSERT INTO ai_unique_index_data "
           "(index_id, ai_object_data_id, value) "
         "VALUES (:index_id, :object_data_id, :value)"
       );
     }
     return GetCachedStatement(
-      "INSERT INTO ai_index_data "
+      "INSERT OR IGNORE INTO ai_index_data "
         "(index_id, ai_object_data_id, value) "
       "VALUES (:index_id, :object_data_id, :value)"
     );
   }
   if (aUnique) {
     return GetCachedStatement(
       "INSERT INTO unique_index_data "
         "(index_id, object_data_id, object_data_key, value) "
       "VALUES (:index_id, :object_data_id, :object_data_key, :value)"
     );
   }
   return GetCachedStatement(
-    "INSERT INTO index_data ("
+    "INSERT OR IGNORE INTO index_data ("
       "index_id, object_data_id, object_data_key, value) "
     "VALUES (:index_id, :object_data_id, :object_data_key, :value)"
   );
 }
 
 already_AddRefed<mozIStorageStatement>
 IDBTransaction::IndexDataDeleteStatement(bool aAutoIncrement,
                                          bool aUnique)
--- a/dom/indexedDB/IndexedDatabase.h
+++ b/dom/indexedDB/IndexedDatabase.h
@@ -46,17 +46,17 @@
 #include "jsapi.h"
 #include "nsAutoPtr.h"
 #include "nsCOMPtr.h"
 #include "nsDebug.h"
 #include "nsDOMError.h"
 #include "nsStringGlue.h"
 #include "nsTArray.h"
 
-#define DB_SCHEMA_VERSION 7
+#define DB_SCHEMA_VERSION 8
 
 #define BEGIN_INDEXEDDB_NAMESPACE \
   namespace mozilla { namespace dom { namespace indexedDB {
 
 #define END_INDEXEDDB_NAMESPACE \
   } /* namespace indexedDB */ } /* namepsace dom */ } /* namespace mozilla */
 
 #define USING_INDEXEDDB_NAMESPACE \
--- a/dom/indexedDB/OpenDatabaseHelper.cpp
+++ b/dom/indexedDB/OpenDatabaseHelper.cpp
@@ -160,16 +160,17 @@ CreateTables(mozIStorageConnection* aDBC
   // Table `index`
   rv = aDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
     "CREATE TABLE object_store_index ("
       "id INTEGER, "
       "object_store_id INTEGER NOT NULL, "
       "name TEXT NOT NULL, "
       "key_path TEXT NOT NULL, "
       "unique_index INTEGER NOT NULL, "
+      "multientry INTEGER NOT NULL DEFAULT 0, "
       "object_store_autoincrement INTERGER NOT NULL, "
       "PRIMARY KEY (id), "
       "UNIQUE (object_store_id, name), "
       "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
         "CASCADE"
     ");"
   ));
   NS_ENSURE_SUCCESS(rv, rv);
@@ -814,16 +815,93 @@ UpgradeSchemaFrom6To7(mozIStorageConnect
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = transaction.Commit();
   NS_ENSURE_SUCCESS(rv, rv);
 
   return NS_OK;
 }
 
+
+nsresult
+UpgradeSchemaFrom7To8(mozIStorageConnection* aConnection)
+{
+  mozStorageTransaction transaction(aConnection, false,
+                                 mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+  // Turn off foreign key constraints before we do anything here.
+  nsresult rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "PRAGMA foreign_keys = OFF;"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "CREATE TEMPORARY TABLE temp_upgrade ("
+      "id, "
+      "object_store_id, "
+      "name, "
+      "key_path, "
+      "unique_index, "
+      "object_store_autoincrement, "
+    ");"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "INSERT INTO temp_upgrade "
+      "SELECT id, object_store_id, name, key_path, "
+      "unique_index, object_store_autoincrement, "
+      "FROM object_store_index;"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "DROP TABLE object_store_index;"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "CREATE TABLE object_store_index ("
+      "id INTEGER, "
+      "object_store_id INTEGER NOT NULL, "
+      "name TEXT NOT NULL, "
+      "key_path TEXT NOT NULL, "
+      "unique_index INTEGER NOT NULL, "
+      "multientry INTEGER NOT NULL, "
+      "object_store_autoincrement INTERGER NOT NULL, "
+      "PRIMARY KEY (id), "
+      "UNIQUE (object_store_id, name), "
+      "FOREIGN KEY (object_store_id) REFERENCES object_store(id) ON DELETE "
+        "CASCADE"
+    ");"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "INSERT INTO object_store_index "
+      "SELECT id, object_store_id, name, key_path, "
+      "unique_index, 0, object_store_autoincrement, "
+      "FROM temp_upgrade;"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+    "DROP TABLE temp_upgrade;"
+  ));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = aConnection->SetSchemaVersion(8);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  rv = transaction.Commit();
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 nsresult
 CreateDatabaseConnection(const nsAString& aName,
                          nsIFile* aDBFile,
                          mozIStorageConnection** aConnection)
 {
   NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
 
   NS_NAMED_LITERAL_CSTRING(quotaVFSName, "quota");
@@ -865,32 +943,33 @@ CreateDatabaseConnection(const nsAString
     NS_ENSURE_SUCCESS(rv, rv);
 
     NS_ASSERTION(NS_SUCCEEDED(connection->GetSchemaVersion(&schemaVersion)) &&
                  schemaVersion == DB_SCHEMA_VERSION,
                  "CreateTables set a bad schema version!");
   }
   else if (schemaVersion != DB_SCHEMA_VERSION) {
     // This logic needs to change next time we change the schema!
-    PR_STATIC_ASSERT(DB_SCHEMA_VERSION == 7);
+    PR_STATIC_ASSERT(DB_SCHEMA_VERSION == 8);
 
 #define UPGRADE_SCHEMA_CASE(_from, _to)                                        \
   if (schemaVersion == _from) {                                                \
     rv = UpgradeSchemaFrom##_from##To##_to (connection);                       \
     NS_ENSURE_SUCCESS(rv, rv);                                                 \
                                                                                \
     rv = connection->GetSchemaVersion(&schemaVersion);                         \
     NS_ENSURE_SUCCESS(rv, rv);                                                 \
                                                                                \
     NS_ASSERTION(schemaVersion == _to, "Bad upgrade function!");               \
   }
 
     UPGRADE_SCHEMA_CASE(4, 5)
     UPGRADE_SCHEMA_CASE(5, 6)
     UPGRADE_SCHEMA_CASE(6, 7)
+    UPGRADE_SCHEMA_CASE(7, 8)
 
 #undef UPGRADE_SCHEMA_CASE
 
     if (schemaVersion != DB_SCHEMA_VERSION) {
       NS_WARNING("Unable to open IndexedDB database, schema doesn't match");
       return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
     }
   }
--- a/dom/indexedDB/nsIIDBIndex.idl
+++ b/dom/indexedDB/nsIIDBIndex.idl
@@ -42,27 +42,29 @@
 interface nsIIDBObjectStore;
 interface nsIIDBRequest;
 
 /**
  * IDBIndex interface.  See
  * http://dev.w3.org/2006/webapi/WebSimpleDB/#idl-def-IDBIndex for more
  * information.
  */
-[scriptable, builtinclass, uuid(1da60889-3db4-4f66-9fd7-b78c1e7969b7)]
+[scriptable, builtinclass, uuid(fcb9a158-833e-4aa9-ab19-ab90cbb50afc)]
 interface nsIIDBIndex : nsISupports
 {
   readonly attribute DOMString name;
 
   readonly attribute DOMString storeName;
 
   readonly attribute DOMString keyPath;
 
   readonly attribute boolean unique;
 
+  readonly attribute boolean multiEntry;
+
   readonly attribute nsIIDBObjectStore objectStore;
 
   [implicit_jscontext]
   nsIIDBRequest
   get(in jsval key);
 
   [implicit_jscontext]
   nsIIDBRequest
--- a/dom/indexedDB/test/Makefile.in
+++ b/dom/indexedDB/test/Makefile.in
@@ -79,16 +79,17 @@ TEST_FILES = \
   test_index_getAll.html \
   test_index_getAllObjects.html \
   test_index_object_cursors.html \
   test_index_update_delete.html \
   test_indexes.html \
   test_indexes_bad_values.html \
   test_key_requirements.html \
   test_leaving_page.html \
+  test_multientry.html \
   test_objectCursors.html \
   test_objectStore_inline_autoincrement_key_added_on_put.html \
   test_objectStore_remove_values.html \
   test_object_identity.html \
   test_odd_result_order.html \
   test_open_empty_db.html \
   test_open_objectStore.html \
   test_optionalArguments.html \
new file mode 100644
--- /dev/null
+++ b/dom/indexedDB/test/test_multientry.html
@@ -0,0 +1,230 @@
+<!--
+  Any copyright is dedicated to the Public Domain.
+  http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+  <title>Indexed Database Property Test</title>
+
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+  <script type="text/javascript;version=1.7">
+    function testSteps()
+    {
+      // Test object stores
+
+      let openRequest = mozIndexedDB.open(window.location.pathname, 1);
+      openRequest.onerror = errorHandler;
+      openRequest.onupgradeneeded = grabEventAndContinueHandler;
+      openRequest.onsuccess = unexpectedSuccessHandler;
+      let event = yield;
+      let db = event.target.result;
+      db.onerror = errorHandler;
+      let tests =
+        [{ add:     { x: 1, id: 1 },
+           indexes:[{ v: 1, k: 1 }] },
+         { add:     { x: [2, 3], id: 2 },
+           indexes:[{ v: 1, k: 1 },
+                    { v: 2, k: 2 },
+                    { v: 3, k: 2 }] },
+         { put:     { x: [2, 4], id: 1 },
+           indexes:[{ v: 2, k: 1 },
+                    { v: 2, k: 2 },
+                    { v: 3, k: 2 },
+                    { v: 4, k: 1 }] },
+         { add:     { x: [5, 6, 5, -2, 3], id: 3 },
+           indexes:[{ v:-2, k: 3 },
+                    { v: 2, k: 1 },
+                    { v: 2, k: 2 },
+                    { v: 3, k: 2 },
+                    { v: 3, k: 3 },
+                    { v: 4, k: 1 },
+                    { v: 5, k: 3 },
+                    { v: 6, k: 3 }] },
+         { delete:  IDBKeyRange.bound(1, 3),
+           indexes:[] },
+         { put:     { x: ["food", {}, false, undefined, /x/, [73, false]], id: 2 },
+           indexes:[{ v: "food", k: 2 }] },
+         { add:     { x: [{}, /x/, -12, "food", null, [false], undefined], id: 3 },
+           indexes:[{ v: -12, k: 3 },
+                    { v: "food", k: 2 },
+                    { v: "food", k: 3 }] },
+         { put:     { x: [], id: 2 },
+           indexes:[{ v: -12, k: 3 },
+                    { v: "food", k: 3 }] },
+         { put:     { x: { y: 3 }, id: 3 },
+           indexes:[] },
+         { add:     { x: false, id: 7 },
+           indexes:[] },
+         { delete:  IDBKeyRange.lowerBound(0),
+           indexes:[] },
+        ];
+
+      let store = db.createObjectStore("mystore", { keyPath: "id" });
+      let index = store.createIndex("myindex", "x", { multiEntry: true });
+      is(index.multiEntry, true, "index created with multiEntry");
+
+      let i;
+      for (i = 0; i < tests.length; ++i) {
+        let test = tests[i];
+        let testName = " for " + JSON.stringify(test);
+        let req;
+        if (test.add) {
+          req = store.add(test.add);
+        }
+        else if (test.put) {
+          req = store.put(test.put);
+        }
+        else if (test.delete) {
+          req = store.delete(test.delete);
+        }
+        else {
+          ok(false, "borked test");
+        }
+        req.onsuccess = grabEventAndContinueHandler;
+        let e = yield;
+        
+        req = index.openKeyCursor();
+        req.onsuccess = grabEventAndContinueHandler;
+        for (let j = 0; j < test.indexes.length; ++j) {
+          e = yield;
+          is(req.result.key, test.indexes[j].v, "found expected index key at index " + j + testName);
+          is(req.result.primaryKey, test.indexes[j].k, "found expected index primary key at index " + j + testName);
+          req.result.continue();
+        }
+        e = yield;
+        is(req.result, undefined, "exhausted indexes");
+
+        let tempIndex = store.createIndex("temp index", "x", { multiEntry: true });
+        req = tempIndex.openKeyCursor();
+        req.onsuccess = grabEventAndContinueHandler;
+        for (let j = 0; j < test.indexes.length; ++j) {
+          e = yield;
+          is(req.result.key, test.indexes[j].v, "found expected temp index key at index " + j + testName);
+          is(req.result.primaryKey, test.indexes[j].k, "found expected temp index primary key at index " + j + testName);
+          req.result.continue();
+        }
+        e = yield;
+        is(req.result, undefined, "exhausted temp index");
+        store.deleteIndex("temp index");
+      }
+
+      // Unique indexes
+      tests =
+        [{ add:     { x: 1, id: 1 },
+           indexes:[{ v: 1, k: 1 }] },
+         { add:     { x: [2, 3], id: 2 },
+           indexes:[{ v: 1, k: 1 },
+                    { v: 2, k: 2 },
+                    { v: 3, k: 2 }] },
+         { put:     { x: [2, 4], id: 3 },
+           fail:    true },
+         { put:     { x: [1, 4], id: 1 },
+           indexes:[{ v: 1, k: 1 },
+                    { v: 2, k: 2 },
+                    { v: 3, k: 2 },
+                    { v: 4, k: 1 }] },
+         { add:     { x: [5, 0, 5, 5, 5], id: 3 },
+           indexes:[{ v: 0, k: 3 },
+                    { v: 1, k: 1 },
+                    { v: 2, k: 2 },
+                    { v: 3, k: 2 },
+                    { v: 4, k: 1 },
+                    { v: 5, k: 3 }] },
+         { delete:  IDBKeyRange.bound(1, 2),
+           indexes:[{ v: 0, k: 3 },
+                    { v: 5, k: 3 }] },
+         { add:     { x: [0, 6], id: 8 },
+           fail:    true },
+         { add:     { x: 5, id: 8 },
+           fail:    true },
+         { put:     { x: 0, id: 8 },
+           fail:    true },
+        ];
+
+      store.deleteIndex("myindex");
+      index = store.createIndex("myindex", "x", { multiEntry: true, unique: true });
+      is(index.multiEntry, true, "index created with multiEntry");
+
+      let i;
+      let indexes;
+      for (i = 0; i < tests.length; ++i) {
+        let test = tests[i];
+        let testName = " for " + JSON.stringify(test);
+        let req;
+        if (test.add) {
+          req = store.add(test.add);
+        }
+        else if (test.put) {
+          req = store.put(test.put);
+        }
+        else if (test.delete) {
+          req = store.delete(test.delete);
+        }
+        else {
+          ok(false, "borked test");
+        }
+        
+        if (!test.fail) {
+          req.onsuccess = grabEventAndContinueHandler;
+          let e = yield;
+          indexes = test.indexes;
+        }
+        else {
+          req.onsuccess = unexpectedSuccessHandler;
+          req.onerror = grabEventAndContinueHandler;
+          ok(true, "waiting for error");
+          let e = yield;
+          ok(true, "got error: " + e.type);
+          e.preventDefault();
+          e.stopPropagation();
+        }
+
+        let e;
+        req = index.openKeyCursor();
+        req.onsuccess = grabEventAndContinueHandler;
+        for (let j = 0; j < indexes.length; ++j) {
+          e = yield;
+          is(req.result.key, indexes[j].v, "found expected index key at index " + j + testName);
+          is(req.result.primaryKey, indexes[j].k, "found expected index primary key at index " + j + testName);
+          req.result.continue();
+        }
+        e = yield;
+        is(req.result, undefined, "exhausted indexes");
+
+        let tempIndex = store.createIndex("temp index", "x", { multiEntry: true, unique: true });
+        req = tempIndex.openKeyCursor();
+        req.onsuccess = grabEventAndContinueHandler;
+        for (let j = 0; j < indexes.length; ++j) {
+          e = yield;
+          is(req.result.key, indexes[j].v, "found expected temp index key at index " + j + testName);
+          is(req.result.primaryKey, indexes[j].k, "found expected temp index primary key at index " + j + testName);
+          req.result.continue();
+        }
+        e = yield;
+        is(req.result, undefined, "exhausted temp index");
+        store.deleteIndex("temp index");
+      }
+
+
+      openRequest.onsuccess = grabEventAndContinueHandler;
+      yield;
+
+      let trans = db.transaction(["mystore"], IDBTransaction.READ_WRITE);
+      store = trans.objectStore("mystore");
+      index = store.index("myindex");
+      is(index.multiEntry, true, "index still is multiEntry");
+      trans.oncomplete = grabEventAndContinueHandler;
+      yield;
+
+      finishTest();
+      yield;
+    }
+  </script>
+  <script type="text/javascript;version=1.7" src="helpers.js"></script>
+</head>
+
+<body onload="runTest();"></body>
+
+</html>