tabler

Access relational and NoSQL database backends using a generic SQL-inspired table interface with data integrity checks (SimpleDB and JSON file available)

npm install tabler
1 downloads in the last day
1 downloads in the last week
2 downloads in the last month

Tabler: Generic Table Persistence

Introduction

Tabler lets Node.js developers interact with a variety of databases, both relational and NoSQL, using a simple SQL-inspired table interface. Using Tabler your application can define tables, specify column data types, and perform insert/select/update/delete operations in a generic manner. Store-specific details are abstracted away so that when you need to switch data stores, your application is ready.

Project goals:

  • Provide a familiar table interface for non-relational data stores
  • Provide data integrity checks for data stores that don't have this built in
  • Make it easy to switch data stores

Although there are other projects that abstract away store-specific details for relational/SQL databases, these interfaces are generally too complex to implement in NoSQL datastores. The Tabler interface has been purposely kept very simple so that it can be implemented on top of many types of data stores.

Tabler is Unicode/UTF-8 safe, and so friendly to internationalized apps.

A Quick Example

See the short illustrative example here: https://github.com/aarong/tabler/blob/master/example.js

Available Backends

Amazon SimpleDB

  • Transparently serializes all data types (offsets and zero-pads numbers, Iso8601 for dates, etc)
  • Transparently partitions large columns into multiple SimpleDB attributes
  • Transparently iterates on nextTokens for complex queries
  • Transparently breaks large write operations into batches to bypass SimpleDB limits
  • Limitations: Cannot sort on string columns longer than 1024 bytes because they are partitioned.

JSON File

  • Convenient for local development

More to come

  • MySQL
  • SQLite
  • CouchDb?
  • MongoDb?
  • Redis?
  • Cassandra?

Getting Started

Install the module using NPM:

npm install tabler

Create a Tabler instance:

var Tabler = require('tabler'),
    tabler = new Tabler(
        'simpledb',
        {
            accessKeyId:        "123",
            secretAccessKey:    "456",
            domainNamespace:    "tabler" // All domains used by Tabler are prefixed with this string
        }
    );

or

var Tabler = require('tabler'),
    tabler = new Tabler(
        'jsonfile',
        {
            filename: 'myjsonfile.json'
        }
    );

All the Tabler code that you write after this is completely generic -- you will never have to change it even if you decide to move to a different backend.

Defining Your Tables

tabler.defineTable(
    tableName,
    fieldDefs,
    callback[error]
)

fieldDefs = {
    fieldName: fieldDef,
    fieldName: fieldDef,
    ...
}

where (for example)

fieldDef = {
    // String query conditions are case sensitive -- have the app create a lower-case
    // copy of the column to achieve case insensitivity.
    // For strings, passing the null byte \u0000 is the same as passing a null object.
    // About nulls (applies to all data types): For sorting, null is low. Null is not allowed by default.
    type:       'string',
    nullable:   true,
    byteLength: 50,     // Not the same as character length when using multi-byte UTF-8 characters
    pattern:    /.*/    // Regex pattern that must be matched (this one allows everything)
};

For convenience, you can use the following built-in patterns

tabler.PATTERNS.ALL - Allow any string tabler.PATTERNS.PRINTABLE_NO_BREAKS // Printable characters only, line breaks are not allowed tabler.PATTERNS.PRINTABLE_AND_BREAKS // Printable characters online, line breaks ARE allowed

... or (for example)

fieldDef = {
    // Numbers can hold any number that Javascript expresses without exponential notation (up to 1e20 roughly)
    type:       'number', 
    nullable:   true,
    min:        -100,
    max:        100,
    decPlaces:  2
};

or (for example)

fieldDef = {
    type:       'datetime',
    nullable:   false
};

or (for example)

fieldDef = {
    type:       'object',
    nullable:   true,
    byteLength: 1000, // Maximum serialized byte length (JSON is used)
    schema:     {} // Optional (http://tools.ietf.org/html/draft-zyp-json-schema-02)[JSON Schema object]
};

Important note: You need to use defineTable() every time you create your Tabler instance. Once you have written to a table, DO NOT change its definition in any way, as Tabler assumes that your current definition is valid for every row currently in the database and you will have all kinds of problems if it's not. Instead, create a new table and transfer the data (this will be improved in the future).

To remove a table and its data you can do:

