Bug 1271505 - Support of IDBCursor.continuePrimaryKey. r=khuey
authorBevis Tseng <btseng@mozilla.com>
Tue, 26 Jul 2016 15:26:50 +0800
changeset 308040 cc4286c4da37de45da2de785bb2a314ea4fba083
parent 308039 eda36a4f93032a0e76b35861c48694eed8710f33
child 308041 91c0b3e808250c412e99012540e766a551e69483
push id30528
push usercbook@mozilla.com
push dateThu, 04 Aug 2016 13:58:28 +0000
treeherdermozilla-central@0ba72e8027cf [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskhuey
bugs1271505
milestone51.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 1271505 - Support of IDBCursor.continuePrimaryKey. r=khuey
dom/indexedDB/ActorsParent.cpp
dom/indexedDB/IDBCursor.cpp
dom/indexedDB/IDBCursor.h
dom/indexedDB/PBackgroundIDBCursor.ipdl
dom/webidl/IDBCursor.webidl
testing/web-platform/meta/MANIFEST.json
testing/web-platform/tests/IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm
testing/web-platform/tests/IndexedDB/idbcursor-continuePrimaryKey.htm
testing/web-platform/tests/IndexedDB/interfaces.idl
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -8421,16 +8421,17 @@ private:
   RefPtr<FullObjectStoreMetadata> mObjectStoreMetadata;
   RefPtr<FullIndexMetadata> mIndexMetadata;
 
   const int64_t mObjectStoreId;
   const int64_t mIndexId;
 
   nsCString mContinueQuery;
   nsCString mContinueToQuery;
+  nsCString mContinuePrimaryKeyQuery;
   nsCString mLocale;
 
   Key mKey;
   Key mObjectKey;
   Key mRangeKey;
   Key mSortKey;
 
   CursorOpBase* mCurrentlyRunningOp;
@@ -16156,16 +16157,44 @@ Cursor::VerifyRequestParams(const Cursor
 
           default:
             MOZ_CRASH("Should never get here!");
         }
       }
       break;
     }
 
+    case CursorRequestParams::TContinuePrimaryKeyParams: {
+      const Key& key = aParams.get_ContinuePrimaryKeyParams().key();
+      const Key& primaryKey = aParams.get_ContinuePrimaryKeyParams().primaryKey();
+      MOZ_ASSERT(!key.IsUnset());
+      MOZ_ASSERT(!primaryKey.IsUnset());
+      switch (mDirection) {
+        case IDBCursor::NEXT:
+          if (NS_WARN_IF(key < sortKey ||
+                         (key == sortKey && primaryKey <= mObjectKey))) {
+            ASSERT_UNLESS_FUZZING();
+            return false;
+          }
+          break;
+
+        case IDBCursor::PREV:
+          if (NS_WARN_IF(key > sortKey ||
+                         (key == sortKey && primaryKey >= mObjectKey))) {
+            ASSERT_UNLESS_FUZZING();
+            return false;
+          }
+          break;
+
+        default:
+          MOZ_CRASH("Should never get here!");
+      }
+      break;
+    }
+
     case CursorRequestParams::TAdvanceParams:
       if (NS_WARN_IF(!aParams.get_AdvanceParams().count())) {
         ASSERT_UNLESS_FUZZING();
         return false;
       }
       break;
 
     default:
@@ -27330,16 +27359,23 @@ OpenOp::DoIndexDatabaseWork(DatabaseConn
                           ) +
         directionClause +
         openLimit;
       mCursor->mContinueToQuery =
         queryStart +
         NS_LITERAL_CSTRING(" AND sort_column >= :current_key") +
         directionClause +
         openLimit;
