Transaction invocation

C8 transactions are different from transactions in SQL.

In SQL, transactions are typically started with explicit BEGIN or START TRANSACTION command. Following any series of data retrieval or modification operations, an SQL transaction is finished with a COMMIT command, or rolled back with a ROLLBACK command. There may be client/server communication between the start and the commit/rollback of an SQL transaction.

In C8, a transaction is always a server-side operation, and is executed on the server in one go, without any client interaction. All operations to be executed inside a transaction need to be known by the server when the transaction is started. There are no individual BEGIN, COMMIT or ROLLBACK transaction commands in C8.

Execute transaction

Execution of a server-side transaction, requires following attributes:

  • collections: that defines which collections will be used in the transaction. collections can have these attributes:
  • read: a single collection or a list of collections that will be used in the transaction in read-only mode
  • write: a single collection or a list of collections that will be used in the transaction in write or read mode.
  • action: a Javascript function or a string with Javascript code containing all the instructions to be executed inside the transaction. If the code runs through successfully, the transaction will be committed at the end. If the code throws an exception, the transaction will be rolled back and all database operations will be rolled back.

Additionally, following optional attributes are supported:

  • waitForSync: boolean flag indicating whether the transaction is forced to be synchronous.
  • params: optional arguments passed to the function specified in action.

Declaration of collections

All collections which are to participate in a transaction need to be declared beforehand. This is a necessity to ensure proper locking and isolation. Collections can be used in a transaction in write mode or in read-only mode.

If any data modification operations are to be executed, the collection must be declared for use in write mode. The write mode allows modifying and reading data from the collection during the transaction (i.e. the write mode includes the read mode).

Contrary, using a collection in read-only mode will only allow performing read operations on a collection. Any attempt to write into a collection used in read-only mode will make the transaction fail.

Collections for a transaction are declared by providing them in the collections attribute of the object passed to the _executeTransaction function. The collections attribute has the sub-attributes read and write:

{
  collections: {
    write: [ "users", "logins" ],
    read: [ "recommendations" ]
  }
}

read and write are optional attributes, and only need to be specified if the operations inside the transactions demand for it.

The contents of read or write can each be lists arrays collection names or a single collection name (as a string):

{
  collections: {
    write: "users",
    read: "recommendations"
  }
}

Note: It is currently optional to specify collections for read-only access. Even without specifying them, it is still possible to read from such collections from within a transaction, but with relaxed isolation. Please refer to Transactions Locking for more details.

In order to make a transaction fail when a non-declared collection is used inside for reading, the optional allowImplicit sub-attribute of collections can be set to false:

{
  collections: {
    read: "recommendations",
    allowImplicit: false  /* this disallows read access to other collections
                             than specified */
  },
  action: function () {
    var db = require("@c8db_db").db;
    return db.foobar.toArray(); /* will fail because db.foobar must not be accessed
                                   for reading inside this transaction */
  }
}

The default value for allowImplicit is true. Write-accessing collections that have not been declared in the collections array is never possible, regardless of the value of allowImplicit.

Declaration of data modification and retrieval operations

All data modification and retrieval operations that are to be executed inside the transaction need to be specified in a Javascript function, using the action attribute:

{
  collections: {
    write: "users"
  },
  action: function () {
    // all operations go here
  }
}

Any valid Javascript code is allowed inside action but the code may only access the collections declared in collections. action may be a Javascript function as shown above, or a string representation of a Javascript function:

{
  collections: {
    write: "users"
  },
  action: "function () { doSomething(); }"
}

Please note that any operations specified in action will be executed on the server, in a separate scope. Variables will be bound late. Any variables used inside action should be defined inside action itself:

{
  collections: {
    write: "users"
  },
  action: function () {
    var db = require(...).db;
    db.users.save({ ... });
  }
}

When the code inside the action attribute is executed, the transaction is already started and all required locks have been acquired. When the code inside the action attribute finishes, the transaction will automatically commit. There is no explicit commit command.

To make a transaction abort and roll back all changes, an exception needs to be thrown and not caught inside the transaction:

{
  collections: {
    write: "users"
  },
  action: function () {
    var db = require("@c8db_db").db;
    db.users.save({ _key: "hello" });
    // will abort and roll back the transaction
    throw "doh!";
  }
}

There is no explicit abort or roll back command.

As mentioned earlier, a transaction will commit automatically when the end of the action function is reached and no exception has been thrown. In this case, the user can return any legal JavaScript value from the function:

{
  collections: {
    write: "users"
  },
  action: function () {
    var db = require("@c8db_db").db;
    db.users.save({ _key: "hello" });
    // will commit the transaction and return the value "hello"
    return "hello";
  }
}

Custom exceptions

One may wish to define custom exceptions inside of a transaction. To have the exception propagate upwards properly, please throw an an instance of base JavaScript Error class or a derivative. To specify an error number, include it as the errorNumber field. As an example:

{
  collections: {},
  action: function () {
    var err = new Error('My error context');
    err.errorNumber = 1234;
    throw err;
  }
}

Examples

The first example will write 3 documents into a collection named c1. The c1 collection needs to be declared in the write attribute of the collections attribute passed to the executeTransaction function.

The action attribute contains the actual transaction code to be executed. This code contains all data modification operations (3 in this example).

{
  collections: {
    write: [ "c1" ]
  },
  action: function () {
    var db = require("@c8db_db").db;
    db.c1.save({ _key: "key1" });
    db.c1.save({ _key: "key2" });
    db.c1.save({ _key: "key3" });
  }
}

Aborting the transaction by throwing an exception in the action function will revert all changes, so as if the transaction never happened:

{
  collections: {
    write: [ "c1" ]
  },
  action: function () {
    var db = require("@c8db_db").db;
    db.c1.save({ _key: "key1" });
    db.c1.count(); // 1
    db.c1.save({ _key: "key2" });
    db.c1.count(); // 2
    throw "doh!";
  }
}

The automatic rollback is also executed when an internal exception is thrown at some point during transaction execution:

{
  collections: {
    write: [ "c1" ]
  },
  action: function () {
    var db = require("@c8db_db").db;
    db.c1.save({ _key: "key1" });
    // will throw duplicate a key error, not explicitly requested by the user
    db.c1.save({ _key: "key1" });  
    // we'll never get here...
  }
}

As required by the consistency principle, aborting or rolling back a transaction will also restore secondary indexes to the state at transaction start.

Cross-collection transactions

There's also the possibility to run a transaction across multiple collections. In this case, multiple collections need to be declared in the collections attribute, e.g.:

{
  collections: {
    write: [ "c1", "c2" ]
  },
  action: function () {
    var db = require("@c8db_db").db;
    db.c1.save({ _key: "key1" });
    db.c2.save({ _key: "key2" });
  }
}

Again, throwing an exception from inside the action function will make the transaction abort and roll back all changes in all collections:

{
  collections: {
    write: [ "c1", "c2" ]
  },
  action: function () {
    var db = require("@c8db_db").db;
    for (var i = 0; i < 100; ++i) {
      db.c1.save({ _key: "key" + i });
      db.c2.save({ _key: "key" + i });
    }
    db.c1.count(); // 100
    db.c2.count(); // 100
    // abort
    throw "doh!"
  }
}
Top