diff --git a/.travis.yml b/.travis.yml index b9207e5..4689cb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,6 @@ language: node_js node_js: - - '0.8' - - '0.10' + - '4' + - '6' + - '8' + - '10' diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..b0f649a --- /dev/null +++ b/Changelog.md @@ -0,0 +1,19 @@ +### v0.1.28 - 13 March 2025 + +- Replace deprecated (in Node.js 22) `util.isDate(val)` with `val instanceof Date` + +### v0.1.27 - 27 Jun 2018 + +- Handle objects & symbols causing crash in driver 'escapeVal' (#54) + +### v0.1.26 - 16 May 2015 + +- Add support for SELECT TOP construct for mssql dialect for limit (#41) + +### v0.1.25 - 22 Mar 2015 + +- Added support for left/right joins (#22) + +### v0.1.24 - 23 Dec 2014 + +- Allow insertion of empty row using default values (#37) diff --git a/Readme.md b/Readme.md index 7bb37a2..f6b87e3 100644 --- a/Readme.md +++ b/Readme.md @@ -1,11 +1,11 @@ -## NodeJS SQL query builder +# NodeJS SQL query builder [![Build Status](https://secure.travis-ci.org/dresende/node-sql-query.png?branch=master)](http://travis-ci.org/dresende/node-sql-query) ## Install ```sh -npm install sql-query +npm install sql-query --save ``` ## Dialects @@ -17,4 +17,740 @@ npm install sql-query ## About -This module is used by [ORM](http://dresende.github.com/node-orm2) to build SQL queries in the different supported dialects. Sorry there is no API documentation but there are a couple of tests you can read and find out how to use it if you want. +This module is used by [ORM](http://dresende.github.com/node-orm2) to build SQL queries in the different supported dialects. +Sorry the API documentation is not complete. There are tests in ./test/integration that you can read. + + +# Usage +```js +var sql = require('sql-query'), + sqlQuery = sql.Query(); //for dialect: sql.Query('postgresql') +``` + +## Create +```js +var sqlCreate = sqlQuery.create(); + +sqlCreate + .table('table1') + .build() + +"CREATE TABLE 'table1'()" + + +sqlCreate + .table('table1') + .field('id','id') + .build() + +"CREATE TABLE 'table1'('id' INTEGER PRIMARY KEY AUTO_INCREMENT)" + + +sqlCreate + .table('table1') + .fields({id: 'id', a_text: 'text'}) + .build() + +"CREATE TABLE 'table1'('id' INTEGER PRIMARY KEY AUTO_INCREMENT,'a_text' TEXT)" + + +sqlCreate + .table('table1') + .fields({id: 'id', a_num: 'int'}) + .build() + +"CREATE TABLE 'table1'('id' INTEGER PRIMARY KEY AUTO_INCREMENT,'a_num' INTEGER)" + + +sqlCreate + .table('table1') + .fields({id: 'id', a_num: 'float'}) + .build() + +"CREATE TABLE 'table1'('id' INTEGER PRIMARY KEY AUTO_INCREMENT,'a_num' FLOAT(12,2))" + + +sqlCreate + .table('table1') + .fields({id: 'id', a_bool: 'bool'}) + .build() + +"CREATE TABLE 'table1'('id' INTEGER PRIMARY KEY AUTO_INCREMENT,'a_bool' TINYINT(1))" +``` + +## Select +```js +var sqlSelect = sqlQuery.select(); + +sqlSelect + .from('table1') + .build(); + +"SELECT * FROM `table1`" + + +sqlSelect + .from('table1') + .select('id', 'name') + .build(); + +"SELECT `id`, `name` FROM `table1`" + + +sqlSelect + .from('table1') + .select('id', 'name') + .as('label') + .build(); + +"SELECT `id`, `name` AS `label` FROM `table1`" + + +sqlSelect + .from('table1') + .select('id', 'name') + .select('title') + .as('label') + .build(); + +"SELECT `id`, `name`, `title` AS `label` FROM `table1`" + + +sqlSelect + .from('table1') + .select('id', 'name') + .as('label') + .select('title') + .build(); + +"SELECT `id`, `name` AS `label`, `title` FROM `table1`" + + +sqlSelect + .from('table1') + .select([ 'id', 'name' ]) + .build(); + +"SELECT `id`, `name` FROM `table1`" + + +sqlSelect + .from('table1') + .select() + .build(); + +"SELECT * FROM `table1`" + + +sqlSelect + .from('table1') + .select(['abc','def', { a: 'ghi', sql: 'SOMEFUNC(ghi)' }]) + .build(); + +"SELECT `abc`, `def`, (SOMEFUNC(ghi)) AS `ghi` FROM `table1`" + + +sqlSelect + .calculateFoundRows() + .from('table1') + .build(); + +"SELECT SQL_CALC_FOUND_ROWS * FROM `table1`" + + +sqlSelect + .calculateFoundRows() + .from('table1') + .select('id') + .build(); + +"SELECT SQL_CALC_FOUND_ROWS `id` FROM `table1`" + + +sqlSelect + .from('table1') + .select('id1', 'name') + .from('table2', 'id2', 'id1') + .select('id2') + .build(); + +"SELECT `t1`.`id1`, `t1`.`name`, `t2`.`id2` FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .select('id1') + .from('table2', 'id2', 'id1', { joinType: 'left inner' }) + .select('id2') + .build(); + +"SELECT `t1`.`id1`, `t2`.`id2` FROM `table1` `t1` LEFT INNER JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .select('id1', 'name') + .from('table2', 'id2', 'table1', 'id1') + .select('id2') + .build(); + +"SELECT `t1`.`id1`, `t1`.`name`, `t2`.`id2` FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .from('table2', 'id2', 'table1', 'id1') + .count() + .build(); + +"SELECT COUNT(*) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .from('table2', 'id2', 'table1', 'id1') + .count(null, 'c') + .build(); + +"SELECT COUNT(*) AS `c` FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .from('table2', 'id2', 'table1', 'id1') + .count('id') + .build(); + +"SELECT COUNT(`t2`.`id`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .count('id') + .from('table2', 'id2', 'table1', 'id1') + .count('id') + .build(); + +"SELECT COUNT(`t1`.`id`), COUNT(`t2`.`id`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .from('table2', 'id2', 'table1', 'id1') + .count('id') + .count('col') + .build(); + +"SELECT COUNT(`t2`.`id`), COUNT(`t2`.`col`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .from('table2', 'id2', 'table1', 'id1') + .fun('AVG', 'col') + .build(); + +"SELECT AVG(`t2`.`col`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" + + +sqlSelect + .from('table1') + .from('table2',['id2a', 'id2b'], 'table1', ['id1a', 'id1b']) + .count('id') + .build(); + +"SELECT COUNT(`t2`.`id`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2a` = `t1`.`id1a` AND `t2`.`id2b` = `t1`.`id1b`" +``` + +## Where +```js +var sqlSelect = sqlQuery.select(); + +sqlSelect + .from('table1') + .where() + .build(); + +"SELECT * FROM `table1`" + + +sqlSelect + .from('table1') + .where(null) + .build(); + +"SELECT * FROM `table1`" + + +sqlSelect + .from('table1') + .where({ col: 1 }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = 1" + + +sqlSelect + .from('table1') + .where({ col: 0 }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = 0" + + +sqlSelect + .from('table1') + .where({ col: null }) + .build(); + +"SELECT * FROM `table1` WHERE `col` IS NULL" + + +sqlSelect + .from('table1') + .where({ col: sql.eq(null) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` IS NULL" + + +sqlSelect + .from('table1') + .where({ col: sql.ne(null) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` IS NOT NULL" + + +sqlSelect + .from('table1') + .where({ col: undefined }) + .build(); + +"SELECT * FROM `table1` WHERE `col` IS NULL" + + +sqlSelect + .from('table1') + .where({ col: false }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = false" + + +sqlSelect + .from('table1') + .where({ col: "" }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = ''" + + +sqlSelect + .from('table1') + .where({ col: true }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = true" + + +sqlSelect + .from('table1') + .where({ col: 'a' }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = 'a'" + + +sqlSelect + .from('table1') + .where({ col: 'a\'' }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = 'a\\''" + + +sqlSelect + .from('table1') + .where({ col: [ 1, 2, 3 ] }) + .build(); + +"SELECT * FROM `table1` WHERE `col` IN (1, 2, 3)" + + +sqlSelect + .from('table1') + .where({ col: [] }) + .build(); + +"SELECT * FROM `table1` WHERE FALSE" + + +sqlSelect + .from('table1') + .where({ col1: 1, col2: 2 }) + .build(); + +"SELECT * FROM `table1` WHERE `col1` = 1 AND `col2` = 2" + + +sqlSelect + .from('table1') + .where({ col1: 1 }, { col2: 2 }) + .build(); + +"SELECT * FROM `table1` WHERE (`col1` = 1) AND (`col2` = 2)" + + +sqlSelect + .from('table1') + .where({ col: 1 }).where({ col: 2 }) + .build(); + +"SELECT * FROM `table1` WHERE (`col` = 1) AND (`col` = 2)" + + +sqlSelect + .from('table1') + .where({ col1: 1, col2: 2 }).where({ col3: 3 }) + .build(); + +"SELECT * FROM `table1` WHERE (`col1` = 1 AND `col2` = 2) AND (`col3` = 3)" + + +sqlSelect + .from('table1') + .from('table2', 'id', 'id') + .where('table1', { col: 1 }, 'table2', { col: 2 }) + .build(); + +"SELECT * FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id` = `t1`.`id` WHERE (`t1`.`col` = 1) AND (`t2`.`col` = 2)" + + +sqlSelect + .from('table1') + .from('table2', 'id', 'id') + .where('table1', { col: 1 }, { col: 2 }) + .build(); + +"SELECT * FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id` = `t1`.`id` WHERE (`t1`.`col` = 1) AND (`col` = 2)" + + +sqlSelect + .from('table1') + .where({ col: sql.gt(1) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` > 1" + + +sqlSelect + .from('table1') + .where({ col: sql.gte(1) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` >= 1" + + +sqlSelect + .from('table1') + .where({ col: sql.lt(1) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` < 1" + + +sqlSelect + .from('table1') + .where({ col: sql.lte(1) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` <= 1" + + +sqlSelect + .from('table1') + .where({ col: sql.eq(1) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` = 1" + + +sqlSelect + .from('table1') + .where({ col: sql.ne(1) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` <> 1" + + +sqlSelect + .from('table1') + .where({ col: sql.between('a', 'b') }) + .build(); + +"SELECT * FROM `table1` WHERE `col` BETWEEN 'a' AND 'b'" + + +sqlSelect + .from('table1') + .where({ col: sql.not_between('a', 'b') }) + .build(); + +"SELECT * FROM `table1` WHERE `col` NOT BETWEEN 'a' AND 'b'" + + +sqlSelect + .from('table1') + .where({ col: sql.like('abc') }) + .build(); + +"SELECT * FROM `table1` WHERE `col` LIKE 'abc'" + + +sqlSelect + .from('table1') + .where({ col: + sql.not_like('abc') }) + .build(); + +"SELECT * FROM `table1` WHERE `col` NOT LIKE 'abc'" + + +sqlSelect + .from('table1') + .where({ col: sql.not_in([ 1, 2, 3 ]) }) + .build(); + +"SELECT * FROM `table1` WHERE `col` NOT IN (1, 2, 3)" + + +sqlSelect + .from('table1') + .where({ __sql: [["LOWER(`stuff`) LIKE 'peaches'"]] }) + .build(); + +"SELECT * FROM `table1` WHERE LOWER(`stuff`) LIKE 'peaches'" + + +sqlSelect + .from('table1') + .where({ __sql: [["LOWER(`stuff`) LIKE ?", ['peaches']]] }) + .build(); + +"SELECT * FROM `table1` WHERE LOWER(`stuff`) LIKE 'peaches'" + + +sqlSelect + .from('table1') + .where({ __sql: [["LOWER(`stuff`) LIKE ? AND `number` > ?", ['peaches', 12]]] }) + .build(); + +"SELECT * FROM `table1` WHERE LOWER(`stuff`) LIKE 'peaches' AND `number` > 12" + + +sqlSelect + .from('table1') + .where({ __sql: [["LOWER(`stuff`) LIKE ? AND `number` == ?", ['peaches']]] }) + .build(); + +"SELECT * FROM `table1` WHERE LOWER(`stuff`) LIKE 'peaches' AND `number` == NULL" +``` + +## Order +```js +var sqlSelect = sqlQuery.select(); + +sqlSelect + .from('table1') + .order('col') + .build(); + +"SELECT * FROM `table1` ORDER BY `col` ASC" + + +sqlSelect + .from('table1') + .order('col', 'A') + .build(); + +"SELECT * FROM `table1` ORDER BY `col` ASC" + + +sqlSelect + .from('table1') + .order('col', 'Z') + .build(); + +"SELECT * FROM `table1` ORDER BY `col` DESC" + + +sqlSelect + .from('table1') + .order('col').order('col2', 'Z') + .build(); + +"SELECT * FROM `table1` ORDER BY `col` ASC, `col2` DESC" + + +sqlSelect + .from('table1') + .order('col', []) + .build(); + +"SELECT * FROM `table1` ORDER BY col" + + +sqlSelect + .from('table1') + .order('?? DESC', ['col']) + .build(); + +"SELECT * FROM `table1` ORDER BY `col` DESC" + + +sqlSelect + .from('table1') + .order('ST_Distance(??, ST_GeomFromText(?,4326))', ['geopoint', 'POINT(-68.3394 27.5578)']) + .build(); + +"SELECT * FROM `table1` ORDER BY ST_Distance(`geopoint`, ST_GeomFromText('POINT(-68.3394 27.5578)',4326))" +``` + +## Limit + +```js +var sqlSelect = sqlQuery.select(); + +sqlSelect + .from('table1') + .limit(123) + .build(); + +"SELECT * FROM `table1` LIMIT 123" + + +sqlSelect + .from('table1') + .limit('123456789') + .build(); + +"SELECT * FROM `table1` LIMIT 123456789" +``` + +## Select function + +```js +var sqlSelect = sqlQuery.select(); + +sqlSelect + .from('table1') + .fun('myfun', 'col1') + .build(); + +"SELECT MYFUN(`col1`) FROM `table1`" + + +sqlSelect + .from('table1') + .fun('myfun', [ 'col1', 'col2']) + .build(); + +"SELECT MYFUN(`col1`, `col2`) FROM `table1`" + + +sqlSelect + .from('table1') + .fun('dbo.fnBalance', [ 80, null, null], 'balance') + .build(); + +"SELECT DBO.FNBALANCE(80, NULL, NULL) AS `balance` FROM `table1`" + + +sqlSelect + .from('table1') + .fun('myfun', [ 'col1', 'col2'], 'alias') + .build(); + +"SELECT MYFUN(`col1`, `col2`) AS `alias` FROM `table1`" + + +sqlSelect + .from('table1') + .fun('myfun', [ 'col1', sqlQuery.Text('col2') ], 'alias') + .build(); + +"SELECT MYFUN(`col1`, 'col2') AS `alias` FROM `table1`" +``` + +## Insert + +```js +var sqlInsert = sqlQuery.insert(); + +sqlInsert + .into('table1') + .build(); + +"INSERT INTO `table1`" + + +sqlInsert + .into('table1') + .set({}) + .build(); + +"INSERT INTO `table1` VALUES()" + + +sqlInsert + .into('table1') + .set({ col: 1 }) + .build(); + +"INSERT INTO `table1` (`col`) VALUES (1)" + + +sqlInsert + .into('table1') + .set({ col1: 1, col2: 'a' }) + .build(); + +"INSERT INTO `table1` (`col1`, `col2`) VALUES (1, 'a')" +``` + +## Update + +```js +var sqlUpdate = sqlQuery.update() + +sqlUpdate + .into('table1') + .build(); + +"UPDATE `table1`" + + +sqlUpdate + .into('table1') + .set({ col: 1 }) + .build(); + +"UPDATE `table1` SET `col` = 1" + + +sqlUpdate + .into('table1') + .set({ col1: 1, col2: 2 }) + .build(); + +"UPDATE `table1` SET `col1` = 1, `col2` = 2" + + +sqlUpdate + .into('table1') + .set({ col1: 1, col2: 2 }).where({ id: 3 }) + .build(); + +"UPDATE `table1` SET `col1` = 1, `col2` = 2 WHERE `id` = 3" +``` diff --git a/lib/Create.js b/lib/Create.js index df2f4d8..1ffb028 100644 --- a/lib/Create.js +++ b/lib/Create.js @@ -7,16 +7,16 @@ exports.CreateQuery = CreateQuery; * @returns {string} */ var tmpl = function (template, args) { - var has = {}.hasOwnProperty; - for (var key in args) { - if (!has.call(args, key)) { - continue; - } - var token = '{{' + key + '}}'; - template = template.split(token).join(args[key]); - } + var has = {}.hasOwnProperty; + for (var key in args) { + if (!has.call(args, key)) { + continue; + } + var token = '{{' + key + '}}'; + template = template.split(token).join(args[key]); + } - return template; + return template; }; /** * Builds a list of fields according to the used dialect @@ -25,20 +25,20 @@ var tmpl = function (template, args) { * @returns {string} */ var buildFieldsList = function (dialect, structure) { - if (!structure) { - return ""; - } - var tpl = "'{{NAME}}' {{TYPE}}", fields = [], has = {}.hasOwnProperty; - for (var field in structure) { - if (has.call(structure, field)) { - fields.push(tmpl(tpl, { - NAME: field, - TYPE: dialect.DataTypes[structure[field]] - })); - } - } + if (!structure) { + return ""; + } + var tpl = "'{{NAME}}' {{TYPE}}", fields = [], has = {}.hasOwnProperty; + for (var field in structure) { + if (has.call(structure, field)) { + fields.push(tmpl(tpl, { + NAME: field, + TYPE: dialect.DataTypes[structure[field]] + })); + } + } - return fields.join(','); + return fields.join(','); }; /** @@ -49,64 +49,64 @@ var buildFieldsList = function (dialect, structure) { * @constructor */ function CreateQuery(Dialect, opts) { - var sql = {}; - var tableName = null; - var structure = {}; + var sql = {}; + var tableName = null; + var structure = {}; - return { - /** - * Set the table name - * @param table_name - * @returns {*} - */ - table: function (table_name) { - tableName = table_name; + return { + /** + * Set the table name + * @param table_name + * @returns {*} + */ + table: function (table_name) { + tableName = table_name; - return this; - }, - /** - * Add a field - * @param name - * @param type - * @returns {Object} - */ - field: function (name, type) { - structure[name] = type; + return this; + }, + /** + * Add a field + * @param name + * @param type + * @returns {Object} + */ + field: function (name, type) { + structure[name] = type; - return this; - }, - /** - * Set all the fields - * @param fields - * @returns {Object} - */ - fields: function (fields) { - if (!fields) { - return structure; - } - structure = fields; + return this; + }, + /** + * Set all the fields + * @param fields + * @returns {Object} + */ + fields: function (fields) { + if (!fields) { + return structure; + } + structure = fields; - return this; - }, + return this; + }, - /** - * Build a query from the passed params - * @returns {string} - */ - build: function () { - var query, fieldsList, template = "CREATE TABLE '{{TABLE_NAME}}'({{FIELDS_LIST}})"; + /** + * Build a query from the passed params + * @returns {string} + */ + build: function () { + var query, fieldsList, template = "CREATE TABLE '{{TABLE_NAME}}'({{FIELDS_LIST}})"; - if(!tableName){ - return ''; - } + if(!tableName){ + return ''; + } - fieldsList = buildFieldsList(Dialect, structure); + fieldsList = buildFieldsList(Dialect, structure); - return tmpl(template, { - TABLE_NAME: tableName, - FIELDS_LIST: fieldsList - }); + return tmpl(template, { + TABLE_NAME: tableName, + FIELDS_LIST: fieldsList + }); - } - }; + } + }; } diff --git a/lib/Dialects/mssql.js b/lib/Dialects/mssql.js index 9937d34..d5c4246 100644 --- a/lib/Dialects/mssql.js +++ b/lib/Dialects/mssql.js @@ -1,12 +1,11 @@ -var util = require("util"); var helpers = require("../Helpers"); exports.DataTypes = { - id: 'INT IDENTITY(1,1) NOT NULL PRIMARY KEY', - int: 'INT', - float: 'FLOAT', - bool: 'BIT', - text: 'TEXT' + id: 'INT IDENTITY(1,1) NOT NULL PRIMARY KEY', + int: 'INT', + float: 'FLOAT', + bool: 'BIT', + text: 'TEXT' }; @@ -25,12 +24,12 @@ exports.escapeId = function () { return exports.escapeVal(el.escapes.shift()); }); } - return "[" + el + "]"; + return "[" + el + "]"; }).join("."); }; exports.escapeVal = function (val, timeZone) { - if (val === undefined || val === null) { + if (val === undefined || val === null || typeof val === "symbol") { return 'NULL'; } @@ -41,7 +40,7 @@ exports.escapeVal = function (val, timeZone) { return "(" + val.map(exports.escapeVal.bind(this)).join(", ") + ")"; } - if (util.isDate(val)) { + if (val instanceof Date) { return "'" + helpers.dateToString(val, timeZone || "local", { dialect: 'mssql' }) + "'"; } @@ -60,7 +59,15 @@ exports.escapeVal = function (val, timeZone) { return val ? 1 : 0; case "function": return val(exports); + case "string": + break; + default: + val = JSON.stringify(val); } return "'" + val.replace(/\'/g, "''") + "'"; }; + +exports.defaultValuesStmt = "DEFAULT VALUES"; + +exports.limitAsTop = true; diff --git a/lib/Dialects/mysql.js b/lib/Dialects/mysql.js index 1d9afa9..7d8ec98 100644 --- a/lib/Dialects/mysql.js +++ b/lib/Dialects/mysql.js @@ -1,12 +1,11 @@ -var util = require("util"); var helpers = require("../Helpers"); exports.DataTypes = { - id: 'INTEGER PRIMARY KEY AUTO_INCREMENT', - int: 'INTEGER', - float: 'FLOAT(12,2)', - bool: 'TINYINT(1)', - text: 'TEXT' + id: 'INTEGER PRIMARY KEY AUTO_INCREMENT', + int: 'INTEGER', + float: 'FLOAT(12,2)', + bool: 'TINYINT(1)', + text: 'TEXT' }; @@ -30,7 +29,7 @@ exports.escapeId = function () { }; exports.escapeVal = function (val, timeZone) { - if (val === undefined || val === null) { + if (val === undefined || val === null || typeof val === "symbol") { return 'NULL'; } @@ -42,7 +41,7 @@ exports.escapeVal = function (val, timeZone) { return arrayToList(val, timeZone || "local"); } - if (util.isDate(val)) { + if (val instanceof Date) { val = helpers.dateToString(val, timeZone || "local", { dialect: 'mysql' }); } else { switch (typeof val) { @@ -113,3 +112,5 @@ function bufferToString(buffer) { return "X'" + hex+ "'"; } + +exports.defaultValuesStmt = "VALUES()"; diff --git a/lib/Dialects/postgresql.js b/lib/Dialects/postgresql.js index 5b40275..0a42d72 100644 --- a/lib/Dialects/postgresql.js +++ b/lib/Dialects/postgresql.js @@ -1,12 +1,11 @@ -var util = require("util"); var helpers = require("../Helpers"); exports.DataTypes = { - id: 'SERIAL PRIMARY KEY', - int: 'INTEGER', - float: 'REAL', - bool: 'SMALLINT', - text: 'TEXT' + id: 'SERIAL PRIMARY KEY', + int: 'INTEGER', + float: 'REAL', + bool: 'SMALLINT', + text: 'TEXT' }; @@ -30,7 +29,7 @@ exports.escapeId = function () { }; exports.escapeVal = function (val, timeZone) { - if (val === undefined || val === null) { + if (val === undefined || val === null || typeof val === "symbol") { return 'NULL'; } @@ -41,7 +40,7 @@ exports.escapeVal = function (val, timeZone) { return "(" + val.map(exports.escapeVal.bind(this)).join(", ") + ")"; } - if (util.isDate(val)) { + if (val instanceof Date) { return "'" + helpers.dateToString(val, timeZone || "local", { dialect: 'postgresql' }) + "'"; } @@ -60,8 +59,14 @@ exports.escapeVal = function (val, timeZone) { return val ? "true" : "false"; case "function": return val(exports); + case "string": + break; + default: + val = JSON.stringify(val); } // No need to escape backslashes with default PostgreSQL 9.1+ config. // Google 'postgresql standard_conforming_strings' for details. return "'" + val.replace(/\'/g, "''") + "'"; }; + +exports.defaultValuesStmt = "DEFAULT VALUES"; diff --git a/lib/Dialects/sqlite.js b/lib/Dialects/sqlite.js index f05af53..4ec0a34 100644 --- a/lib/Dialects/sqlite.js +++ b/lib/Dialects/sqlite.js @@ -1,14 +1,12 @@ -var util = require("util"); var helpers = require("../Helpers"); - exports.DataTypes = { - isSQLITE: true, - id: 'INTEGER PRIMARY KEY AUTOINCREMENT', - int: 'INTEGER', - float: 'FLOAT(12,2)', - bool: 'TINYINT(1)', - text: 'TEXT' + isSQLITE: true, + id: 'INTEGER PRIMARY KEY AUTOINCREMENT', + int: 'INTEGER', + float: 'FLOAT(12,2)', + bool: 'TINYINT(1)', + text: 'TEXT' }; exports.escape = function (query, args) { @@ -18,7 +16,7 @@ exports.escape = function (query, args) { exports.escapeId = require("./mysql").escapeId; exports.escapeVal = function (val, timeZone) { - if (val === undefined || val === null) { + if (val === undefined || val === null || typeof val === "symbol") { return 'NULL'; } @@ -29,7 +27,7 @@ exports.escapeVal = function (val, timeZone) { return "(" + val.map(exports.escapeVal.bind(this)).join(", ") + ")"; } - if (util.isDate(val)) { + if (val instanceof Date) { return "'" + helpers.dateToString(val, timeZone || "local", { dialect: 'sqlite' }) + "'"; } @@ -48,9 +46,15 @@ exports.escapeVal = function (val, timeZone) { return val ? 1 : 0; case "function": return val(exports); + case "string": + break; + default: + val = JSON.stringify(val); } // No need to escape backslashes with default PostgreSQL 9.1+ config. // Google 'postgresql standard_conforming_strings' for details. return "'" + val.replace(/\'/g, "''") + "'"; }; + +exports.defaultValuesStmt = "DEFAULT VALUES"; diff --git a/lib/Helpers.js b/lib/Helpers.js index 6cd10ad..561c6fd 100644 --- a/lib/Helpers.js +++ b/lib/Helpers.js @@ -16,49 +16,49 @@ module.exports.escapeQuery = function (Dialect, query, args) { } module.exports.dateToString = function (date, timeZone, opts) { - var dt = new Date(date); + var dt = new Date(date); - if (timeZone != 'local') { - var tz = convertTimezone(timeZone); + if (timeZone != 'local') { + var tz = convertTimezone(timeZone); - dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000)); - if (tz !== false) { - dt.setTime(dt.getTime() + (tz * 60000)); - } - } + dt.setTime(dt.getTime() + (dt.getTimezoneOffset() * 60000)); + if (tz !== false) { + dt.setTime(dt.getTime() + (tz * 60000)); + } + } - var year = dt.getFullYear(); - var month = zeroPad(dt.getMonth() + 1); - var day = zeroPad(dt.getDate()); - var hour = zeroPad(dt.getHours()); - var minute = zeroPad(dt.getMinutes()); - var second = zeroPad(dt.getSeconds()); - var milli = zeroPad(dt.getMilliseconds(), 3); + var year = dt.getFullYear(); + var month = zeroPad(dt.getMonth() + 1); + var day = zeroPad(dt.getDate()); + var hour = zeroPad(dt.getHours()); + var minute = zeroPad(dt.getMinutes()); + var second = zeroPad(dt.getSeconds()); + var milli = zeroPad(dt.getMilliseconds(), 3); - if (opts.dialect == 'mysql') { - return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second + '.' + milli; - } else { - return year + '-' + month + '-' + day + 'T' + hour + ':' + minute + ':' + second + '.' + milli + 'Z'; - } + if (opts.dialect == 'mysql' || timeZone == 'local') { + return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second + '.' + milli; + } else { + return year + '-' + month + '-' + day + 'T' + hour + ':' + minute + ':' + second + '.' + milli + 'Z'; + } } function zeroPad(number, n) { - if (arguments.length == 1) n = 2; + if (arguments.length == 1) n = 2; - number = "" + number; + number = "" + number; - while (number.length < n) { - number = "0" + number; - } - return number; + while (number.length < n) { + number = "0" + number; + } + return number; } function convertTimezone(tz) { - if (tz == "Z") return 0; + if (tz == "Z") return 0; - var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/); - if (m) { - return (m[1] == '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60; - } - return false; + var m = tz.match(/([\+\-\s])(\d\d):?(\d\d)?/); + if (m) { + return (m[1] == '-' ? -1 : 1) * (parseInt(m[2], 10) + ((m[3] ? parseInt(m[3], 10) : 0) / 60)) * 60; + } + return false; } diff --git a/lib/Insert.js b/lib/Insert.js index 6d24e99..cdda986 100644 --- a/lib/Insert.js +++ b/lib/Insert.js @@ -25,8 +25,12 @@ function InsertQuery(Dialect, opts) { cols.push(Dialect.escapeId(k)); vals.push(Dialect.escapeVal(sql.set[k], opts.timezone)); } - query.push("(" + cols.join(", ") + ")"); - query.push("VALUES (" + vals.join(", ") + ")"); + if (cols.length == 0) { + query.push(Dialect.defaultValuesStmt); + } else { + query.push("(" + cols.join(", ") + ")"); + query.push("VALUES (" + vals.join(", ") + ")"); + } } return query.join(" "); diff --git a/lib/Query.js b/lib/Query.js index bb66cda..f8e9b6d 100644 --- a/lib/Query.js +++ b/lib/Query.js @@ -27,9 +27,9 @@ function Query(opts) { escape : Helpers.escapeQuery.bind(Helpers, Dialect), escapeId : Dialect.escapeId.bind(Dialect), escapeVal : Dialect.escapeVal.bind(Dialect), - create: function(){ - return new CreateQuery(Dialect, opts); - }, + create: function(){ + return new CreateQuery(Dialect, opts); + }, select: function () { return new SelectQuery(Dialect, opts); }, diff --git a/lib/Remove.js b/lib/Remove.js index 6fe367b..cc4aa55 100644 --- a/lib/Remove.js +++ b/lib/Remove.js @@ -25,7 +25,12 @@ function RemoveQuery(Dialect, opts) { build: function () { var query = [], tmp; - query.push("DELETE FROM"); + // limit as: SELECT TOP n (MSSQL only) + if (Dialect.limitAsTop && sql.hasOwnProperty("limit")) { + query.push("DELETE TOP " + sql.limit + " FROM"); + } else { + query.push("DELETE FROM"); + } query.push(Dialect.escapeId(sql.table)); query = query.concat(Where.build(Dialect, sql.where, opts)); @@ -46,15 +51,17 @@ function RemoveQuery(Dialect, opts) { } } - // limit - if (sql.hasOwnProperty("limit")) { - if (sql.hasOwnProperty("offset")) { - query.push("LIMIT " + sql.limit + " OFFSET " + sql.offset); - } else { - query.push("LIMIT " + sql.limit); + // limit for all Dialects but MSSQL + if (!Dialect.limitAsTop) { + if (sql.hasOwnProperty("limit")) { + if (sql.hasOwnProperty("offset")) { + query.push("LIMIT " + sql.limit + " OFFSET " + sql.offset); + } else { + query.push("LIMIT " + sql.limit); + } + } else if (sql.hasOwnProperty("offset")) { + query.push("OFFSET " + sql.offset); } - } else if (sql.hasOwnProperty("offset")) { - query.push("OFFSET " + sql.offset); } return query.join(" "); diff --git a/lib/Select.js b/lib/Select.js index a414ecb..a06ef03 100644 --- a/lib/Select.js +++ b/lib/Select.js @@ -92,7 +92,7 @@ function SelectQuery(Dialect, opts) { fun_stack = []; return this; }, - from: function (table, from_id, to_table, to_id) { + from: function (table, from_id, to_table, to_id, fromOpts) { var from = { t: table, // table a: "t" + (sql.from.length + 1) // alias @@ -104,25 +104,32 @@ function SelectQuery(Dialect, opts) { } var a, f = from_id, t; - if (arguments.length == 3) { - a = sql.from[sql.from.length - 1].a; - t = to_table; + var args = Array.prototype.slice.call(arguments); + var last = args[args.length - 1]; + + if (typeof last == 'object' && !Array.isArray(last)) { + from.opts = args.pop(); + } + + if (args.length == 3) { + a = sql.from[sql.from.length - 1].a; + t = to_table; } else { - a = get_table_alias(to_table); - t = to_id; + a = get_table_alias(to_table); + t = to_id; } from.j = []; if (f.length && t.length) { - if (Array.isArray(f) && Array.isArray(t) && f.length == t.length) { - for (i = 0; i < f.length; i++) { - from.j.push([f[i], a, t[i]]); - } - } else { - from.j.push([f, a, t]); - } + if (Array.isArray(f) && Array.isArray(t) && f.length == t.length) { + for (i = 0; i < f.length; i++) { + from.j.push([f[i], a, t[i]]); + } + } else { + from.j.push([f, a, t]); + } } else { - throw new Error(); + throw new Error(); } sql.from.push(from); @@ -194,7 +201,7 @@ function SelectQuery(Dialect, opts) { return this; }, build: function () { - var query = [], tmp, i, ii, j, ord, str, select_all = true; + var query = [], tmp, from, i, ii, j, ord, str, select_all = true; var having = []; if (fun_stack.length) { @@ -203,6 +210,11 @@ function SelectQuery(Dialect, opts) { query.push("SELECT"); + // limit as: SELECT TOP n (MSSQL only) + if (Dialect.limitAsTop && sql.hasOwnProperty("limit")) { + query.push("TOP " + sql.limit); + } + for (i = 0; i < sql.from.length; i++) { sql.from[i].a = "t" + (i + 1); } @@ -318,30 +330,35 @@ function SelectQuery(Dialect, opts) { } for (i = 0; i < sql.from.length; i++) { + from = sql.from[i]; + if (i > 0) { + if (from.opts && from.opts.joinType) { + query.push(from.opts.joinType.toUpperCase()); + } query.push("JOIN"); } if (sql.from.length == 1 && !sql.where_exists) { - query.push(Dialect.escapeId(sql.from[i].t)); + query.push(Dialect.escapeId(from.t)); } else { - query.push(Dialect.escapeId(sql.from[i].t) + " " + Dialect.escapeId(sql.from[i].a)); + query.push(Dialect.escapeId(from.t) + " " + Dialect.escapeId(from.a)); } if (i > 0) { - query.push("ON"); - - for (ii = 0; ii < sql.from[i].j.length; ii++) { - if (ii > 0) { - query.push("AND"); - } - query.push( - Dialect.escapeId(sql.from[i].a, sql.from[i].j[ii][0]) + - " = " + - Dialect.escapeId(sql.from[i].j[ii][1], sql.from[i].j[ii][2]) - ); - } + query.push("ON"); + + for (ii = 0; ii < from.j.length; ii++) { + if (ii > 0) { + query.push("AND"); + } + query.push( + Dialect.escapeId(from.a, from.j[ii][0]) + + " = " + + Dialect.escapeId(from.j[ii][1], from.j[ii][2]) + ); + } if (i < sql.from.length - 1) { - query.push(")"); + query.push(")"); } } } @@ -387,15 +404,17 @@ function SelectQuery(Dialect, opts) { } } - // limit - if (sql.hasOwnProperty("limit")) { - if (sql.hasOwnProperty("offset")) { - query.push("LIMIT " + sql.limit + " OFFSET " + sql.offset); - } else { - query.push("LIMIT " + sql.limit); + // limit for all Dialects but MSSQL + if (!Dialect.limitAsTop) { + if (sql.hasOwnProperty("limit")) { + if (sql.hasOwnProperty("offset")) { + query.push("LIMIT " + sql.limit + " OFFSET " + sql.offset); + } else { + query.push("LIMIT " + sql.limit); + } + } else if (sql.hasOwnProperty("offset")) { + query.push("OFFSET " + sql.offset); } - } else if (sql.hasOwnProperty("offset")) { - query.push("OFFSET " + sql.offset); } return query.join(" "); diff --git a/lib/Where.js b/lib/Where.js index 513591a..88b45af 100644 --- a/lib/Where.js +++ b/lib/Where.js @@ -26,25 +26,25 @@ exports.build = function (Dialect, where, opts) { function buildOrGroup(Dialect, where, opts) { opts = opts || {}; - + if (where.e) { - // EXISTS + // EXISTS - wheres = []; - if(Array.isArray(where.e.l[0]) && Array.isArray(where.e.l[1])) { - for (i = 0; i < where.e.l[0].length; i++) { - wheres.push(Dialect.escapeId(where.e.l[0][i]) + " = " + Dialect.escapeId(where.e.tl, where.e.l[1][i])); - } - } else { - wheres.push(Dialect.escapeId(where.e.l[0]) + " = " + Dialect.escapeId(where.e.tl, where.e.l[1])); - } + wheres = []; + if(Array.isArray(where.e.l[0]) && Array.isArray(where.e.l[1])) { + for (i = 0; i < where.e.l[0].length; i++) { + wheres.push(Dialect.escapeId(where.e.l[0][i]) + " = " + Dialect.escapeId(where.e.tl, where.e.l[1][i])); + } + } else { + wheres.push(Dialect.escapeId(where.e.l[0]) + " = " + Dialect.escapeId(where.e.tl, where.e.l[1])); + } return [ "EXISTS (" + "SELECT * FROM " + Dialect.escapeId(where.e.t) + " " + "WHERE " + wheres.join(" AND ") + " " + "AND " + buildOrGroup(Dialect, { t: null, w: where.w }, opts) + - ")" + ")" ]; } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..27767b0 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,20 @@ +{ + "name": "sql-query", + "version": "0.1.28", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "urun": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/urun/-/urun-0.0.8.tgz", + "integrity": "sha1-N5mgKTcUwRFdMmpOaeKl+W7nhuI=", + "dev": true + }, + "utest": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/utest/-/utest-0.0.8.tgz", + "integrity": "sha1-/AlFH+aXuQCNDEMv4NtDnWzzeRQ=", + "dev": true + } + } +} diff --git a/package.json b/package.json index 28b1093..0d398d5 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,20 @@ "keywords": [ "sql", "query" - ], - "version": "0.1.23", + ], + "version": "0.1.28", "license": "MIT", "repository": { "url": "http://github.com/dresende/node-sql-query" }, "contributors": [ - { "name" : "Benjamin Pannell", "email" : "admin@sierrasoftworks.com" }, - { "name" : "Arek W" } + { + "name": "Benjamin Pannell", + "email": "admin@sierrasoftworks.com" + }, + { + "name": "Arek W" + } ], "scripts": { "test": "make" @@ -24,7 +29,7 @@ }, "analyse": false, "devDependencies": { - "utest": "0.0.6", - "urun": "0.0.6" + "utest": "0.0.8", + "urun": "0.0.8" } } diff --git a/test/integration/test-dialect-mssql.js b/test/integration/test-dialect-mssql.js index db0d0f5..71e792b 100644 --- a/test/integration/test-dialect-mssql.js +++ b/test/integration/test-dialect-mssql.js @@ -69,9 +69,19 @@ assert.equal( "0" ); +assert.equal( + dialect.escapeVal({ key: 'value' }), + "'{\"key\":\"value\"}'" +); + +assert.equal( + dialect.escapeVal(Symbol("why does this exist")), + "NULL" +) + assert.equal( dialect.escapeVal(new Date(d.getTime() + tzOffsetMillis)), - "'2013-09-04T19:15:11.133Z'" + "'2013-09-04 19:15:11.133'" ); assert.equal( @@ -93,3 +103,14 @@ assert.equal( dialect.escapeVal(new Date(d.getTime())), dialect.escapeVal(new Date(d.getTime()), 'local') ); + +assert.equal( + dialect.defaultValuesStmt, + "DEFAULT VALUES" +); + +//Assert that mssql is configured to use the SELECT TOP as a contruct for limit +assert.equal( + dialect.limitAsTop, + true +); diff --git a/test/integration/test-dialect-mysql.js b/test/integration/test-dialect-mysql.js index 62653b7..e689c58 100644 --- a/test/integration/test-dialect-mysql.js +++ b/test/integration/test-dialect-mysql.js @@ -69,6 +69,16 @@ assert.equal( "false" ); +assert.equal( + dialect.escapeVal({ key: 'value' }), + "`key` = 'value'" +); + +assert.equal( + dialect.escapeVal(Symbol("why does this exist")), + "NULL" +) + assert.equal( dialect.escapeVal(new Date(d.getTime() + tzOffsetMillis)), "'2013-09-04 19:15:11.133'" @@ -93,3 +103,14 @@ assert.equal( dialect.escapeVal(new Date(d.getTime())), dialect.escapeVal(new Date(d.getTime()), 'local') ); + +assert.equal( + dialect.defaultValuesStmt, + "VALUES()" +); + +//For all dialects but mssql limitAsTop should be undefined or false +assert.equal( + dialect.limitAsTop || false, + false +); \ No newline at end of file diff --git a/test/integration/test-dialect-postgresql.js b/test/integration/test-dialect-postgresql.js index c8a2e77..55c9fac 100644 --- a/test/integration/test-dialect-postgresql.js +++ b/test/integration/test-dialect-postgresql.js @@ -74,9 +74,19 @@ assert.equal( "false" ); +assert.equal( + dialect.escapeVal({ key: 'value' }), + "'{\"key\":\"value\"}'" +); + +assert.equal( + dialect.escapeVal(Symbol("why does this exist")), + "NULL" +) + assert.equal( dialect.escapeVal(new Date(d.getTime() + tzOffsetMillis)), - "'2013-09-04T19:15:11.133Z'" + "'2013-09-04 19:15:11.133'" ); assert.equal( @@ -98,3 +108,14 @@ assert.equal( dialect.escapeVal(new Date(d.getTime())), dialect.escapeVal(new Date(d.getTime()), 'local') ); + +assert.equal( + dialect.defaultValuesStmt, + "DEFAULT VALUES" +); + +//For all dialects but mssql limitAsTop should be undefined or false +assert.equal( + dialect.limitAsTop || false, + false +); diff --git a/test/integration/test-dialect-sqlite.js b/test/integration/test-dialect-sqlite.js index 3f45662..c4a3d6a 100644 --- a/test/integration/test-dialect-sqlite.js +++ b/test/integration/test-dialect-sqlite.js @@ -69,9 +69,19 @@ assert.equal( "0" ); +assert.equal( + dialect.escapeVal({ key: 'value' }), + "'{\"key\":\"value\"}'" +); + +assert.equal( + dialect.escapeVal(Symbol("why does this exist")), + "NULL" +) + assert.equal( dialect.escapeVal(new Date(d.getTime() + tzOffsetMillis)), - "'2013-09-04T19:15:11.133Z'" + "'2013-09-04 19:15:11.133'" ); assert.equal( @@ -93,3 +103,14 @@ assert.equal( dialect.escapeVal(new Date(d.getTime())), dialect.escapeVal(new Date(d.getTime()), 'local') ); + +assert.equal( + dialect.defaultValuesStmt, + "DEFAULT VALUES" +); + +//For all dialects but mssql limitAsTop should be undefined or false +assert.equal( + dialect.limitAsTop || false, + false +); diff --git a/test/integration/test-insert.js b/test/integration/test-insert.js index 7b2171c..3c272bf 100644 --- a/test/integration/test-insert.js +++ b/test/integration/test-insert.js @@ -6,6 +6,11 @@ assert.equal( "INSERT INTO `table1`" ); +assert.equal( + common.Insert().into('table1').set({}).build(), + "INSERT INTO `table1` VALUES()" +); + assert.equal( common.Insert().into('table1').set({ col: 1 }).build(), "INSERT INTO `table1` (`col`) VALUES (1)" diff --git a/test/integration/test-select.js b/test/integration/test-select.js index 7b8b81e..5b9b3dd 100644 --- a/test/integration/test-select.js +++ b/test/integration/test-select.js @@ -59,6 +59,13 @@ assert.equal( "SELECT `t1`.`id1`, `t1`.`name`, `t2`.`id2` FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" ); +assert.equal( + common.Select().from('table1').select('id1') + .from('table2', 'id2', 'id1', { joinType: 'left inner' }).select('id2').build(), + "SELECT `t1`.`id1`, `t2`.`id2` FROM `table1` `t1` LEFT INNER JOIN `table2` `t2` ON `t2`.`id2` = `t1`.`id1`" +) + + assert.equal( common.Select().from('table1').select('id1', 'name') .from('table2', 'id2', 'table1', 'id1').select('id2').build(), @@ -102,7 +109,7 @@ assert.equal( ); assert.equal( - common.Select().from('table1') - .from('table2',['id2a', 'id2b'], 'table1', ['id1a', 'id1b']).count('id').build(), - "SELECT COUNT(`t2`.`id`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2a` = `t1`.`id1a` AND `t2`.`id2b` = `t1`.`id1b`" + common.Select().from('table1') + .from('table2',['id2a', 'id2b'], 'table1', ['id1a', 'id1b']).count('id').build(), + "SELECT COUNT(`t2`.`id`) FROM `table1` `t1` JOIN `table2` `t2` ON `t2`.`id2a` = `t1`.`id1a` AND `t2`.`id2b` = `t1`.`id1b`" );