diff --git a/lib/adapters/mysql.js b/lib/adapters/mysql.js index 14766aab..071fb9c0 100644 --- a/lib/adapters/mysql.js +++ b/lib/adapters/mysql.js @@ -45,22 +45,36 @@ function MySQL(client) { require('util').inherits(MySQL, BaseSQL); -MySQL.prototype.query = function (sql, callback) { +/** + * Query the database + * + * @param {String} sql The SQL String to execute + * @param {Array} params The params to bind to the SQL Statement, if + * SQL has placeholders with '?', optional + * @param {Function} callback The callback to execute after query returns, will + * receive `err` and `data` parameters from mysql driver + */ +MySQL.prototype.query = function (sql, params, callback) { if (!this.schema.connected) { return this.schema.on('connected', function () { - this.query(sql, callback); + this.query(sql, params, callback); }.bind(this)); } var client = this.client; var time = Date.now(); var log = this.log; + + if(typeof params === 'function') { + callback = params; + params = []; + } if (typeof callback !== 'function') throw new Error('callback should be a function'); - this.client.query(sql, function (err, data) { + this.client.query(sql, params, function (err, data) { if (err && err.message.match(/^unknown database/i)) { var dbName = err.message.match(/^unknown database '(.*?)'/i)[1]; client.query('CREATE DATABASE ' + dbName, function (error) { if (!error) { - client.query(sql, callback); + client.query(sql, params, callback); } else { callback(err); } @@ -73,48 +87,73 @@ MySQL.prototype.query = function (sql, callback) { }; /** - * Must invoke callback(err, id) + * Create a new row in the table + * + * @param {Object} model The schema model + * @param {Object} data The data to insert + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver and inserted `id` as parameters + * @todo toFields is not ready for prepared statements */ MySQL.prototype.create = function (model, data, callback) { + if (typeof callback !== 'function') { + throw new Error('callback should be a function'); + } + var fields = this.toFields(model, data); var sql = 'INSERT INTO ' + this.tableEscaped(model); if (fields) { - sql += ' SET ' + fields; + sql += ' SET ' + fields.sql; } else { sql += ' VALUES ()'; } - this.query(sql, function (err, info) { + this.query(sql, fields.params, function (err, info) { callback(err, info && info.insertId); }); }; +/** + * Update or create a row in the database table + * + * + * @param {Object} model The schema model + * @param {Object} data The data to insert + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver and inserted `id` as parameters + * @todo fix docs + */ MySQL.prototype.updateOrCreate = function (model, data, callback) { var mysql = this; var fieldsNames = []; var fieldValues = []; var combined = []; + var combinedValues = []; var props = this._models[model].properties; Object.keys(data).forEach(function (key) { if (props[key] || key === 'id') { var k = '`' + key + '`'; var v; if (key !== 'id') { - v = mysql.toDatabase(props[key], data[key]); + v = mysql.toDatabase(props[key], data[key], true); } else { v = data[key]; } fieldsNames.push(k); fieldValues.push(v); - if (key !== 'id') combined.push(k + ' = ' + v); + if (key !== 'id') { + combined.push(k + ' = ?'); + combinedValues.push(v) + } } }); - + + var fieldValuesPrepare = new Array(fieldValues.length + 1).join('?, '); var sql = 'INSERT INTO ' + this.tableEscaped(model); sql += ' (' + fieldsNames.join(', ') + ')'; - sql += ' VALUES (' + fieldValues.join(', ') + ')'; + sql += ' VALUES (' + fieldValuesPrepare.substr(0, fieldValuesPrepare.length - 2) + ')'; sql += ' ON DUPLICATE KEY UPDATE ' + combined.join(', '); - this.query(sql, function (err, info) { +this.query(sql, fieldValues.concat(combinedValues), function (err, info) { if (!err && info && info.insertId) { data.id = info.insertId; } @@ -122,17 +161,37 @@ MySQL.prototype.updateOrCreate = function (model, data, callback) { }); }; +/** + * Transform the model data to SQL Field statement part + * > `fieldname` = `value` + * + * @param {Object} model The schema model + * @param {Object} data The data to insert + * @return {String} comma seperated string with field=val statements + */ MySQL.prototype.toFields = function (model, data) { var fields = []; + var params = []; 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])); + fields.push('`' + key.replace(/\./g, '`.`') + '` = ?'); + params.push(this.toDatabase(props[key], data[key], true)) } }.bind(this)); - return fields.join(','); + return { + sql : fields.join(','), + params : params + }; }; +/** + * Helper function to transform a Javascript Date Object + * into a SQL DATETIME String with format 'YYYY-MM-DD HH:II:SS' + * + * @param {Date} val the Date object + * @return {String} format for DATETIME column + */ function dateToMysql(val) { return val.getUTCFullYear() + '-' + fillZeros(val.getUTCMonth() + 1) + '-' + @@ -146,7 +205,39 @@ function dateToMysql(val) { } } -MySQL.prototype.toDatabase = function (prop, val) { +/** + * Transform model data into SQL ready values to work with them + * + * The model data will be transformed by its type or by the + * model property definition. + * + * NULL values are returned as 'NULL' + * + * If value is an object assume operational usage like between, gte, lte and + * transform for operator `between`, `inq` and `nin` the value. + * - BETWEEN: will return string `val0 AND val1` + * - INQ + NIN: will return string or if multiple oper. values a comma seperated string + * + * + * If until here the the SQL ready return value could not be detected, + * try to use the model property definition of this column to detect the correct + * value representation for the database. + * + * Model Property configured as... + * - NUMBER: return the value + * - DATE : return the value as DATETIME string + * - BOOL : return 1 or 0 + * else return the escaped string (if `noEscape` param is not set) + * + * @param {Object} prop the definition of this property from schema.define + * @param {Mixed} val the value to find the correct database representation + * @param {Boolean} noEscape flag to indicate, that the returning value should not be escaped. + * will be removed, needed for compability while changing to prepared statements + * @todo not completly ready for prepared statement usage + * @todo remove noEscape parameter, after this function is ready for prepStmts + * @return {String/Number} + */ +MySQL.prototype.toDatabase = function (prop, val, noEscape) { if (val === null) return 'NULL'; if (val.constructor.name === 'Object') { var operator = Object.keys(val)[0] @@ -163,19 +254,28 @@ MySQL.prototype.toDatabase = function (prop, val) { } } } - if (!prop) return val; if (prop.type.name === 'Number') return val; if (prop.type.name === 'Date') { if (!val) return 'NULL'; if (!val.toUTCString) { val = new Date(val); } - return '"' + dateToMysql(val) + '"'; + return noEscape === true ? dateToMysql(val) : '"' + dateToMysql(val) + '"'; } if (prop.type.name == "Boolean") return val ? 1 : 0; - return this.client.escape(val.toString()); + return noEscape === true ? val.toString() : this.client.escape(val.toString()); }; +/** + * Transform the data from the database into JS objects if needed + * + * Because nearly all datatypes between database and JS are compatible, + * only transform the DATETIME value into Date Objects + * + * @param {Object} model the model + * @param {Object} data the data to transform + * @return {Object} the data + */ MySQL.prototype.fromDatabase = function (model, data) { if (!data) return null; var props = this._models[model].properties; @@ -191,20 +291,96 @@ MySQL.prototype.fromDatabase = function (model, data) { return data; }; +/** + * Escape column names + * This will escape direct column names or with prefixed table name + * > columnname ==> `columnname` + * > tablename.columnname ==> `tablename`.`columnname` + * + * @param {String} the column- or table-.columnname to escape + * @return {String} + */ MySQL.prototype.escapeName = function (name) { return '`' + name.replace(/\./g, '`.`') + '`'; }; +/** + * Query the database for all records + * + * Will return all columns from a table or filter the result with + * - WHERE: Where conditions are always AND linked + * Usage: Model.all({ + * where: { + * column1 : value1, + * column2 : value2, + * column3 : { + * gt : value3 + * } + * } + * }); + * - ORDER: Order by one or multiple columns + * Usage: Order by one column + * Model.all({ + * order: 'column1 ASC' + * }); + * Usage: Order by multiple columns + * Model.all({ + * order: [ + * 'column1 ASC', + * 'column2 DESC' + * ] + * }); + * - LIMIT: Limit the results + * Usage: limit result by 10 rows + * Model.all({ + * limit: 10 + * }); + * + * - LIMIT/OFFSET: OFFSET can only be used only in conjunction with limit + * and will skip the `x` first results + * Usage: Show 10, skip the first 2 results + * Model.all({ + * limit: 10, 2 + * }); + * Usage: If only the OFFSET is needed, try a big number as limit to not cut off the result + * Model.all({ + * limit: Math.pow(2, 63), 2 + * }); + * NOTE: (tested with Node 0.6.15 on linux mint 12 + * SQL DB support up to 2^64 => 18446744073709551615 (unsigned, bigint) + * NodeJS calcs 2^64 to 18446744073709552000, which breaks SQL max value + * Tested only 2^63 as valid, but you should not have so many rows in + * one table and try to query with limit/offset on this table ;) + * + * @param {Object} model the model + * @param {Object} filter the filters to apply + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver and an array of `results` + * @todo refactor remove join filter from other branch which was accidently merged + * @todo refacor refactor/rewrite to remove the escaping option and try a smater solution + * for collecting `params` for prepared statements + */ MySQL.prototype.all = function all(model, filter, callback) { var sql = 'SELECT * FROM ' + this.tableEscaped(model); var self = this; var props = this._models[model].properties; + var params = []; if (filter) { - if (filter.where) { - sql += ' ' + buildWhere(filter.where); + if (filter.join) { + sql = 'SELECT ' + this.tableEscaped(model) + '.* FROM ' + this.tableEscaped(model); + sql += ' ' + buildJoin(this.table(model), filter.join); + if (filter.where) { + var k = Object.keys(filter.where)[0], + f = this.escapeName(filter.join.modelName + '.' + k) + ' = ' + filter.where[k]; + sql += ' WHERE ' + f; + } + } else if (filter.where) { + var where = buildWhere(filter.where); + sql += ' ' + where.sql; + params = params.concat(where.params) } if (filter.order) { @@ -216,8 +392,7 @@ MySQL.prototype.all = function all(model, filter, callback) { } } - - this.query(sql, function (err, data) { + this.query(sql, params, function (err, data) { if (err) { return callback(err, []); } @@ -228,11 +403,21 @@ MySQL.prototype.all = function all(model, filter, callback) { return sql; + function buildJoin(thisClass, relationClass) { + var rel = relationClass.modelName, + qry = relationClass.foreignKey, + f1 = rel + '.' + qry, // should use railway.utils.camelize + f2 = thisClass + '.id'; + return 'INNER JOIN ' + self.tableEscaped(rel) + ' ON ' + self.escapeName(f1) + ' = ' + self.escapeName(f2); + } + function buildWhere(conds) { var cs = []; + var params = []; + Object.keys(conds).forEach(function (key) { var keyEscaped = '`' + key.replace(/\./g, '`.`') + '`' - var val = self.toDatabase(props[key], conds[key]); + var val = self.toDatabase(props[key], conds[key], true); if (conds[key] === null) { cs.push(keyEscaped + ' IS NULL'); } else if (conds[key].constructor.name === 'Object') { @@ -264,16 +449,21 @@ MySQL.prototype.all = function all(model, filter, callback) { sqlCond + ' != '; break; } - sqlCond += (condType == 'inq' || condType == 'nin') ? '(' + val + ')' : val; + sqlCond += (condType == 'inq' || condType == 'nin') ? '(?)' : '?'; cs.push(sqlCond); + params.push(val); } else { - cs.push(keyEscaped + ' = ' + val); + cs.push(keyEscaped + ' = ?'); + params.push(val); } }); if (cs.length === 0) { return ''; } - return 'WHERE ' + cs.join(' AND '); + return { + sql : 'WHERE ' + cs.join(' AND '), + params : params + }; } function buildOrderBy(order) { @@ -284,9 +474,152 @@ MySQL.prototype.all = function all(model, filter, callback) { function buildLimit(limit, offset) { return 'LIMIT ' + (offset ? (offset + ', ' + limit) : limit); } +}; + + + +/** + * Update an existing record in the database + * + * Use this method to call .save() on a Model instance from an + * early SELECT and update the values of this record in the database + * + * @param {Object} model The schema model + * @param {Object} data The data to update, must inlcude `id` attribute + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver + */ +MySQL.prototype.save = function (model, data, callback) { + var fields = this.toFields(model, data); + var sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' + fields.sql + ' WHERE ' + this.escapeName('id') + ' = ?'; + + fields.params.push(data.id); + + this.query(sql, fields.params, function (err) { + callback(err); + }); +}; + +/** + * Check if a row with the specified ID exists in the database + * + * @param {Object} model The schema model + * @param {Object} data The data to update, must inlcude `id` attribute + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver and bool `exists` as parameter + */ +MySQL.prototype.exists = function (model, id, callback) { + var sql = 'SELECT 1 FROM ' + + this.tableEscaped(model) + ' WHERE ' + this.escapeName('id') + ' = ? LIMIT 1'; + + this.query(sql, [id], function (err, data) { + if (err) return callback(err); + callback(null, data.length === 1); + }); +}; + +/** + * Find a record by the ID and return this row + * + * @param {Object} model The schema model + * @param {Object} id The id to find + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver and the `record` as parameter + */ +MySQL.prototype.find = function find(model, id, callback) { + var sql = 'SELECT * FROM ' + + this.tableEscaped(model) + ' WHERE ' + this.escapeName('id') + ' = ? LIMIT 1'; + + this.query(sql, [id], function (err, data) { + if (data && data.length === 1) { + data[0].id = id; + } else { + data = [null]; + } + callback(err, this.fromDatabase(model, data[0])); + }.bind(this)); +}; + +/** + * Delete a row in the database by the ID + * + * @param {Object} model The schema model + * @param {Object} id The id of the row to delete + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver + */ +MySQL.prototype.destroy = function destroy(model, id, callback) { + var sql = 'DELETE FROM ' + + this.tableEscaped(model) + ' WHERE ' + this.escapeName('id') + ' = ?'; + + this.query(sql, [id], function (err) { + callback(err); + }); +}; + +/** + * Count number of entries in database + * Specify `where` to count entries with a number of entries + * + * MODEL.count({ + * column : 'foo' + * }, function() { + * // result + * }) + * + * @param {Object} model The schema model + * @param {Function} callback The callback to execute after query returns, will + * receive `err` from mysql driver and `result` as integer + * @param {Object} where Object literal with where conditions + * @todo change `count(*)` to `count(id)` for performence + * @todo `where` does not support operators like lte, gte.. + */ +MySQL.prototype.count = function count(model, callback, where) { + var self = this; + var props = this._models[model].properties; + var conds = where ? buildWhere(where) : {sql: '', params : []}; + var sql = 'SELECT count(*) AS cnt FROM ' + this.tableEscaped(model) + ' ' + conds.sql; + + this.query(sql, conds.params, function(err, res) { + if(err) { + return callback(err); + } + callback(err, res && res[0] && res[0].cnt); + }); + function buildWhere(conds) { + var cs = []; + var params = []; + + Object.keys(conds || {}).forEach(function (key) { + var keyEscaped = self.escapeName(key); + if (conds[key] === null) { + cs.push(keyEscaped + ' IS NULL'); + } else { + cs.push(keyEscaped + ' = ?'); + params.push(self.toDatabase(props[key], conds[key], true)) + } + }); + + return { + sql: cs.length ? ' WHERE ' + cs.join(' AND ') : '', + params: params + } + } }; +/** + * Update the schema in the database for all schema models + * + * The autoupdate will look for existing tables and update those tables + * or create them new if they do not exist. + * Could be used for changes in production systems to not flush the hole + * data and reimport them. + * + * @see this.alterTable() + * @see this.createTable() + * @param {Function} cb callback that is called after everything is done, no paramters + */ MySQL.prototype.autoupdate = function (cb) { var self = this; var wait = 0; @@ -300,7 +633,7 @@ MySQL.prototype.autoupdate = function (cb) { } }); }); - + function done(err) { if (err) { console.log(err); @@ -311,6 +644,13 @@ MySQL.prototype.autoupdate = function (cb) { } }; +/** + * Check if the current database is sync'd with the model definition + * This will not alter the database! + * + * @param {Function} cb callback after the check is done with parameters + * `err` from mysql driver and bool `isActual` value + */ MySQL.prototype.isActual = function (cb) { var ok = false; var self = this; @@ -333,6 +673,23 @@ MySQL.prototype.isActual = function (cb) { } }; +/** + * Update the database schema to the current schema definition in models + * + * This method will perform + * + * - ADD COLUMN if new column(s) were added in schema model + * - DROP COLUMN if column(s) were removed in schema model + * - CHANGE COLUMN if properties of a column were changed in schema model + * + * Also internally used to check if the database schema is actual + * + * @param {Object} model The schema models + * @param {Array} actualFields An array of the columns in the database + * @param {Function} done Internal use, callback after a non altering check is done + * @param {Boolean} checkOnly Flag to indicate that only checking should be performed + * @todo add feature: INDEX + */ MySQL.prototype.alterTable = function (model, actualFields, done, checkOnly) { var self = this; var m = this._models[model]; @@ -391,6 +748,27 @@ MySQL.prototype.alterTable = function (model, actualFields, done, checkOnly) { } }; +/** + * Create a string that represent all column + * with properties to create a db table. + * + * @param {Object} model the schema model + * @return {String} a comma seperated string with all columns to create + * @todo refactor: with this approach it is not possible to create indexes over multiple columns + * it would be easier if returning string would just contain the columns with + * name, type, (un)signed, auto_increment + * and then in the createTable() method add the primary_key, index, uniqe, ... + * statements like this example + * ``` + * CREATE TABLE test ( + * id INT NOT NULL, + * last_name CHAR(30) NOT NULL, + * first_name CHAR(30) NOT NULL, + * PRIMARY KEY (id), + * INDEX name (last_name,first_name) + * ); + * ``` + */ MySQL.prototype.propertiesSQL = function (model) { var self = this; var sql = ['`id` INT(11) NOT NULL AUTO_INCREMENT UNIQUE PRIMARY KEY']; @@ -401,12 +779,45 @@ MySQL.prototype.propertiesSQL = function (model) { }; +/** + * Used to get the column property settings like NOT NULL/NULL and DEFAULT + * + * @param {Object} model the schema model + * @param {String} prop the property to create the SQL settings for + * @return {String} + * @todo This version is still not finished. It had some problems with `Date` + * default values and added some parts of it. But its not clean and stable. + * Should be cleaned up... + */ MySQL.prototype.propertySettingsSQL = function (model, prop) { - var p = this._models[model].properties[prop]; + var p = this._models[model].properties[prop], + d = p.default, + defaultValue; + + if(typeof d === 'function') { + if(d === Date.now) { + defaultValue = dateToMysql(new Date(d())); + } else { + defaultValue = d(); + } + } else if(d instanceof Date) { + defaultValue = dateToMysql(d); + } else { + defaultValue = d; + } + return datatype(p) + ' ' + - (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL'); + (p.allowNull === false || p['null'] === false ? 'NOT NULL' : 'NULL') + ' ' + + (d ? 'DEFAULT ' + this.client.escape(defaultValue) : ''); }; +/** + * Helper function find the correct database type for a column + * based on the configured schema model + * + * @param {Object} property configuration from schema model + * @return {String} + */ function datatype(p) { var dt = ''; switch (p.type.name) { diff --git a/test/datatypes.js b/test/datatypes.js new file mode 100644 index 00000000..bb8d81cd --- /dev/null +++ b/test/datatypes.js @@ -0,0 +1,379 @@ +var assert = require('assert'); +var should = require('should'); + + +var Schema = require('../index').Schema; +var Text = Schema.Text; + +var credentials = { + database: 'jugglingdb', + username: 'jugglingdb_dev', + password: 'jugglingdb' +}; + +describe('init and connect to database', function() { + var schema, adapter; + + before(function() { + schema = new Schema('mysql', credentials); + + schema.log = function(msg) { + if (process.env.SHOW_SQL) { + console.log(msg); + } + } + }); + + it('should establish a connection to database', function(done) { + if(schema.connected) { + schema.connected.should.be.true; + } else { + schema.on('connected', function() { + Object.should.be.ok; + done(); + }); + } + }); + + describe('definition of models', function() { + + var cDataTypes; + + it('should define models', function (done) { + + cDataTypes = schema.define('cDataTypes', { + sString_1 : String, + sString_2 : {type : String}, + sString_len : {type: String, limit: 200}, + sString_default_1 : {type: String, default : 1}, + sString_default_2 : {type: String, default : 'foo'}, + sString_default_3 : {type: String, default : function() {return 'fnFoo';}}, + sString_index : {type: String, index: true}, + nNumber_1 : Number, + nNumber_2 : {type : Number}, + nNumber_len : {type: Number, limit: 5}, + // only numeric values are allowed as default value + nNumber_default_1 : {type: Number, default : 1}, + nNumber_default_2 : {type: Number, default : function() {return '1';}}, + nNumber_index : {type: Number, index: true}, + tText_1 : Schema.Text, + tText_2 : {type : Schema.Text}, + tText_len : {type: Schema.Text, limit: 1000}, + // text fields cannot have a default value + tText_index : {type: Schema.Text, index: true}, + bBool_1: Boolean, + bBool_2 : {type : Boolean}, + bBool_len : {type: Boolean, limit: 200}, + // bool are tinyint(1) so only 1 and 0 are valid + bBool_default_1 : {type: Boolean, default : 1}, + bBool_default_2 : {type: Boolean, default : 0}, + bBool_index : {type: Boolean, index: true}, + dDate_1 : Date, + dDate_2 : {type : Date}, + dDate_len : {type: Date, limit: 200}, + dDate_default_1 : {type: Date, default : 0}, + dDate_default_2 : {type: Date, default : Date.now, foo : 1}, + dDate_default_3 : {type: Date, default : function() { return '2012-05-03 17:57:35';}}, + dDate_default_4 : {type: Date, default : new Date()}, + dDate_index : {type: Date, index: true}, + }); + + schema.automigrate(function() { + done(); + }); + }); + }); + + describe('validate the automigrated models', function() { + var client; + + before(function() { + client = schema.adapter.client; + }); + + it('should have created the table', function(done) { + client.query('SHOW TABLES', function(err, result, info) { + + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array); + result.length.should.be.above(0); + + var tables = result.map(function(table) { + for(var i in table) { + if(table.hasOwnProperty(i)) { + return table[i]; + } + return null; + } + }); + tables.should.include('cDataTypes'); + + done(); + }); + }); + + describe('validate the created table', function () { + + var columns = {}; + + before(function (done) { + client.query('DESCRIBE cDataTypes', function(err, result) { + + result.map(function(column) { + columns[column.Field] = { + name : column.Field, + type : column.Type, + nullVal : column.Null, + key : column.Key, + defaultValue : column.Default, + extra : column.Extra + } + }); + + done(); + }); + }); + + it('should have created all columns', function (done) { + columns.should.have.keys('id', + 'sString_1', 'sString_2', 'sString_len', + 'sString_default_1', 'sString_default_2', 'sString_default_3', + 'sString_index', + 'nNumber_1', 'nNumber_2', 'nNumber_len', + 'nNumber_default_1', 'nNumber_default_2', + 'nNumber_index', + 'tText_1', 'tText_2', 'tText_len', + 'tText_index', + 'bBool_1', 'bBool_2', 'bBool_len', + 'bBool_default_1', 'bBool_default_2', + 'bBool_index', + 'dDate_1', 'dDate_2', 'dDate_len', + 'dDate_default_1', 'dDate_default_2', 'dDate_default_3', 'dDate_default_4', + 'dDate_index' + ); + done(); + }); + + it('should haved generated automatically an `id` column', function () { + columns.id.should.have.property('type', 'int(11)'); + columns.id.should.have.property('nullVal', 'NO'); + columns.id.should.have.property('key', 'PRI'); + columns.id.should.have.property('defaultValue', null); + columns.id.should.have.property('extra', 'auto_increment'); + }); + + it('should have created correct string related fields', function(done) { + columns.sString_1.should.have.property('type', 'varchar(255)'); + columns.sString_1.should.have.property('nullVal', 'YES'); + columns.sString_1.should.have.property('key', ''); + columns.sString_1.should.have.property('defaultValue', null); + columns.sString_1.should.have.property('extra', ''); + + columns.sString_2.should.have.property('type', 'varchar(255)'); + columns.sString_2.should.have.property('nullVal', 'YES'); + columns.sString_2.should.have.property('key', ''); + columns.sString_2.should.have.property('defaultValue', null); + columns.sString_2.should.have.property('extra', ''); + + columns.sString_len.should.have.property('type', 'varchar(200)'); + columns.sString_len.should.have.property('nullVal', 'YES'); + columns.sString_len.should.have.property('key', ''); + columns.sString_len.should.have.property('defaultValue', null); + columns.sString_len.should.have.property('extra', ''); + + columns.sString_default_1.should.have.property('type', 'varchar(255)'); + columns.sString_default_1.should.have.property('nullVal', 'YES'); + columns.sString_default_1.should.have.property('key', ''); + columns.sString_default_1.should.have.property('defaultValue', '1'); + columns.sString_default_1.should.have.property('extra', ''); + + columns.sString_default_2.should.have.property('type', 'varchar(255)'); + columns.sString_default_2.should.have.property('nullVal', 'YES'); + columns.sString_default_2.should.have.property('key', ''); + columns.sString_default_2.should.have.property('defaultValue', 'foo'); + columns.sString_default_2.should.have.property('extra', ''); + + columns.sString_default_3.should.have.property('type', 'varchar(255)'); + columns.sString_default_3.should.have.property('nullVal', 'YES'); + columns.sString_default_3.should.have.property('key', ''); + columns.sString_default_3.should.have.property('defaultValue', 'fnFoo'); + columns.sString_default_3.should.have.property('extra', ''); + +// INDEX not working at the moment +// columns.sString_index.should.have.property('type', 'varchar(255)'); +// columns.sString_index.should.have.property('nullVal', 'YES'); +// columns.sString_index.should.have.property('key', ''); +// columns.sString_index.should.have.property('defaultValue', null); +// columns.sString_index.should.have.property('extra', ''); + + done(); + }); + + + it('should have created correct number related fields', function(done) { + columns.nNumber_1.should.have.property('type', 'int(11)'); + columns.nNumber_1.should.have.property('nullVal', 'YES'); + columns.nNumber_1.should.have.property('key', ''); + columns.nNumber_1.should.have.property('defaultValue', null); + columns.nNumber_1.should.have.property('extra', ''); + + columns.nNumber_2.should.have.property('type', 'int(11)'); + columns.nNumber_2.should.have.property('nullVal', 'YES'); + columns.nNumber_2.should.have.property('key', ''); + columns.nNumber_2.should.have.property('defaultValue', null); + columns.nNumber_2.should.have.property('extra', ''); + + columns.nNumber_len.should.have.property('type', 'int(5)'); + columns.nNumber_len.should.have.property('nullVal', 'YES'); + columns.nNumber_len.should.have.property('key', ''); + columns.nNumber_len.should.have.property('defaultValue', null); + columns.nNumber_len.should.have.property('extra', ''); + + columns.nNumber_default_1.should.have.property('type', 'int(11)'); + columns.nNumber_default_1.should.have.property('nullVal', 'YES'); + columns.nNumber_default_1.should.have.property('key', ''); + columns.nNumber_default_1.should.have.property('defaultValue', '1'); + columns.nNumber_default_1.should.have.property('extra', ''); + + columns.nNumber_default_2.should.have.property('type', 'int(11)'); + columns.nNumber_default_2.should.have.property('nullVal', 'YES'); + columns.nNumber_default_2.should.have.property('key', ''); + columns.nNumber_default_2.should.have.property('defaultValue', '1'); + columns.nNumber_default_2.should.have.property('extra', ''); + +// INDEX not working at the moment +// columns.nNumber_index.should.have.property('type', 'int(11)'); +// columns.nNumber_index.should.have.property('nullVal', 'YES'); +// columns.nNumber_index.should.have.property('key', ''); +// columns.nNumber_index.should.have.property('defaultValue', null); +// columns.nNumber_index.should.have.property('extra', ''); + + done(); + }); + + + it('should have created correct text related fields', function(done) { + columns.tText_1.should.have.property('type', 'text'); + columns.tText_1.should.have.property('nullVal', 'YES'); + columns.tText_1.should.have.property('key', ''); + columns.tText_1.should.have.property('defaultValue', null); + columns.tText_1.should.have.property('extra', ''); + + columns.tText_2.should.have.property('type', 'text'); + columns.tText_2.should.have.property('nullVal', 'YES'); + columns.tText_2.should.have.property('key', ''); + columns.tText_2.should.have.property('defaultValue', null); + columns.tText_2.should.have.property('extra', ''); + + columns.tText_len.should.have.property('type', 'text'); + columns.tText_len.should.have.property('nullVal', 'YES'); + columns.tText_len.should.have.property('key', ''); + columns.tText_len.should.have.property('defaultValue', null); + columns.tText_len.should.have.property('extra', ''); + +// INDEX not working at the moment +// columns.tText_index.should.have.property('type', 'varchar(255)'); +// columns.tText_index.should.have.property('nullVal', 'YES'); +// columns.tText_index.should.have.property('key', ''); +// columns.tText_index.should.have.property('defaultValue', null); +// columns.tText_index.should.have.property('extra', ''); + + done(); + }); + + + it('should have created correct boolean related fields', function(done) { + columns.bBool_1.should.have.property('type', 'tinyint(1)'); + columns.bBool_1.should.have.property('nullVal', 'YES'); + columns.bBool_1.should.have.property('key', ''); + columns.bBool_1.should.have.property('defaultValue', null); + columns.bBool_1.should.have.property('extra', ''); + + columns.bBool_2.should.have.property('type', 'tinyint(1)'); + columns.bBool_2.should.have.property('nullVal', 'YES'); + columns.bBool_2.should.have.property('key', ''); + columns.bBool_2.should.have.property('defaultValue', null); + columns.bBool_2.should.have.property('extra', ''); + + // on boolean any `limit` config will be overridden to tinyint(1) + columns.bBool_len.should.have.property('type', 'tinyint(1)'); + columns.bBool_len.should.have.property('nullVal', 'YES'); + columns.bBool_len.should.have.property('key', ''); + columns.bBool_len.should.have.property('defaultValue', null); + columns.bBool_len.should.have.property('extra', ''); + + columns.bBool_default_1.should.have.property('type', 'tinyint(1)'); + columns.bBool_default_1.should.have.property('nullVal', 'YES'); + columns.bBool_default_1.should.have.property('key', ''); + columns.bBool_default_1.should.have.property('defaultValue', '1'); + columns.bBool_default_1.should.have.property('extra', ''); + + columns.bBool_default_2.should.have.property('type', 'tinyint(1)'); + columns.bBool_default_2.should.have.property('nullVal', 'YES'); + columns.bBool_default_2.should.have.property('key', ''); + columns.bBool_default_2.should.have.property('defaultValue', null); + columns.bBool_default_2.should.have.property('extra', ''); + +// INDEX not working at the moment +// columns.bBool_index.should.have.property('type', 'tinyint(1)'); +// columns.bBool_index.should.have.property('nullVal', 'YES'); +// columns.bBool_index.should.have.property('key', ''); +// columns.bBool_index.should.have.property('defaultValue', null); +// columns.bBool_index.should.have.property('extra', ''); + + done(); + }); + + + + + it('should have created correct date related fields', function(done) { + columns.dDate_1.should.have.property('type', 'datetime'); + columns.dDate_1.should.have.property('nullVal', 'YES'); + columns.dDate_1.should.have.property('key', ''); + columns.dDate_1.should.have.property('defaultValue', null); + columns.dDate_1.should.have.property('extra', ''); + + columns.dDate_2.should.have.property('type', 'datetime'); + columns.dDate_2.should.have.property('nullVal', 'YES'); + columns.dDate_2.should.have.property('key', ''); + columns.dDate_2.should.have.property('defaultValue', null); + columns.dDate_2.should.have.property('extra', ''); + + columns.dDate_len.should.have.property('type', 'datetime'); + columns.dDate_len.should.have.property('nullVal', 'YES'); + columns.dDate_len.should.have.property('key', ''); + columns.dDate_len.should.have.property('defaultValue', null); + columns.dDate_len.should.have.property('extra', ''); + + columns.dDate_default_1.should.have.property('type', 'datetime'); + columns.dDate_default_1.should.have.property('nullVal', 'YES'); + columns.dDate_default_1.should.have.property('key', ''); + columns.dDate_default_1.should.have.property('defaultValue', null); + columns.dDate_default_1.should.have.property('extra', ''); + + columns.dDate_default_2.should.have.property('type', 'datetime'); + columns.dDate_default_2.should.have.property('nullVal', 'YES'); + columns.dDate_default_2.should.have.property('key', ''); + columns.dDate_default_2.should.have.property('defaultValue').with.match(/\d{4}\-\d{2}\-\d{2} \d{2}:\d{2}:\d{2}/); + columns.dDate_default_2.should.have.property('extra', ''); + + columns.dDate_default_3.should.have.property('type', 'datetime'); + columns.dDate_default_3.should.have.property('nullVal', 'YES'); + columns.dDate_default_3.should.have.property('key', ''); + columns.dDate_default_3.should.have.property('defaultValue', '2012-05-03 17:57:35'); + columns.dDate_default_3.should.have.property('extra', ''); + +// INDEX not working at the moment +// columns.dDate_index.should.have.property('type', 'datetime'); +// columns.dDate_index.should.have.property('nullVal', 'YES'); +// columns.dDate_index.should.have.property('key', ''); +// columns.dDate_index.should.have.property('defaultValue', null); +// columns.dDate_index.should.have.property('extra', ''); + + done(); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 00000000..d9777dcf --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1,3 @@ +--require should +--reporter spec +--ui bdd \ No newline at end of file diff --git a/test/prepared.js b/test/prepared.js new file mode 100644 index 00000000..a7cede59 --- /dev/null +++ b/test/prepared.js @@ -0,0 +1,588 @@ +var should = require('should'); +var crypto = require('crypto'); + + +var Schema = require('../index').Schema; +var Text = Schema.Text; + +var credentials = { + database: 'jugglingdb', + username: 'jugglingdb_dev', + password: 'jugglingdb' +}; + +var tablename1 = 'test_preparedstatements'; +var tablename2 = 'test_preparedstatements_relation'; +var sqlStatements = []; + +function findRemoveSQLStatement(find) { + var i = sqlStatements.indexOf(find); + if(i !== -1) { + i = sqlStatements.splice(i, 1); + sqlStatements = []; + return i; + } + sqlStatements = []; + return false; +} + +describe('init and connect to database', function() { + var schema, adapter, cDataTypes, cDataTypesRelation; + + before(function() { + schema = new Schema('mysql', credentials); + + schema.log = function(msg) { + if (process.env.SHOW_SQL) { + console.log("QUERY LOG: >" + msg + "<"); + } + + sqlStatements.push(msg); + } + }); + + it('should establish a connection to database', function(done) { + if(schema.connected) { + schema.connected.should.be.true; + } else { + schema.on('connected', function() { + Object.should.be.ok; + done(); + }); + } + }); + + describe('definition of models', function() { + + it('should define models', function(done) { + + cDataTypes = schema.define(tablename1, { + sString : {type : String}, + nNumber : {type : Number}, + tText : {type : Schema.Text}, + bBool : {type : Boolean, default: false}, + dDate : {type : Date} + }); + + cDataTypesRelation = schema.define(tablename2, { + sString : {type : String}, + nNumber : {type : Number}, + tText : {type : Schema.Text}, + bBool : {type : Boolean, default: false}, + dDate : {type : Date} + }); + + cDataTypes.hasMany(cDataTypesRelation, {as: 'relation', foreignKey: 'relationId'}); + + schema.automigrate(function() { + done(); + }); + }); + }); + + describe('validate the automigrated models', function() { + var client; + + before(function() { + client = schema.adapter.client; + }); + + it('should have created the table', function(done) { + client.query('SHOW TABLES', function(err, result, info) { + + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array); + result.length.should.be.above(0); + + var tables = result.map(function(table) { + for(var i in table) { + if(table.hasOwnProperty(i)) { + return table[i]; + } + return null; + } + }); + tables.should.include('cDataTypes'); + + done(); + }); + }); + }); + + describe('test prepared statements', function() { + var testDataLength = 2; + + describe('MODEL.all()...', function() { + var testingCounter = 0; + + before(function(done) { + cDataTypes.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename1+'`'); + done(); + }); + }); + + beforeEach(function(done) { + // Insert dummy data + for(var i = 0; i < testDataLength; i++, testingCounter++) { + + cDataTypes.create({ + sString : 'string-' + testingCounter, + nNumber : i, + tText : 'text-string-' + testingCounter, + bBool : i % 2, + dDate : new Date(2012, 8, 1, 12, 0, testingCounter, 0) + }); + } + done(); + }); + + it('should not use prepared statements', function(done) { + cDataTypes.all(function(err, result) { + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array).with.lengthOf(testDataLength); + + findRemoveSQLStatement('SELECT * FROM `'+tablename1+'`').should.not.be.false; + + done(); + }); + }); + + describe('MODEL.all({order: sString DESC}) ...', function() { + it('should not use prepared statements', function(done) { + cDataTypes.all({ + order: 'sString DESC' + }, function(err, result) { + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array).with.lengthOf(4); + + findRemoveSQLStatement("SELECT * FROM `"+tablename1+"` ORDER BY sString DESC").should.not.be.false; + done(); + }); + }); + }); + + describe('MODEL.all({limit: 2}) ...', function() { + it('should not use prepared statements', function(done) { + cDataTypes.all({ + limit : 2 + }, function(err, result) { + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array).with.lengthOf(2); + + findRemoveSQLStatement("SELECT * FROM `"+tablename1+"` LIMIT 2").should.not.be.false; + done(); + }); + }); + }); + + describe('MODEL.all({limit 10, skip: 2}) ...', function() { + it('should not use prepared statements', function(done) { + cDataTypes.all({ + limit : Math.pow(2, 63), + offset : 2 + }, function(err, result) { + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array).with.lengthOf(6); + + findRemoveSQLStatement("SELECT * FROM `"+tablename1+"` LIMIT 2, 9223372036854776000").should.not.be.false; + done(); + }); + }); + }); + + describe('MODEL.all({where: {}}) ...', function() { + it('should use prepared statements', function(done) { + cDataTypes.all({ + where : { + sString : 'string-1' + } + }, function(err, result) { + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array).with.lengthOf(1); + + findRemoveSQLStatement("SELECT * FROM `"+tablename1+"` WHERE `sString` = ?").should.not.be.false; + done(); + }); + }); + }); + + describe('MODEL.all({where: {}}) ...', function() { + it('should use prepared statements', function(done) { + cDataTypes.all({ + where : { + sString : 'string-1', + nNumber : 1, + tText : 'text-string-1', + bBool : 1, + dDate : new Date(2012, 8, 1, 12, 0, 1, 0) + } + }, function(err, result) { + should.not.exist(err); + should.exist(result); + result.should.be.instanceOf(Array).with.lengthOf(1); + + findRemoveSQLStatement("SELECT * FROM `"+tablename1+"` WHERE `sString` = ? " + + "AND `nNumber` = ? AND `tText` = ? " + + "AND `bBool` = ? AND `dDate` = ?").should.not.be.false; + done(); + }); + }); + }); + }); + + + + describe('MODEL.find() ...', function() { + var testingCounter = 0; + + before(function(done) { + cDataTypes.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename1+'`'); + done(); + }); + }); + + beforeEach(function(done) { + // Insert dummy data + for(var i = 0; i < testDataLength; i++, testingCounter++) { + + cDataTypes.create({ + sString : 'string-' + testingCounter, + nNumber : i, + tText : 'text-string-' + testingCounter, + bBool : i % 2, + dDate : new Date(2012, 8, 1, 12, 0, testingCounter, 0) + }); + } + done(); + }); + + describe('MODEL.find()', function() { + it('should use prepared statements', function(done) { + cDataTypes.find(13, function(err, row) { + should.not.exist(err); + should.exist(row); + + row.id.should.eql(13) + + findRemoveSQLStatement("SELECT * FROM `"+tablename1+"` WHERE `id` = ? LIMIT 1").should.not.be.false; + done(); + }); + }); + }); + }); + + + describe('MODEL.save() ...', function() { + var testingCounter = 0; + + before(function(done) { + cDataTypes.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename1+'`'); + done(); + }); + }); + + beforeEach(function(done) { + // Insert dummy data + var total = 0; + for(var i = 0; i < testDataLength; i++, testingCounter++) { + + cDataTypes.create({ + sString : 'string-' + testingCounter, + nNumber : i, + tText : 'text-string-' + testingCounter, + bBool : i % 2, + dDate : new Date(2012, 8, 1, 12, 0, testingCounter, 0) + }, isDone); + } + function isDone() { + if(++total === testDataLength) { + done() + } + } + }); + it('should use prepared statements', function(done) { + + var newInstance = new cDataTypes({ + sString : 'upsert-1', + nNumber : 123, + tText : 'text-string-upsert-1', + bBool : 1, + dDate : new Date(2012, 8, 1, 12, 0, 0, 0) + }); + + cDataTypes.find(15, function(err, result) { + result.sString = 'NEWVALUE'; + result.save(function(err) { + + findRemoveSQLStatement("UPDATE `"+tablename1+"` SET `sString` = ?,`nNumber` = ?,`tText` = ?,`bBool` = ?,`dDate` = ? WHERE `id` = ?").should.not.be.false; + + + done(); + }); + }) + }); + }); + + describe('MODEL.updateOrCreate() ...', function() { + var testingCounter = 0; + + before(function(done) { + cDataTypes.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename1+'`'); + done(); + }); + }); + + beforeEach(function(done) { + // Insert dummy data + var total = 0; + for(var i = 0; i < testDataLength; i++, testingCounter++) { + + cDataTypes.create({ + sString : 'string-' + testingCounter, + nNumber : i, + tText : 'text-string-' + testingCounter, + bBool : i % 2, + dDate : new Date(2012, 8, 1, 12, 0, testingCounter, 0) + }, isDone); + } + function isDone() { + if(++total === testDataLength) { + done() + } + } + }); + + describe('new record', function() { + + it('should use prepared statements', function(done) { + + var newInstance = new cDataTypes({ + sString : 'upsert-1', + nNumber : 123, + tText : 'text-string-upsert-1', + bBool : 1, + dDate : new Date(2012, 8, 1, 12, 0, 0, 0) + }); + + cDataTypes.updateOrCreate(newInstance, function(err, row) { + + should.not.exist(err); + should.exist(row); + + row.should.have.keys('id', 'sString', 'nNumber', 'tText', 'bBool', 'dDate'); + row.id.should.eql(19); + row.sString.should.eql('upsert-1'); + row.nNumber.should.eql(123); + row.tText.should.eql('text-string-upsert-1'); + row.bBool.should.eql(1); + row.dDate.should.eql(1346500800000); //@todo bug? shouldn't it return a JS Date Object? + + findRemoveSQLStatement("INSERT INTO `"+tablename1+"` SET `sString` = ?,`nNumber` = ?,`tText` = ?,`bBool` = ?,`dDate` = ?").should.not.be.false; + done(); + }); + }); + }); + + describe('existing record', function() { + + it('should use prepared statements', function(done) { + + cDataTypes.find(19, function(err, result) { + result.sString = 'EDITED'; + result.nNumber = 999; + findRemoveSQLStatement(); + + cDataTypes.updateOrCreate(result, function(err, row) { + should.not.exist(err); + should.exist(row); + + row.should.have.keys('id', 'sString', 'nNumber', 'tText', 'bBool', 'dDate'); + row.id.should.eql(19); + row.sString.should.eql('EDITED'); + row.nNumber.should.eql(999); + row.tText.should.eql('text-string-upsert-1'); + row.bBool.should.eql(1); + row.dDate.should.eql(1346500800000); //@todo bug? shouldn't it return a JS Date Object? + + findRemoveSQLStatement("INSERT INTO `"+tablename1+"` (`id`, `sString`, `nNumber`, `tText`, `bBool`, `dDate`) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE `sString` = ?, `nNumber` = ?, `tText` = ?, `bBool` = ?, `dDate` = ?").should.not.be.false; + done(); + }) + }) + }); + }); + }); + + describe('MODEL.count() ...', function() { + var testingCounter = 0; + + before(function(done) { + cDataTypes.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename1+'`'); + done(); + }); + }); + + beforeEach(function(done) { + // Insert dummy data + var total = 0; + for(var i = 0; i < testDataLength; i++, testingCounter++) { + + cDataTypes.create({ + sString : 'string-count-' + testingCounter, + nNumber : i, + tText : 'text-string-count-' + testingCounter, + bBool : i % 2, + dDate : new Date(2012, 8, 1, 12, 0, testingCounter, 0) + }, isDone); + } + function isDone() { + if(++total === testDataLength) { + done() + } + } + }); + + describe('MODEL.count()', function() { + + it('should not use prepared statements', function(done) { + + cDataTypes.count(function(err, result) { + should.not.exist(err); + should.exist(result); + + result.should.eql(2); + //@todo: Space at the end of query, not needed, check sources to remove this + findRemoveSQLStatement("SELECT count(*) AS cnt FROM `"+tablename1+"` ").should.not.be.false; + + done() + }) + }); + }); + + describe('MODEL.count({})', function() { + + it('should not use prepared statements', function(done) { + + cDataTypes.count({ + sString : 'string-count-0' + }, function(err, result) { + should.not.exist(err); + should.exist(result); + + result.should.eql(1); + //@todo: Space between `tablename1` and WHRE, not needed, check sources to remove this + findRemoveSQLStatement("SELECT count(*) AS cnt FROM `"+tablename1+"` WHERE `sString` = ?").should.not.be.false; + + done() + }) + }); + }); + + describe('MODEL.count({()', function() { + var testingCounter = 0; + + before(function(done) { + cDataTypes.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename1+'`'); + cDataTypesRelation.destroyAll(function() { + findRemoveSQLStatement('DELETE FROM `'+tablename2+'`'); + done(); + }) + }); + }); + + beforeEach(function(done) { + // Insert dummy data + var total = 0; + for(var i = 0; i < testDataLength; i++, testingCounter++) { + cDataTypes.create({ + sString : 'string-rel-' + testingCounter, + nNumber : i, + tText : 'text-string-rel-' + testingCounter, + bBool : i % 2, + dDate : new Date(2012, 8, 1, 12, 0, testingCounter, 0) + }, function(err, row) { + + var totelRel = 0, + testRelLength = 5, + i; + + for(i = 0; i < testRelLength; i++) { + row.relation.build({ + sString : 'string-rel-relation-1-' + i, + nNumber : row.nNumber + 100, + tText : 'text-string-rel-relation-1-' + i, + bBool : (i % 2 === 0) ? 1 : 0, + dDate : new Date(2012, 8, 1, 12, 0, 0, 0) + }).save(function(err, relRow) { + if(err) { + throw err; + } + relIsDone(); + }); + } + + function relIsDone() { + if(++totelRel === testRelLength) { + isDone() + } + } + }); + } + + function isDone() { + if(++total === testDataLength) { + done() + } + } + }); + it('MODEL.relation() should use prepared statements', function(done) { + + cDataTypes.find(28, function(err, row) { + + row.relation(function(err, result) { + should.not.exist(err); + should.exist(result); + + result.should.be.lengthOf(5); + findRemoveSQLStatement("SELECT * FROM `"+tablename2+"` WHERE `relationId` = ?").should.not.be.false; + done() + }); + }) + + }); + + }); + }); +}); \ No newline at end of file