UNPKG

tedious

Version:

A TDS driver, for connecting to MS SQLServer databases.

370 lines (292 loc) 9.01 kB
"use strict"; /* globals $PropertyType */ const EventEmitter = require('events').EventEmitter; const Transform = require('readable-stream').Transform; const WritableTrackingBuffer = require('./tracking-buffer/writable-tracking-buffer'); const TOKEN_TYPE = require('./token/token').TYPE; const Message = require('./message'); const PACKET_TYPE = require('./packet').TYPE; const _require = require('./errors'), RequestError = _require.RequestError; const FLAGS = { nullable: 1 << 0, caseSen: 1 << 1, updateableReadWrite: 1 << 2, updateableUnknown: 1 << 3, identity: 1 << 4, computed: 1 << 5, // introduced in TDS 7.2 fixedLenCLRType: 1 << 8, // introduced in TDS 7.2 sparseColumnSet: 1 << 10, // introduced in TDS 7.3.B hidden: 1 << 13, // introduced in TDS 7.2 key: 1 << 14, // introduced in TDS 7.2 nullableUnknown: 1 << 15 // introduced in TDS 7.2 }; const DONE_STATUS = { FINAL: 0x00, MORE: 0x1, ERROR: 0x2, INXACT: 0x4, COUNT: 0x10, ATTN: 0x20, SRVERROR: 0x100 }; class BulkLoad extends EventEmitter { // eslint-disable-line no-use-before-define constructor(table, connectionOptions, { checkConstraints = false, fireTriggers = false, keepNulls = false, lockTable = false }, callback) { if (typeof checkConstraints !== 'boolean') { throw new TypeError('The "options.checkConstraints" property must be of type boolean.'); } if (typeof fireTriggers !== 'boolean') { throw new TypeError('The "options.fireTriggers" property must be of type boolean.'); } if (typeof keepNulls !== 'boolean') { throw new TypeError('The "options.keepNulls" property must be of type boolean.'); } if (typeof lockTable !== 'boolean') { throw new TypeError('The "options.lockTable" property must be of type boolean.'); } super(); this.error = undefined; this.canceled = false; this.executionStarted = false; this.table = table; this.options = connectionOptions; this.callback = callback; this.columns = []; this.columnsByName = {}; this.firstRowWritten = false; this.streamingMode = false; this.rowToPacketTransform = new RowTransform(this); // eslint-disable-line no-use-before-define this.bulkOptions = { checkConstraints, fireTriggers, keepNulls, lockTable }; } addColumn(name, type, { output = false, length, precision, scale, objName = name, nullable = true }) { if (this.firstRowWritten) { throw new Error('Columns cannot be added to bulk insert after the first row has been written.'); } if (this.executionStarted) { throw new Error('Columns cannot be added to bulk insert after execution has started.'); } const column = { type: type, name: name, value: null, output: output, length: length, precision: precision, scale: scale, objName: objName, nullable: nullable }; if ((type.id & 0x30) === 0x20) { if (column.length == null && type.resolveLength) { column.length = type.resolveLength(column); } } if (type.hasPrecision) { if (column.precision == null && type.resolvePrecision) { column.precision = type.resolvePrecision(column); } } if (type.hasScale) { if (column.scale == null && type.resolveScale) { column.scale = type.resolveScale(column); } } this.columns.push(column); this.columnsByName[name] = column; } addRow(...input) { this.firstRowWritten = true; let row; if (input.length > 1 || !input[0] || typeof input[0] !== 'object') { row = input; } else { row = input[0]; } // write each column if (Array.isArray(row)) { this.rowToPacketTransform.write(row); } else { const object = row; this.rowToPacketTransform.write(this.columns.map(column => { return object[column.objName]; })); } } getOptionsSql() { const addOptions = []; if (this.bulkOptions.checkConstraints) { addOptions.push('CHECK_CONSTRAINTS'); } if (this.bulkOptions.fireTriggers) { addOptions.push('FIRE_TRIGGERS'); } if (this.bulkOptions.keepNulls) { addOptions.push('KEEP_NULLS'); } if (this.bulkOptions.lockTable) { addOptions.push('TABLOCK'); } if (addOptions.length > 0) { return ` WITH (${addOptions.join(',')})`; } else { return ''; } } getBulkInsertSql() { let sql = 'insert bulk ' + this.table + '('; for (let i = 0, len = this.columns.length; i < len; i++) { const c = this.columns[i]; if (i !== 0) { sql += ', '; } sql += '[' + c.name + '] ' + c.type.declaration(c); } sql += ')'; sql += this.getOptionsSql(); return sql; } getTableCreationSql() { let sql = 'CREATE TABLE ' + this.table + '(\n'; for (let i = 0, len = this.columns.length; i < len; i++) { const c = this.columns[i]; if (i !== 0) { sql += ',\n'; } sql += '[' + c.name + '] ' + c.type.declaration(c); if (c.nullable !== undefined) { sql += ' ' + (c.nullable ? 'NULL' : 'NOT NULL'); } } sql += '\n)'; return sql; } getColMetaData() { const tBuf = new WritableTrackingBuffer(100, null, true); // TokenType tBuf.writeUInt8(TOKEN_TYPE.COLMETADATA); // Count tBuf.writeUInt16LE(this.columns.length); for (let j = 0, len = this.columns.length; j < len; j++) { const c = this.columns[j]; // UserType if (this.options.tdsVersion < '7_2') { tBuf.writeUInt16LE(0); } else { tBuf.writeUInt32LE(0); } // Flags let flags = FLAGS.updateableReadWrite; if (c.nullable) { flags |= FLAGS.nullable; } else if (c.nullable === undefined && this.options.tdsVersion >= '7_2') { flags |= FLAGS.nullableUnknown; } tBuf.writeUInt16LE(flags); // TYPE_INFO c.type.writeTypeInfo(tBuf, c, this.options); // ColName tBuf.writeBVarchar(c.name, 'ucs2'); } return tBuf.data; } setTimeout(timeout) { this.timeout = timeout; } createDoneToken() { // It might be nice to make DoneToken a class if anything needs to create them, but for now, just do it here const tBuf = new WritableTrackingBuffer(this.options.tdsVersion < '7_2' ? 9 : 13); tBuf.writeUInt8(TOKEN_TYPE.DONE); const status = DONE_STATUS.FINAL; tBuf.writeUInt16LE(status); tBuf.writeUInt16LE(0); // CurCmd (TDS ignores this) tBuf.writeUInt32LE(0); // row count - doesn't really matter if (this.options.tdsVersion >= '7_2') { tBuf.writeUInt32LE(0); // row count is 64 bits in >= TDS 7.2 } return tBuf.data; } // This method switches the BulkLoad object into streaming mode and returns // a stream.Writable for streaming rows to the server. getRowStream() { if (this.firstRowWritten) { throw new Error('BulkLoad cannot be switched to streaming mode after first row has been written using addRow().'); } if (this.executionStarted) { throw new Error('BulkLoad cannot be switched to streaming mode after execution has started.'); } this.streamingMode = true; return this.rowToPacketTransform; } getMessageStream() { const message = new Message({ type: PACKET_TYPE.BULK_LOAD }); this.rowToPacketTransform.pipe(message); this.rowToPacketTransform.once('finish', () => { this.removeListener('cancel', onCancel); }); const onCancel = () => { this.rowToPacketTransform.emit('error', RequestError('Canceled.', 'ECANCEL')); this.rowToPacketTransform.destroy(); }; this.once('cancel', onCancel); return message; } cancel() { if (this.canceled) { return; } this.canceled = true; this.emit('cancel'); } } module.exports = BulkLoad; // A transform that converts rows to packets. class RowTransform extends Transform { constructor(bulkLoad) { super({ writableObjectMode: true }); this.bulkLoad = bulkLoad; this.mainOptions = bulkLoad.options; this.columns = bulkLoad.columns; this.columnMetadataWritten = false; } _transform(row, encoding, callback) { if (!this.columnMetadataWritten) { this.push(this.bulkLoad.getColMetaData()); this.columnMetadataWritten = true; } const buf = new WritableTrackingBuffer(64, 'ucs2', true); buf.writeUInt8(TOKEN_TYPE.ROW); for (let i = 0; i < this.columns.length; i++) { const c = this.columns[i]; c.type.writeParameterData(buf, { length: c.length, scale: c.scale, precision: c.precision, value: row[i] }, this.mainOptions); } this.push(buf.data); process.nextTick(callback); } _flush(callback) { this.push(this.bulkLoad.createDoneToken()); process.nextTick(callback); } }