diff --git a/gulpfile.js b/gulpfile.js index 5e6064f..36b6912 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,7 +18,7 @@ gulp.task('coverage', function(callback) { .pipe(istanbul()) .pipe(istanbul.hookRequire()) .on('finish', function() { - gulp.src(['./tests/*.js'], {read: false}) + gulp.src(['./tests/**/*.js'], {read: false}) .pipe(mocha({ reporter: 'spec', bail: true diff --git a/lib/dialects/base/blocks.js b/lib/dialects/base/blocks.js index 6b29b1c..5421138 100644 --- a/lib/dialects/base/blocks.js +++ b/lib/dialects/base/blocks.js @@ -333,10 +333,11 @@ module.exports = function(dialect) { dialect.blocks.add('insert:values', function(params) { var values = params.values; + var fields = params.fields; if (!_.isArray(values)) values = [values]; - var fields = params.fields || _(values) + fields = fields || _(values) .chain() .map(function(row) { return _(row).keys(); @@ -345,13 +346,15 @@ module.exports = function(dialect) { .uniq() .value(); + values = _(values).map(function(row) { + return _(fields).map(function(field) { + return dialect.buildBlock('value', {value: row[field]}); + }); + }); + return dialect.buildTemplate('insertValues', { - fields: fields, - values: _(values).map(function(row) { - return _(fields).map(function(field) { - return dialect.buildBlock('value', {value: row[field]}); - }); - }) + values: values, + fields: fields }); }); diff --git a/lib/dialects/base/index.js b/lib/dialects/base/index.js index 5518974..8cb38ab 100644 --- a/lib/dialects/base/index.js +++ b/lib/dialects/base/index.js @@ -347,7 +347,8 @@ Dialect.prototype.buildTemplate = function(type, params) { return ''; } else { if (self.blocks.has(type + ':' + block)) block = type + ':' + block; - return self.buildBlock(block, params) + space; + var result = self.buildBlock(block, params); + return result === '' ? result : result + space; } }).trim(); }; diff --git a/lib/dialects/mssql/blocks.js b/lib/dialects/mssql/blocks.js new file mode 100644 index 0000000..afcf3b4 --- /dev/null +++ b/lib/dialects/mssql/blocks.js @@ -0,0 +1,79 @@ +'use strict'; + +var _ = require('underscore'); + +module.exports = function(dialect) { + var parentValueBlock = dialect.blocks.get('value'); + + dialect.blocks.set('value', function(params) { + var value = params.value; + + var result; + if (_.isBoolean(value)) { + result = String(Number(value)); + } else { + result = parentValueBlock(params); + } + + return result; + }); + + dialect.blocks.set('limit', function(params) { + var result = ''; + if (_.isUndefined(params.offset)) { + result = 'top(' + dialect.builder._pushValue(params.limit) + ')'; + } + return result; + }); + + dialect.blocks.set('offset', function(params) { + var prefix = (!params.sort) ? 'order by 1 ' : ''; + var offset = 'offset ' + dialect.builder._pushValue(params.offset) + ' '; + var limit = ''; + var suffix = 'rows'; + + if (params.limit) { + limit = 'rows fetch next ' + dialect.builder._pushValue(params.limit) + ' '; + suffix = 'rows only'; + } + + return prefix + offset + limit + suffix; + }); + + dialect.blocks.set('returning', function(params) { + var result = dialect.buildBlock('fields', {fields: params.returning}); + + if (result) result = 'output ' + result; + + return result; + }); + + dialect.blocks.set('insert:values', function(params) { + var values = params.values; + var fields = params.fields; + var returning = params.returning; + + if (!_.isArray(values)) values = [values]; + + fields = fields || _(values) + .chain() + .map(function(row) { + return _(row).keys(); + }) + .flatten() + .uniq() + .value(); + + values = _(values).map(function(row) { + return _(fields).map(function(field) { + return dialect.buildBlock('value', {value: row[field]}); + }); + }); + + return dialect.buildTemplate('insertValues', { + values: values, + fields: fields, + returning: returning + }); + }); +}; diff --git a/lib/dialects/mssql/index.js b/lib/dialects/mssql/index.js index 8374184..4675e9b 100644 --- a/lib/dialects/mssql/index.js +++ b/lib/dialects/mssql/index.js @@ -3,11 +3,22 @@ var BaseDialect = require('../base'); var _ = require('underscore'); var util = require('util'); +var templatesInit = require('./templates'); +var blocksInit = require('./blocks'); var Dialect = module.exports = function(builder) { BaseDialect.call(this, builder); + + // init templates + templatesInit(this); + + // init blocks + blocksInit(this); }; util.inherits(Dialect, BaseDialect); -Dialect.prototype.config = _({}).extend(BaseDialect.prototype.config, {}); +Dialect.prototype.config = _({}).extend(BaseDialect.prototype.config, { + identifierPrefix: '[', + identifierSuffix: ']' +}); diff --git a/lib/dialects/mssql/templates.js b/lib/dialects/mssql/templates.js new file mode 100644 index 0000000..0741576 --- /dev/null +++ b/lib/dialects/mssql/templates.js @@ -0,0 +1,130 @@ +'use strict'; + +var templateChecks = require('../../utils/templateChecks'); +var orRegExp = /^(rollback|abort|replace|fail|ignore)$/i; + +module.exports = function(dialect) { + dialect.templates.set('select', { + pattern: '{with} {withRecursive} select {limit} {distinct} {fields} ' + + 'from {from} {table} {query} {select} {expression} {alias} ' + + '{join} {condition} {group} {having} {sort} {offset}', + defaults: { + fields: {} + }, + validate: function(type, params) { + templateChecks.onlyOneOfProps(type, params, ['with', 'withRecursive']); + templateChecks.propType(type, params, 'with', 'object'); + templateChecks.propType(type, params, 'withRecursive', 'object'); + + templateChecks.propType(type, params, 'distinct', 'boolean'); + + templateChecks.propType(type, params, 'fields', ['array', 'object']); + + templateChecks.propType(type, params, 'from', ['string', 'array', 'object']); + + templateChecks.atLeastOneOfProps(type, params, ['table', 'query', 'select', 'expression']); + templateChecks.onlyOneOfProps(type, params, ['table', 'query', 'select', 'expression']); + + templateChecks.propType(type, params, 'table', 'string'); + templateChecks.propType(type, params, 'query', 'object'); + templateChecks.propType(type, params, 'select', 'object'); + templateChecks.propType(type, params, 'expression', ['string', 'object']); + + templateChecks.propType(type, params, 'alias', ['string', 'object']); + + templateChecks.propType(type, params, 'join', ['array', 'object']); + + templateChecks.propType(type, params, 'condition', ['array', 'object']); + templateChecks.propType(type, params, 'having', ['array', 'object']); + + templateChecks.propType(type, params, 'group', ['string', 'array']); + + templateChecks.propType(type, params, 'sort', ['string', 'array', 'object']); + + templateChecks.propType(type, params, 'offset', ['number', 'string']); + templateChecks.propType(type, params, 'limit', ['number', 'string']); + } + }); + + dialect.templates.set('insert', { + pattern: '{with} {withRecursive} insert {or} into {table} {values} ' + + '{condition}', + validate: function(type, params) { + templateChecks.onlyOneOfProps(type, params, ['with', 'withRecursive']); + templateChecks.propType(type, params, 'with', 'object'); + templateChecks.propType(type, params, 'withRecursive', 'object'); + + templateChecks.propType(type, params, 'or', 'string'); + templateChecks.propMatch(type, params, 'or', orRegExp); + + templateChecks.requiredProp(type, params, 'table'); + templateChecks.propType(type, params, 'table', 'string'); + + templateChecks.requiredProp(type, params, 'values'); + templateChecks.propType(type, params, 'values', ['array', 'object']); + + templateChecks.propType(type, params, 'condition', ['array', 'object']); + + } + }); + + dialect.templates.set('insertValues', { + pattern: '({fields}) {returning} values {values}', + validate: function(type, params) { + templateChecks.requiredProp('values', params, 'fields'); + templateChecks.propType('values', params, 'fields', 'array'); + templateChecks.minPropLength('values', params, 'fields', 1); + + templateChecks.propType(type, params, 'returning', ['array', 'object']); + + templateChecks.requiredProp('values', params, 'values'); + templateChecks.propType('values', params, 'values', 'array'); + templateChecks.minPropLength('values', params, 'values', 1); + } + }); + + dialect.templates.set('update', { + pattern: '{with} {withRecursive} update {or} {table} {alias} {modifier} {returning} ' + + '{condition} ', + validate: function(type, params) { + templateChecks.onlyOneOfProps(type, params, ['with', 'withRecursive']); + templateChecks.propType(type, params, 'with', 'object'); + templateChecks.propType(type, params, 'withRecursive', 'object'); + + templateChecks.propType(type, params, 'or', 'string'); + templateChecks.propMatch(type, params, 'or', orRegExp); + + templateChecks.requiredProp(type, params, 'table'); + templateChecks.propType(type, params, 'table', 'string'); + + templateChecks.propType(type, params, 'returning', ['array', 'object']); + + templateChecks.propType(type, params, 'alias', 'string'); + + templateChecks.requiredProp(type, params, 'modifier'); + templateChecks.propType(type, params, 'modifier', 'object'); + + templateChecks.propType(type, params, 'condition', ['array', 'object']); + + } + }); + + dialect.templates.set('remove', { + pattern: '{with} {withRecursive} delete from {table} {returning} {alias} {condition} ', + validate: function(type, params) { + templateChecks.onlyOneOfProps(type, params, ['with', 'withRecursive']); + templateChecks.propType(type, params, 'with', 'object'); + templateChecks.propType(type, params, 'withRecursive', 'object'); + + templateChecks.requiredProp(type, params, 'table'); + templateChecks.propType(type, params, 'table', 'string'); + + templateChecks.propType(type, params, 'returning', ['array', 'object']); + + templateChecks.propType(type, params, 'alias', 'string'); + + templateChecks.propType(type, params, 'condition', ['array', 'object']); + + } + }); +}; diff --git a/tests/6_dialects/1_mssql.js b/tests/6_dialects/1_mssql.js new file mode 100644 index 0000000..bd5b9ea --- /dev/null +++ b/tests/6_dialects/1_mssql.js @@ -0,0 +1,244 @@ +'use strict'; + +var jsonSql = require('../../lib')({ + dialect: 'mssql', + namedValues: false +}); +var expect = require('chai').expect; + +describe('MSSQL dialect', function() { + describe('limit / offset', function() { + it('should be ok with `limit` property', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['id', 'name'], + limit: 1, + condition: { + 'active': {$eq: '1'} + } + }); + expect(result.query).to.be.equal( + 'select top(1) [id], [name] from [users] where [active] = $1;' + ); + }); + + it('should be ok with `limit` and `sort` properties', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['id', 'name'], + limit: 1, + sort: { + 'lastLogin': -1 + } + }); + expect(result.query).to.be.equal( + 'select top(1) [id], [name] from [users] order by [lastLogin] desc;' + ); + }); + + it('should be ok with `offset` property', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['id', 'name'], + offset: 2, + }); + expect(result.query).to.be.equal( + 'select [id], [name] from [users] order by 1 offset 2 rows;' + ); + }); + it('should be ok with `offset` and `sort` properties', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['id', 'name'], + offset: 2, + sort: { + 'lastLogin': -1 + } + }); + expect(result.query).to.be.equal( + 'select [id], [name] from [users] order by [lastLogin] desc offset 2 rows;' + ); + }); + + it('should be ok with `limit` and `offset` properties', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['id', 'name'], + limit: 4, + offset: 2 + }); + expect(result.query).to.be.equal( + 'select [id], [name] from [users] order by 1 offset 2 rows fetch next 4 rows only;' + ); + }); + + it('should be ok with `limit` and `offset` and `sort` properties', function() { + var result = jsonSql.build({ + table: 'users', + fields: ['id', 'name'], + limit: 4, + offset: 2, + sort: { + 'lastLogin': -1 + } + }); + expect(result.query).to.be.equal( + 'select [id], [name] from [users] order by [lastLogin] desc ' + + 'offset 2 rows fetch next 4 rows only;' + ); + }); + }); + describe('Insert', function() { + it('should be ok with different `values` property', function() { + var date = new Date(); + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: { + id: 1, + name: 'Max', + date: date, + lastLogin: null, + active: true + } + }); + expect(result.query).to.be.equal( + 'insert into [users] ([id], [name], [date], [lastLogin], [active]) ' + + 'values (1, $1, $2, null, 1);' + ); + }); + + it('should be ok with different `values` property with option `separatedValues` = false', + function() { + var options = jsonSql.options; + jsonSql.configure({ + dialect: 'mssql', + separatedValues: false + }); + var date = new Date(); + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: { + id: 1, + name: 'Max', + date: date, + lastLogin: null, + active: true + } + }); + expect(result.query).to.be.equal( + 'insert into [users] ([id], [name], [date], [lastLogin], [active]) ' + + 'values (1, \'Max\', \'' + date.toISOString() + '\', null, 1);' + ); + jsonSql.configure(options); + } + ); + + it('should be ok with array `values` property', function() { + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: [ + { id: 1, name: 'Max' }, + { id: 2, name: 'Jane' } + ] + }); + expect(result.query).to.be.equal('insert into [users] ([id], [name]) values (1, $1), (2, $2);'); + }); + + + it('should throw error with a null `returning` property', function() { + expect(function() { + jsonSql.build({ + type: 'insert', + table: 'users', + values: { + name: 'Max' + }, + returning: null + }); + }).to.throw( + '`returning` property should have one of expected types: "array", "object" ' + + 'in `insertValues` clause' + ); + }); + + it('should be ok with `returning` property', function() { + var result = jsonSql.build({ + type: 'insert', + table: 'users', + values: { + name: 'Max' + }, + returning: [ + {table: 'inserted', name: 'id'} + ], + }); + expect(result.query).to.be.equal( + 'insert into [users] ([name]) output [inserted].[id] values ($1);' + ); + }); + + }); + + describe('Update', function() { + it('should throw error with a null `returning` property', function() { + expect(function() { + jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + $dec: { + age: 3 + } + }, + returning: null + }); + }).to.throw( + '`returning` property should have one of expected types: "array", "object" ' + + 'in `update` clause' + ); + }); + + it('should be ok with `returning` property', function() { + var result = jsonSql.build({ + type: 'update', + table: 'users', + modifier: { + $dec: { + age: 3 + } + }, + returning: ['inserted.*', 'deleted.*'] + }); + expect(result.query).to.be.equal( + 'update [users] set [age] = [age] - 3 output [inserted].*, [deleted].*;' + ); + }); + }); + + describe('Delete', function() { + it('should throw error with a null `returning` property', function() { + expect(function() { + jsonSql.build({ + type: 'remove', + table: 'users', + returning: null + }); + }).to.throw( + '`returning` property should have one of expected types: "array", "object" ' + + 'in `remove` clause' + ); + }); + + it('should be ok with `returning` property', function() { + var result = jsonSql.build({ + type: 'remove', + table: 'users', + returning: ['deleted.*'] + }); + expect(result.query).to.be.equal('delete from [users] output [deleted].*;'); + }); + }); +});