+      mCursor->mContinuePrimaryKeyQuery =
+        queryStart +
+        NS_LITERAL_CSTRING(" AND sort_column >= :current_key "
+                            "AND index_table.object_data_key >= :object_key "
+                          ) +
+        directionClause +
+        openLimit;
       break;
     }
 
     case IDBCursor::NEXT_UNIQUE: {
       Key upper;
       bool open;
       GetRangeKeyInfo(false, &upper, &open);
       if (usingKeyRange && !upper.IsUnset()) {
@@ -27375,16 +27411,23 @@ OpenOp::DoIndexDatabaseWork(DatabaseConn
                           ) +
         directionClause +
         openLimit;
       mCursor->mContinueToQuery =
         queryStart +
         NS_LITERAL_CSTRING(" AND sort_column <= :current_key") +
         directionClause +
         openLimit;
+      mCursor->mContinuePrimaryKeyQuery =
+        queryStart +
+        NS_LITERAL_CSTRING(" AND sort_column <= :current_key "
+                            "AND index_table.object_data_key <= :object_key "
+                          ) +
+        directionClause +
+        openLimit;
       break;
     }
 
     case IDBCursor::PREV_UNIQUE: {
       Key lower;
       bool open;
       GetRangeKeyInfo(true, &lower, &open);
       if (usingKeyRange && !lower.IsUnset()) {
@@ -27552,16 +27595,23 @@ OpenOp::DoIndexKeyDatabaseWork(DatabaseC
                                   "object_data_key > :object_key )") +
         directionClause +
         openLimit;
       mCursor->mContinueToQuery =
         queryStart +
         NS_LITERAL_CSTRING(" AND sort_column >= :current_key ") +
         directionClause +
         openLimit;
+      mCursor->mContinuePrimaryKeyQuery =
+        queryStart +
+        NS_LITERAL_CSTRING(" AND sort_column >= :current_key "
+                            "AND object_data_key >= :object_key "
+                          ) +
+        directionClause +
+        openLimit;
       break;
     }
 
     case IDBCursor::NEXT_UNIQUE: {
       Key upper;
       bool open;
       GetRangeKeyInfo(false, &upper, &open);
       if (usingKeyRange && !upper.IsUnset()) {
@@ -27597,16 +27647,23 @@ OpenOp::DoIndexKeyDatabaseWork(DatabaseC
                                   "object_data_key < :object_key )") +
         directionClause +
         openLimit;
       mCursor->mContinueToQuery =
         queryStart +
         NS_LITERAL_CSTRING(" AND sort_column <= :current_key ") +
         directionClause +
         openLimit;
+      mCursor->mContinuePrimaryKeyQuery =
+        queryStart +
+        NS_LITERAL_CSTRING(" AND sort_column <= :current_key "
+                            "AND object_data_key <= :object_key "
+                          ) +
+        directionClause +
+        openLimit;
       break;
     }
 
     case IDBCursor::PREV_UNIQUE: {
       Key lower;
       bool open;
       GetRangeKeyInfo(true, &lower, &open);
       if (usingKeyRange && !lower.IsUnset()) {
@@ -27637,16 +27694,17 @@ nsresult
 Cursor::
 OpenOp::DoDatabaseWork(DatabaseConnection* aConnection)
 {
   MOZ_ASSERT(aConnection);
   aConnection->AssertIsOnConnectionThread();
   MOZ_ASSERT(mCursor);
   MOZ_ASSERT(mCursor->mContinueQuery.IsEmpty());
   MOZ_ASSERT(mCursor->mContinueToQuery.IsEmpty());
+  MOZ_ASSERT(mCursor->mContinuePrimaryKeyQuery.IsEmpty());
   MOZ_ASSERT(mCursor->mKey.IsUnset());
   MOZ_ASSERT(mCursor->mRangeKey.IsUnset());
 
   PROFILER_LABEL("IndexedDB",
                  "Cursor::OpenOp::DoDatabaseWork",
                  js::ProfileEntry::Category::STORAGE);
 
   nsresult rv;
@@ -27719,16 +27777,20 @@ ContinueOp::DoDatabaseWork(DatabaseConne
   MOZ_ASSERT(!mCursor->mContinueQuery.IsEmpty());
   MOZ_ASSERT(!mCursor->mContinueToQuery.IsEmpty());
   MOZ_ASSERT(!mCursor->mKey.IsUnset());
 
   const bool isIndex =
     mCursor->mType == OpenCursorParams::TIndexOpenCursorParams ||
     mCursor->mType == OpenCursorParams::TIndexOpenKeyCursorParams;
 
+  MOZ_ASSERT_IF(isIndex &&
+                (mCursor->mDirection == IDBCursor::NEXT ||
+                 mCursor->mDirection == IDBCursor::PREV),
+                !mCursor->mContinuePrimaryKeyQuery.IsEmpty());
   MOZ_ASSERT_IF(isIndex, mCursor->mIndexId);
   MOZ_ASSERT_IF(isIndex, !mCursor->mObjectKey.IsUnset());
 
   PROFILER_LABEL("IndexedDB",
                  "Cursor::ContinueOp::DoDatabaseWork",
                  js::ProfileEntry::Category::STORAGE);
 
   // We need to pick a query based on whether or not a key was passed to the
@@ -27736,52 +27798,57 @@ ContinueOp::DoDatabaseWork(DatabaseConne
   // is greater than (or less than, if we're running a PREV cursor) the current
   // key. If a key was passed we'll grab the next item in the database that is
   // greater than (or less than, if we're running a PREV cursor) or equal to the
   // key that was specified.
 
   // Note: Changing the number or order of SELECT columns in the query will
   // require changes to CursorOpBase::PopulateResponseFromStatement.
   bool hasContinueKey = false;
+  bool hasContinuePrimaryKey = false;
   uint32_t advanceCount = 1;
-
-  if (mParams.type() == CursorRequestParams::TContinueParams) {
-    // Always go to the next result.
-    if (mParams.get_ContinueParams().key().IsUnset()) {
-      hasContinueKey = false;
-    } else {
+  Key& currentKey = mCursor->IsLocaleAware() ? mCursor->mSortKey : mCursor->mKey;
+
+  switch (mParams.type()) {
+    case CursorRequestParams::TContinueParams:
+      if (!mParams.get_ContinueParams().key().IsUnset()) {
+        hasContinueKey = true;
+        currentKey = mParams.get_ContinueParams().key();
+      }
+      break;
+    case CursorRequestParams::TContinuePrimaryKeyParams:
+      MOZ_ASSERT(!mParams.get_ContinuePrimaryKeyParams().key().IsUnset());
+      MOZ_ASSERT(!mParams.get_ContinuePrimaryKeyParams().primaryKey().IsUnset());
+      MOZ_ASSERT(mCursor->mDirection == IDBCursor::NEXT ||
+                 mCursor->mDirection == IDBCursor::PREV);
       hasContinueKey = true;
-    }
-  } else {
-    advanceCount = mParams.get_AdvanceParams().count();
-    hasContinueKey = false;
+      hasContinuePrimaryKey = true;
+      currentKey = mParams.get_ContinuePrimaryKeyParams().key();
+      break;
+    case CursorRequestParams::TAdvanceParams:
+      advanceCount = mParams.get_AdvanceParams().count();
+      break;
+    default:
+      MOZ_CRASH("Should never get here!");
   }
 
   const nsCString& continueQuery =
+    hasContinuePrimaryKey ? mCursor->mContinuePrimaryKeyQuery :
     hasContinueKey ? mCursor->mContinueToQuery : mCursor->mContinueQuery;
 
   MOZ_ASSERT(advanceCount > 0);
   nsAutoCString countString;
   countString.AppendInt(advanceCount);
 
   nsCString query = continueQuery + countString;
 
   NS_NAMED_LITERAL_CSTRING(currentKeyName, "current_key");
   NS_NAMED_LITERAL_CSTRING(rangeKeyName, "range_key");
   NS_NAMED_LITERAL_CSTRING(objectKeyName, "object_key");
 
-  const bool localeAware = mCursor->IsLocaleAware();
-
-  Key& currentKey = mCursor->mKey;
-  if (hasContinueKey) {
-    currentKey = mParams.get_ContinueParams().key();
-  } else if (localeAware) {
-    currentKey = mCursor->mSortKey;
-  }
-
   const bool usingRangeKey = !mCursor->mRangeKey.IsUnset();
 
   DatabaseConnection::CachedStatement stmt;
   nsresult rv = aConnection->GetCachedStatement(query, &stmt);
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
@@ -27813,16 +27880,26 @@ ContinueOp::DoDatabaseWork(DatabaseConne
       (mCursor->mDirection == IDBCursor::NEXT ||
        mCursor->mDirection == IDBCursor::PREV)) {
     rv = mCursor->mObjectKey.BindToStatement(stmt, objectKeyName);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
   }
 
+  // Bind object key if primaryKey is specified.
+  if (hasContinuePrimaryKey) {
+    rv = mParams.get_ContinuePrimaryKeyParams().primaryKey()
+      .BindToStatement(stmt, objectKeyName);
+    if (NS_WARN_IF(NS_FAILED(rv))) {
+      return rv;
+    }
+  }
+
+
   bool hasResult;
   for (uint32_t index = 0; index < advanceCount; index++) {
     rv = stmt->ExecuteStep(&hasResult);
     if (NS_WARN_IF(NS_FAILED(rv))) {
       return rv;
     }
 
     if (!hasResult) {
--- a/dom/indexedDB/IDBCursor.cpp
+++ b/dom/indexedDB/IDBCursor.cpp
@@ -511,16 +511,126 @@ IDBCursor::Continue(JSContext* aCx,
   }
 
   mBackgroundActor->SendContinueInternal(ContinueParams(key));
 
   mContinueCalled = true;
 }
 
 void
+IDBCursor::ContinuePrimaryKey(JSContext* aCx,
+                             JS::Handle<JS::Value> aKey,
+                             JS::Handle<JS::Value> aPrimaryKey,
+                             ErrorResult &aRv)
+{
+  AssertIsOnOwningThread();
+
+  if (!mTransaction->IsOpen()) {
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_TRANSACTION_INACTIVE_ERR);
+    return;
+  }
+
+  if (IsSourceDeleted()) {
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR);
+    return;
+  }
+
+  if ((mType != Type_Index && mType != Type_IndexKey) ||
+      (mDirection != NEXT && mDirection != PREV)) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_ACCESS_ERR);
+    return;
+  }
+
+  if (!mHaveValue || mContinueCalled) {
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_NOT_ALLOWED_ERR);
+    return;
+  }
+
+  Key key;
+  aRv = key.SetFromJSVal(aCx, aKey);
+  if (aRv.Failed()) {
+    return;
+  }
+
+#ifdef ENABLE_INTL_API
+  if (IsLocaleAware() && !key.IsUnset()) {
+    Key tmp;
+    aRv = key.ToLocaleBasedKey(tmp, mSourceIndex->Locale());
+    if (aRv.Failed()) {
+      return;
+    }
+    key = tmp;
+  }
+
+  const Key& sortKey = IsLocaleAware() ? mSortKey : mKey;
+#else
+  const Key& sortKey = mKey;
+#endif
+
+  if (key.IsUnset()) {
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    return;
+  }
+
+  Key primaryKey;
+  aRv = primaryKey.SetFromJSVal(aCx, aPrimaryKey);
+  if (aRv.Failed()) {
+    return;
+  }
+
+  if (primaryKey.IsUnset()) {
+    aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+    return;
+  }
+
+  switch (mDirection) {
+    case NEXT:
+      if (key < sortKey ||
+          (key == sortKey && primaryKey <= mPrimaryKey)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+        return;
+      }
+      break;
+
+    case PREV:
+      if (key > sortKey ||
+          (key == sortKey && primaryKey >= mPrimaryKey)) {
+        aRv.Throw(NS_ERROR_DOM_INDEXEDDB_DATA_ERR);
+        return;
+      }
+      break;
+
+    default:
+      MOZ_CRASH("Unknown direction type!");
+  }
+
+  const uint64_t requestSerialNumber = IDBRequest::NextSerialNumber();
+  mRequest->SetLoggingSerialNumber(requestSerialNumber);
+
+  IDB_LOG_MARK("IndexedDB %s: Child  Transaction[%lld] Request[%llu]: "
+                 "database(%s).transaction(%s).objectStore(%s)."
+                 "index(%s).cursor(%s).continuePrimaryKey(%s, %s)",
+               "IndexedDB %s: C T[%lld] R[%llu]: IDBCursor.continuePrimaryKey()",
+               IDB_LOG_ID_STRING(),
+               mTransaction->LoggingSerialNumber(),
+               requestSerialNumber,
+               IDB_LOG_STRINGIFY(mTransaction->Database()),
+               IDB_LOG_STRINGIFY(mTransaction),
+               IDB_LOG_STRINGIFY(mSourceIndex->ObjectStore()),
+               IDB_LOG_STRINGIFY(mSourceIndex),
+               IDB_LOG_STRINGIFY(mDirection),
+               IDB_LOG_STRINGIFY(key),
+               IDB_LOG_STRINGIFY(primaryKey));
+
+  mBackgroundActor->SendContinueInternal(ContinuePrimaryKeyParams(key, primaryKey));
+
+  mContinueCalled = true;
+}
+
+void
 IDBCursor::Advance(uint32_t aCount, ErrorResult &aRv)
 {
   AssertIsOnOwningThread();
 
   if (!aCount) {
     aRv.ThrowTypeError<MSG_INVALID_ADVANCE_COUNT>();
     return;
   }
--- a/dom/indexedDB/IDBCursor.h
+++ b/dom/indexedDB/IDBCursor.h
@@ -149,16 +149,22 @@ public:
   GetValue(JSContext* aCx,
            JS::MutableHandle<JS::Value> aResult,
            ErrorResult& aRv);
 
   void
   Continue(JSContext* aCx, JS::Handle<JS::Value> aKey, ErrorResult& aRv);
 
   void
+  ContinuePrimaryKey(JSContext* aCx,
+                     JS::Handle<JS::Value> aKey,
+                     JS::Handle<JS::Value> aPrimaryKey,
+                     ErrorResult& aRv);
+
+  void
   Advance(uint32_t aCount, ErrorResult& aRv);
 
   already_AddRefed<IDBRequest>
   Update(JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv);
 
   already_AddRefed<IDBRequest>
   Delete(JSContext* aCx, ErrorResult& aRv);
 
--- a/dom/indexedDB/PBackgroundIDBCursor.ipdl
+++ b/dom/indexedDB/PBackgroundIDBCursor.ipdl
@@ -21,24 +21,31 @@ namespace mozilla {
 namespace dom {
 namespace indexedDB {
 
 struct ContinueParams
 {
   Key key;
 };
 
+struct ContinuePrimaryKeyParams
+{
+  Key key;
+  Key primaryKey;
+};
+
 struct AdvanceParams
 {
   uint32_t count;
 };
 
 union CursorRequestParams
 {
   ContinueParams;
+  ContinuePrimaryKeyParams;
   AdvanceParams;
 };
 
 struct ObjectStoreCursorResponse
 {
   Key key;
   SerializedStructuredCloneReadInfo cloneInfo;
 };
--- a/dom/webidl/IDBCursor.webidl
+++ b/dom/webidl/IDBCursor.webidl
@@ -31,16 +31,19 @@ interface IDBCursor {
 
     [Throws]
     void       advance ([EnforceRange] unsigned long count);
 
     [Throws]
     void       continue (optional any key);
 
     [Throws]
+    void       continuePrimaryKey(any key, any primaryKey);
+
+    [Throws]
     IDBRequest delete ();
 };
 
 [Exposed=(Window,Worker,System)]
 interface IDBCursorWithValue : IDBCursor {
     [Throws]
     readonly    attribute any value;
 };
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -11401,16 +11401,24 @@
         "path": "IndexedDB/idbcursor-advance.htm",
         "url": "/IndexedDB/idbcursor-advance.htm"
       },
       {
         "path": "IndexedDB/idbcursor-continue.htm",
         "url": "/IndexedDB/idbcursor-continue.htm"
       },
       {
+        "path": "IndexedDB/idbcursor-continuePrimaryKey.htm",
+        "url": "/IndexedDB/idbcursor-continuePrimaryKey.htm"
+      },
+      {
+        "path": "IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm",
+        "url": "/IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm"
+      },
+      {
         "path": "IndexedDB/idbcursor-direction-index-keyrange.htm",
         "url": "/IndexedDB/idbcursor-direction-index-keyrange.htm"
       },
       {
         "path": "IndexedDB/idbcursor-direction-index.htm",
         "url": "/IndexedDB/idbcursor-direction-index.htm"
       },
       {
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/IndexedDB/idbcursor-continuePrimaryKey-exception-order.htm
@@ -0,0 +1,381 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>IDBCursor.continuePrimaryKey() - Exception Orders </title>
+<link rel="author" title="Mozilla" href="https://www.mozilla.org">
+<link rel="help" href="http://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support.js"></script>
+
+<script>
+function setup_test_store(db) {
+  var records = [ { iKey: "A", pKey: 1 },
+                  { iKey: "A", pKey: 2 },
+                  { iKey: "A", pKey: 3 },
+                  { iKey: "A", pKey: 4 },
+                  { iKey: "B", pKey: 5 },
+                  { iKey: "B", pKey: 6 },
+                  { iKey: "B", pKey: 7 },
+                  { iKey: "C", pKey: 8 },
+                  { iKey: "C", pKey: 9 },
+                  { iKey: "D", pKey: 10 } ];
+
+    var store = db.createObjectStore("test", { keyPath: "pKey" });
+    var index = store.createIndex("idx", "iKey");
+
+    for(var i = 0; i < records.length; i++) {
+        store.add(records[i]);
+    }
+
+    return store;
+}
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            store.deleteIndex("idx");
+        });
+        txn.oncomplete = function() {
+            assert_throws("TransactionInactiveError", function() {
+                cursor.continuePrimaryKey("A", 4);
+            }, "transaction-state check should precede deletion check");
+            t.done();
+        };
+    },
+    null,
+    "TransactionInactiveError v.s. InvalidStateError(deleted index)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var cursor_rq = store.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            db.deleteObjectStore("test");
+
+            assert_throws("InvalidStateError", function() {
+                cursor.continuePrimaryKey("A", 4);
+            }, "deletion check should precede index source check");
+            t.done();
+        });
+    },
+    null,
+    "InvalidStateError(deleted source) v.s. InvalidAccessError(incorrect source)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor(null, "nextunique");
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            store.deleteIndex("idx");
+
+            assert_throws("InvalidStateError", function() {
+              cursor.continuePrimaryKey("A", 4);
+            }, "deletion check should precede cursor direction check");
+            t.done();
+        });
+    },
+    null,
+    "InvalidStateError(deleted source) v.s. InvalidAccessError(incorrect direction)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = db.createObjectStore("test", {keyPath:"pKey"});
+        var index = store.createIndex("idx", "iKey");
+
+        store.add({ iKey: "A", pKey: 1 });
+
+        var cursor_rq = index.openCursor(null, "nextunique");
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            if (e.target.result) {
+                cursor = e.target.result;
+                cursor.continue();
+                return;
+            }
+
+            assert_throws("InvalidAccessError", function() {
+                cursor.continuePrimaryKey("A", 4);
+            }, "direction check should precede got_value_flag check");
+            t.done();
+        });
+    },
+    null,
+    "InvalidAccessError(incorrect direction) v.s. InvalidStateError(iteration complete)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = db.createObjectStore("test", {keyPath:"pKey"});
+        var index = store.createIndex("idx", "iKey");
+
+        store.add({ iKey: "A", pKey: 1 });
+
+        var cursor_rq = index.openCursor(null, "nextunique");
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            if (!cursor) {
+                cursor = e.target.result;
+                assert_true(!!cursor, "acquire cursor");
+
+                cursor.continue();
+
+                assert_throws("InvalidAccessError", function() {
+                    cursor.continuePrimaryKey("A", 4);
+                }, "direction check should precede iteration ongoing check");
+                t.done();
+            }
+        });
+    },
+    null,
+    "InvalidAccessError(incorrect direction) v.s. InvalidStateError(iteration ongoing)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var cursor_rq = store.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            if (!cursor) {
+	            cursor = e.target.result;
+	            assert_true(!!cursor, "acquire cursor");
+
+	            cursor.continue();
+
+	            assert_throws("InvalidAccessError", function() {
+	                cursor.continuePrimaryKey("A", 4);
+	            }, "index source check should precede iteration ongoing check");
+	            t.done();
+            }
+        });
+    },
+    null,
+    "InvalidAccessError(incorrect source) v.s. InvalidStateError(iteration ongoing)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = db.createObjectStore("test", {keyPath:"pKey"});
+
+        store.add({ iKey: "A", pKey: 1 });
+
+        var cursor_rq = store.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            if (e.target.result) {
+                cursor = e.target.result;
+                cursor.continue();
+                return;
+            }
+
+	        assert_throws("InvalidAccessError", function() {
+	            cursor.continuePrimaryKey("A", 4);
+	        }, "index source check should precede got_value_flag check");
+	        t.done();
+        });
+    },
+    null,
+    "InvalidAccessError(incorrect source) v.s. InvalidStateError(iteration complete)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            if (!cursor) {
+	            cursor = e.target.result;
+	            assert_true(!!cursor, "acquire cursor");
+
+	            cursor.continue();
+
+	            assert_throws("InvalidStateError", function() {
+	                cursor.continuePrimaryKey(null, 4);
+	            }, "iteration ongoing check should precede unset key check");
+	            t.done();
+            }
+        });
+    },
+    null,
+    "InvalidStateError(iteration ongoing) v.s. DataError(unset key)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = db.createObjectStore("test", {keyPath:"pKey"});
+        var index = store.createIndex("idx", "iKey");
+
+        store.add({ iKey: "A", pKey: 1 });
+
+        var cursor_rq = index.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            if (e.target.result) {
+                cursor = e.target.result;
+                cursor.continue();
+                return;
+            }
+
+            assert_throws("InvalidStateError", function() {
+                cursor.continuePrimaryKey(null, 4);
+            }, "got_value_flag check should precede unset key check");
+            t.done();
+        });
+    },
+    null,
+    "InvalidStateError(iteration complete) v.s. DataError(unset key)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey(null, 4);
+            }, "DataError is expected if key is unset.");
+            t.done();
+        });
+    },
+    null,
+    "DataError(unset key)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor();
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("A", null);
+            }, "DataError is expected if primary key is unset.");
+            t.done();
+        });
+    },
+    null,
+    "DataError(unset primary key)"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor(IDBKeyRange.lowerBound("B"));
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            assert_equals(cursor.key, "B", "expected key");
+            assert_equals(cursor.primaryKey, 5, "expected primary key");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("A", 6);
+            }, "DataError is expected if key is lower then current one.");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("B", 5);
+            }, "DataError is expected if primary key is equal to current one.");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("B", 4);
+            }, "DataError is expected if primary key is lower than current one.");
+
+            t.done();
+        });
+    },
+    null,
+    "DataError(keys are lower then current one) in 'next' direction"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        var store = setup_test_store(db);
+        var index = store.index("idx");
+        var cursor_rq = index.openCursor(IDBKeyRange.upperBound("B"), "prev");
+        var cursor;
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            cursor = e.target.result;
+            assert_true(!!cursor, "acquire cursor");
+
+            assert_equals(cursor.key, "B", "expected key");
+            assert_equals(cursor.primaryKey, 7, "expected primary key");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("C", 6);
+            }, "DataError is expected if key is larger then current one.");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("B", 7);
+            }, "DataError is expected if primary key is equal to current one.");
+
+            assert_throws("DataError", function() {
+                cursor.continuePrimaryKey("B", 8);
+            }, "DataError is expected if primary key is larger than current one.");
+
+            t.done();
+        });
+    },
+    null,
+    "DataError(keys are larger then current one) in 'prev' direction"
+);
+</script>
+
+<div id="log"></div>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/IndexedDB/idbcursor-continuePrimaryKey.htm
@@ -0,0 +1,238 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>IDBCursor.continuePrimaryKey()</title>
+<link rel="author" title="Mozilla" href="https://www.mozilla.org">
+<link rel="help" href="http://w3c.github.io/IndexedDB/#dom-idbcursor-continueprimarykey">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="support.js"></script>
+
+<script>
+function setup_test_store(db) {
+  var records = [ { iKey: "A", pKey: 1 },
+                  { iKey: "A", pKey: 2 },
+                  { iKey: "A", pKey: 3 },
+                  { iKey: "A", pKey: 4 },
+                  { iKey: "B", pKey: 5 },
+                  { iKey: "B", pKey: 6 },
+                  { iKey: "B", pKey: 7 },
+                  { iKey: "C", pKey: 8 },
+                  { iKey: "C", pKey: 9 },
+                  { iKey: "D", pKey: 10 } ];
+
+    var store = db.createObjectStore("test", { keyPath: "pKey" });
+    store.createIndex("idx", "iKey");
+
+    for(var i = 0; i < records.length; i++) {
+        store.add(records[i]);
+    }
+}
+
+indexeddb_test(
+    function(t, db, txn) {
+        setup_test_store(db);
+    },
+    function(t, db) {
+	    var cursor_rq = db.transaction("test")
+	                      .objectStore("test")
+	                      .index("idx")
+	                      .openCursor();
+
+        var expectedResults = [ { iKey: "A", pKey: 1 },
+                                { iKey: "A", pKey: 2 },
+                                { iKey: "A", pKey: 4 },
+                                { iKey: "B", pKey: 6 },
+                                { iKey: "C", pKey: 8 },
+                                { iKey: "D", pKey: 10 } ];
+	    var results = [];
+
+	    cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+	    cursor_rq.onsuccess = t.step_func(function(e) {
+            var cursor = e.target.result;
+            if (!cursor) {
+                assert_equals(JSON.stringify(results),
+                              JSON.stringify(expectedResults)
+                              ,"check iternation results");
+                t.done();
+                return;
+            }
+
+            if (results.length == 0) {
+                cursor.continuePrimaryKey("A", 2);
+            } else if (results.length == 1) {
+                cursor.continuePrimaryKey("A", 4);
+            } else if (results.length == 2) {
+                cursor.continuePrimaryKey("B", 6);
+            } else if (results.length == 3) {
+                cursor.continuePrimaryKey("C", 8);
+            } else if (results.length == 4) {
+                cursor.continuePrimaryKey("D", 10);
+            } else if (results.length == 5) {
+                cursor.continue();
+            } else {
+                assert_unreached('the iternation shall be done');
+            }
+
+            results.push(cursor.value);
+        });
+    },
+    "continuePrimaryKey() on cursor, direction: 'next'"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        setup_test_store(db);
+    },
+    function(t, db) {
+        var cursor_rq = db.transaction("test")
+                          .objectStore("test")
+                          .index("idx")
+                          .openKeyCursor();
+
+        var expectedResults = [ { iKey: "A", pKey: 1 },
+                                { iKey: "A", pKey: 2 },
+                                { iKey: "A", pKey: 4 },
+                                { iKey: "B", pKey: 6 },
+                                { iKey: "C", pKey: 8 },
+                                { iKey: "D", pKey: 10 } ];
+        var results = [];
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            var cursor = e.target.result;
+            if (!cursor) {
+                assert_equals(JSON.stringify(results),
+                              JSON.stringify(expectedResults)
+                              ,"check iternation results");
+                t.done();
+                return;
+            }
+
+            if (results.length == 0) {
+                cursor.continuePrimaryKey("A", 2);
+            } else if (results.length == 1) {
+                cursor.continuePrimaryKey("A", 4);
+            } else if (results.length == 2) {
+                cursor.continuePrimaryKey("B", 6);
+            } else if (results.length == 3) {
+                cursor.continuePrimaryKey("C", 8);
+            } else if (results.length == 4) {
+                cursor.continuePrimaryKey("D", 10);
+            } else if (results.length == 5) {
+                cursor.continue();
+            } else {
+                assert_unreached('the iternation shall be done');
+            }
+
+            assert_equals(cursor.value, undefined, "cursor.value shall be undefined");
+            results.push({iKey: cursor.key, pKey: cursor.primaryKey });
+        });
+    },
+    "continuePrimaryKey() on key cursor, direction: 'next'"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        setup_test_store(db);
+    },
+    function(t, db) {
+        var cursor_rq = db.transaction("test")
+                          .objectStore("test")
+                          .index("idx")
+                          .openCursor(null, "prev");
+
+        var expectedResults = [ { iKey: "D", pKey: 10 },
+                                { iKey: "C", pKey: 8 },
+                                { iKey: "B", pKey: 6 },
+                                { iKey: "A", pKey: 4 },
+                                { iKey: "A", pKey: 2 },
+                                { iKey: "A", pKey: 1 } ];
+        var results = [];
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            var cursor = e.target.result;
+            if (!cursor) {
+                assert_equals(JSON.stringify(results),
+                              JSON.stringify(expectedResults)
+                              ,"check iternation results");
+              t.done();
+              return;
+            }
+
+            if (results.length == 0) {
+                cursor.continuePrimaryKey("C", 8);
+            } else if (results.length == 1) {
+                cursor.continuePrimaryKey("B", 6);
+            } else if (results.length == 2) {
+                cursor.continuePrimaryKey("A", 4);
+            } else if (results.length == 3) {
+                cursor.continuePrimaryKey("A", 2);
+            } else if (results.length == 4) {
+                cursor.continuePrimaryKey("A", 1);
+            } else if (results.length == 5) {
+                cursor.continue();
+            } else {
+                assert_unreached('the iternation shall be done');
+            }
+
+            results.push(cursor.value);
+        });
+    },
+    "continuePrimaryKey() on cursor, direction: 'prev'"
+);
+
+indexeddb_test(
+    function(t, db, txn) {
+        setup_test_store(db);
+    },
+    function(t, db) {
+        var cursor_rq = db.transaction("test")
+                          .objectStore("test")
+                          .index("idx")
+                          .openKeyCursor(null, "prev");
+
+        var expectedResults = [ { iKey: "D", pKey: 10 },
+                                { iKey: "C", pKey: 8 },
+                                { iKey: "B", pKey: 6 },
+                                { iKey: "A", pKey: 4 },
+                                { iKey: "A", pKey: 2 },
+                                { iKey: "A", pKey: 1 } ];
+        var results = [];
+
+        cursor_rq.onerror = t.unreached_func('openCursor should succeed');
+        cursor_rq.onsuccess = t.step_func(function(e) {
+            var cursor = e.target.result;
+            if (!cursor) {
+                assert_equals(JSON.stringify(results),
+                              JSON.stringify(expectedResults)
+                              ,"check iternation results");
+                t.done();
+                return;
+            }
+
+            if (results.length == 0) {
+                cursor.continuePrimaryKey("C", 8);
+            } else if (results.length == 1) {
+                cursor.continuePrimaryKey("B", 6);
+            } else if (results.length == 2) {
+                cursor.continuePrimaryKey("A", 4);
+            } else if (results.length == 3) {
+                cursor.continuePrimaryKey("A", 2);
+            } else if (results.length == 4) {
+                cursor.continuePrimaryKey("A", 1);
+            } else if (results.length == 5) {
+                cursor.continue();
+            } else {
+                assert_unreached('the iternation shall be done');
+            }
+
+            assert_equals(cursor.value, undefined, "cursor.value shall be undefined");
+            results.push({iKey: cursor.key, pKey: cursor.primaryKey });
+        });
+    },
+    "continuePrimaryKey() on key cursor, direction: 'prev'"
+);
+</script>
+
+<div id="log"></div>
--- a/testing/web-platform/tests/IndexedDB/interfaces.idl
+++ b/testing/web-platform/tests/IndexedDB/interfaces.idl
@@ -122,16 +122,17 @@ interface IDBIndex {
 interface IDBCursor {
     readonly    attribute (IDBObjectStore or IDBIndex) source;
     readonly    attribute IDBCursorDirection           direction;
     readonly    attribute any                          key;
     readonly    attribute any                          primaryKey;
     IDBRequest update (any value);
     void       advance ([EnforceRange] unsigned long count);
     void       continue (optional any key);
+    void       continuePrimaryKey(any key, any primaryKey);
     IDBRequest delete ();
 };
 
 interface IDBCursorWithValue : IDBCursor {
     readonly    attribute any value;
 };
 
 interface IDBTransaction : EventTarget {