From 31092a9bd13107395251a31c7d583da1a9d7b465 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Sat, 28 May 2022 22:44:15 -0500 Subject: [PATCH 1/6] much simpler implementation that relies on existing blocks --- src/defines.ts | 3 +- src/parser.ts | 79 +++++++++++++++++- src/tokenizer.ts | 3 + test/index.spec.ts | 2 +- test/parser/oracle.spec.ts | 162 +++++++++++++++++++++++++++++++++++++ 5 files changed, 243 insertions(+), 6 deletions(-) create mode 100644 test/parser/oracle.spec.ts diff --git a/src/defines.ts b/src/defines.ts index 0d4b8d1..dc601b2 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -1,4 +1,4 @@ -export const DIALECTS = ['mssql', 'sqlite', 'mysql', 'psql', 'generic'] as const; +export const DIALECTS = ['mssql', 'sqlite', 'mysql', 'oracle', 'psql', 'generic'] as const; export type Dialect = typeof DIALECTS[number]; export type StatementType = | 'INSERT' @@ -27,6 +27,7 @@ export type StatementType = | 'ALTER_TRIGGER' | 'ALTER_FUNCTION' | 'ALTER_INDEX' + | 'ANON_BLOCK' | 'UNKNOWN'; export type ExecutionType = 'LISTING' | 'MODIFICATION' | 'UNKNOWN'; diff --git a/src/parser.ts b/src/parser.ts index 64e6312..4e128d7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -51,17 +51,28 @@ export const EXECUTION_TYPES: Record = { ALTER_FUNCTION: 'MODIFICATION', ALTER_INDEX: 'MODIFICATION', UNKNOWN: 'UNKNOWN', + ANON_BLOCK: 'UNKNOWN', }; -const statementsWithEnds = ['CREATE_TRIGGER', 'CREATE_FUNCTION']; +const statementsWithEnds = ['CREATE_TRIGGER', 'CREATE_FUNCTION', 'ANON_BLOCK']; const blockOpeners: Record = { generic: ['BEGIN', 'CASE'], psql: ['BEGIN', 'CASE', 'LOOP', 'IF'], mysql: ['BEGIN', 'CASE', 'LOOP', 'IF'], mssql: ['BEGIN', 'CASE'], sqlite: ['BEGIN', 'CASE'], + oracle: ['DECLARE', 'BEGIN', 'CASE'], }; +const statementBlockOpeners: Record = { + generic: [], + psql: [], + mysql: [], + mssql: [], + sqlite: [], + oracle: [], +} + interface ParseOptions { isStrict: boolean; dialect: Dialect; @@ -193,6 +204,7 @@ export function parse(input: string, isStrict = true, dialect: Dialect = 'generi const statement = statementParser.getStatement(); if (statement.endStatement) { + console.log('statement.end', token) statement.end = token.end; topLevelStatement.body.push(statement as ConcreteStatement); statementParser = null; @@ -250,6 +262,12 @@ function createStatementParserByToken(token: Token, options: ParseOptions): Stat return createDeleteStatementParser(options); case 'TRUNCATE': return createTruncateStatementParser(options); + case 'DECLARE': + case 'BEGIN': + if (options.dialect === 'oracle') { + return createBlockStatementParser(options); + } + // eslint-disable-next-line no-fallthrough default: break; } @@ -285,6 +303,34 @@ function createSelectStatementParser(options: ParseOptions) { return stateMachineStatementParser(statement, steps, options); } +function createBlockStatementParser(options: ParseOptions) { + console.log('creating block statement parser!') + const statement = createInitialStatement(); + statement.type = 'ANON_BLOCK'; + + const steps: Step[] = [ + // Select + { + preCanGoToNext: () => false, + validation: { + acceptTokens: [ + { type: 'keyword', value: 'DECLARE' }, + { type: 'keyword', value: 'BEGIN' }, + ], + }, + add: (token) => { + console.log('ADD', token) + if (statement.start < 0) { + statement.start = token.start; + } + }, + postCanGoToNext: () => true, + }, + ]; + + return stateMachineStatementParser(statement, steps, options); +} + function createInsertStatementParser(options: ParseOptions) { const statement = createInitialStatement(); @@ -540,6 +586,9 @@ function stateMachineStatementParser( let prevToken: Token; let prevPrevToken: Token; + let lastBlockOpener: Token; + let anonBlockStarted = false; + let openBlocks = 0; /* eslint arrow-body-style: 0, no-extra-parens: 0 */ @@ -574,6 +623,7 @@ function stateMachineStatementParser( throw new Error('This statement has already got to the end.'); } + console.log("addToken token, openblocks", token.value, openBlocks); if ( statement.type && token.type === 'semicolon' && @@ -597,14 +647,35 @@ function stateMachineStatementParser( return; } + console.log('block check:'); if ( token.type === 'keyword' && blockOpeners[dialect].includes(token.value) && - prevPrevToken.value.toUpperCase() !== 'END' + prevPrevToken?.value.toUpperCase() !== 'END' ) { + if ( + dialect === 'oracle' && + lastBlockOpener?.value === 'DECLARE' && + token.value.toUpperCase() === 'BEGIN' + ) { + console.log('---> oracle && follows DECLARE'); + // don't open a new block! + setPrevToken(token); + lastBlockOpener = token; + return; + } + console.log('---> incrementing open blocks', statement.type, anonBlockStarted); openBlocks++; + lastBlockOpener = token; setPrevToken(token); - return; + if (statement.type === 'ANON_BLOCK' && !anonBlockStarted) { + console.log('---> not returning, setting anonBlockStarted'); + anonBlockStarted = true; + // don't return + } else { + console.log('---> normal block, returning'); + return; + } } if ( @@ -614,7 +685,7 @@ function stateMachineStatementParser( statement.parameters.push(token.value); } - if (statement.type) { + if (statement.type && statement.start >= 0) { // statement has already been identified // just wait until end of the statement return; diff --git a/src/tokenizer.ts b/src/tokenizer.ts index 3927265..79dc64e 100644 --- a/src/tokenizer.ts +++ b/src/tokenizer.ts @@ -25,6 +25,9 @@ const KEYWORDS = [ 'WITH', 'AS', 'MATERIALIZED', + 'BEGIN', + 'DECLARE', + 'CASE', ]; const INDIVIDUALS: Record = { diff --git a/test/index.spec.ts b/test/index.spec.ts index 5d27a55..5ef8026 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; describe('identify', () => { it('should throw error for invalid dialect', () => { expect(() => identify('SELECT * FROM foo', { dialect: 'invalid' as Dialect })).to.throw( - 'Unknown dialect. Allowed values: mssql, sqlite, mysql, psql, generic', + 'Unknown dialect. Allowed values: mssql, sqlite, mysql, oracle, psql, generic', ); }); diff --git a/test/parser/oracle.spec.ts b/test/parser/oracle.spec.ts new file mode 100644 index 0000000..9c56fd5 --- /dev/null +++ b/test/parser/oracle.spec.ts @@ -0,0 +1,162 @@ +import { parse } from '../../src/parser'; +import { expect } from 'chai'; + +describe('Parser for oracle', () => { + describe('Given a CASE Statement', () => { + it('should parse a simple case statement', () => { + const sql = `SELECT CASE WHEN a = 'a' THEN 'foo' ELSE 'bar' END CASE from table;`; + const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eql(1); + }); + }); + + describe('given an anonymous block with an OUT pram', () => { + it('should treat a simple block as a single query', () => { + const sql = `BEGIN + SELECT + cols.column_name INTO :variable + FROM + example_table; + END`; + const result = parse(sql, false, 'oracle'); + console.log(result) + expect(result.body[0].type).to.eq('ANON_BLOCK'); + expect(result.body[0].start).to.eq(0); + expect(result.body[0].end).to.eq(119); + expect(result.body.length).to.eql(1); + }); + + it('should easily identify two blocks', () => { + const sql = `BEGIN + SELECT + cols.column_name INTO :variable + FROM + example_table; + END; + + BEGIN + SELECT + cols.column_name INTO :variable + FROM + example_table; + END + `; + const result = parse(sql, false, 'oracle'); + + expect(result.body[0].type).to.eq('ANON_BLOCK'); + expect(result.body[0].start).to.eq(0); + expect(result.body[0].end).to.eq(120); + expect(result.body.length).to.eql(2); + expect(result.body[1].start).to.eq(131); + }); + + it('should identify a block query and a normal query together', () => { + const sql = `BEGIN + SELECT + cols.column_name INTO :variable + FROM + example_table; + END; + + select * from another_thing + `; + const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eql(2); + expect(result.body[0].start).to.eq(0); + expect(result.body[0].end).to.eq(98); + expect(result.body[1].start).to.eq(107); + }); + }); + describe('given an anonymous block with a variable', () => { + it('should treat a block with DECLARE and another query as two separate queries', () => { + const sql = `DECLARE + PK_NAME VARCHAR(200); + BEGIN + SELECT + cols.column_name INTO PK_NAME + FROM + example_table; + END; + + select * from foo; + `; + const result = parse(sql, false, 'oracle'); + console.log(result); + expect(result.body.length).to.eql(2); + expect(result.body[0].start).to.eq(0); + expect(result.body[0].end).to.eq(166); + }); + + it('Should treat a block with two queries as a single query', () => { + const sql = ` + DECLARE + PK_NAME VARCHAR(200); + FOO integer; + + BEGIN + SELECT + cols.column_name INTO PK_NAME + FROM + example_table; + SELECT 1 INTO FOO from other_example; + END; + `; + const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eql(1); + }); + + it('Should treat a complex block as a single query', () => { + const sql = ` + DECLARE + PK_NAME VARCHAR(200); + + BEGIN + EXECUTE IMMEDIATE ('CREATE SEQUENCE "untitled_table3_seq"'); + + SELECT + cols.column_name INTO PK_NAME + FROM + all_constraints cons, + all_cons_columns cols + WHERE + cons.constraint_type = 'P' + AND cons.constraint_name = cols.constraint_name + AND cons.owner = cols.owner + AND cols.table_name = 'untitled_table3'; + + execute immediate ( + 'create or replace trigger "untitled_table3_autoinc_trg" BEFORE INSERT on "untitled_table3" for each row declare checking number := 1; begin if (:new."' || PK_NAME || '" is null) then while checking >= 1 loop select "untitled_table3_seq".nextval into :new."' || PK_NAME || '" from dual; select count("' || PK_NAME || '") into checking from "untitled_table3" where "' || PK_NAME || '" = :new."' || PK_NAME || '"; end loop; end if; end;' + ); + END; + `; + const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eql(1); + }); + + it('should identify a compound statement with a nested compound statement as a single statement', () => { + const sql = `DECLARE + n_emp_id EMPLOYEES.EMPLOYEE_ID%TYPE := &emp_id1; + BEGIN + DECLARE + n_emp_id employees.employee_id%TYPE := &emp_id2; + v_name employees.first_name%TYPE; + BEGIN + SELECT first_name, CASE foo WHEN 'a' THEN 1 ELSE 2 END CASE as other + INTO v_name + FROM employees + WHERE employee_id = n_emp_id; + + DBMS_OUTPUT.PUT_LINE('First name of employee ' || n_emp_id || + ' is ' || v_name); + EXCEPTION + WHEN no_data_found THEN + DBMS_OUTPUT.PUT_LINE('Employee ' || n_emp_id || ' not found'); + END; + END;`; + // yes this is still just one statement. + const result = parse(sql, false, 'oracle'); + console.log(result); + expect(result.body.length).to.eql(1); + }); + }); +}); From d6bba1f0c4c16e9a3c085e2cd94e848e267bf849 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Sat, 28 May 2022 22:46:47 -0500 Subject: [PATCH 2/6] lint fixes --- src/parser.ts | 18 ------------------ test/parser/oracle.spec.ts | 3 --- 2 files changed, 21 deletions(-) diff --git a/src/parser.ts b/src/parser.ts index 4e128d7..e3c6959 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -64,15 +64,6 @@ const blockOpeners: Record = { oracle: ['DECLARE', 'BEGIN', 'CASE'], }; -const statementBlockOpeners: Record = { - generic: [], - psql: [], - mysql: [], - mssql: [], - sqlite: [], - oracle: [], -} - interface ParseOptions { isStrict: boolean; dialect: Dialect; @@ -204,7 +195,6 @@ export function parse(input: string, isStrict = true, dialect: Dialect = 'generi const statement = statementParser.getStatement(); if (statement.endStatement) { - console.log('statement.end', token) statement.end = token.end; topLevelStatement.body.push(statement as ConcreteStatement); statementParser = null; @@ -304,7 +294,6 @@ function createSelectStatementParser(options: ParseOptions) { } function createBlockStatementParser(options: ParseOptions) { - console.log('creating block statement parser!') const statement = createInitialStatement(); statement.type = 'ANON_BLOCK'; @@ -319,7 +308,6 @@ function createBlockStatementParser(options: ParseOptions) { ], }, add: (token) => { - console.log('ADD', token) if (statement.start < 0) { statement.start = token.start; } @@ -623,7 +611,6 @@ function stateMachineStatementParser( throw new Error('This statement has already got to the end.'); } - console.log("addToken token, openblocks", token.value, openBlocks); if ( statement.type && token.type === 'semicolon' && @@ -647,7 +634,6 @@ function stateMachineStatementParser( return; } - console.log('block check:'); if ( token.type === 'keyword' && blockOpeners[dialect].includes(token.value) && @@ -658,22 +644,18 @@ function stateMachineStatementParser( lastBlockOpener?.value === 'DECLARE' && token.value.toUpperCase() === 'BEGIN' ) { - console.log('---> oracle && follows DECLARE'); // don't open a new block! setPrevToken(token); lastBlockOpener = token; return; } - console.log('---> incrementing open blocks', statement.type, anonBlockStarted); openBlocks++; lastBlockOpener = token; setPrevToken(token); if (statement.type === 'ANON_BLOCK' && !anonBlockStarted) { - console.log('---> not returning, setting anonBlockStarted'); anonBlockStarted = true; // don't return } else { - console.log('---> normal block, returning'); return; } } diff --git a/test/parser/oracle.spec.ts b/test/parser/oracle.spec.ts index 9c56fd5..3bafc89 100644 --- a/test/parser/oracle.spec.ts +++ b/test/parser/oracle.spec.ts @@ -19,7 +19,6 @@ describe('Parser for oracle', () => { example_table; END`; const result = parse(sql, false, 'oracle'); - console.log(result) expect(result.body[0].type).to.eq('ANON_BLOCK'); expect(result.body[0].start).to.eq(0); expect(result.body[0].end).to.eq(119); @@ -81,7 +80,6 @@ describe('Parser for oracle', () => { select * from foo; `; const result = parse(sql, false, 'oracle'); - console.log(result); expect(result.body.length).to.eql(2); expect(result.body[0].start).to.eq(0); expect(result.body[0].end).to.eq(166); @@ -155,7 +153,6 @@ describe('Parser for oracle', () => { END;`; // yes this is still just one statement. const result = parse(sql, false, 'oracle'); - console.log(result); expect(result.body.length).to.eql(1); }); }); From 6f255927cb65738077953fa70af095ae6e23196b Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Sun, 29 May 2022 22:32:06 -0500 Subject: [PATCH 3/6] added an indentification test --- test/identifier/multiple-statement.spec.ts | 57 ++++++++++++++++++++++ test/parser/oracle.spec.ts | 34 +++++++++++++ 2 files changed, 91 insertions(+) diff --git a/test/identifier/multiple-statement.spec.ts b/test/identifier/multiple-statement.spec.ts index 90cfa56..33e8ac0 100644 --- a/test/identifier/multiple-statement.spec.ts +++ b/test/identifier/multiple-statement.spec.ts @@ -127,6 +127,63 @@ describe('identifier', () => { expect(actual).to.eql(expected); }); + describe('identifying statements with anonymous blocks', () => { + it('should identify a create table then a block', () => { + const actual = identify( + ` + create table + "untitled_table8" ( + "id" integer not null primary key, + "created_at" varchar(255) not null + ); + + DECLARE + PK_NAME VARCHAR(200); + + BEGIN + EXECUTE IMMEDIATE ('CREATE SEQUENCE "untitled_table8_seq"'); + + SELECT + cols.column_name INTO PK_NAME + FROM + all_constraints cons, + all_cons_columns cols + WHERE + cons.constraint_type = 'P' + AND cons.constraint_name = cols.constraint_name + AND cons.owner = cols.owner + AND cols.table_name = 'untitled_table8'; + + execute immediate ( + 'create or replace trigger "untitled_table8_autoinc_trg" BEFORE INSERT on "untitled_table8" for each row declare checking number := 1; begin if (:new."' || PK_NAME || '" is null) then while checking >= 1 loop select "untitled_table8_seq".nextval into :new."' || PK_NAME || '" from dual; select count("' || PK_NAME || '") into checking from "untitled_table8" where "' || PK_NAME || '" = :new."' || PK_NAME || '"; end loop; end if; end;' + ); + + END; + `, + { dialect: 'oracle', strict: false }, + ); + const expected = [ + { + end: 167, + executionType: 'MODIFICATION', + parameters: [], + start: 11, + text: 'create table\n "untitled_table8" (\n "id" integer not null primary key,\n "created_at" varchar(255) not null\n );', + type: 'CREATE_TABLE', + }, + { + end: 1212, + executionType: 'UNKNOWN', + parameters: [], + start: 180, + text: 'DECLARE\n PK_NAME VARCHAR(200);\n\n BEGIN\n EXECUTE IMMEDIATE (\'CREATE SEQUENCE "untitled_table8_seq"\');\n\n SELECT\n cols.column_name INTO PK_NAME\n FROM\n all_constraints cons,\n all_cons_columns cols\n WHERE\n cons.constraint_type = \'P\'\n AND cons.constraint_name = cols.constraint_name\n AND cons.owner = cols.owner\n AND cols.table_name = \'untitled_table8\';\n\n execute immediate (\n \'create or replace trigger "untitled_table8_autoinc_trg" BEFORE INSERT on "untitled_table8" for each row declare checking number := 1; begin if (:new."\' || PK_NAME || \'" is null) then while checking >= 1 loop select "untitled_table8_seq".nextval into :new."\' || PK_NAME || \'" from dual; select count("\' || PK_NAME || \'") into checking from "untitled_table8" where "\' || PK_NAME || \'" = :new."\' || PK_NAME || \'"; end loop; end if; end;\'\n );\n\n END;', + type: 'ANON_BLOCK', + }, + ]; + expect(actual).to.eql(expected); + }); + }); + describe('identifying multiple statements with CTEs', () => { it('should able to detect queries with a CTE in middle query', () => { const actual = identify( diff --git a/test/parser/oracle.spec.ts b/test/parser/oracle.spec.ts index 3bafc89..352712f 100644 --- a/test/parser/oracle.spec.ts +++ b/test/parser/oracle.spec.ts @@ -155,5 +155,39 @@ describe('Parser for oracle', () => { const result = parse(sql, false, 'oracle'); expect(result.body.length).to.eql(1); }); + it('should identify a block query after a create table query', () => { + const sql = `create table + "untitled_table8" ( + "id" integer not null primary key, + "created_at" varchar(255) not null + ); + + DECLARE + PK_NAME VARCHAR(200); + + BEGIN + EXECUTE IMMEDIATE ('CREATE SEQUENCE "untitled_table8_seq"'); + + SELECT + cols.column_name INTO PK_NAME + FROM + all_constraints cons, + all_cons_columns cols + WHERE + cons.constraint_type = 'P' + AND cons.constraint_name = cols.constraint_name + AND cons.owner = cols.owner + AND cols.table_name = 'untitled_table8'; + + execute immediate ( + 'create or replace trigger "untitled_table8_autoinc_trg" BEFORE INSERT on "untitled_table8" for each row declare checking number := 1; begin if (:new."' || PK_NAME || '" is null) then while checking >= 1 loop select "untitled_table8_seq".nextval into :new."' || PK_NAME || '" from dual; select count("' || PK_NAME || '") into checking from "untitled_table8" where "' || PK_NAME || '" = :new."' || PK_NAME || '"; end loop; end if; end;' + ); + + END;`; + const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eq(2); + expect(result.body[0].type).to.eq('CREATE_TABLE'); + expect(result.body[1].type).to.eq('ANON_BLOCK'); + }); }); }); From 956c1ae994a74722eab89f8348a87f3ef5df963d Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Mon, 13 Jun 2022 10:20:24 -0500 Subject: [PATCH 4/6] tidying up tests --- test/parser/oracle.spec.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/test/parser/oracle.spec.ts b/test/parser/oracle.spec.ts index 352712f..4b0402d 100644 --- a/test/parser/oracle.spec.ts +++ b/test/parser/oracle.spec.ts @@ -19,10 +19,10 @@ describe('Parser for oracle', () => { example_table; END`; const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eql(1); expect(result.body[0].type).to.eq('ANON_BLOCK'); expect(result.body[0].start).to.eq(0); expect(result.body[0].end).to.eq(119); - expect(result.body.length).to.eql(1); }); it('should easily identify two blocks', () => { @@ -42,11 +42,13 @@ describe('Parser for oracle', () => { `; const result = parse(sql, false, 'oracle'); + expect(result.body.length).to.eql(2); expect(result.body[0].type).to.eq('ANON_BLOCK'); expect(result.body[0].start).to.eq(0); expect(result.body[0].end).to.eq(120); - expect(result.body.length).to.eql(2); + expect(result.body[1].type).to.eq('ANON_BLOCK'); expect(result.body[1].start).to.eq(131); + expect(result.body[1].end).to.eq(259); }); it('should identify a block query and a normal query together', () => { @@ -61,8 +63,10 @@ describe('Parser for oracle', () => { `; const result = parse(sql, false, 'oracle'); expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eq('ANON_BLOCK'); expect(result.body[0].start).to.eq(0); expect(result.body[0].end).to.eq(98); + expect(result.body[1].type).to.eq('SELECT'); expect(result.body[1].start).to.eq(107); }); }); @@ -81,8 +85,11 @@ describe('Parser for oracle', () => { `; const result = parse(sql, false, 'oracle'); expect(result.body.length).to.eql(2); + expect(result.body[0].type).to.eq('ANON_BLOCK'); expect(result.body[0].start).to.eq(0); expect(result.body[0].end).to.eq(166); + expect(result.body[1].type).to.eq('SELECT'); + expect(result.body[1].start).to.eq(177); }); it('Should treat a block with two queries as a single query', () => { From 9cccd7327cf2eee962e613d41095be4c4766ad74 Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Mon, 13 Jun 2022 10:30:27 -0500 Subject: [PATCH 5/6] adding the anon_block execution type and including in readme updates --- README.md | 4 +- src/defines.ts | 2 +- src/parser.ts | 2 +- test/identifier/multiple-statement.spec.ts | 43 +++++++++++++++++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 831f55f..789e41c 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,7 @@ This way you have sure is a valid query before trying to identify the types. * ALTER_TRIGGER * ALTER_FUNCTION * ALTER_INDEX +* ANON_BLOCK (Oracle Database only) * UNKNOWN (only available if strict mode is disabled) ## Execution types @@ -64,6 +65,7 @@ Execution types allow to know what is the query behavior * `LISTING:` is when the query list the data * `MODIFICATION:` is when the query modificate the database somehow (structure or data) * `INFORMATION:` is show some data information such as a profile data +* `ANON_BLOCK: ` is for a anonymous block query containing multiple statements of unknown type (Oracle Database only) * `UNKNOWN`: (only available if strict mode is disabled) ## Installation @@ -112,7 +114,7 @@ console.log(statements); 1. `input (string)`: the whole SQL script text to be processed 1. `options (object)`: allow to set different configurations 1. `strict (bool)`: allow disable strict mode which will ignore unknown types *(default=true)* - 2. `dialect (string)`: Specify your database dialect, values: `generic`, `mysql`, `psql`, `sqlite` and `mssql`. *(default=generic)* + 2. `dialect (string)`: Specify your database dialect, values: `generic`, `mysql`, `oracle`, `psql`, `sqlite` and `mssql`. *(default=generic)* ## Contributing diff --git a/src/defines.ts b/src/defines.ts index dc601b2..558f12f 100644 --- a/src/defines.ts +++ b/src/defines.ts @@ -30,7 +30,7 @@ export type StatementType = | 'ANON_BLOCK' | 'UNKNOWN'; -export type ExecutionType = 'LISTING' | 'MODIFICATION' | 'UNKNOWN'; +export type ExecutionType = 'LISTING' | 'MODIFICATION' | 'ANON_BLOCK' | 'UNKNOWN'; export interface IdentifyOptions { strict?: boolean; diff --git a/src/parser.ts b/src/parser.ts index e3c6959..d8a6d3a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -51,7 +51,7 @@ export const EXECUTION_TYPES: Record = { ALTER_FUNCTION: 'MODIFICATION', ALTER_INDEX: 'MODIFICATION', UNKNOWN: 'UNKNOWN', - ANON_BLOCK: 'UNKNOWN', + ANON_BLOCK: 'ANON_BLOCK', }; const statementsWithEnds = ['CREATE_TRIGGER', 'CREATE_FUNCTION', 'ANON_BLOCK']; diff --git a/test/identifier/multiple-statement.spec.ts b/test/identifier/multiple-statement.spec.ts index 33e8ac0..0dbca16 100644 --- a/test/identifier/multiple-statement.spec.ts +++ b/test/identifier/multiple-statement.spec.ts @@ -128,6 +128,47 @@ describe('identifier', () => { }); describe('identifying statements with anonymous blocks', () => { + it('should work in strict mode', () => { + const actual = identify( + ` + DECLARE + PK_NAME VARCHAR(200); + + BEGIN + EXECUTE IMMEDIATE ('CREATE SEQUENCE "untitled_table8_seq"'); + + SELECT + cols.column_name INTO PK_NAME + FROM + all_constraints cons, + all_cons_columns cols + WHERE + cons.constraint_type = 'P' + AND cons.constraint_name = cols.constraint_name + AND cons.owner = cols.owner + AND cols.table_name = 'untitled_table8'; + + execute immediate ( + 'create or replace trigger "untitled_table8_autoinc_trg" BEFORE INSERT on "untitled_table8" for each row declare checking number := 1; begin if (:new."' || PK_NAME || '" is null) then while checking >= 1 loop select "untitled_table8_seq".nextval into :new."' || PK_NAME || '" from dual; select count("' || PK_NAME || '") into checking from "untitled_table8" where "' || PK_NAME || '" = :new."' || PK_NAME || '"; end loop; end if; end;' + ); + + END; + `, + { dialect: 'oracle', strict: true }, + ); + const expected = [ + { + end: 1043, + executionType: 'ANON_BLOCK', + parameters: [], + start: 11, + text: 'DECLARE\n PK_NAME VARCHAR(200);\n\n BEGIN\n EXECUTE IMMEDIATE (\'CREATE SEQUENCE "untitled_table8_seq"\');\n\n SELECT\n cols.column_name INTO PK_NAME\n FROM\n all_constraints cons,\n all_cons_columns cols\n WHERE\n cons.constraint_type = \'P\'\n AND cons.constraint_name = cols.constraint_name\n AND cons.owner = cols.owner\n AND cols.table_name = \'untitled_table8\';\n\n execute immediate (\n \'create or replace trigger "untitled_table8_autoinc_trg" BEFORE INSERT on "untitled_table8" for each row declare checking number := 1; begin if (:new."\' || PK_NAME || \'" is null) then while checking >= 1 loop select "untitled_table8_seq".nextval into :new."\' || PK_NAME || \'" from dual; select count("\' || PK_NAME || \'") into checking from "untitled_table8" where "\' || PK_NAME || \'" = :new."\' || PK_NAME || \'"; end loop; end if; end;\'\n );\n\n END;', + type: 'ANON_BLOCK', + }, + ]; + expect(actual).to.eql(expected); + }); + it('should identify a create table then a block', () => { const actual = identify( ` @@ -173,7 +214,7 @@ describe('identifier', () => { }, { end: 1212, - executionType: 'UNKNOWN', + executionType: 'ANON_BLOCK', parameters: [], start: 180, text: 'DECLARE\n PK_NAME VARCHAR(200);\n\n BEGIN\n EXECUTE IMMEDIATE (\'CREATE SEQUENCE "untitled_table8_seq"\');\n\n SELECT\n cols.column_name INTO PK_NAME\n FROM\n all_constraints cons,\n all_cons_columns cols\n WHERE\n cons.constraint_type = \'P\'\n AND cons.constraint_name = cols.constraint_name\n AND cons.owner = cols.owner\n AND cols.table_name = \'untitled_table8\';\n\n execute immediate (\n \'create or replace trigger "untitled_table8_autoinc_trg" BEFORE INSERT on "untitled_table8" for each row declare checking number := 1; begin if (:new."\' || PK_NAME || \'" is null) then while checking >= 1 loop select "untitled_table8_seq".nextval into :new."\' || PK_NAME || \'" from dual; select count("\' || PK_NAME || \'") into checking from "untitled_table8" where "\' || PK_NAME || \'" = :new."\' || PK_NAME || \'"; end loop; end if; end;\'\n );\n\n END;', From 9a9e25f45a637bdf34adaea191298a7d938e309a Mon Sep 17 00:00:00 2001 From: Matthew Rathbone Date: Mon, 13 Jun 2022 10:36:00 -0500 Subject: [PATCH 6/6] english --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 789e41c..d8e26ac 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ Execution types allow to know what is the query behavior * `LISTING:` is when the query list the data * `MODIFICATION:` is when the query modificate the database somehow (structure or data) * `INFORMATION:` is show some data information such as a profile data -* `ANON_BLOCK: ` is for a anonymous block query containing multiple statements of unknown type (Oracle Database only) +* `ANON_BLOCK: ` is for an anonymous block query which may contain multiple statements of unknown type (Oracle Database only) * `UNKNOWN`: (only available if strict mode is disabled) ## Installation