2020-02-02 16:32:17 +05:30
|
|
|
const { times, findIndex, isEmpty, compact, isNil, isFunction, isString, cloneDeep, castArray } = require("lodash");
|
2020-02-02 12:01:06 +05:30
|
|
|
const Tokenizer = require("tokenize-this");
|
2020-02-02 16:32:17 +05:30
|
|
|
const { operators } = require("../operators");
|
2020-02-02 12:01:06 +05:30
|
|
|
|
2020-02-02 16:32:17 +05:30
|
|
|
/**
|
|
|
|
|
* Provides a fluent API to statefully traverse a token list, and perform token-level and
|
|
|
|
|
* range-level manipulations.
|
|
|
|
|
*/
|
|
|
|
|
class SQLTokenListVisitor {
|
|
|
|
|
/**
|
|
|
|
|
* @param {string[]} tokens List of tokens extracted from raw SQL
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
constructor(tokens) {
|
|
|
|
|
if (isString(tokens)) tokens = this.tokenize(tokens);
|
|
|
|
|
this.tokens = tokens;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Splits the SQL string into list of tokens.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} rawStr
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
tokenize(rawStr) {
|
|
|
|
|
const tokens = [];
|
|
|
|
|
const tokenizer = new Tokenizer();
|
2020-02-02 16:32:17 +05:30
|
|
|
tokenizer.tokenize(rawStr, (_token, surroundedBy) => {
|
|
|
|
|
const token = `${surroundedBy || ''}${_token}${surroundedBy || ''}`;
|
|
|
|
|
|
|
|
|
|
// Tokenizer is not SQL aware.
|
|
|
|
|
//
|
|
|
|
|
// We need to look back and check if combining this token with the
|
|
|
|
|
// last one gives us a valid SQL operator, in which case we will need
|
|
|
|
|
// to put them in the same token.
|
|
|
|
|
//
|
|
|
|
|
// This ensures that when joining them back we don't end up with invalid
|
|
|
|
|
// SQL (eg. && -> & &).
|
|
|
|
|
if (!surroundedBy) {
|
|
|
|
|
const operatorCandidates = [];
|
|
|
|
|
if (tokens.length > 0) {
|
|
|
|
|
operatorCandidates.push(tokens[tokens.length - 1] + token);
|
|
|
|
|
}
|
|
|
|
|
if (tokens.length > 1) {
|
|
|
|
|
operatorCandidates.push(
|
|
|
|
|
tokens[tokens.length - 2] +
|
|
|
|
|
tokens[tokens.length - 1] +
|
|
|
|
|
token
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
for (const candidate of operatorCandidates) {
|
|
|
|
|
const isKnownOperator = !!operators[operatorCandidates];
|
|
|
|
|
if (isKnownOperator) {
|
|
|
|
|
times(candidate.length-1, () => tokens.pop());
|
|
|
|
|
tokens.push(candidate);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-02-02 12:01:06 +05:30
|
|
|
tokens.push(token);
|
|
|
|
|
});
|
|
|
|
|
return tokens;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Combine tokens to SQL string
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
toString() {
|
|
|
|
|
return compact(this.tokens).join(' ');
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Return TokenItemVisitor for specified token
|
|
|
|
|
*
|
|
|
|
|
* @param {string} token token to seek
|
|
|
|
|
* @param {boolean} isCaseSensitive if match should be case sensitive
|
|
|
|
|
* @param {number} startIdx position from where we should start looking (towards right)
|
|
|
|
|
*/
|
|
|
|
|
findToken(token, isCaseSensitive = false, startIdx = 0) {
|
|
|
|
|
if (!isString(token)) {
|
|
|
|
|
throw new TypeError(`Expected token to be a string but found: ${token} (type: ${typeof token}) instead`)
|
|
|
|
|
}
|
|
|
|
|
const lowerValue = token.toLowerCase();
|
|
|
|
|
for (let i = startIdx; i < this.tokens.length; i++) {
|
|
|
|
|
const token = this.tokens[i];
|
2020-02-02 12:01:06 +05:30
|
|
|
const didMatch = isCaseSensitive
|
2020-02-02 16:32:17 +05:30
|
|
|
? token === token
|
|
|
|
|
: token.toLowerCase() === lowerValue;
|
|
|
|
|
if (didMatch) return new TokenItemVisitor(i, this);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return TokenItemVisitor for first token in the list
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
first() {
|
|
|
|
|
return new TokenItemVisitor(0, this);
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return TokenItemVisitor for last token in the list
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
last() {
|
2020-02-02 16:32:17 +05:30
|
|
|
return new TokenItemVisitor(this.tokens.length - 1, this);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace range of tokens
|
|
|
|
|
*
|
|
|
|
|
* @param {number} startIdx
|
|
|
|
|
* @param {number} endIdx
|
|
|
|
|
* @param {string | string[]} tokenList
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
_replaceRange(startIdx, endIdx, tokenList) {
|
|
|
|
|
const normalized = this._normalizeTokenList(tokenList);
|
|
|
|
|
this.tokens.splice(startIdx, endIdx - startIdx, ...normalized);
|
|
|
|
|
return new TokenRangeVisitor(startIdx, normalized.length, this);
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace token at specified index
|
|
|
|
|
*
|
|
|
|
|
* @param {number} index
|
|
|
|
|
* @param {string | string[]} tokenList token(s) to be inserted
|
|
|
|
|
*/
|
|
|
|
|
_replaceAtIndex(index, tokenList) {
|
2020-02-02 12:01:06 +05:30
|
|
|
const normalized = this._normalizeTokenList(tokenList);
|
2020-02-02 16:32:17 +05:30
|
|
|
this.tokens.splice(index, 1, ...normalized);
|
|
|
|
|
return new TokenRangeVisitor(index, normalized.length, this);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Insert new token(s) at specified index
|
|
|
|
|
*
|
|
|
|
|
* @param {number} index
|
|
|
|
|
* @param {string | string[]} tokenList token(s) to be inserted
|
|
|
|
|
*/
|
|
|
|
|
_insertAtIndex(index, tokenList) {
|
2020-02-02 12:01:06 +05:30
|
|
|
const normalized = this._normalizeTokenList(tokenList);
|
|
|
|
|
if (normalized.length === 0) return null;
|
2020-02-02 16:32:17 +05:30
|
|
|
this.tokens.splice(index, 0, ...normalized);
|
|
|
|
|
return new TokenRangeVisitor(index, normalized.length, this);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Ensure array and remove empty tokens
|
|
|
|
|
*
|
|
|
|
|
* @param {string[] | string} tokenList
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
_normalizeTokenList(tokenList) {
|
|
|
|
|
return compact(castArray(tokenList));
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Select a range of tokens
|
|
|
|
|
*
|
|
|
|
|
* @param {number} start the index where to begin (inclusive)
|
|
|
|
|
* @param {number} step +1 if we are going forward, -1 if we are going back
|
|
|
|
|
* @param {(token: string, index: number) => boolean} predicate Condition evaluated for each token to determine if we should proceed.
|
|
|
|
|
* @return TokenRangeVisitor
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
_selectRange(start, step, predicate) {
|
|
|
|
|
const range = { start: null, end: null };
|
2020-02-02 16:32:17 +05:30
|
|
|
for (let i = start; i < this.tokens.length && i >= 0; i += step) {
|
|
|
|
|
const token = this.tokens[i];
|
2020-02-02 12:01:06 +05:30
|
|
|
const shouldSelect = predicate(token, i);
|
|
|
|
|
if (shouldSelect) {
|
|
|
|
|
if (i <= start) {
|
2020-02-02 16:32:17 +05:30
|
|
|
// Going backward: Update the start
|
2020-02-02 12:01:06 +05:30
|
|
|
range.start = i;
|
|
|
|
|
}
|
|
|
|
|
if (i >= start) {
|
2020-02-02 16:32:17 +05:30
|
|
|
// Going forward: Update the end
|
|
|
|
|
range.end = i + 1;
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
|
|
|
|
} else break;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
if (isNil(range.start) || isNil(range.end)) {
|
2020-02-02 12:01:06 +05:30
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return new TokenRangeVisitor(range.start, range.end, this);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-02 16:32:17 +05:30
|
|
|
/**
|
|
|
|
|
* Visitor that tracks a position (index) in a token list
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
class TokenItemVisitor {
|
2020-02-02 16:32:17 +05:30
|
|
|
/**
|
|
|
|
|
* @param {number} index
|
|
|
|
|
* @param {string[]} tokenListVisitor
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
constructor(index, tokenListVisitor) {
|
|
|
|
|
this.index = index;
|
|
|
|
|
this.tokenListVisitor = tokenListVisitor;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get token at current index
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
token() {
|
|
|
|
|
return this.tokenListVisitor.tokens[this.index];
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Peek next token in list without actually changing visitor state.
|
|
|
|
|
*
|
|
|
|
|
* Useful for finite lookahead.
|
|
|
|
|
*
|
|
|
|
|
* @param {number} count Number of steps to look ahead. Use negative number to look behind.
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
peek(count = 1) {
|
|
|
|
|
return this.tokenListVisitor.tokens.slice(this.index, this.index + count).join(' ');
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Visit next occurance of specified token towards the right from current position.
|
|
|
|
|
*
|
|
|
|
|
* @param {string} token
|
|
|
|
|
* @param {boolean} isCaseSensitive
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
next(token, isCaseSensitive = false) {
|
|
|
|
|
if (!isNil(token)) {
|
|
|
|
|
return this.tokenListVisitor.findToken(token, isCaseSensitive, this.index + 1);
|
|
|
|
|
}
|
|
|
|
|
this.index += 1;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace the currently visited token.
|
|
|
|
|
*
|
|
|
|
|
* @param {string | string[] | (token: string) => string | string[]}
|
|
|
|
|
* If a single string is passed it will be tokenized
|
|
|
|
|
* If an array of strings is passed, they will be considered to be already tokenized
|
|
|
|
|
* If function is passed, it will be invoked with previous token and its returned value
|
|
|
|
|
* will be treated as above
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
replace(replacement) {
|
|
|
|
|
if (isFunction(replacement)) {
|
2020-02-02 16:32:17 +05:30
|
|
|
const token = cloneDeep(this.tokenListVisitor.tokens[this.index])
|
2020-02-02 12:01:06 +05:30
|
|
|
replacement = replacement(token)
|
|
|
|
|
}
|
|
|
|
|
if (isString(replacement)) replacement = this.tokenListVisitor.tokenize(replacement);
|
|
|
|
|
return this.tokenListVisitor._replaceAtIndex(this.index, replacement)
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Insert one of more tokens before current token.
|
|
|
|
|
*
|
|
|
|
|
* @param {string | string[]} tokens tokenized only if a single string is passed
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
insertBefore(tokens) {
|
|
|
|
|
if (isString(tokens)) tokens = this.tokenListVisitor.tokenize(tokens);
|
|
|
|
|
if (isEmpty(tokens)) throw new Error("Expected to find atleast one token");
|
|
|
|
|
return this.tokenListVisitor._insertAtIndex(this.index, tokens);
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Insert one or more tokens after current token.
|
|
|
|
|
*
|
|
|
|
|
* @param {string | string[]} tokens tokenized only if a single string is passed
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
insertAfter(tokens) {
|
|
|
|
|
if (isString(tokens)) tokens = this.tokenListVisitor.tokenize(tokens);
|
|
|
|
|
if (isEmpty(tokens)) throw new Error("Expected to find atleast one token");
|
|
|
|
|
return this.tokenListVisitor._insertAtIndex(this.index + 1, tokens);
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
_normalizeSelectionPredicate(predicate, targetIndex) {
|
|
|
|
|
if (isNil(predicate)) return takeOneAt(targetIndex);
|
|
|
|
|
if (isFunction(predicate)) return predicate;
|
|
|
|
|
throw new Error(`Invalid predicate supplied: ${predicate}. Expected function or token`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Select some tokens starting from current position (Current token is included)
|
|
|
|
|
*
|
|
|
|
|
* @param {undefined | ((token: string) => boolean)} until
|
|
|
|
|
* If undefined, next token will be selected.
|
|
|
|
|
* If function, every token in sequence will be called with that predicate and will
|
|
|
|
|
* be selected until the predicate returns false, at which point range will end.
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
select(until) {
|
2020-02-02 16:32:17 +05:30
|
|
|
const predicate = this._normalizeSelectionPredicate(until, this.index);
|
|
|
|
|
return this.tokenListVisitor._selectRange(this.index, 1, predicate);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Select some tokens going back from current position (current token is not included)
|
|
|
|
|
*
|
|
|
|
|
* @param {undefined | ((token: string) => boolean)} until
|
|
|
|
|
* Refer select docs for this param
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
selectPrev(until) {
|
|
|
|
|
const start = this.index - 1;
|
2020-02-02 16:32:17 +05:30
|
|
|
const predicate = this._normalizeSelectionPredicate(until, start)
|
|
|
|
|
return this.tokenListVisitor._selectRange(start, -1, predicate);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Select some tokens going forward from current position (current token is not included)
|
|
|
|
|
*
|
|
|
|
|
* @param {undefined | ((token: string) => boolean)} until
|
|
|
|
|
* Refer select docs for this param
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
selectNext(until) {
|
|
|
|
|
const start = this.index + 1;
|
2020-02-02 16:32:17 +05:30
|
|
|
const predicate = this._normalizeSelectionPredicate(until, start)
|
|
|
|
|
return this.tokenListVisitor._selectRange(start, 1, predicate);
|
2020-02-02 12:01:06 +05:30
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-02 16:32:17 +05:30
|
|
|
/**
|
|
|
|
|
* Visitor that tracks a range (start index to end index) in a token list
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
class TokenRangeVisitor {
|
2020-02-02 16:32:17 +05:30
|
|
|
/**
|
|
|
|
|
* @param {number} startIndex
|
|
|
|
|
* @param {number} endIndex
|
|
|
|
|
* @param {string[]} tokenListVisitor
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
constructor(startIndex, endIndex, tokenListVisitor) {
|
|
|
|
|
this.startIndex = startIndex;
|
|
|
|
|
this.endIndex = endIndex;
|
|
|
|
|
this.tokenListVisitor = tokenListVisitor;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get TokenItemVisitor for first token in the selected range
|
|
|
|
|
*/
|
|
|
|
|
first() {
|
2020-02-02 12:01:06 +05:30
|
|
|
return new TokenItemVisitor(this.startIndex, this.tokenListVisitor);
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get TokenItemVisitor for last token in the selected range
|
|
|
|
|
*/
|
|
|
|
|
last() {
|
2020-02-02 12:01:06 +05:30
|
|
|
return new TokenItemVisitor(this.endIndex, this.tokenListVisitor);
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extend selection to left until the predicate returns false.
|
|
|
|
|
*
|
|
|
|
|
* If predicate is not provided then one token to left is added to selection.
|
|
|
|
|
*
|
|
|
|
|
* @param {(token: string) => booleab} [until] predicate used to determine when to stop
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
extendLeft(until) {
|
|
|
|
|
const rangeVisitor = this.tokenListVisitor._selectRange(this.startIndex, -1, until);
|
|
|
|
|
if (!isNil(rangeVisitor)) {
|
|
|
|
|
rangeVisitor.endIndex = this.endIndex;
|
|
|
|
|
}
|
|
|
|
|
return rangeVisitor;
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extend selection to right until the predicate returns false.
|
|
|
|
|
*
|
|
|
|
|
* If predicate is not provided then one token to right is added to selection.
|
|
|
|
|
*
|
|
|
|
|
* @param {(token: string) => booleab} [until] predicate used to determine when to stop
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
extendRight(until) {
|
|
|
|
|
const rangeVisitor = this.tokenListVisitor._selectRange(this.endIndex, 1, until);
|
|
|
|
|
if (!isNil(rangeVisitor)) {
|
|
|
|
|
rangeVisitor.startIndex = this.startIndex;
|
|
|
|
|
}
|
|
|
|
|
return rangeVisitor
|
|
|
|
|
}
|
2020-02-02 16:32:17 +05:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Replace selection with specified token(s)
|
|
|
|
|
*
|
|
|
|
|
* @param {string | string[]} replacement tokenized only if a single string is passed.
|
|
|
|
|
*/
|
2020-02-02 12:01:06 +05:30
|
|
|
replace(replacement) {
|
|
|
|
|
if (isFunction(replacement)) {
|
2020-02-02 16:32:17 +05:30
|
|
|
const tokens = cloneDeep(this.tokenListVisitor.tokens.slice(this.startIndex, this.endIndex))
|
2020-02-02 12:01:06 +05:30
|
|
|
replacement = replacement(tokens)
|
|
|
|
|
}
|
|
|
|
|
if (isString(replacement)) replacement = this.tokenListVisitor.tokenize(replacement);
|
|
|
|
|
return this.tokenListVisitor._replaceRange(
|
|
|
|
|
this.startIndex,
|
|
|
|
|
this.endIndex,
|
|
|
|
|
replacement
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const takeOneAt = (index) => (_token, i) => i === index;
|
|
|
|
|
|
2020-02-02 16:32:17 +05:30
|
|
|
module.exports = { SQLTokenListVisitor };
|