[IndexedDB] Current editor's draft

Hi folks,

There are several unimplemented proposals on strengthening and
expanding IndexedDB. The reason I have not implemented them yet is
because I am not convinced they are necessary in toto. Here's my
attempt at explaining why. I apologize in advance for not responding
to individual proposals due to personal time constraints. I will
however respond in detail on individual bug reports, e.g., as I did
with 9975.

I used the current editor's draft asynchronous API to understand where
some of the remaining programming difficulties remain. Based on this
attempt, I find several areas to strengthen, the most prominent of
which is how we use transactions. Another is to add the concept of a
catalog as a special kind of object store.

Here are the main areas I propose to address in the editor's spec:

1. It is time to separate the dynamic and static scope transaction
creation so that they are asynchronous and synchronous respectively.
2. Provide a catalog object that can be used to atomically add/remove
object stores and indexes as well as modify version.
3.  Cursors may produce a null key or a null value. I don't see how
this is valid signaling for non-preloaded cursors. I think we need to
add a new flag on the cursor to find out if the cursor is exhausted.

A couple of additional points:

1. I did not see any significant benefits of preloaded cursors in
terms of programming ease.
2. *_NO_DUPLICATE simplifies programming as well as aids in good
performance. I have shown one example that illustrates this.
3. Since it seems continue is acceptable to implementers, I am also
suggesting we use delete instead of remove, for consistency sake.

------- IDL --------

[NoInterfaceObject]
interface IDBDatabase {
  readonly attribute DOMString     name;
  readonly attribute DOMString     description;
  readonly attribute DOMStringList objectStores;
  /*
  Open an object store in the specified transaction. The transaction can be
  dynamic scoped, or the requested object store must be in the static scope.
  Returns IDBRequest whose success event of IDBTransactionEvent type contains
  a result with IDBObjectStore and transaction is an IDBTransactionRequest.
  */
  IDBRequest openObjectStore(in DOMString name, in IDBTransaction txn,
  in optional unsigned short mode /* defaults to READ_WRITE */);
  /*
  Open the database catalog in the specified transaction for exclusive access.
  Returns IDBRequest whose success event of IDBTransactionEvent type contains
  a result with IDBCatalog and transaction is an IDBTransactionRequest.
  */
  IDBRequest openCatalog(in IDBTransaction txn);
  /*
  Create a new static scoped transaction asynchronously.
  Returns IDBRequest whose success event of IDBSuccessEvent type contains a
  result with IDBTransactionRequest.
  */
  IDBRequest openTransaction (in optional DOMStringList storeNames /*
defaults to mean all object stores */,
  in optional unsigned short mode /* defaults to READ_WRITE */,
  in optional IDBTransaction parent /* defaults to null */,
  in optional unsigned long timeout /* defaults to no timeout*/);
  /*
  Create a new dynamic scoped transaction. This returns a transaction handle
  synchronously.
  */
  IDBTransaction transaction (in optional IDBTransaction parent /*
defaults to null */,
  in optional unsigned long timeout /* defaults to no timeout*/);
};

[NoInterfaceObject]
interface IDBCatalog {
  readonly attribute DOMString     version;
  /*
  Change the version to the specified value as part of the transaction in
  which the catalog was opened. This version should not have been created
  prior to this call otherwise raises IDBDatabaseException with DATA_ERR.
  */
  void       setVersion ([TreatNullAs=EmptyString] in DOMString version)
  (raises IDBDatabaseException);
  /*
  Create an object store in the specified transaction, when the transaction is
  committed. Raises IDBDatabaseException with CONSTRAINT_ERR if an object
  store exists with the given name.
  */
  void createObjectStore (in DOMString name,
  [TreatNullAs=EmptyString] in optional DOMString keyPath /* defaults
to empty string */,
  in optional boolean autoIncrement /* defaults to true */) (raises
IDBDatabaseException);

  /*
  Remove an object store in the specified transaction, when the transaction is
  committed. Raises IDBDatabaseException with NOT_FOUND_ERR if an object store
  does not exist with the given name.
  */
  IDBRequest removeObjectStore (in DOMString storeName)
  (raises IDBDatabaseException);
  /*
  Create an index on the named object store in the specified transaction, when
  the transaction is committed. Raises IDBDatabaseException with NOT_FOUND_ERR
  if an object store does not exist with the given name and with
  CONSTRAINT_ERR if an index with the given name exists already.
  */
  IDBRequest createIndex (in DOMString objectStore, in DOMString name,
  in DOMString keyPath, in optional boolean unique /* defaults to false */)
  (raises IDBDatabaseException);
  /*
  Remove an index on the named object store in the specified transaction, when
  the transaction is committed. Raises IDBDatabaseException with NOT_FOUND_ERR
  if the named object store or named index does not exist.
  */
  IDBRequest removeIndex (in DOMString objectStore, in DOMString indexName)
  (raises IDBDatabaseException);
};

