Skip to content

Commit 334ffa5

Browse files
authored
Support anonymous blocks for new Oracle dialect (#53)
1 parent e215fd2 commit 334ffa5

File tree

7 files changed

+365
-8
lines changed

7 files changed

+365
-8
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ This way you have sure is a valid query before trying to identify the types.
5555
* ALTER_TRIGGER
5656
* ALTER_FUNCTION
5757
* ALTER_INDEX
58+
* ANON_BLOCK (Oracle Database only)
5859
* UNKNOWN (only available if strict mode is disabled)
5960

6061
## Execution types
@@ -64,6 +65,7 @@ Execution types allow to know what is the query behavior
6465
* `LISTING:` is when the query list the data
6566
* `MODIFICATION:` is when the query modificate the database somehow (structure or data)
6667
* `INFORMATION:` is show some data information such as a profile data
68+
* `ANON_BLOCK: ` is for an anonymous block query which may contain multiple statements of unknown type (Oracle Database only)
6769
* `UNKNOWN`: (only available if strict mode is disabled)
6870

6971
## Installation
@@ -112,7 +114,7 @@ console.log(statements);
112114
1. `input (string)`: the whole SQL script text to be processed
113115
1. `options (object)`: allow to set different configurations
114116
1. `strict (bool)`: allow disable strict mode which will ignore unknown types *(default=true)*
115-
2. `dialect (string)`: Specify your database dialect, values: `generic`, `mysql`, `psql`, `sqlite` and `mssql`. *(default=generic)*
117+
2. `dialect (string)`: Specify your database dialect, values: `generic`, `mysql`, `oracle`, `psql`, `sqlite` and `mssql`. *(default=generic)*
116118

117119
## Contributing
118120

src/defines.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const DIALECTS = ['mssql', 'sqlite', 'mysql', 'psql', 'generic'] as const;
1+
export const DIALECTS = ['mssql', 'sqlite', 'mysql', 'oracle', 'psql', 'generic'] as const;
22
export type Dialect = typeof DIALECTS[number];
33
export type StatementType =
44
| 'INSERT'
@@ -27,9 +27,10 @@ export type StatementType =
2727
| 'ALTER_TRIGGER'
2828
| 'ALTER_FUNCTION'
2929
| 'ALTER_INDEX'
30+
| 'ANON_BLOCK'
3031
| 'UNKNOWN';
3132

32-
export type ExecutionType = 'LISTING' | 'MODIFICATION' | 'UNKNOWN';
33+
export type ExecutionType = 'LISTING' | 'MODIFICATION' | 'ANON_BLOCK' | 'UNKNOWN';
3334

3435
export interface IdentifyOptions {
3536
strict?: boolean;

src/parser.ts

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,17 @@ export const EXECUTION_TYPES: Record<StatementType, ExecutionType> = {
5151
ALTER_FUNCTION: 'MODIFICATION',
5252
ALTER_INDEX: 'MODIFICATION',
5353
UNKNOWN: 'UNKNOWN',
54+
ANON_BLOCK: 'ANON_BLOCK',
5455
};
5556

56-
const statementsWithEnds = ['CREATE_TRIGGER', 'CREATE_FUNCTION'];
57+
const statementsWithEnds = ['CREATE_TRIGGER', 'CREATE_FUNCTION', 'ANON_BLOCK'];
5758
const blockOpeners: Record<Dialect, string[]> = {
5859
generic: ['BEGIN', 'CASE'],
5960
psql: ['BEGIN', 'CASE', 'LOOP', 'IF'],
6061
mysql: ['BEGIN', 'CASE', 'LOOP', 'IF'],
6162
mssql: ['BEGIN', 'CASE'],
6263
sqlite: ['BEGIN', 'CASE'],
64+
oracle: ['DECLARE', 'BEGIN', 'CASE'],
6365
};
6466

6567
interface ParseOptions {
@@ -250,6 +252,12 @@ function createStatementParserByToken(token: Token, options: ParseOptions): Stat
250252
return createDeleteStatementParser(options);
251253
case 'TRUNCATE':
252254
return createTruncateStatementParser(options);
255+
case 'DECLARE':
256+
case 'BEGIN':
257+
if (options.dialect === 'oracle') {
258+
return createBlockStatementParser(options);
259+
}
260+
// eslint-disable-next-line no-fallthrough
253261
default:
254262
break;
255263
}
@@ -285,6 +293,32 @@ function createSelectStatementParser(options: ParseOptions) {
285293
return stateMachineStatementParser(statement, steps, options);
286294
}
287295

296+
function createBlockStatementParser(options: ParseOptions) {
297+
const statement = createInitialStatement();
298+
statement.type = 'ANON_BLOCK';
299+
300+
const steps: Step[] = [
301+
// Select
302+
{
303+
preCanGoToNext: () => false,
304+
validation: {
305+
acceptTokens: [
306+
{ type: 'keyword', value: 'DECLARE' },
307+
{ type: 'keyword', value: 'BEGIN' },
308+
],
309+
},
310+
add: (token) => {
311+
if (statement.start < 0) {
312+
statement.start = token.start;
313+
}
314+
},
315+
postCanGoToNext: () => true,
316+
},
317+
];
318+
319+
return stateMachineStatementParser(statement, steps, options);
320+
}
321+
288322
function createInsertStatementParser(options: ParseOptions) {
289323
const statement = createInitialStatement();
290324

@@ -540,6 +574,9 @@ function stateMachineStatementParser(
540574
let prevToken: Token;
541575
let prevPrevToken: Token;
542576

577+
let lastBlockOpener: Token;
578+
let anonBlockStarted = false;
579+
543580
let openBlocks = 0;
544581

545582
/* eslint arrow-body-style: 0, no-extra-parens: 0 */
@@ -600,11 +637,27 @@ function stateMachineStatementParser(
600637
if (
601638
token.type === 'keyword' &&
602639
blockOpeners[dialect].includes(token.value) &&
603-
prevPrevToken.value.toUpperCase() !== 'END'
640+
prevPrevToken?.value.toUpperCase() !== 'END'
604641
) {
642+
if (
643+
dialect === 'oracle' &&
644+
lastBlockOpener?.value === 'DECLARE' &&
645+
token.value.toUpperCase() === 'BEGIN'
646+
) {
647+
// don't open a new block!
648+
setPrevToken(token);
649+
lastBlockOpener = token;
650+
return;
651+
}
605652
openBlocks++;
653+
lastBlockOpener = token;
606654
setPrevToken(token);
607-
return;
655+
if (statement.type === 'ANON_BLOCK' && !anonBlockStarted) {
656+
anonBlockStarted = true;
657+
// don't return
658+
} else {
659+
return;
660+
}
608661
}
609662

610663
if (
@@ -614,7 +667,7 @@ function stateMachineStatementParser(
614667
statement.parameters.push(token.value);
615668
}
616669

617-
if (statement.type) {
670+
if (statement.type && statement.start >= 0) {
618671
// statement has already been identified
619672
// just wait until end of the statement
620673
return;

src/tokenizer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const KEYWORDS = [
2525
'WITH',
2626
'AS',
2727
'MATERIALIZED',
28+
'BEGIN',
29+
'DECLARE',
30+
'CASE',
2831
];
2932

3033
const INDIVIDUALS: Record<string, Token['type']> = {

test/identifier/multiple-statement.spec.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,104 @@ describe('identifier', () => {
127127
expect(actual).to.eql(expected);
128128
});
129129

130+
describe('identifying statements with anonymous blocks', () => {
131+
it('should work in strict mode', () => {
132+
const actual = identify(
133+
`
134+
DECLARE
135+
PK_NAME VARCHAR(200);
136+
137+
BEGIN
138+
EXECUTE IMMEDIATE ('CREATE SEQUENCE "untitled_table8_seq"');
139+
140+
SELECT
141+
cols.column_name INTO PK_NAME
142+
FROM
143+
all_constraints cons,
144+
all_cons_columns cols
145+
WHERE
146+
cons.constraint_type = 'P'
147+
AND cons.constraint_name = cols.constraint_name
148+
AND cons.owner = cols.owner
149+
AND cols.table_name = 'untitled_table8';
150+
151+
execute immediate (
152+
'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;'
153+
);
154+
155+
END;
156+
`,
157+
{ dialect: 'oracle', strict: true },
158+
);
159+
const expected = [
160+
{
161+
end: 1043,
162+
executionType: 'ANON_BLOCK',
163+
parameters: [],
164+
start: 11,
165+
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;',
166+
type: 'ANON_BLOCK',
167+
},
168+
];
169+
expect(actual).to.eql(expected);
170+
});
171+
172+
it('should identify a create table then a block', () => {
173+
const actual = identify(
174+
`
175+
create table
176+
"untitled_table8" (
177+
"id" integer not null primary key,
178+
"created_at" varchar(255) not null
179+
);
180+
181+
DECLARE
182+
PK_NAME VARCHAR(200);
183+
184+
BEGIN
185+
EXECUTE IMMEDIATE ('CREATE SEQUENCE "untitled_table8_seq"');
186+
187+
SELECT
188+
cols.column_name INTO PK_NAME
189+
FROM
190+
all_constraints cons,
191+
all_cons_columns cols
192+
WHERE
193+
cons.constraint_type = 'P'
194+
AND cons.constraint_name = cols.constraint_name
195+
AND cons.owner = cols.owner
196+
AND cols.table_name = 'untitled_table8';
197+
198+
execute immediate (
199+
'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;'
200+
);
201+
202+
END;
203+
`,
204+
{ dialect: 'oracle', strict: false },
205+
);
206+
const expected = [
207+
{
208+
end: 167,
209+
executionType: 'MODIFICATION',
210+
parameters: [],
211+
start: 11,
212+
text: 'create table\n "untitled_table8" (\n "id" integer not null primary key,\n "created_at" varchar(255) not null\n );',
213+
type: 'CREATE_TABLE',
214+
},
215+
{
216+
end: 1212,
217+
executionType: 'ANON_BLOCK',
218+
parameters: [],
219+
start: 180,
220+
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;',
221+
type: 'ANON_BLOCK',
222+
},
223+
];
224+
expect(actual).to.eql(expected);
225+
});
226+
});
227+
130228
describe('identifying multiple statements with CTEs', () => {
131229
it('should able to detect queries with a CTE in middle query', () => {
132230
const actual = identify(

test/index.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { expect } from 'chai';
44
describe('identify', () => {
55
it('should throw error for invalid dialect', () => {
66
expect(() => identify('SELECT * FROM foo', { dialect: 'invalid' as Dialect })).to.throw(
7-
'Unknown dialect. Allowed values: mssql, sqlite, mysql, psql, generic',
7+
'Unknown dialect. Allowed values: mssql, sqlite, mysql, oracle, psql, generic',
88
);
99
});
1010

0 commit comments

Comments
 (0)