Indexed DB: store/index keyPath property should return same instance

On IDBObjectStore and IDBIndex instances, the keyPath property says is
defined in the spec with: "if this attribute returns an object
(specifically an Array), it returns the same object instance every
time it is inspected". Web platform tests for this were missing.

Since the property is 'any' in Web IDL (since it could be a string or
Array) it can't use the [SameObject] extended attribute. And in the
Blink IDL, it can't use the [SaveSameObject] hint since that requires
[SameObject]. Instead, use the [CachedAttribute] hint and never dirty
the cache.

Bug: 977048
Change-Id: Ic4ede31a3a01e7a8a90c4f54eb8a989a673fcf22
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1674495
Commit-Queue: Daniel Murphy <dmurph@chromium.org>
Reviewed-by: Daniel Murphy <dmurph@chromium.org>
Cr-Commit-Position: refs/heads/master@{#672137}
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_index.h b/third_party/blink/renderer/modules/indexeddb/idb_index.h
index 221839a..0c09700a 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_index.h
+++ b/third_party/blink/renderer/modules/indexeddb/idb_index.h
@@ -58,6 +58,13 @@
   void setName(const String& name, ExceptionState&);
   IDBObjectStore* objectStore() const { return object_store_.Get(); }
   ScriptValue keyPath(ScriptState*) const;
+
+  // Per spec prose, keyPath attribute should return the same object each time
+  // (if it is not just a primitive type). The IDL cannot use [SameObject]
+  // because the key path may not be an 'object'. So use [CachedAttribute],
+  // but never dirty the cache.
+  bool IsKeyPathDirty() const { return false; }
+
   bool unique() const { return Metadata().unique; }
   bool multiEntry() const { return Metadata().multi_entry; }
 
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_index.idl b/third_party/blink/renderer/modules/indexeddb/idb_index.idl
index cc8becb..ebd480e4 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_index.idl
+++ b/third_party/blink/renderer/modules/indexeddb/idb_index.idl
@@ -30,7 +30,7 @@
 ] interface IDBIndex {
     [RaisesException=Setter] attribute DOMString name;
     [SameObject] readonly attribute IDBObjectStore objectStore;
-    [CallWith=ScriptState] readonly attribute any keyPath;
+    [CallWith=ScriptState, CachedAttribute=IsKeyPathDirty] readonly attribute any keyPath;
     readonly attribute boolean multiEntry;
     readonly attribute boolean unique;
 
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_object_store.h b/third_party/blink/renderer/modules/indexeddb/idb_object_store.h
index 8581ce9..c742dbd 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_object_store.h
+++ b/third_party/blink/renderer/modules/indexeddb/idb_object_store.h
@@ -59,6 +59,12 @@
   const IDBObjectStoreMetadata& Metadata() const { return *metadata_; }
   const IDBKeyPath& IdbKeyPath() const { return Metadata().key_path; }
 
+  // Per spec prose, keyPath attribute should return the same object each time
+  // (if it is not just a primitive type). The IDL cannot use [SameObject]
+  // because the key path may not be an 'object'. So use [CachedAttribute],
+  // but never dirty the cache.
+  bool IsKeyPathDirty() const { return false; }
+
   // Implement the IDBObjectStore IDL
   int64_t Id() const { return Metadata().id; }
   const String& name() const { return Metadata().name; }
diff --git a/third_party/blink/renderer/modules/indexeddb/idb_object_store.idl b/third_party/blink/renderer/modules/indexeddb/idb_object_store.idl
index c113f5f..481feda3 100644
--- a/third_party/blink/renderer/modules/indexeddb/idb_object_store.idl
+++ b/third_party/blink/renderer/modules/indexeddb/idb_object_store.idl
@@ -29,7 +29,7 @@
     Exposed=(Window,Worker)
 ] interface IDBObjectStore {
     [RaisesException=Setter] attribute DOMString name;
-    [CallWith=ScriptState] readonly attribute any keyPath;
+    [CallWith=ScriptState, CachedAttribute=IsKeyPathDirty] readonly attribute any keyPath;
     readonly attribute DOMStringList indexNames;
     [SameObject] readonly attribute IDBTransaction transaction;
     readonly attribute boolean autoIncrement;
diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/idbindex_keyPath.any.js b/third_party/blink/web_tests/external/wpt/IndexedDB/idbindex_keyPath.any.js
new file mode 100644
index 0000000..19cf231
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/IndexedDB/idbindex_keyPath.any.js
@@ -0,0 +1,30 @@
+// META: title=IndexedDB: IDBIndex keyPath attribute - same object
+// META: script=support.js
+
+indexeddb_test(
+  (t, db) => {
+    const store = db.createObjectStore('store', {keyPath: ['a', 'b']});
+    store.createIndex('index', ['a', 'b']);
+  },
+  (t, db) => {
+    const tx = db.transaction('store');
+    const store = tx.objectStore('store');
+    const index = store.index('index');
+    assert_equals(typeof index.keyPath, 'object', 'keyPath is an object');
+    assert_true(Array.isArray(index.keyPath), 'keyPath is an array');
+
+    assert_equals(
+      index.keyPath, index.keyPath,
+      'Same object instance is returned each time keyPath is inspected');
+
+    const tx2 = db.transaction('store');
+    const store2 = tx2.objectStore('store');
+    const index2 = store2.index('index');
+
+    assert_not_equals(
+      index.keyPath, index2.keyPath,
+      'Different instances are returned from different index instances.');
+
+    t.done();
+  },
+  `IDBIndex's keyPath attribute returns the same object.`);
diff --git a/third_party/blink/web_tests/external/wpt/IndexedDB/idbobjectstore_keyPath.any.js b/third_party/blink/web_tests/external/wpt/IndexedDB/idbobjectstore_keyPath.any.js
new file mode 100644
index 0000000..91c3674c
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/IndexedDB/idbobjectstore_keyPath.any.js
@@ -0,0 +1,27 @@
+// META: title=IndexedDB: IDBObjectStore keyPath attribute - same object
+// META: script=support.js
+
+indexeddb_test(
+  (t, db) => {
+    db.createObjectStore('store', {keyPath: ['a', 'b']});
+  },
+  (t, db) => {
+    const tx = db.transaction('store');
+    const store = tx.objectStore('store');
+    assert_equals(typeof store.keyPath, 'object', 'keyPath is an object');
+    assert_true(Array.isArray(store.keyPath), 'keyPath is an array');
+
+    assert_equals(
+      store.keyPath, store.keyPath,
+      'Same object instance is returned each time keyPath is inspected');
+
+    const tx2 = db.transaction('store');
+    const store2 = tx2.objectStore('store');
+
+    assert_not_equals(
+      store.keyPath, store2.keyPath,
+      'Different instances are returned from different store instances.');
+
+    t.done();
+  },
+  `IDBObjectStore's keyPath attribute returns the same object.`);