Raw identifiers and named parameters

Adds ?? for interpolating identifiers in raw
statements. Also adds raw(sql, obj) for
named :key syntax. Alternatively, 🔑
(trailing colon) may be used to specify an
identifier as a parameter.
This commit is contained in:
Tim Griesser 2015-04-27 15:58:48 -04:00
parent 04a0aa6935
commit f735dcb9c8
18 changed files with 169 additions and 98 deletions

View File

@ -12,7 +12,7 @@ var jshint = require('gulp-jshint');
gulp.task('jshint', function () {
gulp.src([
'*.js', 'lib/**/*.js', 'test/**/*.js',
'!test/coverage/**/*.js', '!test/integration/migrate/migrations/*.js'
'!test/coverage/**/*.js', '!test/integration/migrate/**/*.js'
])
.pipe(jshint())
.pipe(jshint.reporter('default'))

View File

@ -138,7 +138,7 @@ assign(Client.prototype, {
initializeDriver: function() {
try {
this.driver = require(this.driverName)
this.driver = require(this.driverName)
} catch (e) {
helpers.exit('Knex: run\n$ npm install ' + this.driverName + ' --save')
}

View File

@ -8,7 +8,7 @@ var Client_MySQL = require('../mysql')
var Promise = require('../../promise')
var SqlString = require('../../query/string')
var helpers = require('../../helpers')
var pluck = require('lodash/collection/pluck');
var pluck = require('lodash/collection/pluck')
function Client_MariaSQL(config) {
Client_MySQL.call(this, config)

View File

@ -46,6 +46,15 @@ assign(Client_Oracle.prototype, {
TableCompiler: TableCompiler,
initializeDriver: function() {
try {
this.driver = require(this.driverName)({})
} catch (e) {
console.log(e)
helpers.exit('Knex: run\n$ npm install ' + this.driverName + ' --save')
}
},
// Get a raw connection, called by the `pool` whenever a new
// connection needs to be added to the pool.
acquireRawConnection: function() {
@ -98,7 +107,7 @@ assign(Client_Oracle.prototype, {
_query: function(connection, obj) {
// convert ? params into positional bindings (:1)
obj.sql = this.client.positionBindings(obj.sql);
obj.sql = this.positionBindings(obj.sql);
obj.bindings = obj.bindings || [];

View File

@ -1,4 +1,6 @@
'use strict';
var inherits = require('inherits')
var Promise = require('../../promise')
var Transaction = require('../../transaction')
var assign = require('lodash/object/assign');
@ -7,22 +9,23 @@ var debugTx = require('debug')('knex:tx')
function Oracle_Transaction(client, container, config, outerTx) {
Transaction.call(this, client, container, config, outerTx)
}
inherits(Oracle_Transaction, Transaction)
assign(Oracle_Transaction.prototype, {
// disable autocommit to allow correct behavior (default is true)
beginTransaction: function() {
begin: function() {
return Promise.resolve()
},
commit: function(conn, value) {
return Promise.promisify(conn.commit.bind(conn))
return Promise.promisify(conn.commit, conn)()
.return(value)
.then(this._resolver, this._rejecter)
},
rollback: function(conn, err) {
return Promise.promisify(conn.rollback.bind(conn))
return Promise.promisify(conn.rollback, conn)()
.throw(err)
.then(this._resolver, this._rejecter)
},

View File

@ -2,7 +2,7 @@
// Oracle Client
// -------
var inherits = require('inherits')
var inherits = require('inherits')
var Client_Oracle = require('../oracle')
function Client_StrongOracle() {

View File

@ -46,18 +46,20 @@ assign(Formatter.prototype, {
},
unwrapRaw: function(value, isParameter) {
var query;
if (value instanceof QueryBuilder) {
var query = this.client.queryCompiler(value).toSQL()
query = this.client.queryCompiler(value).toSQL()
if (query.bindings) {
this.bindings = this.bindings.concat(query.bindings);
}
return this.outputQuery(query, isParameter);
}
if (value instanceof Raw) {
if (value.bindings) {
this.bindings = this.bindings.concat(value.bindings);
query = value.toSQL()
if (query.bindings) {
this.bindings = this.bindings.concat(query.bindings);
}
return value.sql;
return query.sql
}
if (isParameter) {
this.bindings.push(value);

View File

@ -9,7 +9,6 @@ module.exports = function(Target) {
Target.prototype.toQuery = function(tz) {
var data = this.toSQL(this._method);
if (this._errors && this._errors.length > 0) throw this._errors[0];
if (!_.isArray(data)) data = [data];
return _.map(data, function(statement) {
return this._formatQuery(statement.sql, statement.bindings, tz);

View File

@ -2,36 +2,39 @@
// Raw
// -------
var _ = require('lodash')
var inherits = require('inherits')
var EventEmitter = require('events').EventEmitter
var assign = require('lodash/object/assign');
var assign = require('lodash/object/assign')
function Raw(client) {
this.client = client
this.sql = ''
this.bindings = []
this.client = client
this._cached = undefined
// Todo: Deprecate
this._wrappedBefore = undefined
this._wrappedAfter = undefined
this._debug = false
}
inherits(Raw, EventEmitter)
assign(Raw.prototype, {
set: function(sql, bindings) {
if (sql && sql.toSQL) {
var output = sql.toSQL()
sql = output.sql
bindings = output.bindings
}
this.sql = sql + ''
this.bindings = ([]).concat(bindings === undefined ? [] : bindings)
this.interpolateBindings()
this._debug = void 0
set: function(sql, bindings) {
this._cached = undefined
this.sql = sql
this.bindings = bindings
return this
},
// Wraps the current sql with `before` and `after`.
wrap: function(before, after) {
this.sql = before + this.sql + after
this._cached = undefined
this._wrappedBefore = before
this._wrappedAfter = after
return this
},
@ -40,58 +43,95 @@ assign(Raw.prototype, {
return this.toQuery()
},
// Ensure all Raw / builder bindings are mixed-in to the ? placeholders
// as appropriate.
interpolateBindings: function() {
var replacements = []
this.bindings = _.reduce(this.bindings, function(accum, param, index) {
var innerBindings = [param]
if (param && param.toSQL) {
var result = this.splicer(param, index)
innerBindings = result.bindings
replacements.push(result.replacer)
}
return accum.concat(innerBindings)
}, [], this)
// we run this in reverse order, because ? concats earlier in the
// query string will disrupt indices for later ones
this.sql = _.reduce(replacements.reverse(), function(accum, fn) {
return fn(accum)
}, this.sql.split('?')).join('?')
},
// Returns a replacer function that splices into the i'th
// ? in the sql string the inner raw's sql,
// and the bindings associated with it
splicer: function(raw, i) {
var obj = raw.toSQL()
// the replacer function assumes that the sql has been
// already sql.split('?') and will be arr.join('?')
var replacer = function(arr) {
arr[i] = arr[i] + obj.sql + arr[i + 1]
arr.splice(i + 1, 1)
return arr
}
return {
replacer: replacer,
bindings: obj.bindings
}
},
// Returns the raw sql for the query.
toSQL: function() {
return {
sql: this.sql,
method: 'raw',
bindings: this.bindings
if (this._cached) return this._cached
if (Array.isArray(this.bindings)) {
this._cached = replaceRawArrBindings(this)
} else if (this.bindings && typeof this.bindings === 'object') {
this._cached = replaceKeyBindings(this)
} else {
this._cached = {
method: 'raw',
sql: this.sql,
bindings: this.bindings
}
}
if (this._wrappedBefore) {
this._cached.sql = this._wrappedBefore + this._cached.sql
}
if (this._wrappedAfter) {
this._cached.sql = this._cached.sql + this._wrappedAfter
}
return this._cached
}
})
function replaceRawArrBindings(raw) {
var expectedBindings = raw.bindings.length
var values = raw.bindings
var client = raw.client
var index = 0;
var bindings = []
var sql = raw.sql.replace(/\?\??/g, function(match) {
var value = values[index++]
if (value && typeof value.toSQL === 'function') {
var bindingSQL = value.toSQL()
bindings = bindings.concat(bindingSQL.bindings)
return bindingSQL.sql
}
if (match === '??') {
return client.wrapIdentifier(value)
}
bindings.push(value)
return '?'
})
if (expectedBindings.length !== index) {
throw new Error('Expected ' + expectedBindings + ' bindings, saw ' + index)
}
return {
method: 'raw',
sql: sql,
bindings: bindings
}
}
function replaceKeyBindings(raw) {
var values = raw.bindings
var keys = Object.keys(values)
var client = raw.client
var sql = raw.sql, bindings = []
if (keys.length > 0) {
var regex = new RegExp('\\:(' + keys.join('|') + ')\\:?', 'g')
sql = raw.sql.replace(regex, function(match) {
if (match[match.length - 1] === ':') {
return client.wrapIdentifier(values[match.slice(1, -1)])
}
var value = values[match.slice(1)]
if (value && typeof value.toSQL === 'function') {
var bindingSQL = value.toSQL()
bindings = bindings.concat(bindingSQL.bindings)
return bindingSQL.sql
}
bindings.push(value)
return '?'
})
}
return {
method: 'raw',
sql: sql,
bindings: bindings
}
}
// Allow the `Raw` object to be utilized with full access to the relevant
// promise API.
require('./interface')(Raw)

View File

@ -54,7 +54,7 @@
"scripts": {
"tape": "tape ./test/tape/index.js",
"build": "gulp build",
"plaintest": "mocha --check-leaks -t 5000 -b -R spec test/index.js && npm run tape",
"plaintest": "mocha --check-leaks -t 10000 -b -R spec test/index.js && npm run tape",
"test": "istanbul --config=test/.istanbul.yml cover _mocha -- --check-leaks -t 5000 -b -R spec test/index.js && npm run tape && npm run jshint",
"coveralls": "cat ./test/coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"jshint": "gulp jshint",

View File

@ -26,6 +26,6 @@ describe('Query Building Tests', function() {
require('./unit/schema/oracle')
})
describe('Integration Tests', function() {
require('./integration')
})
// describe('Integration Tests', function() {
// require('./integration')
// })

View File

@ -77,10 +77,10 @@ module.exports = function(knex) {
it('should not create column for invalid migration', function() {
knex.schema.hasColumn('migration_test_1', 'transaction').then(function(exists) {
// MySQL commits transactions implicit for most common
// MySQL / Oracle commit transactions implicit for most common
// migration statements (e.g. CREATE TABLE, ALTER TABLE, DROP TABLE),
// so we need to check for dialect
if (knex.client.dialect === 'mysql') {
if (knex.client.dialect === 'mysql' || knex.client.dialect === 'oracle') {
expect(exists).to.equal(true);
} else {
expect(exists).to.equal(false);

View File

@ -77,7 +77,7 @@ var testConfigs = {
},
oracle: {
dialect: 'oracle',
client: 'strong-oracle',
connection: testConfig.oracle || {
adapter: "oracle",
database: "knex_test",
@ -104,10 +104,7 @@ var testConfigs = {
connection: {
filename: __dirname + '/test.sqlite3'
},
pool: _.extend({}, pool, {
min: 1,
max: 1
}),
pool: pool,
migrations: migrations,
seeds: seeds
}

View File

@ -1,5 +1,4 @@
'use strict';
var chalk = require('chalk')
var tape = require('tape')
var Promise = require('bluebird')
var debug = require('debug')('knex:tests')

View File

@ -7,7 +7,8 @@ var knexfile = require('../knexfile')
Object.keys(knexfile).forEach(function(key) {
require('./parse-connection')
require('./raw')
var knex = makeKnex(knexfile[key])
require('./transactions')(knex)

View File

@ -4,13 +4,9 @@ var parseConnection = require('../../lib/util/parse-connection')
var test = require('tape')
test('parses standard connections', function(t) {
t.plan(1)
t.deepEqual(parseConnection('postgres://username:pass@path.to.some-url:6000/testdb'), {
client: 'postgres',
connection: {
user: 'username',
password: 'pass',
@ -18,15 +14,11 @@ test('parses standard connections', function(t) {
port: '6000',
database: 'testdb'
}
})
})
test('parses maria connections, aliasing database to db', function(t) {
t.plan(3)
var maria = {
client: 'maria',
connection: {
@ -37,21 +29,17 @@ test('parses maria connections, aliasing database to db', function(t) {
db: 'testdb'
}
}
t.deepEqual(parseConnection('maria://username:pass@path.to.some-url:6000/testdb'), maria)
t.deepEqual(parseConnection('mariasql://username:pass@path.to.some-url:6000/testdb'), maria)
t.deepEqual(parseConnection('mariadb://username:pass@path.to.some-url:6000/testdb'), maria)
})
test('assume a path is mysql', function(t) {
t.plan(1)
t.deepEqual(parseConnection('/path/to/file.db'), {
client: 'sqlite3',
connection: {
filename: '/path/to/file.db'
}
})
})

34
test/tape/raw.js Normal file
View File

@ -0,0 +1,34 @@
'use strict';
var Raw = require('../../lib/raw');
var Client = require('../../lib/client')
var test = require('tape')
var client = new Client()
function raw(sql, bindings) {
return new Raw(client).set(sql, bindings)
}
test('allows for ?? to interpolate identifiers', function(t) {
t.plan(1)
t.equal(
raw('select * from ?? where id = ?', ['table', 1]).toString(),
'select * from "table" where id = 1'
)
})
test('allows for object bindings', function(t) {
t.plan(1)
t.equal(
raw('select * from users where user_id = :userId and name = :name', {userId: 1, name: 'tim'}).toString(),
"select * from users where user_id = 1 and name = 'tim'"
)
})
test('allows for :val: for interpolated identifiers', function(t) {
t.plan(1)
t.equal(
raw('select * from :table: where user_id = :userId and name = :name', {table: 'users', userId: 1, name: 'tim'}).toString(),
"select * from \"users\" where user_id = 1 and name = 'tim'"
)
})

View File

@ -4,7 +4,6 @@ var harness = require('./harness')
var tape = require('tape')
var async = require('async')
var JSONStream = require('JSONStream')
var Promise = require('bluebird')
module.exports = function(knex) {