| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379 |
- 'use strict'
- const debug = require('debug')('mssql:base')
- const { EventEmitter } = require('events')
- const { IDS, objectHasProperty } = require('../utils')
- const globalConnection = require('../global-connection')
- const { TransactionError, PreparedStatementError } = require('../error')
- const shared = require('../shared')
- const { TYPES, declare } = require('../datatypes')
- /**
- * Class PreparedStatement.
- *
- * IMPORTANT: Rememeber that each prepared statement means one reserved connection from the pool. Don't forget to unprepare a prepared statement!
- *
- * @property {String} statement Prepared SQL statement.
- */
- class PreparedStatement extends EventEmitter {
- /**
- * Creates a new Prepared Statement.
- *
- * @param {ConnectionPool|Transaction} [holder]
- */
- constructor (parent) {
- super()
- IDS.add(this, 'PreparedStatement')
- debug('ps(%d): created', IDS.get(this))
- this.parent = parent || globalConnection.pool
- this._handle = 0
- this.prepared = false
- this.parameters = {}
- }
- get connected () {
- return this.parent.connected
- }
- /**
- * Acquire connection from connection pool.
- *
- * @param {Request} request Request.
- * @param {ConnectionPool~acquireCallback} [callback] A callback which is called after connection has established, or an error has occurred. If omited, method returns Promise.
- * @return {PreparedStatement|Promise}
- */
- acquire (request, callback) {
- if (!this._acquiredConnection) {
- setImmediate(callback, new PreparedStatementError('Statement is not prepared. Call prepare() first.', 'ENOTPREPARED'))
- return this
- }
- if (this._activeRequest) {
- setImmediate(callback, new TransactionError("Can't acquire connection for the request. There is another request in progress.", 'EREQINPROG'))
- return this
- }
- this._activeRequest = request
- setImmediate(callback, null, this._acquiredConnection, this._acquiredConfig)
- return this
- }
- /**
- * Release connection back to the pool.
- *
- * @param {Connection} connection Previously acquired connection.
- * @return {PreparedStatement}
- */
- release (connection) {
- if (connection === this._acquiredConnection) {
- this._activeRequest = null
- }
- return this
- }
- /**
- * Add an input parameter to the prepared statement.
- *
- * @param {String} name Name of the input parameter without @ char.
- * @param {*} type SQL data type of input parameter.
- * @return {PreparedStatement}
- */
- input (name, type) {
- if ((/(--| |\/\*|\*\/|')/).test(name)) {
- throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
- }
- if (arguments.length < 2) {
- throw new PreparedStatementError('Invalid number of arguments. 2 arguments expected.', 'EARGS')
- }
- if (type instanceof Function) {
- type = type()
- }
- if (objectHasProperty(this.parameters, name)) {
- throw new PreparedStatementError(`The parameter name ${name} has already been declared. Parameter names must be unique`, 'EDUPEPARAM')
- }
- this.parameters[name] = {
- name,
- type: type.type,
- io: 1,
- length: type.length,
- scale: type.scale,
- precision: type.precision,
- tvpType: type.tvpType
- }
- return this
- }
- /**
- * Replace an input parameter on the request.
- *
- * @param {String} name Name of the input parameter without @ char.
- * @param {*} [type] SQL data type of input parameter. If you omit type, module automaticaly decide which SQL data type should be used based on JS data type.
- * @param {*} value Input parameter value. `undefined` and `NaN` values are automatically converted to `null` values.
- * @return {Request}
- */
- replaceInput (name, type, value) {
- delete this.parameters[name]
- return this.input(name, type, value)
- }
- /**
- * Add an output parameter to the prepared statement.
- *
- * @param {String} name Name of the output parameter without @ char.
- * @param {*} type SQL data type of output parameter.
- * @return {PreparedStatement}
- */
- output (name, type) {
- if (/(--| |\/\*|\*\/|')/.test(name)) {
- throw new PreparedStatementError(`SQL injection warning for param '${name}'`, 'EINJECT')
- }
- if (arguments.length < 2) {
- throw new PreparedStatementError('Invalid number of arguments. 2 arguments expected.', 'EARGS')
- }
- if (type instanceof Function) type = type()
- if (objectHasProperty(this.parameters, name)) {
- throw new PreparedStatementError(`The parameter name ${name} has already been declared. Parameter names must be unique`, 'EDUPEPARAM')
- }
- this.parameters[name] = {
- name,
- type: type.type,
- io: 2,
- length: type.length,
- scale: type.scale,
- precision: type.precision
- }
- return this
- }
- /**
- * Replace an output parameter on the request.
- *
- * @param {String} name Name of the output parameter without @ char.
- * @param {*} type SQL data type of output parameter.
- * @return {PreparedStatement}
- */
- replaceOutput (name, type) {
- delete this.parameters[name]
- return this.output(name, type)
- }
- /**
- * Prepare a statement.
- *
- * @param {String} statement SQL statement to prepare.
- * @param {basicCallback} [callback] A callback which is called after preparation has completed, or an error has occurred. If omited, method returns Promise.
- * @return {PreparedStatement|Promise}
- */
- prepare (statement, callback) {
- if (typeof callback === 'function') {
- this._prepare(statement, callback)
- return this
- }
- return new shared.Promise((resolve, reject) => {
- this._prepare(statement, err => {
- if (err) return reject(err)
- resolve(this)
- })
- })
- }
- /**
- * @private
- * @param {String} statement
- * @param {basicCallback} callback
- */
- _prepare (statement, callback) {
- debug('ps(%d): prepare', IDS.get(this))
- if (typeof statement === 'function') {
- callback = statement
- statement = undefined
- }
- if (this.prepared) {
- return setImmediate(callback, new PreparedStatementError('Statement is already prepared.', 'EALREADYPREPARED'))
- }
- this.statement = statement || this.statement
- this.parent.acquire(this, (err, connection, config) => {
- if (err) return callback(err)
- this._acquiredConnection = connection
- this._acquiredConfig = config
- const req = new shared.driver.Request(this)
- req.stream = false
- req.output('handle', TYPES.Int)
- req.input('params', TYPES.NVarChar, ((() => {
- const result = []
- for (const name in this.parameters) {
- if (!objectHasProperty(this.parameters, name)) {
- continue
- }
- const param = this.parameters[name]
- result.push(`@${name} ${declare(param.type, param)}${param.io === 2 ? ' output' : ''}`)
- }
- return result
- })()).join(','))
- req.input('stmt', TYPES.NVarChar, this.statement)
- req.execute('sp_prepare', (err, result) => {
- if (err) {
- this.parent.release(this._acquiredConnection)
- this._acquiredConnection = null
- this._acquiredConfig = null
- return callback(err)
- }
- debug('ps(%d): prepared', IDS.get(this))
- this._handle = result.output.handle
- this.prepared = true
- callback(null)
- })
- })
- }
- /**
- * Execute a prepared statement.
- *
- * @param {Object} values An object whose names correspond to the names of parameters that were added to the prepared statement before it was prepared.
- * @param {basicCallback} [callback] A callback which is called after execution has completed, or an error has occurred. If omited, method returns Promise.
- * @return {Request|Promise}
- */
- execute (values, callback) {
- if (this.stream || (typeof callback === 'function')) {
- return this._execute(values, callback)
- }
- return new shared.Promise((resolve, reject) => {
- this._execute(values, (err, recordset) => {
- if (err) return reject(err)
- resolve(recordset)
- })
- })
- }
- /**
- * @private
- * @param {Object} values
- * @param {basicCallback} callback
- */
- _execute (values, callback) {
- const req = new shared.driver.Request(this)
- req.stream = this.stream
- req.input('handle', TYPES.Int, this._handle)
- // copy parameters with new values
- for (const name in this.parameters) {
- if (!objectHasProperty(this.parameters, name)) {
- continue
- }
- const param = this.parameters[name]
- req.parameters[name] = {
- name,
- type: param.type,
- io: param.io,
- value: values[name],
- length: param.length,
- scale: param.scale,
- precision: param.precision
- }
- }
- req.execute('sp_execute', (err, result) => {
- if (err) return callback(err)
- callback(null, result)
- })
- return req
- }
- /**
- * Unprepare a prepared statement.
- *
- * @param {basicCallback} [callback] A callback which is called after unpreparation has completed, or an error has occurred. If omited, method returns Promise.
- * @return {PreparedStatement|Promise}
- */
- unprepare (callback) {
- if (typeof callback === 'function') {
- this._unprepare(callback)
- return this
- }
- return new shared.Promise((resolve, reject) => {
- this._unprepare(err => {
- if (err) return reject(err)
- resolve()
- })
- })
- }
- /**
- * @private
- * @param {basicCallback} callback
- */
- _unprepare (callback) {
- debug('ps(%d): unprepare', IDS.get(this))
- if (!this.prepared) {
- return setImmediate(callback, new PreparedStatementError('Statement is not prepared. Call prepare() first.', 'ENOTPREPARED'))
- }
- if (this._activeRequest) {
- return setImmediate(callback, new TransactionError("Can't unprepare the statement. There is a request in progress.", 'EREQINPROG'))
- }
- const req = new shared.driver.Request(this)
- req.stream = false
- req.input('handle', TYPES.Int, this._handle)
- req.execute('sp_unprepare', err => {
- if (err) return callback(err)
- this.parent.release(this._acquiredConnection)
- this._acquiredConnection = null
- this._acquiredConfig = null
- this._handle = 0
- this.prepared = false
- debug('ps(%d): unprepared', IDS.get(this))
- return callback(null)
- })
- }
- }
- module.exports = PreparedStatement
|