diff --git a/lib/adapters/mysql.js b/lib/adapters/mysql.js index c2d1a88a..957a7195 100644 --- a/lib/adapters/mysql.js +++ b/lib/adapters/mysql.js @@ -15,8 +15,8 @@ exports.initialize = function initializeSchema(schema, callback) { }); schema.adapter = new MySQL(schema.client); - schema.client.query('SET TIME_ZONE = "+04:00"', callback); - // process.nextTick(callback); + // schema.client.query('SET TIME_ZONE = "+04:00"', callback); + process.nextTick(callback); }; function MySQL(client) { diff --git a/lib/adapters/sqlite3.js b/lib/adapters/sqlite3.js new file mode 100644 index 00000000..3c6fac3b --- /dev/null +++ b/lib/adapters/sqlite3.js @@ -0,0 +1,402 @@ +/** + * Module dependencies + */ +var sqlite3 = require('sqlite3').verbose(); + +exports.initialize = function initializeSchema(schema, callback) { + var s = schema.settings; + var db = new sqlite3.Database(s.database); + + schema.client = db; + + schema.adapter = new SQLite3(schema.client); + process.nextTick(callback); +}; + +function SQLite3(client) { + this._models = {}; + this.client = client; +} + +SQLite3.prototype.define = function (descr) { + this._models[descr.model.modelName] = descr; +}; + +SQLite3.prototype.defineProperty = function (model, prop, params) { + this._models[model].properties[prop] = params; +}; + +SQLite3.prototype.command = function () { + this.query('run', [].slice.call(arguments)); +}; + +SQLite3.prototype.queryAll = function () { + this.query('all', [].slice.call(arguments)); +}; + +SQLite3.prototype.queryOne = function () { + this.query('get', [].slice.call(arguments)); +}; + +SQLite3.prototype.query = function (method, args) { + var time = Date.now(); + var log = this.log; + var cb = args.pop(); + if (typeof cb === 'function') { + args.push(function (err, data) { + log(args[0], time); + cb.call(this, err, data); + }); + } else { + args.push(cb); + args.push(function (err, data) { + log(args[0], time); + }); + } + this.client[method].apply(this.client, args); +}; + +SQLite3.prototype.save = function (model, data, callback) { + var queryParams = []; + var sql = 'UPDATE ' + model + ' SET ' + + Object.keys(data).map(function (key) { + queryParams.push(data[key]); + return key + ' = ?'; + }).join(', ') + ' WHERE id = ' + data.id; + + this.command(sql, queryParams, function (err) { + callback(err); + }); +}; + +/** + * Must invoke callback(err, id) + */ +SQLite3.prototype.create = function (model, data, callback) { + data = data || {}; + var questions = []; + var values = Object.keys(data).map(function (key) { + questions.push('?'); + return data[key]; + }); + var sql = 'INSERT INTO ' + model + ' (' + Object.keys(data).join(',') + ') VALUES (' + sql += questions.join(','); + sql += ')'; + this.command(sql, values, function (err) { + callback(err, this && this.lastID); + }); +}; + +SQLite3.prototype.toFields = function (model, data) { + var fields = []; + var props = this._models[model].properties; + Object.keys(data).forEach(function (key) { + if (props[key]) { + fields.push('`' + key.replace(/\./g, '`.`') + '` = ' + this.toDatabase(props[key], data[key])); + } + }.bind(this)); + return fields.join(','); +}; + +function dateToMysql(val) { + return val.getUTCFullYear() + '-' + + fillZeros(val.getUTCMonth() + 1) + '-' + + fillZeros(val.getUTCDate()) + ' ' + + fillZeros(val.getUTCHours()) + ':' + + fillZeros(val.getUTCMinutes()) + ':' + + fillZeros(val.getUTCSeconds()); + + function fillZeros(v) { + return v < 10 ? '0' + v : v; + } +} + +SQLite3.prototype.toDatabase = function (prop, val) { + if (prop.type.name === 'Number') return val; + if (val === null) return 'NULL'; + if (prop.type.name === 'Date') { + if (!val) return 'NULL'; + if (!val.toUTCString) { + val = new Date(val); + } + return val; + } + if (prop.type.name == "Boolean") return val ? 1 : 0; + return val.toString(); +}; + +SQLite3.prototype.fromDatabase = function (model, data) { + if (!data) return null; + var props = this._models[model].properties; + Object.keys(data).forEach(function (key) { + var val = data[key]; + if (props[key]) { + if (props[key].type.name === 'Date') { + val = new Date(parseInt(val)); + } + } + data[key] = val; + }); + return data; +}; + +SQLite3.prototype.exists = function (model, id, callback) { + var sql = 'SELECT 1 FROM ' + model + ' WHERE id = ' + id + ' LIMIT 1'; + this.queryOne(sql, function (err, data) { + if (err) return callback(err); + callback(null, data && data['1'] === 1); + }); +}; + +SQLite3.prototype.find = function find(model, id, callback) { + var sql = 'SELECT * FROM ' + model + ' WHERE id = ' + id + ' LIMIT 1'; + this.queryOne(sql, function (err, data) { + if (data) { + data.id = id; + } else { + data = null; + } + callback(err, this.fromDatabase(model, data)); + }.bind(this)); +}; + +SQLite3.prototype.destroy = function destroy(model, id, callback) { + var sql = 'DELETE FROM ' + model + ' WHERE id = ' + id; + this.command(sql, function (err) { + callback(err); + }); +}; + +SQLite3.prototype.all = function all(model, filter, callback) { + + var sql = 'SELECT * FROM ' + model; + var self = this; + var props = this._models[model].properties; + var queryParams = []; + + if (filter) { + + if (filter.where) { + sql += ' ' + buildWhere(filter.where); + } + + if (filter.order) { + sql += ' ' + buildOrderBy(filter.order); + } + + if (filter.limit) { + sql += ' ' + buildLimit(filter.limit, filter.offset || 0); + } + + } + + this.queryAll(sql, queryParams, function (err, data) { + if (err) { + return callback(err, []); + } + callback(null, data.map(function (obj) { + return self.fromDatabase(model, obj); + })); + }.bind(this)); + + return sql; + + function buildWhere(conds) { + var cs = []; + Object.keys(conds).forEach(function (key) { + var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`' + if (conds[key] === null) { + cs.push(keyEscaped + ' IS NULL'); + } else { + cs.push(keyEscaped + ' = ?'); + queryParams.push(self.toDatabase(props[key], conds[key])); + } + }); + return 'WHERE ' + cs.join(' AND '); + } + + function buildOrderBy(order) { + if (typeof order === 'string') order = [order]; + return 'ORDER BY ' + order.join(', '); + } + + function buildLimit(limit, offset) { + return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit); + } + +}; + +SQLite3.prototype.destroyAll = function destroyAll(model, callback) { + this.command('DELETE FROM ' + model, function (err) { + if (err) { + return callback(err, []); + } + callback(err); + }.bind(this)); +}; + +SQLite3.prototype.count = function count(model, callback, where) { + var self = this; + var props = this._models[model].properties; + var queryParams = []; + + this.queryOne('SELECT count(*) as cnt FROM ' + model + buildWhere(where), queryParams, function (err, res) { + callback(err, err ? null : res.cnt); + }); + + function buildWhere(conds) { + var cs = []; + Object.keys(conds || {}).forEach(function (key) { + var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`' + if (conds[key] === null) { + cs.push(keyEscaped + ' IS NULL'); + } else { + cs.push(keyEscaped + ' = ?'); + queryParams.push(self.toDatabase(props[key], conds[key])); + } + }); + return cs.length ? ' WHERE ' + cs.join(' AND ') : ''; + } +}; + +SQLite3.prototype.updateAttributes = function updateAttrs(model, id, data, cb) { + data.id = id; + this.save(model, data, cb); +}; + +SQLite3.prototype.disconnect = function disconnect() { + this.client.close(); +}; + +SQLite3.prototype.automigrate = function (cb) { + var self = this; + var wait = 0; + Object.keys(this._models).forEach(function (model) { + wait += 1; + self.dropTable(model, function () { + self.createTable(model, function (err) { + if (err) console.log(err); + done(); + }); + }); + }); + + function done() { + if (--wait === 0 && cb) { + cb(); + } + } +}; + +SQLite3.prototype.autoupdate = function (cb) { + var self = this; + var wait = 0; + Object.keys(this._models).forEach(function (model) { + wait += 1; + self.queryAll('SHOW FIELDS FROM ' + model, function (err, fields) { + self.alterTable(model, fields, done); + }); + }); + + function done(err) { + if (err) { + console.log(err); + } + if (--wait === 0 && cb) { + cb(); + } + } +}; + +SQLite3.prototype.alterTable = function (model, actualFields, done) { + var self = this; + var m = this._models[model]; + var propNames = Object.keys(m.properties); + var sql = []; + + // change/add new fields + propNames.forEach(function (propName) { + var found; + actualFields.forEach(function (f) { + if (f.Field === propName) { + found = f; + } + }); + + if (found) { + actualize(propName, found); + } else { + sql.push('ADD COLUMN `' + propName + '` ' + self.propertySettingsSQL(model, propName)); + } + }); + + // drop columns + actualFields.forEach(function (f) { + var notFound = !~propNames.indexOf(f.Field); + if (f.Field === 'id') return; + if (notFound || !m.properties[f.Field]) { + sql.push('DROP COLUMN `' + f.Field + '`'); + } + }); + + if (sql.length) { + this.command('ALTER TABLE `' + model + '` ' + sql.join(',\n'), done); + } else { + done(); + } + + function actualize(propName, oldSettings) { + var newSettings = m.properties[propName]; + if (newSettings && changed(newSettings, oldSettings)) { + sql.push('CHANGE COLUMN `' + propName + '` `' + propName + '` ' + self.propertySettingsSQL(model, propName)); + } + } + + function changed(newSettings, oldSettings) { + if (oldSettings.Null === 'YES' && (newSettings.allowNull === false || newSettings.null === false)) return true; + if (oldSettings.Null === 'NO' && !(newSettings.allowNull === false || newSettings.null === false)) return true; + if (oldSettings.Type.toUpperCase() !== datatype(newSettings)) return true; + return false; + } +}; + +SQLite3.prototype.dropTable = function (model, cb) { + this.command('DROP TABLE IF EXISTS ' + model, cb); +}; + +SQLite3.prototype.createTable = function (model, cb) { + this.command('CREATE TABLE ' + model + + ' (\n ' + this.propertiesSQL(model) + '\n)', cb); +}; + +SQLite3.prototype.propertiesSQL = function (model) { + var self = this; + var sql = ['`id` INTEGER PRIMARY KEY']; + Object.keys(this._models[model].properties).forEach(function (prop) { + sql.push('`' + prop + '` ' + self.propertySettingsSQL(model, prop)); + }); + return sql.join(',\n '); + +}; + +SQLite3.prototype.propertySettingsSQL = function (model, prop) { + var p = this._models[model].properties[prop]; + return datatype(p) + ' ' + + (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL'); +}; + +function datatype(p) { + switch (p.type.name) { + case 'String': + return 'VARCHAR(' + (p.limit || 255) + ')'; + case 'Text': + return 'TEXT'; + case 'Number': + return 'INT(11)'; + case 'Date': + return 'DATETIME'; + case 'Boolean': + return 'TINYINT(1)'; + } +} + diff --git a/package.json b/package.json index 5c9a91ea..c5dbcd95 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "main": "index.js", "scripts": { - "test": "ONLY=memory ./support/nodeunit/bin/nodeunit test/*_test.* && ONLY=redis nodeunit test/common_test.js && ONLY=mysql nodeunit test/common_test.js && ONLY=postgres nodeunit test/common_test.js" + "test": "ONLY=memory ./support/nodeunit/bin/nodeunit test/*_test.* && ONLY=redis nodeunit test/common_test.js && ONLY=mysql nodeunit test/common_test.js && ONLY=postgres nodeunit test/common_test.js && ONLY=sqlite3 nodeunit test/common_test.js" }, "engines": [ "node >= 0.4.0" @@ -19,7 +19,8 @@ "mysql": ">= 0.9.4", "sequelize": "*", "pg": "*", - "hashish": "*" + "hashish": "*", + "sqlite3": ">= 2.1.1" }, "devDependencies": { "nodeunit": ">= 0", diff --git a/test/common_test.js b/test/common_test.js index 8764b702..008e6bdc 100644 --- a/test/common_test.js +++ b/test/common_test.js @@ -15,10 +15,13 @@ var schemas = { database: 'myapp_test', username: 'root' }, - postgres: { + postgres: { database: 'myapp_test', username: 'postgres' }, + sqlite3: { + database: ':memory:' + }, neo4j: { url: 'http://localhost:7474/' }, // mongoose: { url: 'mongodb://localhost/test' }, mongoose: {