UNPKG

vertica-nodejs

Version:

Vertica client - pure javascript & libpq with the same API

355 lines (319 loc) 11.9 kB
// Copyright (c) 2022-2024 Open Text. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. 'use strict' const { EventEmitter } = require('events') const Result = require('./result') const utils = require('./utils') const fs = require('fs') const fsPromises = require('fs').promises const stream = require('stream') const glob = require('glob') class Query extends EventEmitter { constructor(config, values, callback) { super() config = utils.normalizeQueryConfig(config, values, callback) this.text = config.text this.values = config.values this.rows = config.rows this.types = config.types this.name = config.name this.binary = config.binary || false this.copyStream = config.copyStream || null // use unique portal name each time this.portal = config.portal || '' this.callback = config.callback this._rowMode = config.rowMode if (process.domain && config.callback) { this.callback = process.domain.bind(config.callback) } this._result = new Result(this._rowMode, this.types) // potential for multiple results this._results = this._result this.isPreparedStatement = false this._canceledDueToError = false this._activeError = false this._promise = null } requiresPreparation() { // named queries must always be prepared if (this.name) { return true } // always prepare if there are max number of rows expected per // portal execution if (this.rows) { return true } // don't prepare empty text queries if (!this.text) { return false } // prepare if there are values if (!this.values) { return false } return this.values.length > 0 } _checkForMultirow() { // if we already have a result with a command property // then we've already executed one query in a multi-statement simple query // turn our results into an array of results if (this._result.command) { if (!Array.isArray(this._results)) { this._results = [this._result] } this._result = new Result(this._rowMode, this.types) this._results.push(this._result) } } // associates row metadata from the supplied // message with this query object // metadata used when parsing row results handleRowDescription(msg) { this._checkForMultirow() this._result.addFields(msg.fields) this._accumulateRows = this.callback || !this.listeners('row').length } handleBindComplete(connection) { connection.execute({portal: this.portal, rows: this.rows}) connection.sync() } handleDataRow(msg) { let row if (this._canceledDueToError) { return } try { row = this._result.parseRow(msg.fields) } catch (err) { this._canceledDueToError = err return } this.emit('row', row, this._result) if (this._accumulateRows) { this._result.addRow(row) } } handleCommandComplete(msg, connection) { this._checkForMultirow() this._result.addCommandComplete(msg) // need to sync after each command complete of a prepared statement // if we were using a row count which results in multiple calls to _getRows if (this.rows) { connection.sync() } } // if a named prepared statement is created with empty query text // the backend will send an emptyQuery message but *not* a command complete message // since we pipeline sync immediately after execute we don't need to do anything here // unless we have rows specified, in which case we did not pipeline the intial sync call handleEmptyQuery(connection) { if (this.rows) { connection.sync() } } handleError(err, connection, internalError = false) { // need to sync after error during a prepared statement if (this._canceledDueToError) { err = this._canceledDueToError this._canceledDueToError = false } // if callback supplied do not emit error event as uncaught error // events will bubble up to node process this._activeError = true if (this.requiresPreparation() && !internalError) { connection.sync() } if (this.callback) { return this.callback(err) } this.emit('error', err) } handleReadyForQuery(con) { if (this._canceledDueToError) { return this.handleError(this._canceledDueToError, con) } if (this.callback && !this._activeError) { this.callback(null, this._results) } this._activeError = false; this.emit('end', this._results) } submit(connection) { if (typeof this.text !== 'string' && typeof this.name !== 'string') { return new Error('A query must have either text or a name. Supplying neither is unsupported.') } const previous = connection.parsedStatements[this.name] if (this.text && previous && this.text !== previous) { return new Error(`Prepared statements must be unique - '${this.name}' was used for a different statement`) } if (this.values && !Array.isArray(this.values)) { return new Error('Query values must be an array') } if (this.requiresPreparation()) { this.prepare(connection) } else { connection.query(this.text) } return null } hasBeenParsed(connection) { return this.name && connection.parsedStatements[this.name] } handlePortalSuspended(connection) { //do nothing, vertica doesn't support result-row count limit } handleEndOfBatchResponse(connection) { if (this.copyStream) { //copy from stdin connection.sendCopyDone() } // else noop, backend will send CopyDoneResponse for copy from local file to continue the process } prepare(connection) { // prepared statements need sync to be called after each command // complete or when an error is encountered this.isPreparedStatement = true this.name = this.name || connection.makeStatementName() // TODO refactor this poor encapsulation if (!this.hasBeenParsed(connection)) { connection.parse({ text: this.text, name: this.name, types: this.types, }) } // [VERTICA specific] The statement needs to be sent, not a portal connection.describe({ type: 'S', name: this.name, }) connection.flush() } // [VERTICA specific] Bind and Execute are not sent until ParameterDescription is received because the dataTypeIDs // are not available for the Bind message otherwise handleParameterDescription(msg, connection) { // because we're mapping user supplied values to // postgres wire protocol compatible values it could // throw an exception, so try/catch this section // parse out the oid from the ParameterDescription message parameters, since that's what we need to bind with var oids = [] for (var i = 0; i < msg.parameters.length; i++) { oids.push(msg.parameters[i].oid) } try { connection.bind({ portal: this.portal, statement: this.name, values: this.values, binary: this.binary, valueMapper: utils.prepareValue, dataTypeIDs: oids, }) } catch (err) { this.handleError(err, connection) return } connection.flush() // flush to force the bind complete in order to continue the sequence } handleCopyInResponse(connection) { connection.sendCopyDataStream(this.copyStream) } async handleVerifyFiles(msg, connection, protocol_version) { msg.protocol_version = protocol_version if (msg.numFiles !== 0) { // we are copying from file, not stdin let expandedFileNames = [] for (const fileName of msg.files) { if (/[*?[\]]/.test(fileName)) { // contains glob pattern const matchingFiles = glob.sync(fileName) expandedFileNames = expandedFileNames.concat(matchingFiles) } else { expandedFileNames.push(fileName) } } const uniqueFileNames = [...new Set(expandedFileNames)] // remove duplicates msg.numFiles = uniqueFileNames.length msg.fileNames = uniqueFileNames for (const fileName of uniqueFileNames) { try { // Check if the data file can be read await fsPromises.access(fileName, fs.constants.R_OK); } catch (readInputFileErr) { // Can't open input file for reading, send CopyError connection.sendCopyError(fileName, 0, '', "Unable to open input file for reading") return; } } } else { // check to make sure the readableStream is in fact a readableStream if (!(this.copyStream instanceof stream.Readable)) { connection.sendCopyError(this.copyStream, 0, '', "Cannot perform copy operation. Stream must be an instance of stream.Readable") return } } if (msg.rejectFile) { try { // Check if the rejections file can be written to, if specified await fsPromises.access(msg.rejectFile, fs.constants.W_OK); } catch (writeRejectsFileErr) { if (writeRejectsFileErr.code === 'ENOENT') { // file doesn't exist, see if we can create it try { const rejectHandle = await fsPromises.open(msg.rejectFile, 'w'); await rejectHandle.close() } catch (createErr) { // can't open or create output file for writing, send CopyError connection.sendCopyError(msg.rejectFile, 0, '', "Unable to open or create rejects file for writing") return } } else { // file exists but we can't open, likely permissions issue connection.sendCopyError(msg.rejectFile, 0, '', "Reject file exists but could not be opened for writing") return } } } if (msg.exceptionFile) { try { // Check if the exceptions file can be written to, if specified await fsPromises.access(msg.exceptionFile, fs.constants.W_OK); } catch (writeExceptionsFileErr) { // Can't open exceptions output file for writing, send CopyError if (writeExceptionsFileErr.code === 'ENOENT') { // file doesn't exist, see if we can create it try { const exceptionHandle = await fsPromises.open(msg.exceptionFile, 'w'); await exceptionHandle.close() } catch (createErr) { // can't open or create output file for writing, send CopyError connection.sendCopyError(msg.exceptionFile, 0, '', "Unable to open or create exception file for writing") return } } else { // file exists but we can't open, likely permissions issue connection.sendCopyError(msg.rejectFile, 0, '', "Exception file exists but could not be opened for writing") return } } } connection.sendVerifiedFiles(msg); // All files are verified } handleLoadFile(msg, connection) { connection.sendCopyDataFiles(msg) } handleWriteFile(msg, connection) { if (msg.fileName.length === 0) { //using returnrejected, fileContents is an array of row numbers, not a string this._result._setRejectedRows(msg.fileContents) } else { // future enhancement, move file IO to util fs.appendFileSync(msg.fileName, msg.fileContents, (err) => { if (err) { console.error('Error writing to file:', err); } }); } } // eslint-disable-next-line no-unused-vars handleCopyDoneResponse(msg, connection) { // noop } } module.exports = Query