Bug 692630: Support multi-entry indexes. r=bent
authorJonas Sicking <jonas@sicking.cc>
Fri, 02 Dec 2011 18:32:46 -0800
changeset 81208 513428a937004e6de2abdffefb18339a8c6e4c5f
parent 81207 1fddf8667d2180f6a347ccff45cc7504fa18d4b1
child 81209 c1b5a0721e6c6619980d287fed8f01ed1e832d67
push id21564
push usermak77@bonardo.net
push dateSat, 03 Dec 2011 11:10:17 +0000
treeherdermozilla-central@a68c96c1d8e0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbent
bugs692630
milestone11.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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>