[NoInterfaceObject]
interface IDBObjectStore {
  const unsigned short READ_WRITE = 0;
  const unsigned short READ_ONLY = 1;
  const unsigned short SNAPSHOT_READ = 2;
  readonly attribute IDBTransaction txn;
  readonly attribute unsigned short mode;
  readonly attribute DOMString      name;
  readonly attribute DOMString      keyPath;
  readonly attribute DOMStringList  indexNames;

  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store was not
  opened in READ_WRITE mode.
  */
  IDBRequest put (in any value, in optional any key)
  (raises IDBDatabaseException);
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store was not
  opened in READ_WRITE mode.
  */
  IDBRequest add (in any value, in optional any key)
  (raises IDBDatabaseException);
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store was not
  opened in READ_WRITE mode.
  */
  IDBRequest delete (in any key) (raises IDBDatabaseException);
  IDBRequest get (in any key);
  IDBRequest openCursor (in optional IDBKeyRange range /* defaults to
all objects */,
  in optional unsigned short direction /* defaults to NEXT */,
  in optional boolean preload /* defaults to false */);
  IDBIndex   index (in DOMString name);
};

[NoInterfaceObject]
interface IDBIndex {
  readonly attribute DOMString name;
  readonly attribute DOMString storeName;
  readonly attribute DOMString keyPath;
  readonly attribute boolean   unique;
  readonly attribute IDBTransaction txn;
  IDBCursor  openObjectCursor (in optional IDBKeyRange range /*
defaults to all objects */,
  in optional unsigned short direction /* defaults to NEXT */,
  in optional boolean preload /* defaults to false */);
  IDBRequest openCursor (in optional IDBKeyRange range /* defaults to
all objects */,
  in optional unsigned short direction /* defaults to NEXT */,
  in optional boolean preload /* defaults to false */);
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store of this
  index was not opened in READ_WRITE mode.
  */
  IDBRequest put (in any value, in optional any key)
  (raises IDBDatabaseException);
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store of this
  index was not opened in READ_WRITE mode.
  */
  IDBRequest add (in any value, in optional any key)
  (raises IDBDatabaseException);
  IDBRequest getObject (in any key);
  IDBRequest get (in any key);
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store of this
  index was not opened in READ_WRITE mode.
  */
  IDBRequest delete (in any key) (raises IDBDatabaseException);
};

[NoInterfaceObject]
interface IDBTransaction : IDBRequest {
  attribute boolean     static;
  attribute IDBDatabase db;
  IDBObjectStore objectStore (in DOMString name,
  in optional unsigned short mode /* if static scope, defaults to the
mode in which the transaction is created and READ_WRITE otherwise */)
  raises (IDBDatabaseException);
  void           commit ();
  attribute Function    onabort;
  attribute Function    ontimeout;
};

[NoInterfaceObject]
interface IDBCursor {
  const unsigned short NEXT = 0;
  const unsigned short NEXT_NO_DUPLICATE = 1;
  const unsigned short PREV = 2;
  const unsigned short PREV_NO_DUPLICATE = 3;
  readonly attribute unsigned short     direction;
  /*
  Could be null.
  */
  readonly attribute any                key;
  /*
  True when all the data in the cursor has been read and false otherwise
  */
  readonly attribute boolean            exhausted;
  /*
  Could be null only if cursor is on an object store with out-of-line
keys and the key was using a null value. Can never be null in an
index, unless there are no more values.
  */
  readonly attribute any                value;
  readonly attribute unsigned long long count;
  readonly attribute IDBTransaction     txn;
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the backing object store
  was not opened in READ_WRITE mode.
  */
  IDBRequest update (in any value);
  /*
  Returns false if this is a pre-loaded cursor and there are no more values
  in the cursor with the optional specified key. Otherwise returns true.
  */
  boolean    continue (in optional any key);
  /*
  Raises IDBDatabaseException with NOT_ALLOWED_ERR if the backing object store
  was not opened in READ_WRITE mode.
  */
  IDBRequest delete () (raises IDBDatabaseException);
};

[NoInterfaceObject]
interface IDBPreloadedCursor: IDBCursor {
  readonly attribute unsigned long long count;
};

