Skip to content

Commit ec306e4

Browse files
committed
feat: skipe separator in begin-end closure option
1 parent 670137b commit ec306e4

File tree

5 files changed

+133
-7
lines changed

5 files changed

+133
-7
lines changed

src/options.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface SplitterOptions {
1515
doubleDashComments: boolean;
1616
multilineComments: boolean;
1717
javaScriptComments: boolean;
18+
skipSeparatorBeginEnd: boolean;
1819
// comments willl be not part of output
1920
ignoreComments: boolean;
2021
// if more commands are on single line, they are not splitted
@@ -42,6 +43,7 @@ export const defaultSplitterOptions: SplitterOptions = {
4243
allowSlashDelimiter: false,
4344
allowDollarDollarString: false,
4445
noSplit: false,
46+
skipSeparatorBeginEnd: false,
4547

4648
doubleDashComments: true,
4749
multilineComments: true,
@@ -89,6 +91,7 @@ export const postgreSplitterOptions: SplitterOptions = {
8991
export const sqliteSplitterOptions: SplitterOptions = {
9092
...defaultSplitterOptions,
9193

94+
skipSeparatorBeginEnd: true,
9295
stringsBegins: ["'", '"'],
9396
stringsEnds: { "'": "'", '"': '"' },
9497
stringEscapes: { "'": "'", '"': '"' },

src/queryParamHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ function createParameterizerContext(sql: string, options: SplitterOptions) {
1111
wasDataOnLine: false,
1212
isCopyFromStdin: false,
1313
isCopyFromStdinCandidate: false,
14+
beginEndIdentLevel: 0,
1415
};
1516
}
1617

src/splitQuery.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { SplitterOptions, defaultSplitterOptions } from './options';
22

33
const SEMICOLON = ';';
4+
const BEGIN_EXTRA_KEYWORDS = ['DEFERRED', 'IMMEDIATE', 'EXCLUSIVE', 'TRANSACTION'];
5+
const BEGIN_EXTRA_KEYWORDS_REGEX = new RegExp(`^(?:${BEGIN_EXTRA_KEYWORDS.join('|')})`, 'i');
6+
const END_EXTRA_KEYWORDS = ['TRANSACTION', 'IF'];
7+
const END_EXTRA_KEYWORDS_REGEX = new RegExp(`^(?:${END_EXTRA_KEYWORDS.join('|')})`, 'i');
48

59
type SplitterSpecialMarkerType = 'copy_stdin_start' | 'copy_stdin_end' | 'copy_stdin_line';
610
export interface SplitStreamContext {
@@ -40,6 +44,7 @@ export interface ScannerContext {
4044
readonly wasDataOnLine: boolean;
4145
readonly isCopyFromStdin: boolean;
4246
readonly isCopyFromStdinCandidate: boolean;
47+
readonly beginEndIdentLevel: number;
4348
}
4449

4550
export interface SplitLineContext extends SplitStreamContext {
@@ -49,6 +54,7 @@ export interface SplitLineContext extends SplitStreamContext {
4954
end: number;
5055
wasDataOnLine: boolean;
5156
currentCommandStart: number;
57+
beginEndIdentLevel: number;
5258

5359
// unread: string;
5460
// currentStatement: string;
@@ -111,6 +117,8 @@ interface Token {
111117
type:
112118
| 'string'
113119
| 'delimiter'
120+
| 'end'
121+
| 'begin'
114122
| 'whitespace'
115123
| 'eoln'
116124
| 'data'
@@ -228,7 +236,8 @@ export function scanToken(context: ScannerContext): Token {
228236
};
229237
}
230238

231-
if (context.currentDelimiter && s.slice(pos).startsWith(context.currentDelimiter)) {
239+
const isInBeginEnd = context.options.skipSeparatorBeginEnd && context.beginEndIdentLevel > 0;
240+
if (context.currentDelimiter && s.slice(pos).startsWith(context.currentDelimiter) && !isInBeginEnd) {
232241
return {
233242
type: 'delimiter',
234243
length: context.currentDelimiter.length,
@@ -350,6 +359,35 @@ export function scanToken(context: ScannerContext): Token {
350359
};
351360
}
352361

362+
if (context.options.skipSeparatorBeginEnd && s.slice(pos).match(/^begin/i)) {
363+
let pos2 = pos + 'BEGIN'.length;
364+
let pos0 = pos2;
365+
366+
while (pos0 < context.end && /[^a-zA-Z0-9]/.test(s[pos0])) pos0++;
367+
368+
if (!BEGIN_EXTRA_KEYWORDS_REGEX.test(s.slice(pos0))) {
369+
return {
370+
type: 'begin',
371+
length: pos2 - pos,
372+
lengthWithoutWhitespace: pos0 - pos,
373+
};
374+
}
375+
}
376+
377+
if (context.options.skipSeparatorBeginEnd && s.slice(pos).match(/^end/i)) {
378+
let pos2 = pos + 'END'.length;
379+
let pos0 = pos2;
380+
381+
while (pos0 < context.end && /[^a-zA-Z0-9]/.test(s[pos0])) pos0++;
382+
383+
if (!END_EXTRA_KEYWORDS_REGEX.test(s.slice(pos0))) {
384+
return {
385+
type: 'end',
386+
length: pos2 - pos,
387+
};
388+
}
389+
}
390+
353391
const dollarString = scanDollarQuotedString(context);
354392
if (dollarString) return dollarString;
355393

@@ -366,6 +404,7 @@ function containsDataAfterDelimiterOnLine(context: ScannerContext, delimiter: To
366404
wasDataOnLine: context.wasDataOnLine,
367405
isCopyFromStdinCandidate: context.isCopyFromStdinCandidate,
368406
isCopyFromStdin: context.isCopyFromStdin,
407+
beginEndIdentLevel: context.beginEndIdentLevel,
369408
};
370409

371410
cloned.position += delimiter.length;
@@ -594,6 +633,18 @@ export function splitQueryLine(context: SplitLineContext) {
594633
markStartCommand(context);
595634
context.isCopyFromStdinCandidate = false;
596635
break;
636+
case 'begin':
637+
if (context.options.skipSeparatorBeginEnd) {
638+
context.beginEndIdentLevel++;
639+
}
640+
movePosition(context, token.length, false);
641+
break;
642+
case 'end':
643+
if (context.options.skipSeparatorBeginEnd && context.beginEndIdentLevel > 0) {
644+
context.beginEndIdentLevel--;
645+
}
646+
movePosition(context, token.length, false);
647+
break;
597648
}
598649
}
599650

@@ -660,6 +711,7 @@ export function splitQuery(sql: string, options: SplitterOptions = null): SplitR
660711
trimCommandStartPosition: 0,
661712
trimCommandStartLine: 0,
662713
trimCommandStartColumn: 0,
714+
beginEndIdentLevel: 0,
663715

664716
wasDataInCommand: false,
665717
isCopyFromStdin: false,

src/splitQueryStream.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export class SplitQueryStream extends stream.Transform {
4545
flushBuffer() {
4646
const lineContext: SplitLineContext = {
4747
...this.context,
48+
beginEndIdentLevel: 0,
4849
position: 0,
4950
currentCommandStart: 0,
5051
wasDataOnLine: false,

src/splitter.test.ts

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
noSplitSplitterOptions,
77
redisSplitterOptions,
88
oracleSplitterOptions,
9+
sqliteSplitterOptions,
910
} from './options';
1011
import { splitQuery } from './splitQuery';
1112

@@ -33,14 +34,10 @@ test('correct split 2 queries - with escaped string', () => {
3334
});
3435

3536
test('incorrect used escape', () => {
36-
const output = splitQuery(
37-
"query1\\",
38-
mysqlSplitterOptions
39-
);
40-
expect(output).toEqual(["query1\\"]);
37+
const output = splitQuery('query1\\', mysqlSplitterOptions);
38+
expect(output).toEqual(['query1\\']);
4139
});
4240

43-
4441
test('delete empty query', () => {
4542
const output = splitQuery(';;;\n;;SELECT * FROM `table1`;;;;;SELECT * FROM `table2`;;; ;;;', mysqlSplitterOptions);
4643
expect(output).toEqual(['SELECT * FROM `table1`', 'SELECT * FROM `table2`']);
@@ -302,3 +299,75 @@ test('postgres copy from stdin', () => {
302299
])
303300
);
304301
});
302+
303+
test('sqlite skipSeparatorBeginEnd', () => {
304+
const input = 'CREATE TRIGGER obj1 AFTER INSERT ON t1 FOR EACH ROW BEGIN SELECT * FROM t1; END';
305+
const output = splitQuery(input, {
306+
...sqliteSplitterOptions,
307+
});
308+
309+
expect(output.length).toBe(1);
310+
expect(output[0]).toEqual(input);
311+
});
312+
313+
test('sqlite skipSeparatorBeginEnd in lowercase', () => {
314+
const input = 'create trigger obj1 after insert on t1 for each row begin select * from t1; end';
315+
const output = splitQuery(input, {
316+
...sqliteSplitterOptions,
317+
});
318+
319+
expect(output.length).toBe(1);
320+
expect(output[0]).toEqual(input);
321+
});
322+
323+
test('sqlite skipSeparatorBeginEnd in TRANSACTION', () => {
324+
const input = `
325+
START TRANSACTION;
326+
327+
UPDATE accounts
328+
SET balance = balance - 100
329+
WHERE account_id = 1;
330+
331+
IF ROW_COUNT() > 0 THEN
332+
COMMIT;
333+
ELSE
334+
ROLLBACK;
335+
END IF;
336+
`;
337+
const output = splitQuery(input, {
338+
...sqliteSplitterOptions,
339+
});
340+
341+
expect(output.length).toBe(5);
342+
expect(output[0]).toEqual('START TRANSACTION');
343+
expect(output[1]).toEqual('UPDATE accounts\nSET balance = balance - 100\nWHERE account_id = 1');
344+
expect(output[2]).toEqual('IF ROW_COUNT() > 0 THEN\n COMMIT');
345+
expect(output[3]).toEqual('ELSE\n ROLLBACK');
346+
expect(output[4]).toEqual('END IF');
347+
});
348+
349+
test('sqlite skipSeparatorBeginEnd in TRANSACTION in lowercase', () => {
350+
const input = `
351+
start transaction;
352+
353+
update accounts
354+
set balance = balance - 100
355+
where account_id = 1;
356+
357+
if row_count() > 0 then
358+
commit;
359+
else
360+
rollback;
361+
end if;
362+
`;
363+
const output = splitQuery(input, {
364+
...sqliteSplitterOptions,
365+
});
366+
367+
expect(output.length).toBe(5);
368+
expect(output[0]).toEqual('start transaction');
369+
expect(output[1]).toEqual('update accounts\nset balance = balance - 100\nwhere account_id = 1');
370+
expect(output[2]).toEqual('if row_count() > 0 then\n commit');
371+
expect(output[3]).toEqual('else\n rollback');
372+
expect(output[4]).toEqual('end if');
373+
});

0 commit comments

Comments
 (0)