This document defines APIs and behaviors of a relational database. The capabilities of the defined relational database are comparable to a subset of SQL-03 standard.

This document has not been submitted to anywhere yet.

Introduction

Relational data manipulation for web apps has been in a difficult state for years. WebSQL has quite a few problems and is hence deprecated. IndexedDB, on the other hand, is an object database. In this document, we define a series of easy-to-use APIs that promote readable, maintainable, and performant code for apps needing relational data.

The APIs proposed in this document are designed for small-scale relational data manipulation. The following example shows how to open a database.

// Open database named 'hr'.
// If the named instance does not exist, create an empty database instance.
// Otherwise, open the existing database named 'hr'.
let dbConnection;
function connect() {
    let rdb = navigator['db'];
    return rdb.open('hr').then((connection) => {
        dbConnection = connection;
        // version is a read-only number that is for reference only.
        if (dbConnection.schema().version == 0) {
            // This is an empty database.
            return createDB();
        }
        else if (dbConnection.schema().version < 2) {
            // Version is smaller than expected, perform upgrades.
            return upgradeDB();
        }
        return dbConnection;
    });
}

function createDB() {
    let tx = dbConnection.createTransaction('readwrite');
    let q1 = dbConnection
        .createTable('Dept')
        .column(/* name */ 'id', /* type */ 'string', /* not null */ true)
        .column('name', 'string', true)
        .column('desc', 'string')
        .primaryKey('id');
    let q2 = dbConnection
        .createTable('Emp')
        .column('id', 'integer', true)
        .column('name', 'string', true)
        .column('deptId', 'string', true)
        .column('title', 'string')
        .primaryKey('id')
        .index('idx_name', 'name', /* unique */ true)
        .index('idx_desc', { name: 'desc', order: 'desc' })
        .foreignKey('fk_DeptId', 'deptId', 'Dept.id');
    let q3 = dbConnection.setVersion(2);
    return tx.exec([q1, q2, q3]).then(() => { return dbConnection; });
}

function upgradeDB() {
    return dbConnection
        .alterTable('Dept')
        .addColumn('desc', 'string')
        .addIndex('idx_desc', { name: 'desc', order: 'desc' })
        .commit()
        .then(() => { return dbConnection; });
}

connect().then(() => {
    // Real work starts here.
});
  

Once the database is opened, queries can be performed.

let dbConnection;
let dept = dbConnection.schema().table('Dept');
function insertData() {
    let deptData = [
        { 'id': 'HR', 'name': 'Human Resources', 'desc': 'Rock stars' },
        { 'id': 'ENG', 'name': 'Engineering', 'desc': 'Hard workers' },
        { 'id': 'NADA', 'name': 'Non existing' },
        { 'id': 'L', 'name': 'Leadership' }
    ];
    return dbConnection.insert().into(dept).values(deptData).commit();
}

function updateData() {
    return dbConnection
        .update(dept)
        .set(dept['desc'], 'Master minds')
        .where(dept['id'].eq('L'))
        .commit();
}

function deleteData() {
    return dbConnection
        .delete()
        .from(dept)
        .where(dept['id'].eq('NADA'))
        .commit();
}

function selectData() {
    return dbConnection.select().from(dept).commit();
}

insertData().then(function () {
    return updateData();
}).then(function () {
    return deleteData();
}).then(function () {
    return selectData();
}).then(function (rows) {
    // Expected returns:
    // [{'id': 'HR', 'name': 'Human Resources', 'desc': 'Rock stars'},
    //  {'id': 'ENG', 'name': 'Engineering', 'desc': 'Hard workers'},
    //  {'id': 'L', 'name': 'Leadership', 'desc': 'Master minds'}]
    console.log(rows);
});
  

The proposed spec defines the behaviors of a database. A database consists of two parts: storage and query engine. Data is stored in the storage as rows, each row is a tuple of values, and the types of values are defined as columns. Same class of rows are grouped together as a table. The database has a concept named schema to define columns inside tables, and the relations among rows. Query engine is responsible for reading and writing rows from the storage, enforcing relations defined in the schema, and providing various functions to manipulate how the rows are read and written.

Schema

Overview

A schema defines the storage structure of a relational database. A schema MUST have a name. The origin of the page and the schema name together uniquely identify a database instance during runtime.

A schema has a referential version number associated with it. Version number is an integer greater than zero and can be freely assigned. It has no effect on identifying a database. It is provided for the convenience of developers to version their schema without extra table.

A schema is structured in a hierachical way. A database schema contains one or more tables. A table contains one or more columns, zero or more indices, and zero or more constraints. The collection of columns in a table defines the fields carried by a row, which is the fundamental unit of data storage in relational database. Each column consists a name and an associated data type. The indices hint the database engine to build index structures so that subsequent queries can be acclerated. Index definitions MAY be honored if the database engine determined that they are necessary. The constraints define the relations of rows and ensures data integrity, and MUST be honored.

The database schema is constructed via builder pattern APIs. Schema can be changed through schema queries within implicit transactions, or explicit batched readwrite transactions.

Naming Rules

Names, also known as identifiers, are used to identify entities in schema, i.e. database, table, column, index, and constraint. All names used in the database schema MUST abide the following rules:

Unlike SQL, names in RDB are case-sensitive.

Column Types

A table column MUST be associated with a given type. Valid types are defined in ColumnType enum:

    enum ColumnType {
      "blob",
      "boolean",
      "date",
      "integer",
      "number",
      "string",
      "object"
    };

    typedef (ArrayBuffer or boolean or Date or long long or double or DOMString or object) ValueType;
    typedef (boolean or Date or long or double or DOMString) IndexableValueType;
  

blob represents JavaScript ArrayBuffer, and date refers to Date object. boolean, number, string and object correpond to the types of the same names in JavaScript. The type number is the Web IDL type double.

