diff --git a/packages/querysplitter/src/options.ts b/packages/querysplitter/src/options.ts new file mode 100644 index 000000000..72dd201c9 --- /dev/null +++ b/packages/querysplitter/src/options.ts @@ -0,0 +1,35 @@ +export interface SplitterOptions { + allowBacktickString: boolean; + allowIndexParenString: boolean; + allowSemicolon: boolean; + allowCustomDelimiter: boolean; + allowGoDelimiter: boolean; + allowDollarDollarString: boolean; +} + +export const defaultSplitterOptions = { + allowBacktickString: false, + allowSemicolon: true, + allowCustomDelimiter: false, + allowGoDelimiter: false, + allowDollarDollarString: false, + allowIndexParenString: false, +}; + +export const mysqlSplitterOptions = { + ...defaultSplitterOptions, + allowCustomDelimiter: true, + allowBacktickString: true, +}; + +export const mssqlSplitterOptions = { + ...defaultSplitterOptions, + allowSemicolon: false, + allowGoDelimiter: true, + allowIndexParenString: true, +}; + +export const postgreSplitterOptions = { + ...defaultSplitterOptions, + allowDollarDollarString: true, +}; diff --git a/packages/querysplitter/src/splitQuery.ts b/packages/querysplitter/src/splitQuery.ts index b557b8074..5256d1260 100644 --- a/packages/querysplitter/src/splitQuery.ts +++ b/packages/querysplitter/src/splitQuery.ts @@ -1,3 +1,5 @@ +import { SplitterOptions, defaultSplitterOptions } from './options'; + const SINGLE_QUOTE = "'"; const DOUBLE_QUOTE = '"'; const BACKTICK = '`'; @@ -8,14 +10,13 @@ const SEMICOLON = ';'; const LINE_FEED = '\n'; const DELIMITER_KEYWORD = 'DELIMITER'; -export interface SplitQueryOptions {} - interface SplitExecutionContext { - options: SplitQueryOptions; + options: SplitterOptions; unread: string; currentDelimiter: string; currentStatement: string; output: string[]; + semicolonKeyTokenRegex: RegExp; } interface FindExpResult { @@ -35,7 +36,7 @@ const newLineRegex = /(?:[\r\n]+|$)/; const delimiterStartRegex = /(?:^|[\n\r]+)[ \f\t\v]*DELIMITER[ \t]+/i; // Best effort only, unable to find a syntax specification on delimiter const delimiterTokenRegex = /^(?:'(.+)'|"(.+)"|`(.+)`|([^\s]+))/; -const semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON); +// const semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON); const quoteEndRegexDict: Record = { [SINGLE_QUOTE]: singleQuoteStringEndRegex, [DOUBLE_QUOTE]: doubleQuoteStringEndRegex, @@ -46,19 +47,22 @@ function escapeRegex(value: string): string { return value.replace(regexEscapeSetRegex, '\\$&'); } -function buildKeyTokenRegex(delimiter: string): RegExp { +function buildKeyTokenRegex(delimiter: string, context: SplitExecutionContext): RegExp { + const { options } = context; return new RegExp( '(?:' + [ escapeRegex(delimiter), SINGLE_QUOTE, DOUBLE_QUOTE, - BACKTICK, + options.allowBacktickString ? BACKTICK : undefined, doubleDashCommentStartRegex.source, HASH_COMMENT_START, cStyleCommentStartRegex.source, - delimiterStartRegex.source, - ].join('|') + + options.allowCustomDelimiter ? delimiterStartRegex.source : undefined, + ] + .filter(x => x !== undefined) + .join('|') + ')', 'i' ); @@ -83,12 +87,12 @@ function findExp(content: string, regex: RegExp): FindExpResult { return result; } -function findKeyToken(content: string, currentDelimiter: string): FindExpResult { +function findKeyToken(content: string, currentDelimiter: string, context: SplitExecutionContext): FindExpResult { let regex; if (currentDelimiter === SEMICOLON) { - regex = semicolonKeyTokenRegex; + regex = context.semicolonKeyTokenRegex; } else { - regex = buildKeyTokenRegex(currentDelimiter); + regex = buildKeyTokenRegex(currentDelimiter, context); } return findExp(content, regex); } @@ -187,14 +191,19 @@ function handleKeyTokenFindResult(context: SplitExecutionContext, findResult: Fi } } -export function splitQuery(sql: string, options: SplitQueryOptions = {}): string[] { +export function splitQuery(sql: string, options: SplitterOptions = null): string[] { const context: SplitExecutionContext = { unread: sql, currentDelimiter: SEMICOLON, currentStatement: '', output: [], - options, + semicolonKeyTokenRegex: null, + options: { + ...defaultSplitterOptions, + ...options, + }, }; + context.semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON, context); let findResult: FindExpResult = { expIndex: -1, exp: null, @@ -204,7 +213,7 @@ export function splitQuery(sql: string, options: SplitQueryOptions = {}): string do { // console.log('context.unread', context.unread); lastUnreadLength = context.unread.length; - findResult = findKeyToken(context.unread, context.currentDelimiter); + findResult = findKeyToken(context.unread, context.currentDelimiter, context); handleKeyTokenFindResult(context, findResult); // Prevent infinite loop by returning incorrect result if (lastUnreadLength === context.unread.length) { diff --git a/packages/querysplitter/src/splitter.test.ts b/packages/querysplitter/src/splitter.test.ts index a27df60e5..0ab31565a 100644 --- a/packages/querysplitter/src/splitter.test.ts +++ b/packages/querysplitter/src/splitter.test.ts @@ -1,3 +1,4 @@ +import { mysqlSplitterOptions } from './options'; import { splitQuery } from './splitQuery'; test('simple query', () => { @@ -6,28 +7,28 @@ test('simple query', () => { }); test('correct split 2 queries', () => { - const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`;'); + const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`;', mysqlSplitterOptions); expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']); }); test('correct split 2 queries - no end semicolon', () => { - const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`'); + const output = splitQuery('SELECT * FROM `table1`;SELECT * FROM `table2`', mysqlSplitterOptions); expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']); }); test('delete empty query', () => { - const output = splitQuery(';;;\n;;SELECT * FROM `table1`;;;;;SELECT * FROM `table2`;;; ;;;'); + const output = splitQuery(';;;\n;;SELECT * FROM `table1`;;;;;SELECT * FROM `table2`;;; ;;;', mysqlSplitterOptions); expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']); }); test('should handle double backtick', () => { const input = ['CREATE TABLE `a``b` (`c"d` INT)', 'CREATE TABLE `a````b` (`c"d` INT)']; - const output = splitQuery(input.join(';\n') + ';'); + const output = splitQuery(input.join(';\n') + ';', mysqlSplitterOptions); expect(output).toEqual(input); }); test('semicolon inside string', () => { - const input = ['CREATE TABLE [a1]', "INSERT INTO [a1] (x) VALUES ('1;2;3;4')"]; - const output = splitQuery(input.join(';\n') + ';'); + const input = ['CREATE TABLE [a;1]', "INSERT INTO [a;1] (x) VALUES ('1;2;3;4')"]; + const output = splitQuery(input.join(';\n') + ';', mysqlSplitterOptions); expect(output).toEqual(input); });