---------------ECMAScript---------

var parts = [{
number: 'p1', name: 'nut', color: 'red', weight: 12.0, city: 'London'}, {
number: 'p2', name: 'bolt', color: 'green', weight: 17.0, city: 'Paris'}, {
number: 'p3', name: 'screw', color: 'blue', weight: 17.0, city: 'Rome'}, {
number: 'p4', name: 'screw', color: 'red', weight: 14.0, city: 'London'}, {
number: 'p5', name: 'cam', color: 'blue', weight: 12.0, city: 'Paris'}, {
number: 'p6', name: 'cog', color: 'red', weight: 19.0, city: 'London'}];

var suppliers = [{
number: 's1', name: 'Smith', status: '20', city: 'London'}, {
number: 's2', name: 'Jones', status: '10', city: 'Paris'}, {
number: 's3', name: 'Blake', status: '30', city: 'Paris'}, {
number: 's4', name: 'Clark', status: '20', city: 'London'}, {
number: 's5', name: 'Adams', status: '30', city: 'Athens'}];

var shipments = [{
part: 'p1', 'supplier: 's1', quantity: 300}, {
part: 'p1', 'supplier: 's2', quantity: 300}, {
part: 'p2', 'supplier: 's1', quantity: 200}, {
part: 'p2', 'supplier: 's2', quantity: 400}, {
part: 'p2', 'supplier: 's3', quantity: 200}, {
part: 'p2', 'supplier: 's4', quantity: 200}, {
part: 'p3', 'supplier: 's1', quantity: 400}, {
part: 'p4', 'supplier: 's1', quantity: 200}, {
part: 'p4', 'supplier: 's4', quantity: 300}, {
part: 'p5', 'supplier: 's1', quantity: 100}, {
part: 'p5', 'supplier: 's4', quantity: 400}, {
part: 'p6', 'supplier: 's1', quantity: 100}];

var db;
var dbRequest = indexedDB.open("parts", 'Part database');
dbRequest.onsuccess = function(event) {
db = event.result;
var txn = db.transaction(); // do not acquire any locks
var catalogRequest = db.openCatalog(txn); // it is automatically READ_WRITE
// this request is to obtain an exclusive lock on the catalog
catalogRequest.ontimeout = function(event) {
  throw new Error("Could not get a transaction to set up database");
}
catalogRequest.onsuccess = function(event) {
  var catalog = event.result;
  if (catalog.version == null) {// This means we have a brand new database
    catalog.setVersion("1");
    catalog.createObjectStore("part", "number", false);
    catalog.createObjectStore("supplier", "number", false);
    catalog.createObjectStore("shipment", id);
    // there is no way to access the object store created here
    // until txn is committed
    catalog.createIndex("part", "partName", "name");
    catalog.createIndex("part", "partColor", "color");
    catalog.createIndex("part", "partCity", "city");
    catalog.createIndex("supplier", "supplierStatus", "status");
    catalog.createIndex("supplier", "supplierCity", "city");
    catalog.createIndex("shipment", "partSupplier", "[part, supplier]");
    // if there is some following code that throws,
    // then I would want to explicitly commit here
    txn.commit();// now no more data access can be performed using txn
    // if such access is attempted, exceptions will be thrown
    txn.onsuccess = function(event) {
      // by this time, any indices that were created in txn, may be eagerly
      // populated if their corresponding object stores had any data
      populate();
    }
    txn.onerror = function(event) {
      throw new Error("Failed to set up database due to error: " +
event.message);
    }
  } else {
    txn.abort(); // because we don't need to do any upgrades
    // and now the catalog object cannot be accessed either
  }
}
};

dbRequest.onerror = function(event) {
if (event.code === IDBDatabaseException.UNKNOWN_ERR)
  throw new Error("Could not open the database");
}

function populate() {
var txnRequest = db.openTransaction(); //requesting to lock the entire
database exclusively
txnRequest.onsuccess = function(event) {
  var txn = txnRequest.transaction;
  var part = txn.objectStore("part", IDBObjectStore.READ_WRITE);
  var supplier = txn.objectStore("supplier", IDBObjectStore.READ_WRITE);
  var shipment = txn.objectStore("shipment", IDBObjectStore.READ_WRITE);
  var data = [{
    store: part, values: parts}, {
    store: supplier, values: suppliers}, {
    store: shipment, values: shipments}];

  for (var storeIndex = 0; storeIndex < data.length; ++storeIndex) {
    var task = data[storeIndex]
    for (var valueIndex = 0; valueIndex < task.values.length; ++valueIndex) {
      // since we are working in a transaction, this should be synchronous
      task.store.add(task.values[index]); // Ignoring the possibility
that there may be errors while adding
    }
  }
  // this time I won't commit explicitly
}
}

function findSuppliers(part, callback) {
var txnRequest = db.openTransaction(["shipment"], IDBObjectStore.SNAPSHOT_READ);
txnRequest.onsuccess = function(event) {
  var shipments = txn.objectStore("shipment");
  // implicitly shipments.transaction should have been populated
  // TODO: How could we create a transaction without acquiring shared lock?
  var partSuppliers = shipments.index("partSuppliers");
  var lookup = KeyRange.only([part]);
  var cursorRequest = partSuppliers.openObjectCursor(lookup, IDBCursor.NEXT);
  // we did not preload the results
  var suppliers = [];
  cursorRequest.onsuccess = function(event) {
    var cursor = event.result;
    //event.transaction === partSuppliers.transaction === shipments.transaction
    if (cursor.exhausted) {
      callback(suppliers);
    } else {
    suppliers.push(cursor.value.supplier);
    // when we continue, we will get the next part supplier
    // the order of this part's suppliers will be the same as used
    // to order the suppliers in the "supplier" object store
    cursor.continue();
    }
  }
  cursorRequest.onerror = function(event) {
    // e.g., we couldn't find the part
    if (event.code === IDBDatabaseException.NOT_FOUND) {
      callback(suppliers);
    }
  }
}
}

function allPartNames(callback) {
var txnRequest = db.openTransaction(["part"], IDBObjectStore.SNAPSHOT_READ);
txnRequest.onsuccess = function(event) {
  var parts = txn.objectStore("part");
  // We don't acquire any locks yet because the individual data access
  // operations will trigger lock acquisition
  var partNames = shipments.index("partNames");
  var lookup = KeyRange.only(partName);
  var cursorRequest = partNames.openCursor(lookup,
IDBCursor.NEXT_NO_DUPLICATE);
  var parts = [];
  cursorRequest.onsuccess = function(event) {
    var cursor = event.result;
    //event.transaction === partNames.transaction === parts.transaction
    if (cursor.exhausted) {
      callback(parts);
    } else {
      parts.push(cursor.value);
      // when we continue, we will get the next unique part name
      cursor.continue();
    }
  }
}
}

function processShipment(shipment, callback) {
// we need to validate the part exists in this city first and that the
supplier is known
var txn = db.transaction(); //synchronous because requesting locks as I go along
var parts = txn.objectStore("part", IDBObjectStore.READ_ONLY);
var partRequest = parts.get(shipment.part);
partRequest.onerror = shipmentProcessingError;
partRequest.onsuccess = function(event) {
  // the required part exists and we have now locked at least that key-value
  // so that it won't disappear when we add the shipment.
  var suppliers = txn.objectStore("supplier", IDBObjectStore.READ_ONLY);
  var supplierRequest = suppliers.get(shipment.supplier);
  supplierRequest.onerror = shipmentProcessingError;
  supplierRequest.onsuccess = function(event) {
    // the required supplier exists and we have now locked that key-value
    // so that it won't disappear when we add the shipment.
    var shipments = db.objectStore("shipment", IDBObjectStore.READ_WRITE);
    var shipmentRequest = shipments.add(shipment);
    supplierRequest.onerror = shipmentProcessingError;
    shipmentRequest.onsuccess = function(event) {
      var txnRequest = event.transaction.commit();
      // before the callback, commit the stored record
      var key = event.result;
      txnRequest.oncommit = function() {
        callback(key); // which is the key generated during storage
      }
      txnRequest.onerror = shipmentProcessingError;
    }
  }
}
}

function shipmentProcessingError(event) {
if (event.code === IDBDatabaseException.TIMEOUT_ERROR) {
  throw new Error("Database is busy. Try later.");
} else if (event.code === IDBDatabaseException.NOT_FOUND_ERROR) {
  throw new Error("Invalid part or supplier specified");
} else if (event.code === IDBDatabaseException.SERIAL_ERROR) {
  throw new Error("Shipment could not be serialized");
} else if (event.code === IDBDatabaseException.DEADLOCK_ERROR) {
  throw new Error("Got mixed up and giving up. Retrying the
transaction may work.");
} else {
  throw new Error("Some error in processing shipment");
}
}

function partsInCities(start, end, callback) {
var txnRequest = db.openTransaction(["part"], IDBObjectStore.SNAPSHOT_READ);
txnRequest.onsuccess = function(event) {
  var parts = db.objectStore("part");
  var cities = parts.index("partCity");
  cityRange = KeyRange.bound(start, end, true, true);
  cursorRequest = cities.openCursor(cityRange, IDBCursor.NEXT);
  cursorRequest.onerror = function(event) {
    if (event.code === IDBDatabaseException.TIMEOUT_ERROR) {
      throw new Error("Database is busy. Try later.");
    } else if (event.code === IDBDatabaseException.NOT_FOUND_ERROR) {
      throw new Error("No matching city found");
    } else {
      throw new Error("Some error in finding parts");
    }
  }
  var parts = [];
  cursorRequest.onsuccess = function(event) {
    var cursor = event.result;
    parts.push(cursor.value);
    if (cursor.exhausted) {
      callback(parts);
    } else {
      cursor.continue();
    }
  }
}
}

function upgradeDB() {
// we will add a new index on supplier name
var txn = db.transaction();
  // this creates a transaction synchronously but does not acquire any locks
  var catalogRequest = db.openCatalog(txn, IDBObectStore.READ_WRITE);
  // this request is to obtain an exclusive lock on the catalog
  catalogRequest.ontimeout = function(event) {
    throw new Error("Could not get a transaction to upgrade database");
  }
  catalogRequest.onsuccess = function(event) {
    var catalog = event.result;
    if (catalog.version === "1") {
      // This means we have suppliers object store defined
catalog.setVersion("2");
      catalog.createIndex("supplier", "supplierName", "name", true);
      // there is no way to access the object store created here
      // until txn is committed
      // if there is some following code that throws,
      // then I would want to explicitly commit here
      txn.commit();// now no more data access can be performed using txn
      // if such access is attempted, exceptions will be thrown
      txn.oncommit = function(event) {
        // should be able to use the new index now in a new transaction
      }
      txn.onabort = function(event) {
        // there may be a constraint violation in the creation of indices
        if (e.code === IDBDatabaseException.CONSTRAINT_ERR) {
          throw new Error("Database contains same name for multiple suppliers.")
        } else {
          throw new Error("Error when upgrading database.")
        }
      }
    } else {
      txn.abort(); // because we don't need to do any upgrades
      // and now the catalog object cannot be accessed either
    }
  }
}
}

function changeCityName(oldName, newName, callback) {
var txnRequest = db.transaction(["part", "supplier"]);
  // Not requesting other object stores
  txnRequest.ontimeout = function(event) {
    throw new Error("Could not get a transaction to change city name");
  }
  txnRequest.onsuccess = function(event) {
    var txn = event.transaction;
    var parts = db.objectStore("part", IDBObjectStore.READ_WRITE);
    var suppliers = db.objectStore("supplier", IDBObjectStore.READ_WRITE);
    // We can run the two in parallel!
    var cityParts = parts.index("partCity");
    var citySuppliers = suppliers.index("supplierCity");
    var city = KeyRange.only(oldName);
    var storesUpdated = 0, recordsUpdated = 0, needingUpdate = 2;
    var c1 = cityParts.openObjectCursor(city, IDBCursor.NEXT);
    var c2 = citySuppliers.openObjectCursor(city, IDBCursor.NEXT);
    var updateCity = function updateCity(cursor) {
      cursor.value.city = newName;
      ++recordsUpdated;
      if (cursor.exhausted) {
        ++storesUpdated;
        if (storesUpdatd === needingUpdate) {
          txn.commit();
          callback(recordsUpdated);
        }
      } else {
        cursor.continue();
      }
    }
    c1.onsuccess = function(event) {
      updateCity(c1.result);
    }
    c2.onsuccess = function(event) {
      updateCity(c2.result);
    }
    c1.onerror = c2.onerror = function(event) {
      if (event.code === IDBDatabaseException.NOT_FOUND_ERROR) {
        --needingUpdate;
      } else if (event.code === IDBDatabaseException.TIMEOUT_ERROR) {
        throw new Error("Database is busy. Try later.");
      } else if (event.code === IDBDatabaseException.SERIAL_ERROR) {
        throw new Error("Record could not be serialized");
      } else if (event.code === IDBDatabaseException.DEADLOCK_ERROR) {
        throw new Error("Got mixed up and giving up");
      } else {
        throw new Error("Some error in processing city name update");
      }
    }
  }
}
}

Received on Tuesday, 6 July 2010 16:37:11 UTC