574 lines
18 KiB
JavaScript
574 lines
18 KiB
JavaScript
// Copyright (c) 2022, 2024, Oracle and/or its affiliates.
|
|
|
|
//-----------------------------------------------------------------------------
|
|
//
|
|
// This software is dual-licensed to you under the Universal Permissive License
|
|
// (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl and Apache License
|
|
// 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose
|
|
// either license.
|
|
//
|
|
// If you elect to accept the software under the Apache License, Version 2.0,
|
|
// the following applies:
|
|
//
|
|
// 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
|
|
//
|
|
// https://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 { Buffer } = require('buffer');
|
|
const constants = require('../constants');
|
|
const errors = require('../errors');
|
|
const protoConstants = require('./protocol/constants');
|
|
|
|
/**
|
|
* It is used to cache the metadata about bind information
|
|
* associated with the statement. This will determine if statement needs
|
|
* to use Execute or Re-Execute.
|
|
*/
|
|
class BindInfo {
|
|
constructor(name, isReturnBind = false) {
|
|
this.bindName = name;
|
|
this.isReturnBind = isReturnBind;
|
|
this.maxSize = 0;
|
|
this.numElements = 0;
|
|
this.maxArraySize = 0;
|
|
this.type = null;
|
|
this.isArray = false;
|
|
this.dir = constants.BIND_IN;
|
|
this.bindVar = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encapsulates the SQL statement run on the connection.
|
|
* It has information like type of stmt, bind infrmation, cursor number, ...
|
|
*/
|
|
module.exports.BindInfo = BindInfo;
|
|
|
|
class Parser {
|
|
constructor() {
|
|
this.returningKeywordFound = false;
|
|
this.pos = 0;
|
|
this.maxPos = 0;
|
|
this.sqlData = "";
|
|
}
|
|
|
|
/**
|
|
* Bind variables are identified as follows:
|
|
* - Quoted and non-quoted bind names are allowed.
|
|
* - Quoted bind names can contain any characters.
|
|
* - Non-quoted bind names must begin with an alphabetic character.
|
|
* - Non-quoted bind names can only contain alphanumeric characters, the
|
|
* underscore, the dollar sign and the pound sign.
|
|
* - Non-quoted bind names cannot be Oracle Database Reserved Names (this
|
|
* is left to the server to detect and return an appropriate error).
|
|
*/
|
|
_parseBindName(stmt) {
|
|
let quotedName = false;
|
|
let inBind = false;
|
|
let digitsOnly = false;
|
|
let startPos = 0;
|
|
let pos = this.pos + 1;
|
|
let bindName;
|
|
let ch;
|
|
|
|
while (pos <= this.maxPos) {
|
|
ch = this.sqlData[pos];
|
|
if (!inBind) {
|
|
if (/\p{space}/u.test(ch)) {
|
|
pos += 1;
|
|
continue;
|
|
} else if (ch === '"') {
|
|
quotedName = true;
|
|
} else if (/\p{N}/u.test(ch)) {
|
|
digitsOnly = true;
|
|
} else if (!/\p{Alpha}/u.test(ch)) {
|
|
break;
|
|
}
|
|
inBind = true;
|
|
startPos = pos;
|
|
} else if (digitsOnly && !(/\p{N}/u.test(ch))) {
|
|
this.pos = pos - 1;
|
|
break;
|
|
} else if (quotedName && ch === '"') {
|
|
this.pos = pos;
|
|
break;
|
|
} else if (!digitsOnly && !quotedName
|
|
&& !(/[\p{L}\p{N}]/u.test(ch))
|
|
&& !['$', '_', '#'].includes(ch)) {
|
|
this.pos = pos - 1;
|
|
break;
|
|
}
|
|
pos += 1;
|
|
}
|
|
|
|
if (inBind) {
|
|
if (quotedName) {
|
|
bindName = stmt.sql.substring(startPos + 1, pos);
|
|
} else if (digitsOnly) {
|
|
bindName = stmt.sql.substring(startPos, pos);
|
|
} else {
|
|
bindName = stmt.sql.substring(startPos, pos).toUpperCase();
|
|
}
|
|
stmt._addBind(bindName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Multiple line comments consist of the characters /* followed by all
|
|
* characters up until * followed by /. This method is called when the first
|
|
* slash is detected and checks for the subsequent asterisk. If found,
|
|
* the comment is traversed and the current position is updated; otherwise,
|
|
* the current position is left untouched.
|
|
*/
|
|
_parseMultiLineComment() {
|
|
let inComment = false;
|
|
let exitingComment = false;
|
|
let pos = this.pos + 1;
|
|
let ch;
|
|
while (pos <= this.maxPos) {
|
|
ch = this.sqlData[pos];
|
|
if (!inComment) {
|
|
if (ch !== '*') {
|
|
break;
|
|
}
|
|
inComment = true;
|
|
} else if (ch === '*') {
|
|
exitingComment = true;
|
|
} else if (exitingComment) {
|
|
if (ch === '/') {
|
|
this.pos = pos;
|
|
break;
|
|
}
|
|
exitingComment = false;
|
|
}
|
|
pos += 1;
|
|
}
|
|
}
|
|
|
|
/** Parses a q-string which consists of the characters "q" and a single
|
|
* quote followed by a start separator, any text that does not contain the
|
|
* end seprator and the end separator and ending quote. The following are
|
|
* examples that demonstrate this:
|
|
* - q'[...]'
|
|
* - q'{...}'
|
|
* - q'<...>'
|
|
* - q'(...)'
|
|
* - q'?...?' (where ? is any character)
|
|
*/
|
|
_parseQstring() {
|
|
let exitingQstring = false;
|
|
let inQstring = false;
|
|
let sep;
|
|
let ch;
|
|
|
|
this.pos += 1;
|
|
|
|
while (this.pos <= this.maxPos) {
|
|
ch = this.sqlData[this.pos];
|
|
if (!inQstring) {
|
|
if (ch === '[') {
|
|
sep = ']';
|
|
} else if (ch === '{') {
|
|
sep = '}';
|
|
} else if (ch === '(') {
|
|
sep = ')';
|
|
} else if (ch === '<') {
|
|
sep = '>';
|
|
} else {
|
|
sep = ch;
|
|
}
|
|
inQstring = true;
|
|
} else if (!exitingQstring && ch === sep) {
|
|
exitingQstring = true;
|
|
} else if (exitingQstring) {
|
|
if (ch === "'") {
|
|
break;
|
|
} else if (ch !== sep) {
|
|
exitingQstring = false;
|
|
}
|
|
}
|
|
this.pos += 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses a quoted string with the given separator. All characters until
|
|
* the separate is detected are discarded.
|
|
*/
|
|
_parseQuotedString(sep) {
|
|
let ch;
|
|
this.pos += 1;
|
|
while (this.pos <= this.maxPos) {
|
|
ch = this.sqlData[this.pos];
|
|
if (ch === sep) {
|
|
break;
|
|
}
|
|
this.pos += 1;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Single line comments consist of two dashes and all characters up to the
|
|
* next line break (or the end of the data). This method is called when
|
|
* the first dash is detected and checks for the subsequent dash. If found,
|
|
* the single line comment is traversed and the current position is updated;
|
|
* otherwise, the current position is left untouched.
|
|
*/
|
|
_parseSingleLineComment() {
|
|
let inComment = false;
|
|
let pos = this.pos + 1;
|
|
let ch;
|
|
|
|
while (pos <= this.maxPos) {
|
|
ch = this.sqlData[pos];
|
|
if (!inComment) {
|
|
if (ch !== '-') {
|
|
return;
|
|
}
|
|
inComment = true;
|
|
} else if (ch === '\n') {
|
|
break;
|
|
}
|
|
pos += 1;
|
|
}
|
|
this.pos = pos;
|
|
}
|
|
|
|
/**
|
|
* Parses the SQL stored in the statement in order to determine the
|
|
* keyword that identifies the type of SQL being executed as well as a
|
|
* list of bind variable names. A check is also made for DML returning
|
|
* statements since the bind variables following the "INTO" keyword are
|
|
* treated differently from other bind variables.
|
|
*/
|
|
parse(stmt) {
|
|
let initialKeywordFound = false;
|
|
let lastWasString = false;
|
|
let ch, lastCh = '', alphaStartCh = '';
|
|
let alphaStartPos = 0, alphaLen;
|
|
let isAlpha, lastWasAlpha = false;
|
|
let keyword;
|
|
|
|
// initialization
|
|
this.pos = 0;
|
|
this.maxPos = stmt.sql.length - 1;
|
|
this.sqlData = stmt.sql;
|
|
|
|
// scan all the characters in the string
|
|
while (this.pos <= this.maxPos) {
|
|
ch = this.sqlData[this.pos];
|
|
// look for certain keywords (initial keyword and the ones for
|
|
// detecting DML returning statements
|
|
isAlpha = /\p{L}/u.test(ch);
|
|
if (isAlpha && !lastWasAlpha) {
|
|
alphaStartPos = this.pos;
|
|
alphaStartCh = ch;
|
|
} else if (!isAlpha && lastWasAlpha) {
|
|
alphaLen = this.pos - alphaStartPos;
|
|
if (!initialKeywordFound) {
|
|
keyword = stmt.sql.substring(alphaStartPos, this.pos).toUpperCase();
|
|
stmt._determineStatementType(keyword);
|
|
initialKeywordFound = true;
|
|
if (stmt.isDdl) {
|
|
break;
|
|
}
|
|
} else if (stmt.isDml && !this.returningKeywordFound
|
|
&& (alphaLen === 9 || alphaLen === 6)
|
|
&& ['r', 'R'].includes(alphaStartCh)) {
|
|
keyword = stmt.sql.substring(alphaStartPos, this.pos).toUpperCase();
|
|
if (['RETURNING', 'RETURN'].includes(keyword)) {
|
|
this.returningKeywordFound = true;
|
|
}
|
|
} else if (this.returningKeywordFound && alphaLen === 4
|
|
&& ['i', 'I'].includes(alphaStartCh)) {
|
|
keyword = stmt.sql.substring(alphaStartPos, this.pos).toUpperCase();
|
|
if (keyword === 'INTO') {
|
|
stmt.isReturning = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// need to keep track of whether the last token parsed was a string
|
|
// (excluding whitespace) as if the last token parsed was a string
|
|
// a following colon is not a bind variable but a part of the JSON
|
|
// constant syntax
|
|
if (ch === "'") {
|
|
lastWasString = true;
|
|
if (['q', 'Q'].includes(lastCh)) {
|
|
this._parseQstring();
|
|
} else {
|
|
this._parseQuotedString(ch);
|
|
}
|
|
} else if (!(/\p{space}/u.test(ch))) {
|
|
if (ch === '-') {
|
|
this._parseSingleLineComment();
|
|
} else if (ch === '/') {
|
|
this._parseMultiLineComment();
|
|
} else if (ch === '"') {
|
|
this._parseQuotedString(ch);
|
|
} else if (ch === ':' && !lastWasString) {
|
|
this._parseBindName(stmt);
|
|
}
|
|
lastWasString = false;
|
|
}
|
|
|
|
this.pos += 1;
|
|
lastWasAlpha = isAlpha;
|
|
lastCh = ch;
|
|
}
|
|
|
|
// if only a single word is found in sql, e.g. in case of commit/rollback
|
|
if (!initialKeywordFound) {
|
|
stmt._determineStatementType(stmt.sql.toUpperCase());
|
|
}
|
|
}
|
|
}
|
|
|
|
class Statement {
|
|
constructor() {
|
|
this.sql = "";
|
|
this.sqlBytes = [];
|
|
this.sqlLength = 0;
|
|
this.cursorId = 0;
|
|
this.requiresDefine = false;
|
|
this.isQuery = false;
|
|
this.isPlSql = false;
|
|
this.isDml = false;
|
|
this.isDdl = false;
|
|
this.isReturning = false;
|
|
this.bindInfoList = [];
|
|
this.queryVars = [];
|
|
this.bindInfoDict = new Map();
|
|
this.requiresFullExecute = false;
|
|
this.noPrefetch = false;
|
|
this.returnToCache = false;
|
|
this.numColumns = 0;
|
|
this.lastRowIndex;
|
|
this.lastRowid;
|
|
this.moreRowsToFetch = true;
|
|
this.inUse = false;
|
|
this.bufferRowIndex = 0;
|
|
this.bufferRowCount = 0;
|
|
this.pendingClear = false;
|
|
this.statementType = constants.STMT_TYPE_UNKNOWN;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// _copy()
|
|
//
|
|
// Copying existing statement into new statement object required by drcp
|
|
//---------------------------------------------------------------------------
|
|
_copy() {
|
|
const copiedStatement = new Statement();
|
|
copiedStatement.sql = this.sql;
|
|
copiedStatement.sqlBytes = this.sqlBytes;
|
|
copiedStatement.sqlLength = this.sqlLength;
|
|
copiedStatement.isQuery = this.isQuery;
|
|
copiedStatement.isPlSql = this.isPlSql;
|
|
copiedStatement.isDml = this.isDml;
|
|
copiedStatement.isDdl = this.isDdl;
|
|
copiedStatement.isReturning = this.isReturning;
|
|
copiedStatement.bindInfoList = [];
|
|
for (const bindInfo of this.bindInfoList) {
|
|
const newBindInfo = new BindInfo(bindInfo.bindName, bindInfo.isReturnBind);
|
|
copiedStatement.bindInfoList.push(newBindInfo);
|
|
}
|
|
const bindInfoDict = copiedStatement.bindInfoDict = new Map();
|
|
for (const bindInfo of copiedStatement.bindInfoList) {
|
|
if (bindInfoDict.has(bindInfo.bindName)) {
|
|
bindInfoDict.get(bindInfo.bindName).push(bindInfo);
|
|
} else {
|
|
bindInfoDict.set(bindInfo.bindName, [bindInfo]);
|
|
}
|
|
}
|
|
copiedStatement.returnToCache = false;
|
|
return copiedStatement;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// _determineStatementType(sql)
|
|
//
|
|
// Determine the type of the SQL statement by examining the first keyword
|
|
// found in the statement
|
|
//---------------------------------------------------------------------------
|
|
_determineStatementType(sqlKeyword) {
|
|
switch (sqlKeyword) {
|
|
case 'DECLARE':
|
|
this.isPlSql = true;
|
|
this.statementType = constants.STMT_TYPE_DECLARE;
|
|
break;
|
|
case 'CALL':
|
|
this.isPlSql = true;
|
|
this.statementType = constants.STMT_TYPE_CALL;
|
|
break;
|
|
case 'BEGIN':
|
|
this.isPlSql = true;
|
|
this.statementType = constants.STMT_TYPE_BEGIN;
|
|
break;
|
|
case 'SELECT':
|
|
this.isQuery = true;
|
|
this.statementType = constants.STMT_TYPE_SELECT;
|
|
break;
|
|
case 'WITH':
|
|
this.isQuery = true;
|
|
break;
|
|
case 'INSERT':
|
|
this.isDml = true;
|
|
this.statementType = constants.STMT_TYPE_INSERT;
|
|
break;
|
|
case 'UPDATE':
|
|
this.isDml = true;
|
|
this.statementType = constants.STMT_TYPE_UPDATE;
|
|
break;
|
|
case 'DELETE':
|
|
this.isDml = true;
|
|
this.statementType = constants.STMT_TYPE_DELETE;
|
|
break;
|
|
case 'MERGE':
|
|
this.isDml = true;
|
|
this.statementType = constants.STMT_TYPE_MERGE;
|
|
break;
|
|
case 'ALTER':
|
|
this.isDdl = true;
|
|
this.statementType = constants.STMT_TYPE_ALTER;
|
|
break;
|
|
case 'CREATE':
|
|
this.isDdl = true;
|
|
this.statementType = constants.STMT_TYPE_CREATE;
|
|
break;
|
|
case 'DROP':
|
|
this.isDdl = true;
|
|
this.statementType = constants.STMT_TYPE_DROP;
|
|
break;
|
|
case 'ANALYZE':
|
|
case 'AUDIT':
|
|
case 'COMMENT':
|
|
case 'GRANT':
|
|
case 'REVOKE':
|
|
case 'TRUNCATE':
|
|
this.isDdl = true;
|
|
break;
|
|
case 'COMMIT':
|
|
this.statementType = constants.STMT_TYPE_COMMIT;
|
|
break;
|
|
case 'ROLLBACK':
|
|
this.statementType = constants.STMT_TYPE_ROLLBACK;
|
|
break;
|
|
default:
|
|
this.statementType = constants.STMT_TYPE_UNKNOWN;
|
|
break;
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// prepare(sql)
|
|
//
|
|
// Prepare the SQL for execution by determining the list of bind names
|
|
// that are found within it. The length of the SQL text is also calculated
|
|
// at this time.
|
|
//---------------------------------------------------------------------------
|
|
_prepare(sql) {
|
|
this.sql = sql;
|
|
this.sqlBytes = Buffer.from(this.sql, 'utf8');
|
|
this.sqlLength = this.sqlBytes.length;
|
|
const parser = new Parser();
|
|
parser.parse(this);
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// _addBinds(sql)
|
|
//
|
|
// Add bind information to the statement by examining the passed SQL for
|
|
// bind variable names.
|
|
//---------------------------------------------------------------------------
|
|
_addBind(name) {
|
|
if (!this.isPlSql || !this.bindInfoDict.has(name)) {
|
|
const info = new BindInfo(name, this.isReturning);
|
|
this.bindInfoList.push(info);
|
|
if (this.bindInfoDict.has(info.bindName)) {
|
|
if (this.isReturning) {
|
|
const origInfo = this.bindInfoDict.get(info.bindName)[0];
|
|
if (!origInfo.isReturnBind) {
|
|
errors.throwErr(errors.ERR_DML_RETURNING_DUP_BINDS, name);
|
|
}
|
|
}
|
|
this.bindInfoDict.get(info.bindName).push(info);
|
|
} else {
|
|
this.bindInfoDict.set(info.bindName, [info]);
|
|
}
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// _setVariable(sql)
|
|
//
|
|
// Set the variable on the bind information and copy across metadata that
|
|
// will be used for binding. If the bind metadata has changed, mark the
|
|
// statement as requiring a full execute. In addition, binding a REF
|
|
// cursor also requires a full execute.
|
|
//---------------------------------------------------------------------------
|
|
_setVariable(bindInfo, variable) {
|
|
if (variable.type._oraTypeNum === protoConstants.TNS_DATA_TYPE_CURSOR) {
|
|
this.requiresFullExecute = true;
|
|
}
|
|
if (variable.maxSize !== bindInfo.maxSize
|
|
|| variable.dir !== bindInfo.dir
|
|
|| variable.isArray !== bindInfo.isArray
|
|
|| variable.values.length > bindInfo.numElements
|
|
|| variable.type != bindInfo.type
|
|
|| variable.maxArraySize != bindInfo.maxArraySize) {
|
|
bindInfo.isArray = variable.isArray;
|
|
bindInfo.numElements = variable.values.length;
|
|
bindInfo.maxSize = variable.maxSize;
|
|
bindInfo.type = variable.type;
|
|
bindInfo.dir = variable.dir;
|
|
bindInfo.maxArraySize = variable.maxArraySize;
|
|
this.requiresFullExecute = true;
|
|
}
|
|
|
|
bindInfo.bindVar = variable;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// _clearAllState
|
|
//
|
|
// clear all state associated with the cursor
|
|
//---------------------------------------------------------------------------
|
|
_clearAllState() {
|
|
this.cursorId = 0;
|
|
this.requiresDefine = false;
|
|
this.noPrefetch = false;
|
|
this.requiresFullExecute = false;
|
|
this.queryVars = [];
|
|
this.numQueryVars = 0;
|
|
this.bufferRowCount = 0;
|
|
this.bufferRowIndex = 0;
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
// _clearState
|
|
//
|
|
// clear some state associated with the cursor
|
|
//---------------------------------------------------------------------------
|
|
_clearState() {
|
|
this.cursorId = 0;
|
|
this.requiresDefine = false;
|
|
this.noPrefetch = false;
|
|
this.requiresFullExecute = false;
|
|
}
|
|
}
|
|
|
|
module.exports.Statement = Statement;
|