restored sails-mysql files for milliseconds support

parent 4b328302
/**
* Module Dependencies
*/
var mysql = require('mysql');
var _ = require('lodash');
var utils = require('./utils');
var sql = module.exports = {
// Convert mysql format to standard javascript object
normalizeSchema: function (schema) {
return _.reduce(schema, function(memo, field) {
// Marshal mysql DESCRIBE to waterline collection semantics
var attrName = field.Field;
var type = field.Type;
// Remove (n) column-size indicators
type = type.replace(/\([0-9]+\)$/,'');
memo[attrName] = {
type: type,
defaultsTo: field.Default,
autoIncrement: field.Extra === 'auto_increment'
};
if(field.primaryKey) {
memo[attrName].primaryKey = field.primaryKey;
}
if(field.unique) {
memo[attrName].unique = field.unique;
}
if(field.indexed) {
memo[attrName].indexed = field.indexed;
}
return memo;
}, {});
},
// @returns ALTER query for adding a column
addColumn: function (collectionName, attrName, attrDef) {
// Escape table name and attribute name
var tableName = mysql.escapeId(collectionName);
// sails.log.verbose("ADDING ",attrName, "with",attrDef);
// Build column definition
var columnDefinition = sql._schema(collectionName, attrDef, attrName);
return 'ALTER TABLE ' + tableName + ' ADD ' + columnDefinition;
},
// @returns ALTER query for dropping a column
removeColumn: function (collectionName, attrName) {
// Escape table name and attribute name
var tableName = mysql.escapeId(collectionName);
attrName = mysql.escapeId(attrName);
return 'ALTER TABLE ' + tableName + ' DROP COLUMN ' + attrName;
},
countQuery: function(collectionName, options, tableDefs){
var query = 'SELECT count(*) as count from `' + collectionName + '`';
return query += sql.serializeOptions(collectionName, options, tableDefs);
},
// Create a schema csv for a DDL query
schema: function(collectionName, attributes) {
return sql.build(collectionName, attributes, sql._schema);
},
_schema: function(collectionName, attribute, attrName) {
attrName = mysql.escapeId(attrName);
var type = sqlTypeCast(attribute);
// Process PK field
if(attribute.primaryKey) {
var columnDefinition = attrName + ' ' + type;
// If type is an integer, set auto increment
if(type === 'TINYINT' || type === 'SMALLINT' || type === 'INT' || type === 'BIGINT') {
return columnDefinition + ' UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY';
}
// Just set NOT NULL on other types
return columnDefinition + ' NOT NULL PRIMARY KEY';
}
// Process NOT NULL field.
// if notNull is true, set NOT NULL constraint
var nullPart = '';
if (attribute.notNull) {
nullPart = ' NOT NULL ';
}
// Process UNIQUE field
if(attribute.unique) {
return attrName + ' ' + type + nullPart + ' UNIQUE KEY';
}
// Process INDEX field (NON-UNIQUE KEY)
if(attribute.index) {
return attrName + ' ' + type + nullPart + ', INDEX(' + attrName + ')';
}
return attrName + ' ' + type + ' ' + nullPart;
},
// Create an attribute csv for a DQL query
attributes: function(collectionName, attributes) {
return sql.build(collectionName, attributes, sql.prepareAttribute);
},
// Create a value csv for a DQL query
// key => optional, overrides the keys in the dictionary
values: function(collectionName, values, key) {
return sql.build(collectionName, values, sql.prepareValue, ', ', key);
},
prepareCriterion: function(collectionName, value, key, parentKey) {
// Special sub-attr case
if (validSubAttrCriteria(value)) {
return sql.where(collectionName, value, null, key);
}
// Build escaped attr and value strings using either the key,
// or if one exists, the parent key
var attrStr, valueStr;
// Special comparator case
if (parentKey) {
attrStr = sql.prepareAttribute(collectionName, value, parentKey);
valueStr = sql.prepareValue(collectionName, value, parentKey);
// Why don't we strip you out of those bothersome apostrophes?
var nakedButClean = String(valueStr).replace(new RegExp('^\'+|\'+$', 'g'), '');
if (key === '<' || key === 'lessThan') {
return attrStr + '<' + valueStr;
}
else if (key === '<=' || key === 'lessThanOrEqual') {
return attrStr + '<=' + valueStr;
}
else if (key === '>' || key === 'greaterThan') {
return attrStr + '>' + valueStr;
}
else if (key === '>=' || key === 'greaterThanOrEqual') {
return attrStr + '>=' + valueStr;
}
else if (key === '!' || key === 'not') {
if (value === null) {
return attrStr + ' IS NOT NULL';
}
else if (_.isArray(value)) {
return attrStr + ' NOT IN(' + valueStr + ')';
}
else {
return attrStr + '<>' + valueStr;
}
}
else if (key === 'like') {
return attrStr + ' LIKE \'' + nakedButClean + '\'';
}
else if (key === 'contains') {
return attrStr + ' LIKE \'%' + nakedButClean + '%\'';
}
else if (key === 'startsWith') {
return attrStr + ' LIKE \'' + nakedButClean + '%\'';
}
else if (key === 'endsWith') {
return attrStr + ' LIKE \'%' + nakedButClean + '\'';
}
else {
throw new Error('Unknown comparator: ' + key);
}
} else {
attrStr = sql.prepareAttribute(collectionName, value, key);
valueStr = sql.prepareValue(collectionName, value, key);
// Special IS NULL case
if (_.isNull(value)) {
return attrStr + ' IS NULL';
} else {
return attrStr + '=' + valueStr;
}
}
},
prepareValue: function(collectionName, value, attrName) {
// Cast dates to SQL
if (_.isDate(value)) {
value = toSqlDate(value);
}
// Cast functions to strings
if (_.isFunction(value)) {
value = value.toString();
}
// Escape (also wraps in quotes)
return mysql.escape(value);
},
prepareAttribute: function(collectionName, value, attrName) {
return mysql.escapeId(collectionName) + '.' + mysql.escapeId(attrName);
},
// // Starting point for predicate evaluation
// // parentKey => if set, look for comparators and apply them to the parent key
where: function(collectionName, where, key, parentKey) {
return sql.build(collectionName, where, sql.predicate, ' AND ', undefined, parentKey);
},
// Recursively parse a predicate calculus and build a SQL query
predicate: function(collectionName, criterion, key, parentKey) {
var queryPart = '';
if (parentKey) {
return sql.prepareCriterion(collectionName, criterion, key, parentKey);
}
// OR
if (key.toLowerCase() === 'or') {
queryPart = sql.build(collectionName, criterion, sql.where, ' OR ');
return ' ( ' + queryPart + ' ) ';
}
// AND
else if (key.toLowerCase() === 'and') {
queryPart = sql.build(collectionName, criterion, sql.where, ' AND ');
return ' ( ' + queryPart + ' ) ';
}
// IN
else if (_.isArray(criterion)) {
queryPart = sql.prepareAttribute(collectionName, null, key) + ' IN (' + sql.values(collectionName, criterion, key) + ')';
return queryPart;
}
// LIKE
else if (key.toLowerCase() === 'like') {
return sql.build(collectionName, criterion, function(collectionName, value, attrName) {
var attrStr = sql.prepareAttribute(collectionName, value, attrName);
// TODO: Handle regexp criterias
if (_.isRegExp(value)) {
throw new Error('RegExp LIKE criterias not supported by the MySQLAdapter yet. Please contribute @ http://github.com/balderdashy/sails-mysql');
}
var valueStr = sql.prepareValue(collectionName, value, attrName);
// Handle escaped percent (%) signs [encoded as %%%]
valueStr = valueStr.replace(/%%%/g, '\\%');
return attrStr + ' LIKE ' + valueStr;
}, ' AND ');
}
// NOT
else if (key.toLowerCase() === 'not') {
throw new Error('NOT not supported yet!');
}
// Basic criteria item
else {
return sql.prepareCriterion(collectionName, criterion, key);
}
},
serializeOptions: function(collectionName, options, tableDefs) {
// Join clause
// allow the key to be named with join or joins
var joins = options.join || options.joins || [];
if (joins.length > 0) {
return this.buildJoinQuery(collectionName, joins, options, tableDefs);
}
return this.buildSingleQuery(collectionName, options, tableDefs);
},
/**
* Build Up a Select Statement Without Joins
*/
buildSingleQuery: function(collectionName, options, tableDefs) {
var queryPart = '';
if(options.where) {
queryPart += 'WHERE ' + sql.where(collectionName, options.where) + ' ';
}
if (options.groupBy) {
queryPart += 'GROUP BY ';
// Normalize to array
if(!_.isArray(options.groupBy)) {
options.groupBy = [options.groupBy];
}
_.each(options.groupBy, function(key) {
queryPart += key + ', ';
});
// Remove trailing comma
queryPart = queryPart.slice(0, -2) + ' ';
}
if (options.sort) {
queryPart += 'ORDER BY ';
// Sort through each sort attribute criteria
_.each(options.sort, function(direction, attrName) {
queryPart += sql.prepareAttribute(collectionName, null, attrName) + ' ';
// Basic MongoDB-style numeric sort direction
if (direction === 1) {
queryPart += 'ASC, ';
} else {
queryPart += 'DESC, ';
}
});
// Remove trailing comma
if(queryPart.slice(-2) === ', ') {
queryPart = queryPart.slice(0, -2) + ' ';
}
}
if (_.has(options, 'limit') && (options.limit !== null && options.limit !== undefined)) {
queryPart += 'LIMIT ' + options.limit + ' ';
}
if (_.has(options, 'skip') && (options.skip !== null && options.skip !== undefined)) {
// Some MySQL hackery here. For details, see:
// http://stackoverflow.com/questions/255517/mysql-offset-infinite-rows
if (!options.limit) {
queryPart += 'LIMIT 18446744073709551610 ';
}
queryPart += 'OFFSET ' + options.skip + ' ';
}
return queryPart;
},
// Put together the CSV aggregation
// separator => optional, defaults to ', '
// keyOverride => optional, overrides the keys in the dictionary
// (used for generating value lists in IN queries)
// parentKey => key of the parent to this object
build: function(collectionName, collection, fn, separator, keyOverride, parentKey) {
separator = separator || ', ';
var $sql = '';
_.each(collection, function(value, key) {
$sql += fn(collectionName, value, keyOverride || key, parentKey);
// (always append separator)
$sql += separator;
});
// (then remove final one)
return String($sql).replace(new RegExp(separator + '+$'), '');
}
};
// Cast waterline types into SQL data types
function sqlTypeCast(attr) {
var type;
var size;
var expandedType;
if(_.isObject(attr) && _.has(attr, 'type')) {
type = attr.type;
} else {
type = attr;
}
type = type && type.toLowerCase();
switch (type) {
case 'string': {
size = 255; // By default.
// If attr.size is positive integer, use it as size of varchar.
if(!Number.isNaN(attr.size) && (parseInt(attr.size) === parseFloat(attr.size)) && (parseInt(attr.size) > 0)) {
size = attr.size;
}
expandedType = 'VARCHAR(' + size + ')';
break;
}
case 'text':
case 'array':
case 'json':
expandedType = 'LONGTEXT';
break;
case 'mediumtext':
expandedType = 'mediumtext';
break;
case 'longtext':
expandedType = 'longtext';
break;
case 'boolean':
expandedType = 'BOOL';
break;
case 'int':
case 'integer': {
size = 32; // By default
if(!Number.isNaN(attr.size) && (parseInt(attr.size) === parseFloat(attr.size)) && (parseInt(size) > 0)) {
size = parseInt(attr.size);
}
// MEDIUMINT gets internally promoted to INT so there is no real benefit
// using it.
switch (size) {
case 8:
expandedType = 'TINYINT';
break;
case 16:
expandedType = 'SMALLINT';
break;
case 32:
expandedType = 'INT';
break;
case 64:
expandedType = 'BIGINT';
break;
default:
expandedType = 'INT';
break;
}
break;
}
case 'float':
case 'double':
expandedType = 'FLOAT';
break;
case 'decimal':
expandedType = 'DECIMAL';
break;
case 'date':
expandedType = 'DATE';
break;
case 'datetime':
expandedType = 'DATETIME(3)';
break;
case 'time':
expandedType = 'TIME';
break;
case 'binary':
expandedType = 'BLOB';
break;
default:
console.error('Unregistered type given: ' + type);
expandedType = 'LONGTEXT';
break;
}
return expandedType;
}
function wrapInQuotes(val) {
return '"' + val + '"';
}
function toSqlDate(date) {
date = date.getFullYear() + '-' +
('00' + (date.getMonth()+1)).slice(-2) + '-' +
('00' + date.getDate()).slice(-2) + ' ' +
('00' + date.getHours()).slice(-2) + ':' +
('00' + date.getMinutes()).slice(-2) + ':' +
('00' + date.getSeconds()).slice(-2) + '.' +
('00' + date.getMilliseconds()).slice(-3);
return date;
}
// Return whether this criteria is valid as an object inside of an attribute
function validSubAttrCriteria(c) {
return _.isObject(c) && (
!_.isUndefined(c.not) || !_.isUndefined(c.greaterThan) || !_.isUndefined(c.lessThan) ||
!_.isUndefined(c.greaterThanOrEqual) || !_.isUndefined(c.lessThanOrEqual) || !_.isUndefined(c['<']) ||
!_.isUndefined(c['<=']) || !_.isUndefined(c['!']) || !_.isUndefined(c['>']) || !_.isUndefined(c['>=']) ||
!_.isUndefined(c.startsWith) || !_.isUndefined(c.endsWith) || !_.isUndefined(c.contains) || !_.isUndefined(c.like));
}
/**
* Utility Functions
*/
// Dependencies
var mysql = require('mysql');
var _ = require('lodash');
var url = require('url');
// Module Exports
var utils = module.exports = {};
/**
* Parse URL string from config
*
* Parse URL string into connection config parameters
*/
utils.parseUrl = function (config) {
if(!_.isString(config.url)) {
return config;
}
var obj = url.parse(config.url);
config.host = obj.hostname || config.host;
config.port = obj.port || config.port;
if(_.isString(obj.pathname)) {
config.database = obj.pathname.split('/')[1] || config.database;
}
if(_.isString(obj.auth)) {
config.user = obj.auth.split(':')[0] || config.user;
config.password = obj.auth.split(':')[1] || config.password;
}
return config;
};
/**
* Prepare values
*
* Transform a JS date to SQL date and functions
* to strings.
*/
utils.prepareValue = function(value) {
if(_.isUndefined(value) || value === null) {
return value;
}
// Cast functions to strings
if (_.isFunction(value)) {
value = value.toString();
}
// Store Arrays and Objects as strings
if (_.isArray(value) || value.constructor && value.constructor.name === 'Object') {
try {
value = JSON.stringify(value);
} catch (e) {
// just keep the value and let the db handle an error
value = value;
}
}
// Cast dates to SQL
if (_.isDate(value)) {
value = utils.toSqlDate(value);
}
return mysql.escape(value);
};
/**
* Builds a Select statement determining if Aggeregate options are needed.
*/
utils.buildSelectStatement = function(criteria, table, schemaDefs) {
var query = '';
if(criteria.groupBy || criteria.sum || criteria.average || criteria.min || criteria.max) {
query = 'SELECT ';
// Append groupBy columns to select statement
if(criteria.groupBy) {
if(_.isArray(criteria.groupBy)) {
_.each(criteria.groupBy, function(opt){
query += opt + ', ';
});
} else {
query += criteria.groupBy + ', ';
}
}
// Handle SUM
if (criteria.sum) {
if(_.isArray(criteria.sum)) {
_.each(criteria.sum, function(opt){
query += 'SUM(' + opt + ') AS ' + opt + ', ';
});
} else {
query += 'SUM(' + criteria.sum + ') AS ' + criteria.sum + ', ';
}
}
// Handle AVG (casting to float to fix percision with trailing zeros)
if (criteria.average) {
if(_.isArray(criteria.average)) {
_.each(criteria.average, function(opt){
query += 'AVG(' + opt + ') AS ' + opt + ', ';
});
} else {
query += 'AVG(' + criteria.average + ') AS ' + criteria.average + ', ';
}
}
// Handle MAX
if (criteria.max) {
if(_.isArray(criteria.max)) {
_.each(criteria.max, function(opt){
query += 'MAX(' + opt + ') AS ' + opt + ', ';
});
} else {
query += 'MAX(' + criteria.max + ') AS ' + criteria.max + ', ';
}
}
// Handle MIN
if (criteria.min) {
if(_.isArray(criteria.min)) {
_.each(criteria.min, function(opt){
query += 'MIN(' + opt + ') AS ' + opt + ', ';
});
} else {
query += 'MIN(' + criteria.min + ') AS ' + criteria.min + ', ';
}
}
// trim trailing comma
query = query.slice(0, -2) + ' ';
// Add FROM clause
return query += 'FROM `' + table + '` ';
}
/**
* If no aggregate options lets just build a normal query
*/
// Add all keys to the select statement for this table
query += 'SELECT ';
var selectKeys = [],
joinSelectKeys = [];
if ( !schemaDefs[table] ) {
throw new Error('Schema definition missing for table: `'+table+'`');
}
_.each(schemaDefs[table], function(schemaDef, key) {
selectKeys.push({ table: table, key: key });
});
// Check for joins
if(criteria.joins || criteria.join) {
var joins = criteria.joins || criteria.join;
_.each(joins, function(join) {
if(!join.select) {
return;
}
_.each(_.keys(schemaDefs[join.child.toLowerCase()]), function(key) {
var _join = _.cloneDeep(join);
_join.key = key;
joinSelectKeys.push(_join);
});
// Remove the foreign key for this join from the selectKeys array
selectKeys = selectKeys.filter(function(select) {
var keep = true;
if(select.key === join.parentKey && join.removeParentKey) {
keep = false;
}
return keep;
});
});
}
// Add all the columns to be selected that are not joins
_.each(selectKeys, function(select) {
query += '`' + select.table + '`.`' + select.key + '`, ';
});
// Add all the columns from the joined tables
_.each(joinSelectKeys, function(select) {
// Create an alias by prepending the child table with the alias of the join
var alias = select.alias.toLowerCase() + '_' + select.child.toLowerCase();
// If this is a belongs_to relationship, keep the foreign key name from the AS part
// of the query. This will result in a selected column like: "user"."id" AS "user_id__id"
if(select.model) {
return query += mysql.escapeId(alias) + '.' + mysql.escapeId(select.key) + ' AS ' +
mysql.escapeId(select.parentKey + '__' + select.key) + ', ';
}
// If a junctionTable is used, the child value should be used in the AS part of the
// select query.
if(select.junctionTable) {
return query += mysql.escapeId(alias) + '.' + mysql.escapeId(select.key) + ' AS ' +
mysql.escapeId(select.alias + '__' + select.key) + ', ';
}
// Else if a hasMany attribute is being selected, use the alias plus the child
return query += mysql.escapeId(alias) + '.' + mysql.escapeId(select.key) + ' AS ' +
mysql.escapeId(select.alias + '__' + select.key) + ', ';
});
// Remove the last comma
query = query.slice(0, -2) + ' FROM `' + table + '` ';
return query;
};
utils.toSqlDate = function toSqlDate(date) {
date = date.getFullYear() + '-' +
('00' + (date.getMonth()+1)).slice(-2) + '-' +
('00' + date.getDate()).slice(-2) + ' ' +
('00' + date.getHours()).slice(-2) + ':' +
('00' + date.getMinutes()).slice(-2) + ':' +
('00' + date.getSeconds()).slice(-2) + "." +
('00' + date.getMilliseconds()).slice(-3);
return date;
};
......@@ -468,7 +468,7 @@ function sqlTypeCast(attr) {
break;
case 'datetime':
expandedType = 'DATETIME';
expandedType = 'DATETIME(3)';
break;
case 'time':
......@@ -499,7 +499,8 @@ function toSqlDate(date) {
('00' + date.getDate()).slice(-2) + ' ' +
('00' + date.getHours()).slice(-2) + ':' +
('00' + date.getMinutes()).slice(-2) + ':' +
('00' + date.getSeconds()).slice(-2);
('00' + date.getSeconds()).slice(-2) + '.' +
('00' + date.getMilliseconds()).slice(-3);
return date;
}
......
......@@ -242,7 +242,8 @@ utils.toSqlDate = function toSqlDate(date) {
('00' + date.getDate()).slice(-2) + ' ' +
('00' + date.getHours()).slice(-2) + ':' +
('00' + date.getMinutes()).slice(-2) + ':' +
('00' + date.getSeconds()).slice(-2);
('00' + date.getSeconds()).slice(-2) + "." +
('00' + date.getMilliseconds()).slice(-3);
return date;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment