diff --git a/.gitignore b/.gitignore index 1002a47..e1b2ce3 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ yarn.lock lib/ node_modules/ webpack/ +mise.toml diff --git a/src/defines.ts b/src/defines.ts index 02eec84..f26ecda 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -71,10 +71,19 @@ export type StatementType = | 'ALTER_FUNCTION' | 'ALTER_INDEX' | 'ALTER_PROCEDURE' + | 'BEGIN_TRANSACTION' + | 'COMMIT' + | 'ROLLBACK' | 'ANON_BLOCK' | 'UNKNOWN'; -export type ExecutionType = 'LISTING' | 'MODIFICATION' | 'INFORMATION' | 'ANON_BLOCK' | 'UNKNOWN'; +export type ExecutionType = + | 'LISTING' + | 'MODIFICATION' + | 'INFORMATION' + | 'ANON_BLOCK' + | 'TRANSACTION' + | 'UNKNOWN'; export interface ParamTypes { positional?: boolean; diff --git a/src/parser.ts b/src/parser.ts index e44507c..b185904 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -87,6 +87,9 @@ export const EXECUTION_TYPES: Record = { ALTER_FUNCTION: 'MODIFICATION', ALTER_INDEX: 'MODIFICATION', ALTER_PROCEDURE: 'MODIFICATION', + BEGIN_TRANSACTION: 'TRANSACTION', + COMMIT: 'TRANSACTION', + ROLLBACK: 'TRANSACTION', UNKNOWN: 'UNKNOWN', ANON_BLOCK: 'ANON_BLOCK', }; @@ -342,7 +345,16 @@ function createStatementParserByToken( if (['bigquery', 'oracle'].includes(options.dialect) && nextToken.value !== 'TRANSACTION') { return createBlockStatementParser(options); } + return createBeginTransactionStatementParser(options); + case 'START': + if (nextToken.value === 'TRANSACTION') { + return createBeginTransactionStatementParser(options); + } break; + case 'COMMIT': + return createCommitStatementParser(options); + case 'ROLLBACK': + return createRollbackStatementParser(options); case 'DECLARE': if (options.dialect === 'oracle') { return createBlockStatementParser(options); @@ -709,6 +721,75 @@ function createShowStatementParser(options: ParseOptions) { return stateMachineStatementParser(statement, steps, options); } +function createBeginTransactionStatementParser(options: ParseOptions) { + const statement = createInitialStatement(); + + const steps: Step[] = [ + { + preCanGoToNext: () => false, + validation: { + acceptTokens: [ + { type: 'keyword', value: 'BEGIN' }, + { type: 'keyword', value: 'START' }, + ], + }, + add: (token) => { + statement.type = 'BEGIN_TRANSACTION'; + if (statement.start < 0) { + statement.start = token.start; + } + }, + postCanGoToNext: () => true, + }, + ]; + + return stateMachineStatementParser(statement, steps, options); +} + +function createCommitStatementParser(options: ParseOptions) { + const statement = createInitialStatement(); + + const steps: Step[] = [ + { + preCanGoToNext: () => false, + validation: { + acceptTokens: [{ type: 'keyword', value: 'COMMIT' }], + }, + add: (token) => { + statement.type = 'COMMIT'; + if (statement.start < 0) { + statement.start = token.start; + } + }, + postCanGoToNext: () => true, + }, + ]; + + return stateMachineStatementParser(statement, steps, options); +} + +function createRollbackStatementParser(options: ParseOptions) { + const statement = createInitialStatement(); + + const steps: Step[] = [ + { + preCanGoToNext: () => false, + validation: { + acceptTokens: [{ type: 'keyword', value: 'ROLLBACK' }], + }, + add: (token) => { + statement.type = 'ROLLBACK'; + if (statement.start < 0) { + statement.start = token.start; + } + }, + postCanGoToNext: () => true, + }, + ]; + + return stateMachineStatementParser(statement, steps, options); +} + function createUnknownStatementParser(options: ParseOptions) { const statement = createInitialStatement(); diff --git a/src/tokenizer.ts b/src/tokenizer.ts index fce2841..a21fb49 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -26,6 +26,15 @@ const KEYWORDS = [ 'AS', 'MATERIALIZED', 'BEGIN', + 'START', + 'COMMIT', + 'ROLLBACK', + 'TRANSACTION', + 'TRAN', + 'WORK', + 'DEFERRED', + 'IMMEDIATE', + 'EXCLUSIVE', 'DECLARE', 'CASE', 'LOOP', diff --git a/test/identifier/multiple-statement.spec.ts b/test/identifier/multiple-statement.spec.ts index 4e2c003..a55b620 100644 --- a/test/identifier/multiple-statement.spec.ts +++ b/test/identifier/multiple-statement.spec.ts @@ -394,8 +394,8 @@ describe('identifier', () => { start: 0, end: 17, text: statements[0], - type: 'UNKNOWN', - executionType: 'UNKNOWN', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', parameters: [], tables: [], }, @@ -412,8 +412,8 @@ describe('identifier', () => { start: 29, end: 35, text: statements[2], - type: 'UNKNOWN', - executionType: 'UNKNOWN', + type: 'COMMIT', + executionType: 'TRANSACTION', parameters: [], tables: [], }, @@ -432,8 +432,8 @@ describe('identifier', () => { start: 0, end: 17 + offset, text: statements[0], - type: 'UNKNOWN', - executionType: 'UNKNOWN', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', parameters: [], tables: [], }, @@ -450,8 +450,8 @@ describe('identifier', () => { start: 29 + offset, end: 35 + offset, text: statements[2], - type: 'UNKNOWN', - executionType: 'UNKNOWN', + type: 'COMMIT', + executionType: 'TRANSACTION', parameters: [], tables: [], }, diff --git a/test/index.spec.ts b/test/index.spec.ts index bf8be98..557cd86 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -115,6 +115,12 @@ describe('getExecutionType', () => { }); }); + ['BEGIN_TRANSACTION', 'COMMIT', 'ROLLBACK'].forEach((type) => { + it(`should return TRANSACTION for ${type}`, () => { + expect(getExecutionType(type)).to.equal('TRANSACTION'); + }); + }); + ['CREATE', 'DROP', 'ALTER'].forEach((action) => { ['DATABASE', 'SCHEMA', 'TABLE', 'VIEW', 'FUNCTION', 'TRIGGER'].forEach((type) => { it(`should return MODIFICATION for ${action}_${type}`, () => { @@ -159,3 +165,237 @@ describe('Regression tests', () => { }); }); }); + +describe('Transaction statements', () => { + it('should identify BEGIN TRANSACTION', () => { + expect(identify('BEGIN TRANSACTION', { strict: false })).to.eql([ + { + start: 0, + end: 16, + text: 'BEGIN TRANSACTION', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('should identify BEGIN without TRANSACTION keyword', () => { + expect(identify('BEGIN;', { strict: false })).to.eql([ + { + start: 0, + end: 5, + text: 'BEGIN;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('should identify START TRANSACTION', () => { + expect(identify('START TRANSACTION', { strict: false })).to.eql([ + { + start: 0, + end: 16, + text: 'START TRANSACTION', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('should identify COMMIT', () => { + expect(identify('COMMIT', { strict: false })).to.eql([ + { + start: 0, + end: 5, + text: 'COMMIT', + type: 'COMMIT', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('should identify ROLLBACK', () => { + expect(identify('ROLLBACK', { strict: false })).to.eql([ + { + start: 0, + end: 7, + text: 'ROLLBACK', + type: 'ROLLBACK', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('should still identify BEGIN as ANON_BLOCK for oracle/bigquery when not followed by TRANSACTION', () => { + expect(identify('BEGIN select 1; END;', { dialect: 'oracle' })).to.eql([ + { + start: 0, + end: 19, + text: 'BEGIN select 1; END;', + type: 'ANON_BLOCK', + executionType: 'ANON_BLOCK', + parameters: [], + tables: [], + }, + ]); + }); + + it('should not identify START REPLICA as a transaction', () => { + expect(() => identify('START REPLICA;', { dialect: 'mysql' })).to.throw( + `Invalid statement parser "START"`, + ); + }); + + it('Should identify ANSI-ish / generic transaction start syntaxes', () => { + expect(identify('START TRANSACTION;', { dialect: 'generic' })).to.eql([ + { + start: 0, + end: 17, + text: 'START TRANSACTION;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect(identify('BEGIN;', { dialect: 'generic' })).to.eql([ + { + start: 0, + end: 5, + text: 'BEGIN;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('Should identify MySQL/MariaDB style transaction start syntaxes', () => { + expect(identify('START TRANSACTION;', { dialect: 'mysql' })).to.eql([ + { + start: 0, + end: 17, + text: 'START TRANSACTION;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect(identify('BEGIN;', { dialect: 'mysql' })).to.eql([ + { + start: 0, + end: 5, + text: 'BEGIN;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect(identify('BEGIN WORK;', { dialect: 'mysql' })).to.eql([ + { + start: 0, + end: 10, + text: 'BEGIN WORK;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect(identify('START TRANSACTION READ ONLY;', { dialect: 'mysql' })).to.eql([ + { + start: 0, + end: 27, + text: 'START TRANSACTION READ ONLY;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect( + identify('START TRANSACTION ISOLATION LEVEL SERIALIZABLE;', { dialect: 'mysql' }), + ).to.eql([ + { + start: 0, + end: 46, + text: 'START TRANSACTION ISOLATION LEVEL SERIALIZABLE;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); + + it('Should identify Postgres style transaction start syntaxes', () => { + expect(identify('BEGIN;', { dialect: 'psql' })).to.eql([ + { + start: 0, + end: 5, + text: 'BEGIN;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect(identify('START TRANSACTION;', { dialect: 'psql' })).to.eql([ + { + start: 0, + end: 17, + text: 'START TRANSACTION;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect(identify('BEGIN TRANSACTION;', { dialect: 'psql' })).to.eql([ + { + start: 0, + end: 17, + text: 'BEGIN TRANSACTION;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + + expect( + identify('BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;', { dialect: 'psql' }), + ).to.eql([ + { + start: 0, + end: 49, + text: 'BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;', + type: 'BEGIN_TRANSACTION', + executionType: 'TRANSACTION', + parameters: [], + tables: [], + }, + ]); + }); +}); diff --git a/test/parser/bigquery.spec.ts b/test/parser/bigquery.spec.ts index d1af5a5..525a170 100644 --- a/test/parser/bigquery.spec.ts +++ b/test/parser/bigquery.spec.ts @@ -69,11 +69,11 @@ describe('Parser for bigquery', () => { expect(result.body[1].type).to.eql('SELECT'); }); - it('parses BEGIN TRANSACTION as UNKNOWN', () => { + it('parses BEGIN TRANSACTION as transaction statement', () => { const result = parse(`BEGIN TRANSACTION; SELECT 1; COMMIT;`, false, 'bigquery'); expect(result.body.length).to.eql(3); - expect(result.body[0].type).to.eql('UNKNOWN'); + expect(result.body[0].type).to.eql('BEGIN_TRANSACTION'); expect(result.body[1].type).to.eql('SELECT'); - expect(result.body[2].type).to.eql('UNKNOWN'); + expect(result.body[2].type).to.eql('COMMIT'); }); });