integer is a special data type representing integers, corresponding to Web IDL type long long, but within the range of [Number#MIN_SAFE_INTEGER, Number#MAX_SAFE_INTEGER]. Integer columns will be converted to and from JavaScript number types using equivalent of Number#toPrecision(1). If the converted number failed test of Number#isSafeInteger(), a DataError will be thrown.

JavaScript types that can be used in a column is defined as ValueType. Only some value types are indexable, indicating that they can be compared, sorted, and used in indices and predicates. These types are defined as IndexableValueType. Non-indexable types, blob and object, MUST NOT be used to create indices and predicates, except the isNull() and isNotNull() predicates.

Ordering

There are two sorting orders defined in the Order enum:

    enum Order {
      "asc",
      "desc"
    };
  

The asc represents ascending sorting order, and is the default sorting order if none were provided. The desc represents descending sorting order.

Ascending sort order is the natural sort order, which means that if two fields are ordered as a, b, then (a <= b) === true. Since blob and object types are not indexable, they cannot be compared and thus the orders have no effect for these two types.

Schema Query

A database instance is created using schema queries. The schema of a database instance can also be altered via schema queries. Schema queries are run in the context of transaction and can be executed any time during the life time of the database.

IExecutionContext returned from ISchemaQueryProvider or its methods can only be used in implicit transactions or batch-executed explicit transactions, i.e. ITransaction.exec().

Any name used inside the schema not obeying Naming Rules will result in raising InvalidSchemaError during the execution of schema queries.

Database-Level Schema Query

The ISchemaQueryProvider is the interface of manipulating database-level schema:

      interface ISchemaQueryProvider {
        IExecutionContext setVersion(unsigned short version);
        IExecutionContext setForeignKeyCheck(boolean value);
        IDatabaseSchema schema();
        ITableBuilder createTable(DOMString tableName);
        ITableChanger alterTable(DOMString tableName);
        IExecutionContext dropTable(DOMString tableName);
      };
    
IExecutionContext setVersion(unsigned short version);
Sets the version number of the schema, MUST be run in the context of a transaction. The version MUST NOT be set to 0, otherwise InvalidSchemaError will be raised.
IExecutionContext setForeignKeyCheck(boolean value);
Enables or disables foreign key constraint enforcement within the database that current connection is associated with. The change will be effective only after this function is called, and will not affect previously executed queries. Attaching the returned execution context to any transaction will make this call uneffective, i.e. this call MUST be committed in an implicit transaction. Misuse of this function MAY result in broken data integrity.
IDatabaseSchema schema();
Returns an IDatabaseSchema object for crafting data manipulation queries.
ITableBuilder createTable(DOMString tableName);
Returns a table builder for creating a new table.
ITableChanger alterTable(DOMString tableName);
Returns a table changer for altering an existing table. The table referred by tableName MUST already exist in the database, otherwise a DataError will be raised.
IExecutionContext dropTable(DOMString tableName);
Removes a table and all its rows from database. The table referred by name MUST already exist in the database, otherwise a DataError will be raised.

Database Schema

IDatabaseSchema represents a database.

      interface IDatabaseSchema {
        readonly attribute DOMString name;
        readonly attribute unsigned short version;
        ITable table(DOMString tableName);
      };
    
DOMString name
Name of the database.
unsigned short version
Version of the database schema.
ITable table(DOMString tableName);
Returns a read-only object that implements ITable interface, whose members are IColumn objects representing columns in the specified table. The table referred by tableName MUST already exist in the database, otherwise a DataError will be raised.

Table Builder

ITableBuilder is used to describe the new table that will be created.

      interface ITableBuilder : IExecutionContext {
        ITableBuilder column(DOMString name, ColumnType type, optional boolean notNull = false);
        ITableBuilder primaryKey((DOMString or sequence< DOMString >) columns, optional boolean autoIncrement = false);
        ITableBuilder foreignKey(DOMString name, (DOMString or sequence< DOMString >) column, (DOMString or sequence< DOMString >) foreign, optional ForeignKeyAction action = "restrict", optional ForeignKeyTiming timing = "immediate");
        ITableBuilder index(DOMString name, IndexedColumnDefinition columns, optional boolean unique = false);
      };
    

All methods returns the same ITableBuilder object that they were called from to make cascade builder pattern possible. If any of the rule described in the following table were violated, an InvalidSchemaError will be thrown. All methods can be called multiple times except primaryKey.

ITableBuilder column(DOMString name, ColumnType type, optional boolean notNull = false);
Adds a column to table. name MUST be unique within the containing table. NOT NULL means the column MUST NOT have a value of undefined or null. When notNull is set to true, the column is treated as NOT NULL.
ITableBuilder primaryKey((DOMString or sequence< DOMString >) columns, optional boolean autoIncrement = false);
Adds a primary key to table. This method MUST NOT be called multiple times. Primary key is a specialized unique index. Each table can have only one primary key. Column(s) that consists primary key, MUST refer to indexable column(s) of the containing table. When autoIncrement is set to true, there MUST be only one column in the column field with integer column type. An auto-increment key will start from zero. When a new row is inserted into the table, the key will increase by one and assigned to that row regardless the original key value given to the row. If the key grows beyond the integer range Number#MAX_SAFE_INTEGER, it will be reset to 0 and start over. If a column is designated as auto-increment primary key, its values become read-only and MUST NOT be updatable by the user (i.e. users cannot call update() on that column). The next auto-increment value cannot be reset even when all rows are deleted from that table.
ITableBuilder foreignKey(DOMString name, (DOMString or sequence< DOMString >) column, (DOMString or sequence< DOMString >) foreign, optional ForeignKeyAction action = 'restrict', optional ForeignKeyTiming timing = 'immediate');
Adds a foreign key to table. See Foreign Key Declaration for details.
ITableBuilder index(DOMString name, IndexedColumnDefinition columns, optional boolean unique = false);
Adds an index to table. The name is the name of the index, and it MUST be unique within the containing table. The columns allows multiple type of definitions. If provided as DOMString, the string MUST refer to an indexable column of the containing table; if provided as sequence< DOMString >, it MUST NOT be empty, and the strings MUST refer to different indexable columns of the containing table. In these two cases, default sort order of asc is used. If provided as sequence< IndexedColumnSpec >, it MUST NOT be empty, and each IndexedColumnSpec MUST refer to different column. The optional unique parameter specifies whether keys in index are unique or not, default to false.

This specification does not support named primary key. Moreover, primary keys can only have ascending order.

An index can be keyed by one or more columns. A unique key means that the value or combination of values used as index key is unique within the index. By default, all indices allow duplicate keys unless the unique is set to true. A ConstraintError will be thrown if user tried to insert/update a row with duplicated key when a unique index is specified.

The WebIDL below defines indexed column specification, IndexedColumnSpec and IndexedColumnDefinition:

      dictionary IndexedColumnSpec {
        DOMString name;
        Order order = "asc";
      };
      typedef (DOMString or sequence< DOMString > or IndexedColumnSpec or sequence< IndexedColumnSpec >) IndexedColumnDefinition;
    
name
Name of the indexed column, MUST refer to an indexable column of the containing table.
order
(Optional) Sorting order of the column, default to asc.

If an index definition overlapped with implicit indices created by constraints (for example, create an index on the same column of the primary key), it will be up to the query engine to determine whether or not to create this index.

Cross-table indices or views are not supported in this specification.

Foreign Key Declaration

Foreign keys provides referential integrity, which means that a value in a column MUST match the values appearing in some row of another table. A foreign key is defined with five parameters:

  • name is a unique name within the containing table that identifies this constraint.
  • column is the name of local (referring) column(s). It MUST be indexable.
  • foreign is the full name of the remote (referred) column(s). It MUST consist a unique index or primary key. The number of columns and column types MUST exactly match what are provided in the column parameter.
  • action is an optional parameter, and default to restrict.
  • timing is an optional paramter, and default to immediate.

A chainned foreign key means the remote column is also a local column of another foreign key, which forms a dependency. The query engine MAY support chainned foreign key. The query engine MAY support referred column to be within the containing table.

ForeignKeyAction enum is defined as following:

      enum ForeignKeyAction {
        "restrict",
        "cascade"
      };
    
restrict
Any constraint violation results in cancelling the operation that violated the constraint. This is the default value.
cascade
Constraint violation results in modifying related tables as necessary to maintain data integrity.

ForeignKeyTiming enum is defined below:

      enum ForeignKeyTiming {
        "immediate",
        "deferrable"
      };
    
immediate
The constraint is enforced during execution of each individual query. This is the default value.
deferrable
The constraint is enforced right before a transaction is committed. The constraint can be violated by individual queries during the lifetime of the enclosing transaction, without any error being thrown.

This specification does NOT support granuarity of cascade, e.g. ON DELETE UPDATE ON UPDATE NO_ACTION. It also does NOT support granuarity of deferral, e.g. DEFERRABLE INITIALLY IMMEDIATE.

Table Changer

ITableChanger is used to change the schema of an existing table, and MUST be used within the context of a transaction. If the table already contains data, altering table schema can cause constraint violations and fail the transaction.

      interface ITableChanger : IExecutionContext {
        ITableChanger rename(DOMString newTableName);
        ITableChanger addColumn(DOMString name, ColumnType type, optional boolean notNull = false, optional ValueType defaultValue = null);
        ITableChanger dropColumn(DOMString name);
        ITableChanger addPrimaryKey((DOMString or sequence< DOMString >) columns);
        ITableChanger addForeignKey(DOMString name, (DOMString or sequence< DOMString >) column, (DOMString or sequence< DOMString >) foreign, optional ForeignKeyAction action = "restrict", optional ForeignKeyTiming timing = "immediate");
        ITableChanger addIndex(DOMString name, IndexedColumnDefinition columns, optional boolean unique = false);
        ITableChanger dropConstraintOrIndex(DOMString name);
        IColumnChanger setColumn(DOMString name);
      };
    

All methods returns the same ITableChanger object that they were called from to make cascade builder pattern possible, except that setColumn will return a IColumnChanger instead. If any of the rule described in the following table were violated, an InvalidSchemaError will be thrown. All methods can be called multiple times.

ITableChanger rename(DOMString newTableName);
Renames the table, newTableName MUST be unique within the database, and MUST NOT be the same as existing table name.
ITableChanger addColumn(DOMString name, ColumnType type, optional boolean notNull = false, optional ValueType defaultValue = null);
Adds a new column to table, name MUST be unique within the containing table. If defaultValue is given, it MUST be of the same type as specified in type. Every existing row in the table will be added a new column with defaultValue.
ITableChanger dropColumn(DOMString name);
Removes a column from table. The name MUST refer to an existing column in the table schema. The referred column MUST NOT be the only column in the table schema, and MUST NOT be referred by any index nor constraints. Every existing row in the table will be modified to remove the field represented by that column.
ITableChanger addPrimaryKey((DOMString or sequence< DOMString >) columns);
Adds a primary key to table, the table MUST not have existing primary key. The key MUST NOT auto increment.
ITableChanger dropPrimaryKey();
Removes primary key from table. If the table does not have primary key, this method will do nothing. If the primary key is auto-increment primary key, an UnsupportedError will be thrown.
ITableChanger addForeignKey(DOMString name, (DOMString or sequence< DOMString >) column, (DOMString or sequence< DOMString >) foreign, optional ForeignKeyAction action = 'restrict', optional ForeignKeyTiming timing = 'immediate')
Adds a foreign key to the table.
ITableChanger addIndex(DOMString name, IndexedColumnDefinition columns, optional boolean unique = false);
Adds an index to the table. The name is the name of the index and MUST be unique within the containing table. If unique is set to true, add a unique index.
ITableChanger dropConstraintOrIndex(DOMString name);
Removes a constraint or index in table by name. The name MUST refer to an existing constraint or index.
IColumnChanger setColumn(DOMString name);
Changes a column in the table, and the name MUST refer to an existing column in the table schema.

This specification does not support adding or dropping auto increment primary key.

Column Changer

Column changer, modeled as IColumnChanger, is defined as following:

      interface IColumnChanger {
        ITableChanger set(DOMString newColumnName, optional boolean notNull = true);
      };
    
ITableChanger set(DOMString newColumnName, optional boolean notNull = true);
Changes the column's name or NOT NULL property. The returned ITableChanger is the parent object that creates this object.

Changing column data type is not supported by this spec. JavaScript is famous of tricky type conversions and it is unlikely to have conversion rules that covers every corner case.

Schema Query Examples

The following example demonstrates how to create a database schema.

let dbConnection;
let tx = dbConnection.createTransaction('readwrite');
let q1 = dbConnection
    .createTable('Dept')
    .column(/* name */ 'id', /* column_type */ 'string', /* not_null */ true)
    .column('name', 'string', true)
    .primaryKey('id');
let q2 = dbConnection
    .createTable('Emp')
    .column('id', 'number', true)
    .column('name', 'string', true)
    .column('deptId', 'string', true)
    .column('title', 'string')
    .primaryKey('id')
    .index('idx_Desc', { 'name': 'desc', 'order': 'desc' })
    .foreignKey('fk_DeptId', 'deptId', 'Dept.id');
tx.exec([q1, q2]).then(() => {
    // Table created, do something here.
});
    

The following example shows how to alter database schema during schema upgrade.

let dbConnection;
let version = dbConnection.schema().version;
if (version < 2) {
  // Need DB upgrade.
  let tx = dbConnection.createTransaction('readwrite');
  let setVersion = dbConnection.setVersion(2);
  let createNewTable = dbConnection
      .createTable('NewTable')
      .column('id', 'string', true)
      .column('name', 'string', true);
    
  let alterTable = dbConnection
      .alterTable('Emp')
      .addColumn('location', 'string', true, 'LAX')
      .setColumn('title').set('title', true) // Change to NOT NULL
      .addIndex('idx_location', 'location');

  let dropTable = dbConnection.dropTable('Foo');

  // MUST use exec() instead of attach().
  return tx
      .exec([setVersion, createNewTable, alterTable, dropTable])
      .then(() => {
        // Schema change has finished, start work here.
      });
}

    

Database

Overview

A database's origin is the same as the origin of the document or worker. Each origin has an associated set of databases.

The database origin is not affected by changes to document.domain.

Every database has a predefined layout of the data structures persisted, which is called a schema. The database name specified in schema identifies the database within a specific origin. The name MUST satisfy the Naming Rules, and stay constant for the lifetime of the database. Each database also has a version number for reference only.

Databases have a delete pending flag which is used during deletion. When a database is requested to be deleted the flag is set to true and all attempts at opening the database are stalled until database is deleted.

The act of opening a database creates a connection. There MAY be multiple connections to a given persistent database at any given time, where temporary database accepts only one connection. Each connection has a closePending flag which initially is set to false.

When a connection is initially created it is in opened state. The connection can be closed through several means. If the connection is garbage collected or the execution context where the connection was created is destroyed (for example due to the user navigating away from that page), the connection is closed. The connection can also be closed explicitly via the API. When the connection is closed the closePending flag is always set to true regardless of its previous state. All uncommitted transactions associated with a connection that is marked close pending will be canceled.

Users can create observers associated with a given database. Each observer consists of a select query. When the results of that select query change, the observer will be triggered. The change MAY come from other connections. When the connection associated with a registered observer is marked as close pending, the observer MUST be unregistered automatically.

Relational Database

The relational database is part of Navigator as defined in following WebIDL.

    partial interface Navigator {
      readonly attribute IRelationalDatabase db;
    };
  
IRelationalDatabase db
Provides the entry point of accessing relational database functions.

The relational database is defined as IRelationalDatabase:

    interface IRelationalDatabase {
      readonly attribute IDatabaseFunctionProvider fn;

      Promise< DatabaseConnection > open(DOMString name, OpenDatabaseOptions? opt);
      Promise< void > drop(DOMString name);
    };
  
IDatabaseFunctionProvider fn
Provides a namespace for aggregation functions that can be used in queries.
Promise< DatabaseConnection > open(DOMString name, OpenDatabaseOptions? opt)
Opens a database connection using provided name. If the database does not exist, create a new database instance and returns connection to it. If the database is marked delete pending, the promise will be rejected with BlockingError. If the OpenDatabaseOptions is not specified, a persistent database with the given name will be opened/created.
Promise< void > drop()
Deletes a database using provided name. This MUST follow the delete database algorithm. If the database does not exist, the returned promise will resolve immediately. If the database is marked delete pending, the promise will resolve immediately.

OpenDatabaseOptions is a dictionary object used to specify options for opening a database:

    dictionary OpenDatabaseOptions {
      RDBStorageType storageType;
    };
  
RDBStorageType storageType
Specifies the type of storage used to back the database. The storageType is fixed for the lifetime of returned database connection.
The RDBStorageType is an enum:
    enum RDBStorageType {
      "persistent",
      "temporary"
    };
  
persistent
Data will be persisted in permanent storage.
temporary
Data will be persisted in memory only: the relational database acts as in-memory cache of data and offers only the relational query capability.

Delete Database Algorithm

The delete database algorithm is performed in following steps:

  1. Marks this database as delete pending.
  2. Marks each connection to this database as close pending, which will effectively prevent new transactions from creating.
  3. Removes all observers.
  4. Waits for all pending transactions to be cancelled.
  5. Closes all connections.
  6. Deletes the database.
  7. Resolves the promise.

Connection

The database connection is the means by which the browser session and underlying database communicate with each other. IDatabaseConnection models a database connection:

    interface IDatabaseConnection {
      ITransaction createTransaction(optional TransactionMode mode = "readonly");
      Promise< Error > close();
      IBindableValue bind(unsigned short index);
    };
  
ITransaction createTransaction(optional TransactionMode mode = "readonly");
Creates an explicit transaction. If the connection is marked as close pending, throws BlockingError.
Promise< Error > close();
Closes the connection. The connection will be marked as close pending immediately after this call, and no more new transaction can be created within this connection. It is a best efforts call and may resolve with error, or NULL if everything succeeded.
IBindableValue bind(unsigned short index);
Returns a bindable value that can be used in parameterized query.

The actual database connection returned from Navigator is a class object, DatabaseConnection, that implements four different interfaces:

    interface DatabaseConnection : IDatabaseConnection {
      readonly attribute DOMString name;
    };
    DatabaseConnection implements IDatabaseObserver;
    DatabaseConnection implements IDataQueryProvider;
    DatabaseConnection implements ISchemaQueryProvider;
  
name
Name of the database. It MUST be the same as the name field specified in schema.

The ISchemaQueryProvider provides database-level schema query and allows creating or changing the database schema. The IDataQueryProvider provides APIs to construct data manipulation query to be used to manipulate data within a transaction. The IDatabaseObserver provides change observation for the results of a select query, regardless the changes are from this connection or not.

Transaction

A transaction is an atomic unit of reading and writing data in a database. The term atomic means it is guaranteed that all operations inside this transaction to be committed as a whole, or none get committed. A committed transaction guarantees all data being written to storage. The operations inside a transaction are referred as queries. A query means an interaction with the database: data retrieval, data persistence, or change of schema.

Transaction execution behavior is modeled as the following: A TransactionResults to represent the return values from a finished transaction, and IExecutionContext to control the transaction lifetime.

    typedef (void or sequence< object >) TransactionResults;

    interface IExecutionContext {
      Promise< TransactionResults > commit();
      Promise< void > rollback();
    };
  
Promise< TransactionResults > commit();
Runs commit algorithm.
Promise< void > rollback();
Runs rollback algorithm.

All transactions are associated with a database connection, which is the connection that the transaction is created within. A transaction has a predetermined TransactionMode to indicate what access this transaction is asking for. The mode is set when the transaction is created and remains fixed for the life of the transaction. There are two transaction modes:

    enum TransactionMode {
      "readonly",
      "readwrite"
    };
  
readonly
Transaction intends to read data/schema only.
readwrite
Transaction intends to read/write data and/or read/change schema.

A transaction has a scope that determines the rows affected by this transaction. The scope is automatically calculated. The scope of a transaction MAY dynamically change during the lifetime of the transaction.

A transaction can be created explicitly or implicitly. An explicit transaction is created directly from IDatabaseConnection.createTransaction(). An implicit transaction is created when the commit algorithm needs one. The explicit transaction is defined as ITransaction interface:

    interface ITransaction : IExecutionContext {
      Promise< void > begin();
      Promise< TransactionResults > exec(sequence< IExecutionContext > queries);
      Promise< TransactionResults > attach(IExecutionContext query);
    };
  
Promise< void > begin();
Starts sequence mode execution. The transaction will be kept alive until commit() or rollback() of this transaction is triggered, or the transaction is timed out by implementation (which the implementation MUST throw TimeoutError and reject any further calls on this transaction object).
Promise< TransactionResults > exec(sequence< IExecutionContext > queries);
Starts batch mode execution. The queries will be executed in the order they appear in the sequence, and the transaction will be auto-committed after all sequence has run. If any of the query failed, or TimeoutError happens, the transaction will be automatically rolled back.
Promise< TransactionResults > attach(IExecutionContext query);
Schedules a query to existing sequence mode execution. The promise will be resolved with the results when the query is executed. The query MUST NOT be an explicit transaction, i.e. ITransaction object.

This specification does not support nested transactions.

The lifetime of an explicit transaction is controlled by the user. There are two ways to start execution: batch mode and sequence mode. In batch mode, an explicit transaction is started with exec(queriesArray) and returns a result Promise. All queries will be executed sequentially in the order received, and the results of last query in the transaction are returned in the promise. In sequence mode, user calls begin() to start the transaction, calls attach(query) to attach a query to the transaction's execution context. The user can call commit() to indicate the end of transaction, and a final result Promise will be returned; or the user can call rollback() to abort the transaction, and all modifications done by previous attached queries will be discarded.

When a transaction is committed or rolled back, it is said to be finished. A finished transaction MUST NOT be started nor rolled back. If a transaction's connection is lost before transaction being finished, the transaction is effectively rolled back and no change will be persisted.

Actual time out length for a given transaction is implementation dependent. Implementation MAY choose an appropriate value, or just set it to indefinite.

Commit Algorithm

An IExecutionContext can be obtained from IQuery or ISchemaQueryProvider. The execution context is not associated with any ITransaction object in the beginning, which means it is an unbound context. When the IExecutionContext.commit() is called, an implicit transaction will be associated with the execution context. When ITransaction.attach() is called, the execution context passed as parameter will be associated with the transaction being called. In both cases, the context is a bound context, indicating existence of an associated transaction. An execution context MUST be associated with a transaction before execution.

The commit algorithm is described in following steps:

  1. If the execution context is a bound context, go to step 3.
  2. If the context can be carried out using readonly transaction mode, create a transaction using that mode; otherwise create a readwrite transaction. Associate the transaction with the execution context.
  3. If the associated transaction is marked as pending finish, throws TransactionStateError.
  4. Mark the associated transaction as pending finish, which will prohibit the transaction from attaching more queries, or calling rollback.
  5. Retrieves the result promise R of the last execution context in the associated transaction from query engine.
  6. If the transaction is already running in sequence mode, returns R.
  7. If the associated transaction is not started, notifies the query engine to execute it in batch mode, and returns R.
  8. When R resolves or rejects, mark transaction as finished.

Rollback Algorithm

The rollback algorithm is described in following steps:

  1. If the execution context is an unbound context, returns a resolved promise.
  2. If the associated tranaction is marked as pending finish or finished, throws TransactionStateError.
  3. If the associated transaction is not executed yet, mark the transaction as finished and returns a resolved promise.
  4. Marks the associated transaction as finish pending, and notifies query engine to rollback. Returns a promise.
  5. When the query engine rolled back the transaction, resolve promise; otherwise, reject promise with IntegrityError.

Transaction Snapshots

A snapshot of the database is the contents of all rows in the database at the time when the snapshot is taken. At any given time, query engine MUST guarantee only one connection can create snapshot on a given scope. Conceptually, each transaction operates on its own snapshot of database, and the snapshot is taken at the beginning of transaction. The beginning of transaction is determined by either calling ITransaction.begin or ITransaction.exec for explicit transactions, or IExecutionContext.commit for implicit transactions.

All changes performed by queries inside the transaction will only be effective on that snapshot. The end of transaction is determined by resolution or rejection of the Promise returned from ITransaction.exec or IExecutionContext.commit. If the transaction is committed, i.e. resolution of Promise, the database MUST be updated to that snapshot. If the transaction is rolled back, i.e. rejection of Promise, the snapshot is discarded.

The final data persisted on database is determined by the last transaction snapshot committed by the query engine. The transaction snapshots MAY come from other sessions other than current session. The query engine MUST guarantee only one snapshot can be persisted at a given time if there were multiple snapshots on a given scope. If there were multiple snapshots on a given scope, and one of them is persisted while the others are open, all the opened snapshots will be invalidated and their associated transactions must be rolled back.

It's up to the implementation to determine whether a table scope or a row scope to be used in transaction snapshots. The minimum requirement for this specification is to support table scope, i.e. different transactions MUST be able to operate on different tables simultaneously.

Observers

Observers monitor row changes inside a database. If the results from registered query change due to data changes in the database, the callback in corresponding observer will be invoked. Observer management are performed through IDatabaseObserver interface:

    callback observerCallback = void ();

    interface IDatabaseObserver {
      DOMString observe(ISelectQuery query, observerCallback callbackFn);
      void unobserve(DOMString observerKey);
    };
  
DOMString observe(ISelectQuery query, observerCallback callbackFn);
Registers a select query for observation, returns a unique observer key within the connection. When the results of the query changes due to other data manipulations, observerCallback defined by the user will be invoked.
void unobserve(DOMString observerKey);
Removes a registered observer in the connection.

Although observers are registered/unregistered via a database connection object, the changes they receive may come from other connections. The callback function is only a signal of query result change, and the users are responsible to re-execute observed query in a transaction if desired. Unlike trigger in relational databases, observers do not require implementations to execute the observed query.

The specification supports observations only for single select queries, not for readonly transactions.

Data Manipulation

Data manipulation of a database is carried out through transactions and the queries within. In this section, the APIs for constructing queries will be detailed. Due to the complexity, data selection will be discussed in its own section.

Schema Representation

The fundamental part of data manipulation is to specify what to manipulate. In a relational database, this implies table and columns in the schema. The schema objects for data manipulation are acquired through IDatabaseSchema objects obtained from ISchemaQueryProvider.schema() offered by DatabaseConnection.

Table Representation

The table objects returned by IDatabaseSchema.table() implements the ITable interface. These table objects MUST have properties with same names as columns of that table, and the properties are Column objects, see column representation for more details.

      interface ITable {
        ITable as(DOMString alias);
        DOMString getName();
        DOMString getAlias();
      };
    
ITable as(DOMString alias)
Creates an alias for the table, this is useful for self-join.
DOMString getName()
Returns table name.
DOMString getAlias()
Returns table alias. If no alias is defined for this table schema object, null is returned.

It is a delibrate design to use getName() and getAlias() in ITable, because the string-based properties of ITable are IColumn objects. In order to create less name conflicts, the get functions are used.

Column Representation

Column representation objects have a base interface as defined in the IColumn interface:

      interface IColumn {
        readonly attribute DOMString name;
        readonly attribute DOMString table;
        readonly attribute ColumnType type;
        readonly attribute DOMString fullName;
        readonly attribute boolean nullable;
        IColumn as(DOMString alias);
      };
    
name
The name of the column.
table
The name of the table containing this column.
type
Data type of the column.
fullName
The full name of the column. When the containing table does not have alias, or used in foreign key definition, it will be `${ITable#getName}.${IColumn.name}`, otherwise `${ITable#getAlias}.${IColumn.name}`.
nullable
When false, the column is NOT NULL
IColumnas(DOMString alias);
Creates an alias for the column. The alias will be honored in select queries only.

The objects also provide interfaces for creating predicates, therefore they are defined by the Column interface.

      interface Column : IColumn {
      };
      Column implements IComparisonPredicate;
      Column implements ITruthPredicate;
    

Bindable Values

Bindable values provides flexibilities for determining the actual values to be used in queries at run-time. They serve as placeholders of values. Bindable values are typed as IBindableValue:

    interface IBindableValue {
      readonly attribute unsigned short index;
    };
  
index
Index indicating which value to use in future bind() code. One can bind at most 255 bindable values for any given statement. This limitation is created for simpler implementation.

Type check of bound value is performed by query engine before executing the query. To assign value to IBindableValue, caller MUST use IQuery.bind prior to query execution. A query with any unassigned IBindableValue, or any assigned IBindableValue with mismatched type, MUST be rejected with BindingError during its execution.

The following example demonstrate how to perform parameterized query using data binding:

let dbConnection;
let query;
function init() {
    let dept = dbConnection.schema().table('Dept');
    let emp = dbConnection.schema().table('Employee');
    query = dbConnection
        .select(emp['name'].as('ename'), dept['name'])
        .from(dept, emp)
        .where(dept['id'].eq(emp['id']).and(emp['id'].eq(dbConnection.bind(0))));
}

function updateModel(employeeId) {
    query.bind(employeeId).commit().then(rows => {
        console.log(rows[0]['ename'], rows[0]['Department.name']);
    });
}

  

Common Query Behaviors

Data manipulation queries are created from the IDataQueryProvider interface, which is implemented by the DatabaseConnection object.

    interface IDataQueryProvider {
      ISelectQuery select(IColumn... columns);
      IInsertQuery insert();
      IInsertQuery insertOrReplace();
      IUpdateQuery update(ITable table);
      IDeleteQuery delete();
    };
  
ISelectQuery select(IColumn... columns);
Creates a select query to retrieve data from database. The parameters of this function forms the projection list of the generated select query. If no column is given, then the projection list will contain all columns in the select scope.
IInsertQuery insert();
Creates an insert query to insert data into database.
IInsertQuery insertOrReplace();
Similar to insert query, but behaves differently for primary key conflicts.
IUpdateQuery update(ITable table);
Creates an update query to update existing data in the database.
IDeleteQuery delete();
Creates a delete query to remove data from database.

All queries MUST inherit from this base IQuery interface.

    interface IQuery : IExecutionContext {
      Promise< DOMString > explain();
      IQuery bind(any... values);
      IQuery clone();
      DOMString toSql();
    };
  
Promise< DOMString > explain();
Explains how this query will be executed in transaction. The returned string representation will be implementation-dependent. The specification recommends implementations to return the query execution plan and other useful information for developers to do performance tuning of their queries.
IQuery bind(any... values);
Creates a clone of this query, assigns values to IBindableValue inside the cloned query, and returns the cloned query for execution. The order of values provided in the parameter of this function corresponds to the binding index provided in IDatabaseConnection.bind, i.e. the first parameter will be assigned to index 0, the second parameter to index 1, and so on. If the values provided cannot be assigned (e.g. obvious type mismatch), a BindingError MAY be thrown.
IQuery clone();
Creates a clone of this query with a new execution context. The returned query type will be the same as the original, for example, cloning an ISelectQuery will effectively return an ISelectQuery.
DOMString toSql();
Converts the query to equivalent SQL statements. The returned SQL string will also be implementation-dependent, or null if the implementation does not support this feature. The specification recommends SQL-03 standard for generating SQL statements.

Insert Query

Insert queries are used to insert rows into tables. There are two variations of insert queries: insert and insertOrReplace. Both variations support the IInsertQuery interface:

    interface IInsertQuery : IQuery {
      IInsertQuery into(ITable table);
      IInsertQuery values((object or sequence< object > or IBindableValue) rows);
    };
  
IInsertQuery into(ITable table);
Specifies the target table to insert rows into. For insertOrReplace, the table MUST have a primary key, otherwise IntegrityError will be thrown.
IInsertQuery values((object or sequence< object > or IBindableValue) rows);
Supplies the row(s) to be inserted. When given an object as parameter, or the bindable value resolved as an object, the object will act as a single row; otherwise, each element in the given array is treated as an individual row.

All methods of IInsertQuery returns the calling IInsertQuery object for cascade builder pattern. The methods MUST be called once and only once per query before its execution, otherwise SyntaxError MUST be raised.

If an IInsertQuery object is created from insert, and a row with duplicated primary key is given, ConstraintError MUST be thrown if query engine detected integrity violation. On the other hand, if the IInsertQuery object is from insertOrReplace, the offended row MUST be silently replaced by the new row provided, effectively as removing the offended row and insert the offending row.

If any of the inserted row causes integrity violation other than primary key, a ConstraintError MUST be thrown regardless.

If any of the given rows does not exactly match the table schema of the table specified, DataError MUST be raised. Exact match means that each column specified in the table schema MUST appear as a property of any object provided as row.

IBindableValue can also resolve as an array of objects to provide multiple rows in one call.

Delete Query

Delete queries are used to delete rows in a table. The rows to be deleted are specified via Search Condition. If the search condition is not provided, all rows in the table specified by from() will be deleted. The delete query implements the IDeleteQuery interface:

    interface IDeleteQuery : IQuery {
      IDeleteQuery from(ITable table);
      IDeleteQuery where(ILogicalPredicate searchCondition);
    };
  
IDeleteQuery from(ITable table);
Specifies the target table to delete rows from. This method MUST be called before execution, or SyntaxError will be thrown.
IDeleteQuery where(ILogicalPredicate searchCondition);
Specifies the search condition for filtering rows.

All methods of IDeleteQuery returns the calling IDeleteQuery object for cascade builder pattern. The methods can only be called once per query, otherwise SyntaxError MUST be raised.

If the table is empty, or the search condition returns no matching rows, the delete query will finish successfully without doing anything. If deleting rows cause data integrity violation (for example, deleting a row that is referred in a foreign key), a ConstraintError MUST be raised.

Search Condition

Search condition specifies how the query engine should search the rows for query. They are represented by logical predicates, will be evaluated for rows in the database during query execution, and returns true if the evaluated row is a match or false otherwise. Each logical predicate implicitly carries a scope, which defines the tables that the predicate shall be evaluated against. All rows in these tables will be evaluated. The query engine MAY apply speed up techniques to accelerate the evaluation, for example, using indices.

Logical predicates are obtained from either comparison, truth, or logical predicates. Comparison and truth predicates are obtained from IColumn objects. Complement of predicate is provided by the not function in IDatabaseFunctionProvider.

The comparison or truth predicates can only compare the column type of the column associated. An implicit typeof will be called against the provided value to get the value type. If the provided value type is not the same as the column type, a TypeError will be thrown.

This spec does not provide implicit type conversion. All types MUST be explicit.

Comparison Predicate

The comparison predicates evaluates the relation between a column's value and the given target. Only indexable data types, denoted as IndexableValueType, and their binding counterparts, can be compared. These types are denoted as ComparableValueType and constructs the fundamentals of IComparisonPredicate:

      typedef (boolean or Date or number or DOMString) IndexableValueType;
      typedef (IndexableValueType or IBindableValue) ComparableValueType;

      interface IComparisonPredicate {
        ILogicalPredicate eq((ComparableValueType or IColumn) value);
        ILogicalPredicate neq((ComparableValueType or IColumn) value);
        ILogicalPredicate lt((ComparableValueType or IColumn) value);
        ILogicalPredicate lte((ComparableValueType or IColumn) value);
        ILogicalPredicate gt((ComparableValueType or IColumn) value);
        ILogicalPredicate gte((ComparableValueType or IColumn) value);
      };
    
ILogicalPredicate eq((ComparableValueType or IColumn) value);
The associated column equals to value. Equality is evaluated using the rule of JavaScript == operator.
ILogicalPredicate neq((ComparableValueType or IColumn) value);
The associated column does not equal to value. Inequality is evaluated using the rule of JavaScript != operator.
ILogicalPredicate lt((ComparableValueType or IColumn) value);
The associated column is smaller than value. Relation is evaluated using the rule of JavaScript < operator.
ILogicalPredicate lte((ComparableValueType or IColumn) value);
The associated column is smaller than or equals to value. Relation is evaluated using the rule of JavaScript <= operator.
ILogicalPredicate gt((ComparableValueType or IColumn) value);
The associated column is greater than value. Relation is evaluated using the rule of JavaScript > operator.
ILogicalPredicate gte((ComparableValueType or IColumn) value);
The associated column is greater than or equals to value. Relation is evaluated using the rule of JavaScript >= operator.

Giving IColumn as the parameter of IComparisonPredicate creates implicit inner join syntax. The search condition given by this predicate MUST be evaluated as if given in the form of innerJoin. If the given IColumn is of different type other than the associated column of IComparisonPredicate, a TypeError MUST be thrown.

Truth Predicate

The truth predicates evaluate whether the given condition is true for the associated column or not. They are defined by ITruthPredicate:

      interface ITruthPredicate {
        ILogicalPredicate between(ComparableValueType lhs, ComparableValueType rhs);
        ILogicalPredicate startsWith((IBindableValue or DOMString) value);
        ILogicalPredicate endsWith((IBindableValue or DOMString) value);
        ILogicalPredicate in((sequence< ComparableValueType > or IBindableValue or ISelectQuery) values);
        ILogicalPredicate isNull();
        ILogicalPredicate isNotNull();
      };
    
ILogicalPredicate between(ComparableValueType lhs, ComparableValueType rhs);
The associated column is within the range between lhs and rhs. The relation is evaluated using the rule of JavaScript <= and >=.
ILogicalPredicate startsWith((IBindableValue or DOMString) value);
The associated column starts with the string provided. If the column is not of string type, TypeError will be thrown.
ILogicalPredicate endsWith((IBindableValue or DOMString) value);
The associated column starts with the string provided. If the column is not of string type, TypeError will be thrown.
ILogicalPredicate in((sequence< ComparableValueType > or IBindableValue or ISelectQuery) values);
The associated column equals to one of the value inside values. Equality is evaluated using the rule of JavaScript === operator. If the values are specified as an ISelectQuery, the given query will be evaluated first and generate values from its projection list. If the projection list is of different data type, the implementation will throw TypeError.
ILogicalPredicate isNull();
The associated column is null or undefined.
ILogicalPredicate isNotNull();
The associated column is neither null nor undefined.

Like SQL, empty string is not null.

This spec does not support constant expressions and therefore IComparisonPredicate and ITruthPredicate MUST be associated with an IColumn object.

Logical Predicate

Logical predicate is the representation of search condition. Logical predicates implement ILogicalPredicate:

      interface ILogicalPredicate {
        ILogicalPredicate and(ILogicalPredicate... childPredicate);
        ILogicalPredicate or(ILogicalPredicate... childPredicate);
        ILogicalPredicate clone();
      };
    
ILogicalPredicate and(ILogicalPredicate... childPredicate);
Logical AND of logical predicates. The returning logical predicate will evaluate to true if and only if all predicates in its parameter are evaluated true.
ILogicalPredicate or(ILogicalPredicate... childPredicate);
Logical OR of logical predicates. The returning logical predicate will evaluate to true if any of the predicates in its parameter is evaluated true.
ILogicalPredicate clone();
Clone the search condition, including the binding place holders.

The NOT operation is provided by IDatabaseFunctionProvider.not().

The predicate scope is the union of all tables involved for creating the predicate. For example, IColumn.eq() creates an ILogicalPredicate, and scope of that predicate is the table associated with the creating IColumn object.

Complementing Predicates

The predicates can be complemented using a global function not(). The function is provided in IDatabaseFunctionProvider:

      interface IDatabaseFunctionProvider : IAggregateFunction {
        ILogicalPredicate not(ILogicalPredicate predicate);
      };
    
ILogicalPredicate not(ILogicalPredicate predicate);
Complement the evaluation result of passed in predicate, i.e. returns false if the predicate evaluated true, and vice versa.

Subqueries

Select queries that can be evaluated into IndexableValueType or sequence< IndexableValueType >can be used in predicates as ComparableValueType values. The evaluated type MUST be identical as the type requested by predicate. The query engine MUST evaluate these subqueries first and automatic bind the values into the main query.

let dbConnection;
function getDepartmentsWithFounders() {
    let dept = dbConnection.schema().table('Department');
    let emp = dbConnection.schema().table('Employee');
    return dbConnection
        .select(dept.name)
        .from(dept)
        .where(dept['id'].in(
            dbConnection
                .select(emp['deptId'])
                .from(emp)
                .where(emp['id'].lt(12))))
        .commit();
}

    

Update Query

Update queries updates the value of a column in all rows that matches the search condition. If the search condition is not provided, then the update will be applied to all rows of the table. Update queries are modeled in IUpdateQuery interface:

    interface IUpdateQuery : IQuery {
      IUpdateQuery set(IColumn column, ValueType value);
      IUpdateQuery where(ILogicalPredicate searchCondition);
    };
  
IUpdateQuery set(IColumn column, ValueType value);
Set the column to specified value. If the value type is not the same as the column type, a TypeError will be thrown. If the updated value will result in integrity check failure, a ConstraintError will be thrown. This function can be called multiple times per query, but each column can only be used once, otherwise a SyntaxError will be thrown.
IUpdateQuery where(ILogicalPredicate searchCondition);
Specify the search condition of update. This function can only be called once per query, otherwise SyntaxError will be thrown.

Data Selection

Data selection is done via select queries, which select rows matching search conditions or join conditions from database. If the search conditions are not specified, Cartesian product of all rows from the tables specified by from() will be returned.

Scope of select queries MUST be evaluated using select scope algorithm before execution. The select queries implement ISelectQuery interface:

    interface ISelectQuery : IQuery {
      ISelectQuery from(ITable... tables);
      ISelectQuery where(ILogicalPredicate searchCondition);
      ISelectQuery innerJoin(ITable table, ILogicalPredicate joinCondition);
      ISelectQuery leftOuterJoin(ITable table, ILogicalPredicate joinCondition);
      ISelectQuery limit((unsigned long or IBindableValue) numberOfRows);
      ISelectQuery skip((unsigned long or IBindableValue) numberOfRows);
      ISelectQuery orderBy(IColumn column, optional Order order);
      ISelectQuery groupBy(IColumn... column);
      ISelectQuery union(ISelectQuery... query);
      ISelectQuery intersect(ISelectQuery... query);
      ISelectQuery except(ISelectQuery... query);
    };
  
ISelectQuery from(ITable... tables);
Specifies the scope of the select query. This function can only be called once per query, otherwise SyntaxError will be thrown.
ISelectQuery where(ILogicalPredicate searchCondition);
Specifies search condition. This function can only be called once per query, otherwise SyntaxError will be thrown.
ISelectQuery innerJoin(ITable table, ILogicalPredicate joinCondition);
Specifies explicit inner join. This function can be called multiple times per query.
ISelectQuery leftOuterJoin(ITable table, ILogicalPredicate joinCondition);
Specifies left outer join. This function can be called multiple times per query.
ISelectQuery limit((unsigned long or IBindableValue) numberOfRows);
Specifies maximum number of rows to return. If the number is 0, the query is effectively an no-op. This function can only be called once per query, otherwise SyntaxError will be thrown.
ISelectQuery skip((unsigned long or IBindableValue) numberOfRows);
Specifies maximum number of rows to skip from returning. If the number is greater than total number of rows filtered, an empty array will be returned. This function can only be called once per query, otherwise SyntaxError will be thrown.
ISelectQuery orderBy(IColumn column, optional Order order);
Specify how to sort the returning row. This function can be called multiple times per query. The invocation order of this function will affect the results of sorting, , see sorting algorithm.
ISelectQuery groupBy(IColumn... column);
Specify how to group returning rows in aggregation, see aggregation for more details. This function can only be called once per query, otherwise SyntaxError will be thrown. Caller MUST provide one or more columns in the parameter, otherwise SyntaxError will be thrown. If the same column is provided twice in the parameters, it will be treated as provided only once.
ISelectQuery union(ISelectQuery... query);
Combine results of two or more select queries into a single result set that includes all the rows that belong to all queries in the union. See union projection for details.
ISelectQuery intersect(ISelectQuery... query);
See intersect and except.
ISelectQuery except(ISelectQuery... query);
See intersect and except.

There is not a hard rule regarding the invocation order of ISelectQuery member functions. However, it is recommended that the functions are invoked in the order of their appearance order in the interface for better readability.

Cartesian Product

The Cartesian product of two tables is calculated using the following algorithm.

  1. Let T1 and T2 denote the tables involved in the Cartesian product, and the target is to have T1 × T2.
  2. Let R1 = [R11, R12, R13, ..., R1M] denote the rows of T1.
  3. Let R2 = [R21, R22, R23, ..., R2N] denote the rows of T2.
  4. Cartesian product of T1 and T2 are performed in two steps.
    1. Let R = R1 × R2. This makes R looks like [[R11, R21], [R11, R22], ..., [R11, R2N], [R12, R21], ..., [R1M, R1N]].
    2. Flatten all pairs in R as a row, where T1's columns are expanded before T2's columns. T1's columns appear in the order of schema declaration, and the same rule applies to T2's column.

Select Scope Algorithm

The scope of a select query is determined by the following algorithm.

  1. Let T1 denote the set of tables specified by from()
  2. If innerJoin() existed in query:
    1. Let T2 denote the table specified by inner join
    2. Let S2 denote the scope of the join condition
    3. If S2 ⊄ (T1 ∪ T2), throws SyntaxError
  3. If leftOuterJoin() existed in query:
    1. Let T3 denote the table specified by left outer join
    2. Let S3 denote the scope of the join condition
    3. If S3 ⊄ (T1 ∪ T3), throws SyntaxError
  4. Let T4 denote valid projection scope: T1 ∪ T2 ∪ T3
  5. If where() existed in query:
    1. Let S4 denote the scope of search condition
    2. If S4 ⊄ T4, throws SyntaxError
  6. If any column in orderBy(), groupBy(), or the projection list passed in the constructor of ISelectQuery object is not in the valid projection scope, throw SyntaxError.

Sorting Algorithm

The sorting algorithm is represented in the pseudo JavaScript code below.


    var orderBySpecs;  // Array of orderBy() specs given in this query.
    filteredRows.sort(function(lhs, rhs) {
      for (var i = 0; i < orderBySpecs.length; ++i) {
        var spec = orderBySpecs[i];
        var col = spec.column;
        if (lhs[col] == rhs[col]) {
          continue;
        }
        if ((spec.order == 'asc' && lhs[col] < rhs[col]) ||
            (spec.order == 'desc' && lhs[col] > rhs[col])) {
          return -1;
        } else {
          return 1;
        }
      }
      return 0;
    });
  

Inner Join

Inner joins create a new result table by combining column values of two tables involved in the join based upon the predicate given as join condition. Inner joins MAY be specified either implicitly through search conditions, or explicitly via innerJoin(). The following example demonstrates two equivalent inner join syntaxes:

let dbConnection;
let tableA = dbConnection.schema().table('A');
let tableB = dbConnection.schema().table('B');
// Implicit inner join.
let q1 = dbConnection
    .select(tableA['key'], tableA['value'], tableB['timestamp'])
    .from(tableA, tableB)
    .where(tableA['key'].eq(tableB['key']));
// Explicit inner join.
let q2 = dbConnection
    .select(tableA['key'], tableA['value'], tableB['timestamp'])
    .from(tableA)
    .innerJoin(tableB, tableA['key'].eq(tableB['key']));
  

The results of inner join MUST be equivalent to the outcome of the following algorithm:

  1. Let T1 and T2 denote tables involved in the inner join.
  2. Let C denotes the Cartesian product of T1 and T2.
  3. For each row in C, evaluate the predicate provided as join condition. Return the rows whose evaluation is true.

Conceptually the inner join will create a new table. Implementation MAY NOT create actual table as long as the returned results are correct.

The user is not allowed to mix implicit and explicit join. Implementations will throw either SyntaxError or RuntimeError for invalid queries violating this rule.

Left Outer Join

This specification supports only left outer join, which preserves the unmatched rows from the first (left) table, joining them with a NULL row in the shape of the second (right) table. The algorithm is described below:

  1. Let Tl and Tr denote the left and right table involved in the left-outer join. Let cl denote columns of Tl, and cr for Tr.
  2. Create an empty result row array R.
  3. For each row r in Tl
    1. Find matching rows set Rm within Tr so that for each rm in R the join condition is satisfied by the pair (r, rm).
    2. If Rm is empty, push a row with columns Tr and fill every column with null.
    3. For each rm in Rm, push a new row with columns cl from r, followed by columns cr from rm.
  4. Return result array R.

Aggregate Functions

Aggregate functions are used only in the projection list of a select query. The aggregation functions supported are defined in IAggregateFunction:

    interface IAggregateFunction {
      IColumn avg(IColumn col);
      IColumn count(optional IColumn col = null);
      IColumn distinct(IColumn... col);
      IColumn max(IColumn col);
      IColumn min(IColumn col);
      IColumn sum(IColumn col);
    };
  
IColumn avg(IColumn col);
Calculate the arithematic average of non-null values of column col for all rows grouped. If all rows are null, returns null. The returned column is always typed as number. If the col is not typed as number, TypeError will be thrown.
IColumn count(IColumn col);
Count the number of rows. If col is NOT null, returns number of rows in the group whose col is not null, otherwise returns the total number of rows in the group.
IColumn distinct(IColumn... col);
Returns distinct value tuples of the specified columns in all grouped rows. null is considered as a unique value.
IColumn max(IColumn col);
Returns the maximum value of the column col for all rows grouped. The maximum value is determined by the last value of the orderBy() performed on that column. The max() function returns null if and only if all values grouped are null. The type of col MUST be either number, string, or date, otherwise TypeError will be thrown.
IColumn min(IColumn col);
Returns the minimum value of the column col for all rows grouped. The minimum value is determined by the first value of the orderBy() performed on that column. The max() function returns null if and only if all values grouped are null. The type of col MUST be either number, string, or date, otherwise TypeError will be thrown.
IColumn sum(IColumn col);
Calculate the arithematic sum of non-null values of column col for all rows grouped. If all rows are null, returns null. The returned column is always typed as number. If the col is not typed as number, TypeError will be thrown.

The IColumn objects returned by IAggregateFunction MUST NOT be convertible to Column objects.

You cannot use min or max on blob, boolean, and object columns.

Limitations of Aggregate Functions

Aggregation can only happen on a single table. If the select scope contains more than one table, a SyntaxError will be thrown.

The aggregation can happen for the given table without a groupBy() function, which implies grouping on all rows of that table.

If the groupBy() is present, the projection list MUST consist of either the columns in projection list, or aggregation functions. The parameters of aggregation functions MUST be either undefined if allowed by signature, or from the columns provided as groupBy() parameters.

If grouping and search conditions are both provided, search conditions will be evaluated first, and grouping will be operated on top of the filtered rows.

Aggregation by expression is not supported, i.e. no CASE WHEN equivalents. Also, DISTINCT in SQL is partially implemented in this specification. You can only use it in the projection list, NOT in left joins or other cases.

Union Projection

Union projection is different from joins. It just attempts to combine the projected results into rows using the following algorithm:

  1. For any two different queries in union queries, check if their projected columns are compatible.
    1. Let Qx and Qy denote the queries to be checked.
    2. For any column C in Qx, it MUST satisfy one of the following condition:
      1. No column in Qy has the same name, and C is nullable.
      2. Another column C' in Qy has the same name or alias as C, and the column has exactly the same data type.
      Otherwise a TypeError will be thrown.
  2. Union all qualified columns as new projection list, execute the queries. Query engine CAN decide the execution orders. Final projected rows MUST be generated in the order of union queries, i.e. the rows from union() query first, then in the order of the parameters inside union().
let dbConnection;
function getOrderList() {
    let order = dbConnection.schema().table('Order');
    let archive = dbConnection.schema().table('ArchivedOrder');
    return dbConnection
        .select(order['item'], order['amount'])
        .from(order)
        .where(order['category'].eq('Clothing'))
        .union(dbConnection
            .select(archive['item'], archive['amount'])
            .from(archive)
            .where(archive.category.eq('Clothing')))
        .commit();
}

  

Intersect and Except

Intersect and except are different from joins, they just attempt to perform set operations on the projected results from involving queries. The set of projected columns from any two queries involved MUST be identical in name/alias and data type, otherwise a TypeError will be thrown.

Results returned from intersect() include all the rows that exist in all involved queries.

Results returned from except() exists only in the calling query.

Errors

All errors in this specification are DOMException with the specified error type.

FieldDescription
BlockingError Attempts to open a delete pending database, or to create a transaction in a close pending connection.
BindingError The values given in IQuery.bind have type match, or the executing query has unprovided bindable values.
ConcurrencyError In-flight transaction is automatically rolled back because the snapshot it based on has been obsoleted by other committed transactions.
ConstraintError Constraints are violated.
DataError Data given to a column is invalid. For example, assigning null or undefined to a NOT NULL column, or assigning values of different column type .
IntegrityError Query engine is not able to ensure data integrity. For example, failed to rollback a sequence mode transaction.
InvalidSchemaError Provided schema is invalid.
OutOfMemoryError Executing the query demands too much memory than the implementation could provide. Users are responsible for changing their queries to consume less memory/resources. For example, inserting too many rows into a temporary database, or selecting too many rows from a persistent database.
RuntimeError Invalid query has slipped through syntax checks but caught by the query execution engine.
SyntaxError Illegal use of query builder objects, for example, calling into() twice for IInsertQuery.
TimeoutError Transaction takes too long to resolve (commit) or reject (rollback), and is automatically rolled back.
TransactionStateError Attempts to commit/rollback/attach query to a finished/pending finish transaction.
TypeError Attempts to compare/union two different data types.
UnsupportedError Attempts to use MAY support features that are not supported yet.

Acknowledgements

Many thanks to people who have contributed to this specification and helped to improve it by sending suggestions and corrections.

We also want to thank the authors of ReSpec, Node.js, and gulp.js for the powerful and handy tools that are used to produce this spec.