tabler.deletetable( // Delete the table and all its data tableName, callback[error] )

Reading and Writing Rows

See below for definitions of method arguments and output variables. Note that Tabler assigns a unqiue id to every row (a UUID string).

The API is designed so that you can apply each operation (insert, select, update, select) to either one row specified by ids, multiple rows specified by an array of ids, or multiple rows specified by a query condtition.

Due to the limitations some of of the underlying data stores, these operations cannot be guaranteed to be atomic (i.e. if you are performing many writes, a failure could result in some writes taking place and others not).

Inserting

tabler.insertOne(
    tableName,
    rowWithoutId or rowWithId,
    callback[error, rowId]
)

tabler.insertMulti(
    tableName,
    rowsWithoutIdArray or rowsWithIdArray,
    callback[error, rowIdArray] // Output row IDs same order as input
)

Selecting and counting

tabler.selectOne(
    tableName,
    fieldNames, // Empty for no fields, null for all fields
    rowId,
    callback[error, rowWithoutId] // null if row not found
)

tabler.selectMulti(
    tableName,
    fieldNames,     // Empty array for no fields (checking existence), null for all fields
    rowIdArray,
    callback[error, rowsWithIdHash] // Rows not found are not included in the hash
)

tabler.selectQuery(
    tableName,
    fieldNames,     // Empty array for no fields (getting row IDs only), null for all fields
    condition,
    selectOptions,  // Optional
    callback[error, rowsWithIdArray]
)

tabler.countQuery(
    tableName,
    condition,
    callback[error, numRows]
)

Updating

tabler.updateOne( // Does not create a row if the id doesn't exist, but returns success
    tableName,
    rowWithoutId // Omitted fields are not altered
    rowId,
    callback[error]
)

tabler.updateMulti( // Does not create a row if the id doesn't exist, but returns success
    tableName,
    rowWithoutId, // The same value is applied to all rows, omitted fields are not altered
    rowIdArray,
    callback[error]
)

tabler.updateQuery(
    tableName,
    rowWithoutId, // The same value is applied to all rows, omitted fields are not altered
    condition,
    callback[error]
)

Deleting

tabler.deleteOne(
    tableName,
    rowId,
    callback[error]
)

tabler.deleteMulti(
    tableName,
    rowIdArray,
    callback[error]
)

tabler.deleteQuery(
    tableName,
    condition,
    callback[error]
)

Arguments and output details

fieldNames = [fieldName, fieldName, ...]

rowWithoutId = {
    fieldName: fieldValue,
    fieldName: fieldValue,
    ...
}

fieldValue = (for example) 'abc'        for string fields
                        or 123.4        for number fields
                        or new Date()   for datetime fields
                        or {a:1, b:2}   for object fields
                        or null         for any field type, if allowed

rowsWithoutIdArray = [rowWithoutId, rowWithoutId, ...]

rowWithId = {
    __id:       GUID,
    fieldName:  fieldValue,
    ...
}

rowsWithIdHash = {
    id1: rowWithId,
    id2: rowWithId,
    ...
}

rowsWithIdArray = [rowWithId, rowWithId, ...]

condition = [ // All conditions joined using AND
    [fieldName, comparisonOperator, fieldValue],
    [fieldName, comparisonOperator, fieldValue],
    ...
]

comparisonOperator = "=", "!=", "<", ">", "<=", or ">="

selectOptions = {
    orderBy:    'fieldName',    // Optional
    orderDir:   'desc',         // Optional, asc (default) or desc
    limit:      10              // Optional, default 0 (all matching rows)
}

error = {
    code: 'ERROR_CODE', // Always present
    (error-specific members)
}

More Interface Notes

Tables and field names are case sensitive, but you cannot define multiple tables or fields with the same lowercase name. This ensures that the interface can be implemented on data stores that have either case-sensitive or case-insensitive table names.

The string "" is not allowed in field names, as it is reserved for Tabler's use. In particular, id is used throughout the API to access the row Id.

Running the tests

cd tests
node stringtabler.js jsonfile
node stringtabler.js simpledb accesskeyid secretaccesskey
node tabler.js jsonfile
node tabler.js simpledb accesskeyid secretaccesskey

Looking Forward

This is just a braindump of my ideas for the project, in no particular order (some of these ideas need to be developed a bit).

  • More backends
  • Cannot generally guarantee that operations are ACID, so have a utility that can be used to check consistency of the data with with your table definitions, and list/update/delete any rows that violate your definitions.
  • Store table definitions in a meta table, then you just define your tables once like in SQL databases
  • Update the interface: createDatabase, useDatabase, dropDatabase, createTable, dropTable.
  • Import/export utility (save/load your databases to/from JSON files or sometihng). Need to think about how to make exports reflect a single point in time as best possible (timestamp writes somehow?).
  • Improve tests (test the entire table against an array each write so that all unintented writes are found).
  • Allow the user to specify unique and foreign key constraints. Provide adequately informative error objects so that apps don't run a bunch of double-checks in order to generate various error codes/messages.
  • Transactions (would be normal transactions on backends that support them, and the best you can do to replicate that functionality for other backends).
  • Indexes (would only apply to some stores, but always best to specify them for future flexibility).
  • Field definition option for case-insensitive string matching (right now you need to manually create a lower-case column)
npm loves you