├── .npmignore ├── jest.config.js ├── src ├── SQLSurveyorOptions.ts ├── models │ ├── QueryType.ts │ ├── TokenType.ts │ ├── ParsingErrorType.ts │ ├── ParsingError.ts │ ├── OutputColumn.ts │ ├── Token.ts │ ├── ReferencedColumn.ts │ ├── TokenLocation.ts │ ├── ReferencedTable.ts │ ├── ParsedSql.ts │ └── ParsedQuery.ts ├── parsing │ ├── CaseChangingStream.ts │ ├── TrackingErrorStrategy.ts │ ├── BaseSqlQueryListener.ts │ ├── PLpgSQLQueryListener.ts │ ├── PlSqlQueryListener.ts │ ├── MySQLQueryListener.ts │ └── TSQLQueryListener.ts ├── lexing │ └── TokenTypeIdentifier.ts └── SQLSurveyor.ts ├── index.ts ├── tsconfig.json ├── LICENSE ├── package.json ├── parse.ts ├── test ├── options.test.ts ├── equivalence.test.ts ├── mysql.test.ts ├── plsql.test.ts ├── tsql.test.ts └── plpgsql.test.ts ├── .gitignore └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | test/ 3 | jest.config.js 4 | index.ts 5 | tsconfig.json 6 | parse.ts -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /src/SQLSurveyorOptions.ts: -------------------------------------------------------------------------------- 1 | export interface SQLSurveyorOptions { 2 | logErrors: boolean, 3 | throwErrors: boolean 4 | } -------------------------------------------------------------------------------- /src/models/QueryType.ts: -------------------------------------------------------------------------------- 1 | export enum QueryType { 2 | DML = "DML", 3 | DDL = "DDL", 4 | STORED_PROCEDURE = "STORED_PROCEDURE" 5 | } -------------------------------------------------------------------------------- /src/models/TokenType.ts: -------------------------------------------------------------------------------- 1 | export enum TokenType { 2 | COMMENT = 'COMMENT', 3 | IDENTIFIER = 'IDENTIFIER', 4 | KEYWORD = 'KEYWORD', 5 | LITERAL = 'LITERAL', 6 | OPERATOR = 'OPERATOR' 7 | } -------------------------------------------------------------------------------- /src/models/ParsingErrorType.ts: -------------------------------------------------------------------------------- 1 | export enum ParsingErrorType { 2 | FAILED_PREDICATE = 'FAILED_PREDICATE', 3 | MISSING_TOKEN = 'MISSING_TOKEN', 4 | MISMATCHED_INPUT = 'MISMATCHED_INPUT', 5 | NO_VIABLE_ALTERNATIVE = 'NO_VIABLE_ALTERNATIVE', 6 | UNWANTED_TOKEN = 'UNWANTED_TOKEN' 7 | } -------------------------------------------------------------------------------- /src/models/ParsingError.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "./Token"; 2 | import { ParsingErrorType } from "./ParsingErrorType"; 3 | 4 | export class ParsingError { 5 | token: Token; 6 | type: ParsingErrorType; 7 | 8 | constructor(token: Token, type: ParsingErrorType) { 9 | this.token = token; 10 | this.type = type; 11 | } 12 | } -------------------------------------------------------------------------------- /src/models/OutputColumn.ts: -------------------------------------------------------------------------------- 1 | export class OutputColumn { 2 | columnName: string; 3 | columnAlias: string; 4 | tableName: string; 5 | tableAlias: string; 6 | 7 | constructor(columnName: string, columnAlias: string, tableName: string, tableAlias: string) { 8 | this.columnName = columnName; 9 | this.columnAlias = columnAlias; 10 | this.tableName = tableName; 11 | this.tableAlias = tableAlias; 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/models/Token.ts: -------------------------------------------------------------------------------- 1 | import { TokenLocation } from "./TokenLocation"; 2 | import { TokenType } from "./TokenType"; 3 | 4 | export class Token { 5 | value: string; 6 | type: TokenType; 7 | location: TokenLocation; 8 | 9 | constructor(value: string, type: TokenType, location: TokenLocation) { 10 | this.value = value; 11 | this.type = type; 12 | this.location = location; 13 | } 14 | 15 | setValue(input: string): void { 16 | this.value = this.location.getToken(input); 17 | } 18 | } -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export { SQLDialect } from 'antlr4ts-sql'; 2 | 3 | export * from './src/SQLSurveyor'; 4 | export * from './src/SQLSurveyorOptions'; 5 | 6 | export * from './src/models/ParsedQuery'; 7 | export * from './src/models/ParsedSql'; 8 | export * from './src/models/ReferencedColumn'; 9 | export * from './src/models/ReferencedTable'; 10 | export * from './src/models/Token'; 11 | export * from './src/models/TokenLocation'; 12 | export * from './src/models/TokenType'; 13 | export * from './src/models/ParsingError'; 14 | export * from './src/models/ParsingErrorType'; 15 | -------------------------------------------------------------------------------- /src/models/ReferencedColumn.ts: -------------------------------------------------------------------------------- 1 | import { TokenLocation } from "./TokenLocation"; 2 | 3 | export class ReferencedColumn { 4 | columnName: string; 5 | tableName: string; 6 | tableAlias: string; 7 | locations: Set; 8 | 9 | constructor(columnName: string, tableName: string, tableAlias: string, location: TokenLocation) { 10 | this.columnName = columnName; 11 | this.tableName = tableName; 12 | this.tableAlias = tableAlias; 13 | this.locations = new Set(); 14 | if (location !== null && location !== undefined) { 15 | this.locations.add(location); 16 | } 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2019", 5 | "moduleResolution": "node", 6 | "esModuleInterop": true, 7 | "resolveJsonModule": true, 8 | "sourceMap": true, 9 | "allowJs": false, 10 | "incremental": true, 11 | "declaration": true, 12 | "outDir": "dist", 13 | "experimentalDecorators": true, 14 | "baseUrl": "./src", 15 | "paths": { 16 | "@/*": ["./*"] 17 | }, 18 | "strictPropertyInitialization": false 19 | }, 20 | "include": ["**/*.ts"], 21 | "exclude": ["node_modules", "dist", "**/*.d.ts", "test", "parse.ts"] 22 | } -------------------------------------------------------------------------------- /src/models/TokenLocation.ts: -------------------------------------------------------------------------------- 1 | export class TokenLocation { 2 | startIndex: number; 3 | stopIndex: number; 4 | lineStart: number; 5 | lineEnd: number; 6 | 7 | constructor(lineStart: number, lineEnd: number, startIndex: number, stopIndex: number) { 8 | this.lineStart = lineStart; 9 | this.lineEnd = lineEnd; 10 | this.startIndex = startIndex; 11 | this.stopIndex = stopIndex; 12 | } 13 | 14 | getToken(input: string): string { 15 | return input.substring(this.startIndex, this.stopIndex + 1); 16 | } 17 | 18 | static clone(token: TokenLocation): TokenLocation { 19 | if (token === undefined || token === null) { 20 | return null; 21 | } 22 | return new TokenLocation(token.lineStart, token.lineEnd, token.startIndex, token.stopIndex); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Matt @ modelDBA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/models/ReferencedTable.ts: -------------------------------------------------------------------------------- 1 | import { TokenLocation } from "./TokenLocation"; 2 | 3 | export class ReferencedTable { 4 | tableName: string; 5 | schemaName: string; 6 | databaseName: string; 7 | aliases: Set; 8 | locations: Set; 9 | 10 | constructor(tableName: string) { 11 | this.tableName = tableName; 12 | this.schemaName = null; 13 | this.databaseName = null; 14 | this.aliases = new Set(); 15 | this.locations = new Set(); 16 | } 17 | 18 | static clone(referencedTable: ReferencedTable): ReferencedTable { 19 | if (referencedTable === undefined || referencedTable === null) { 20 | return null; 21 | } 22 | const clonedReferencedTable: ReferencedTable = new ReferencedTable(referencedTable.tableName); 23 | clonedReferencedTable.schemaName = referencedTable.schemaName; 24 | clonedReferencedTable.databaseName = referencedTable.databaseName; 25 | referencedTable.aliases.forEach(alias => clonedReferencedTable.aliases.add(alias)); 26 | referencedTable.locations.forEach(location => clonedReferencedTable.locations.add(TokenLocation.clone(location))); 27 | return clonedReferencedTable; 28 | } 29 | } -------------------------------------------------------------------------------- /src/parsing/CaseChangingStream.ts: -------------------------------------------------------------------------------- 1 | import { Interval } from "antlr4ts-sql"; 2 | 3 | export class CaseChangingStream { 4 | 5 | _stream; 6 | _upper; 7 | 8 | get index() { 9 | return this._stream.index; 10 | } 11 | 12 | get size() { 13 | return this._stream.size; 14 | } 15 | 16 | get sourceName() { 17 | return null; 18 | } 19 | 20 | constructor(stream: any, upper: any) { 21 | this._stream = stream; 22 | this._upper = upper; 23 | } 24 | 25 | LA(offset) { 26 | var c = this._stream.LA(offset); 27 | if (c <= 0) { 28 | return c; 29 | } 30 | return String.fromCodePoint(c)[this._upper ? "toUpperCase" : "toLowerCase"]().codePointAt(0); 31 | } 32 | 33 | reset() { 34 | return this._stream.reset(); 35 | } 36 | 37 | consume() { 38 | return this._stream.consume(); 39 | } 40 | 41 | LT(offset) { 42 | return this._stream.LT(offset); 43 | } 44 | 45 | mark() { 46 | return this._stream.mark(); 47 | } 48 | 49 | release(marker) { 50 | return this._stream.release(marker); 51 | } 52 | 53 | seek(_index) { 54 | return this._stream.seek(_index); 55 | }; 56 | 57 | getText(interval: Interval): string { 58 | return this._stream.getText(interval.a, interval.b); 59 | } 60 | 61 | toString() { 62 | return this._stream.toString(); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sql-surveyor", 3 | "version": "1.4.1", 4 | "description": "High-level SQL parser. Identify tables, columns, aliases and more from your SQL script in one easy to consume object. Supports PostgreSQL, MySQL, SQL Server and Oracle (PL/SQL) dialects.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --build --clean && tsc", 9 | "parsetest": "npx ts-node parse.ts", 10 | "test": "npx jest", 11 | "cleantest": "npm run build && npm test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/modeldba/sql-surveyor.git" 16 | }, 17 | "keywords": [ 18 | "sql", 19 | "parser", 20 | "database", 21 | "postgresql", 22 | "mysql", 23 | "sqlserver", 24 | "oracle", 25 | "plpgsql", 26 | "plsql", 27 | "tsql" 28 | ], 29 | "author": "modelDBA", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/modeldba/sql-surveyor/issues" 33 | }, 34 | "homepage": "https://modeldba.com/sql-surveyor", 35 | "dependencies": { 36 | "antlr4ts-sql": "^1.1.0" 37 | }, 38 | "devDependencies": { 39 | "@types/jest": "^26.0.23", 40 | "jest": "^26.6.3", 41 | "ts-jest": "^26.5.6", 42 | "ts-node": "^9.1.1", 43 | "typescript": "^4.2.3" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /parse.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect } from './dist/index'; 2 | 3 | const input = 'SELECT * FROM tableName t1 \r\n JOIN tableName2 t2 ON t1.id = t2.id'; 4 | // const input = 'SELECT t1.val FROM tableName as t1 where t1.col1 = 1 and (select 1 from tableName2) > 0'; 5 | // const input = 'SELECT * from [database].[dbo].[tableName] t1 \r\n JOIN tableName2 \r\n ON t1.val = otherVal;'; 6 | // const input = 'SELECT * from "dbo"."tableName" t1 \r\n JOIN tableName2 \r\n ON t1.val = otherVal;'; 7 | // const input = 'SELECT t.column1, t.* FROM table1 t WHERE t.col in (select col from table2 where col3 = col4)'; 8 | // const input = 'with my_depts as (select dept_num from departments where department_name = \'test\') select * from my_depts;'; 9 | // const input = 'with my_depts as (select dept_num from departments where department_name = \'test\'), my_emps as (select emp_num from employees) select * from my_depts;'; 10 | // const input = 'with my_depts as (select dept_num from departments where department_name = \'test\'), my_emps as (select emp_num from employees) select * from my_depts where dept_num in (select * from departmentemployees);'; 11 | // const input = 'UPDATE departments SET department_name = \'lol\' WHERE dept_num = 1'; 12 | // const input = 'SELECT * FROM tab WHERE col1 ='; 13 | // const input = 'SELECT * FROM; SELECT * FROM tab2'; 14 | // const input = 'SELECT t1.columnA as ca, t2.columnB FROM table1 t1 JOIN table2 t2 ON t1.id = t2.table1_id'; 15 | const surveyor = new SQLSurveyor(SQLDialect.MYSQL); 16 | const parsedSql = surveyor.survey(input); 17 | console.dir(parsedSql.parsedQueries, { depth: null }); 18 | -------------------------------------------------------------------------------- /src/models/ParsedSql.ts: -------------------------------------------------------------------------------- 1 | import { ParsedQuery } from "./ParsedQuery"; 2 | import { TokenLocation } from "./TokenLocation"; 3 | 4 | export class ParsedSql { 5 | 6 | parsedQueries: { [queryStartIndex: number]: ParsedQuery }; 7 | 8 | constructor() { 9 | this.parsedQueries = {}; 10 | } 11 | 12 | getQueryAtLocation(stringIndex: number): ParsedQuery { 13 | const queryIndex: number = this.getQueryIndexAtLocation(stringIndex); 14 | if (queryIndex !== null) { 15 | return Object.values(this.parsedQueries)[queryIndex]; 16 | } 17 | return null; 18 | } 19 | 20 | getQueryIndexAtLocation(stringIndex: number): number { 21 | if (stringIndex === undefined || stringIndex === null) { 22 | return null; 23 | } 24 | const queryStartIndices: string[] = Object.keys(this.parsedQueries); 25 | for (let i = 0; i < queryStartIndices.length; i++) { 26 | const currentQueryStartIndex: number = Number(queryStartIndices[i]); 27 | let nextQueryStartIndex: number = null; 28 | if (queryStartIndices[i + 1] !== undefined) { 29 | nextQueryStartIndex = Number(queryStartIndices[i + 1]); 30 | } 31 | if (stringIndex >= currentQueryStartIndex 32 | && (nextQueryStartIndex === null || stringIndex < nextQueryStartIndex)) { 33 | return i; 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | getQueryLocations(): TokenLocation[] { 40 | const locations: TokenLocation[] = []; 41 | for (const parsedQuery of Object.values(this.parsedQueries)) { 42 | locations.push(parsedQuery.queryLocation); 43 | } 44 | return locations; 45 | } 46 | 47 | _addQuery(parsedQuery: ParsedQuery): void { 48 | this.parsedQueries[parsedQuery.queryLocation.startIndex] = parsedQuery; 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /test/options.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect, ParsedSql, TokenLocation } from '../dist/index'; 2 | 3 | let surveyorDefault: SQLSurveyor = null; 4 | let surveyorLogOnly: SQLSurveyor = null; 5 | let surveyorThrowOnly: SQLSurveyor = null; 6 | let surveyorThrowAndLog: SQLSurveyor = null; 7 | let surveyorSilent: SQLSurveyor = null; 8 | beforeAll(() => { 9 | surveyorDefault = new SQLSurveyor(SQLDialect.MYSQL); 10 | surveyorLogOnly = new SQLSurveyor(SQLDialect.MYSQL, { 11 | logErrors: true, 12 | throwErrors: false 13 | }); 14 | surveyorThrowOnly = new SQLSurveyor(SQLDialect.MYSQL, { 15 | logErrors: false, 16 | throwErrors: true 17 | }); 18 | surveyorThrowAndLog = new SQLSurveyor(SQLDialect.MYSQL, { 19 | logErrors: true, 20 | throwErrors: true 21 | }); 22 | surveyorSilent = new SQLSurveyor(SQLDialect.MYSQL, { 23 | logErrors: false, 24 | throwErrors: false 25 | }); 26 | }); 27 | 28 | test('error handling options', () => { 29 | // Test based on known bug -- Can't handle subqueries in the FROM clause 30 | const sql = 'SELECT * FROM (SELECT a, b, c FROM tableName) sub; SELECT id FROM tableName2;'; 31 | 32 | const originalConsoleError = console.error; 33 | try { 34 | console.error = () => {}; 35 | for (const dialect in SQLDialect) { 36 | surveyorDefault.setDialect(SQLDialect[dialect]); 37 | const parsedSql: ParsedSql = surveyorDefault.survey(sql); 38 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 39 | 40 | surveyorLogOnly.setDialect(SQLDialect[dialect]); 41 | const parsedSql2: ParsedSql = surveyorLogOnly.survey(sql); 42 | expect(Object.keys(parsedSql2.parsedQueries).length).toBe(2); 43 | 44 | surveyorThrowOnly.setDialect(SQLDialect[dialect]); 45 | const parsedSql3: ParsedSql = surveyorThrowOnly.survey(sql); 46 | expect(Object.keys(parsedSql3.parsedQueries).length).toBe(1); 47 | 48 | surveyorThrowAndLog.setDialect(SQLDialect[dialect]); 49 | const parsedSql4: ParsedSql = surveyorThrowAndLog.survey(sql); 50 | expect(Object.keys(parsedSql4.parsedQueries).length).toBe(1); 51 | 52 | surveyorSilent.setDialect(SQLDialect[dialect]); 53 | const parsedSql5: ParsedSql = surveyorSilent.survey(sql); 54 | expect(Object.keys(parsedSql5.parsedQueries).length).toBe(2); 55 | } 56 | } finally { 57 | console.error = originalConsoleError; 58 | } 59 | }); -------------------------------------------------------------------------------- /src/parsing/TrackingErrorStrategy.ts: -------------------------------------------------------------------------------- 1 | import { DefaultErrorStrategy, Parser, FailedPredicateException, NoViableAltException, InputMismatchException } from "antlr4ts-sql"; 2 | import { ParsingError } from "../models/ParsingError"; 3 | import { Token } from "../models/Token"; 4 | import { TokenLocation } from "../models/TokenLocation"; 5 | import { ParsingErrorType } from "../models/ParsingErrorType"; 6 | 7 | /** 8 | * An error strategy that keeps track of any parsing errors, 9 | * but does not otherwise change the DefaultErrorStrategy 10 | */ 11 | export class TrackingErrorStrategy extends DefaultErrorStrategy { 12 | 13 | errors: ParsingError[] = []; 14 | 15 | protected reportFailedPredicate(recognizer: Parser, e: FailedPredicateException): void { 16 | const token = this._getToken(recognizer); 17 | this.errors.push(new ParsingError(token, ParsingErrorType.FAILED_PREDICATE)); 18 | 19 | super.reportFailedPredicate(recognizer, e); 20 | } 21 | 22 | protected reportInputMismatch(recognizer: Parser, e: InputMismatchException): void { 23 | const token = this._getToken(recognizer); 24 | this.errors.push(new ParsingError(token, ParsingErrorType.MISMATCHED_INPUT)); 25 | 26 | super.reportInputMismatch(recognizer, e); 27 | } 28 | 29 | protected reportMissingToken(recognizer: Parser): void { 30 | const token = this._getToken(recognizer); 31 | this.errors.push(new ParsingError(token, ParsingErrorType.MISSING_TOKEN)); 32 | 33 | super.reportMissingToken(recognizer); 34 | } 35 | 36 | protected reportNoViableAlternative(recognizer: Parser, e: NoViableAltException): void { 37 | const token = this._getToken(recognizer); 38 | this.errors.push(new ParsingError(token, ParsingErrorType.NO_VIABLE_ALTERNATIVE)); 39 | 40 | super.reportNoViableAlternative(recognizer, e); 41 | } 42 | 43 | protected reportUnwantedToken(recognizer: Parser): void { 44 | const token = this._getToken(recognizer); 45 | this.errors.push(new ParsingError(token, ParsingErrorType.UNWANTED_TOKEN)); 46 | 47 | super.reportUnwantedToken(recognizer); 48 | } 49 | 50 | _getToken(recognizer: Parser): Token { 51 | const token = new Token(null, null, new TokenLocation(recognizer.currentToken.line, recognizer.currentToken.line, recognizer.currentToken.startIndex, recognizer.currentToken.stopIndex)); 52 | if (token.location.startIndex > token.location.stopIndex) { 53 | // Error was at , set token at end 54 | token.location.startIndex = token.location.stopIndex; 55 | } 56 | return token; 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | ## Node.js .gitignore 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variables file 76 | .env 77 | .env.test 78 | 79 | # parcel-bundler cache (https://parceljs.org/) 80 | .cache 81 | .parcel-cache 82 | 83 | # Next.js build output 84 | .next 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | 116 | .yarn/cache 117 | .yarn/unplugged 118 | .yarn/build-state.yml 119 | .pnp.* 120 | 121 | ## Java .gitignore 122 | 123 | # Compiled class file 124 | *.class 125 | 126 | # Log file 127 | *.log 128 | 129 | # BlueJ files 130 | *.ctxt 131 | 132 | # Mobile Tools for Java (J2ME) 133 | .mtj.tmp/ 134 | 135 | # Package Files # 136 | *.jar 137 | *.war 138 | *.nar 139 | *.ear 140 | *.zip 141 | *.tar.gz 142 | *.rar 143 | 144 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 145 | hs_err_pid* -------------------------------------------------------------------------------- /src/lexing/TokenTypeIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { TokenType } from "../models/TokenType"; 2 | import { Parser, SQLDialect } from "antlr4ts-sql"; 3 | 4 | export class TokenTypeIdentifier { 5 | 6 | _dialect: SQLDialect; 7 | _parser: Parser; 8 | 9 | constructor(dialect: SQLDialect, parser: Parser) { 10 | this._dialect = dialect; 11 | this._parser = parser; 12 | } 13 | 14 | getTokenType(typeId: number): TokenType { 15 | if (this._dialect === SQLDialect.TSQL) { 16 | return this._getTSQLTokenType(typeId); 17 | } else if (this._dialect === SQLDialect.PLSQL) { 18 | return this._getPLSQLTokenType(typeId); 19 | } else if (this._dialect === SQLDialect.PLpgSQL) { 20 | return this._getPLpgSQLTokenType(typeId); 21 | } else if (this._dialect === SQLDialect.MYSQL) { 22 | return this._getMYSQLTokenType(typeId); 23 | } 24 | return null; 25 | } 26 | 27 | _getTSQLTokenType(typeId: number): TokenType { 28 | const displayName = this._parser.vocabulary.getDisplayName(typeId); 29 | const symbolicName = this._parser.vocabulary.getSymbolicName(typeId); 30 | if (symbolicName === 'ID' || symbolicName === 'SQUARE_BRACKET_ID') { 31 | return TokenType.IDENTIFIER; 32 | } else if (symbolicName.includes('COMMENT')) { 33 | return TokenType.COMMENT; 34 | } else if (displayName === symbolicName) { 35 | return TokenType.LITERAL; 36 | } else if (displayName.substring(1, displayName.length - 1) !== symbolicName) { 37 | return TokenType.OPERATOR; 38 | } else if (displayName.substring(1, displayName.length - 1) === symbolicName) { 39 | return TokenType.KEYWORD; 40 | } 41 | return null; 42 | } 43 | 44 | _getPLSQLTokenType(typeId: number): TokenType { 45 | const displayName = this._parser.vocabulary.getDisplayName(typeId); 46 | const symbolicName = this._parser.vocabulary.getSymbolicName(typeId); 47 | if (symbolicName === 'ID' || symbolicName === 'REGULAR_ID' || symbolicName === 'DELIMITED_ID') { 48 | return TokenType.IDENTIFIER; 49 | } else if (symbolicName.includes('COMMENT')) { 50 | return TokenType.COMMENT; 51 | } else if (displayName === symbolicName) { 52 | return TokenType.LITERAL; 53 | } else if (displayName.substring(1, displayName.length - 1) !== symbolicName) { 54 | return TokenType.OPERATOR; 55 | } else if (displayName.substring(1, displayName.length - 1) === symbolicName) { 56 | return TokenType.KEYWORD; 57 | } 58 | return null; 59 | } 60 | 61 | _getPLpgSQLTokenType(typeId: number): TokenType { 62 | const displayName = this._parser.vocabulary.getDisplayName(typeId); 63 | const symbolicName = this._parser.vocabulary.getSymbolicName(typeId); 64 | if (symbolicName === 'Identifier' || symbolicName === 'QuotedIdentifier') { 65 | return TokenType.IDENTIFIER; 66 | } else if (displayName !== symbolicName 67 | || displayName === 'BeginDollarStringConstant' || displayName === 'EndDollarStringConstant') { 68 | return TokenType.OPERATOR; 69 | } else if (symbolicName.toUpperCase().includes('COMMENT')) { 70 | return TokenType.COMMENT; 71 | } else if (symbolicName.endsWith('_NUMBER') 72 | || symbolicName.toUpperCase().endsWith('_LITERAL') 73 | || symbolicName === 'Text_between_Dollar') { 74 | return TokenType.LITERAL; 75 | } else if (symbolicName.toUpperCase() === symbolicName) { 76 | return TokenType.KEYWORD; 77 | } 78 | return null; 79 | } 80 | 81 | _getMYSQLTokenType(typeId: number): TokenType { 82 | const displayName = this._parser.vocabulary.getDisplayName(typeId); 83 | const symbolicName = this._parser.vocabulary.getSymbolicName(typeId); 84 | if (symbolicName === 'IDENTIFIER' || symbolicName === 'BACK_TICK_QUOTED_ID') { 85 | return TokenType.IDENTIFIER; 86 | } else if (symbolicName.endsWith('_OPERATOR') || (displayName !== symbolicName)) { 87 | return TokenType.OPERATOR; 88 | } else if (symbolicName.endsWith('_SYMBOL')) { 89 | return TokenType.KEYWORD; 90 | } else if (symbolicName.endsWith('_NUMBER') || symbolicName.endsWith('_TEXT')) { 91 | return TokenType.LITERAL; 92 | } else if (symbolicName.includes('_COMMENT')) { 93 | return TokenType.COMMENT; 94 | } 95 | return null; 96 | } 97 | 98 | } -------------------------------------------------------------------------------- /src/parsing/BaseSqlQueryListener.ts: -------------------------------------------------------------------------------- 1 | import { TokenLocation } from '../models/TokenLocation'; 2 | import { ParsedSql } from '../models/ParsedSql'; 3 | import { SQLSurveyorOptions } from '../SQLSurveyorOptions'; 4 | 5 | export class BaseSqlQueryListener { 6 | 7 | input: string; 8 | options: SQLSurveyorOptions; 9 | tableNameLocations: { [tableName: string]: TokenLocation[] }; 10 | tableAlias: { [tableName: string]: string[] }; 11 | 12 | parsedSql: ParsedSql; 13 | 14 | constructor(input: string, options: SQLSurveyorOptions) { 15 | this.input = input; 16 | this.options = options; 17 | this.tableNameLocations = {}; 18 | this.tableAlias = {}; 19 | 20 | this.parsedSql = new ParsedSql(); 21 | } 22 | 23 | _getClauseLocation(ctx: any): TokenLocation { 24 | let stopLine = ctx._start._line; 25 | let stopIndex = this.input.length; 26 | if (ctx._stop !== undefined) { 27 | stopLine = ctx._stop._line; 28 | stopIndex = ctx._stop.stop; 29 | } 30 | const queryLocation: TokenLocation = new TokenLocation(ctx._start._line, stopLine, ctx._start.start, stopIndex); 31 | return queryLocation; 32 | } 33 | 34 | _getAliasStartIndex(value: string, dbmsOpenQuoteChar: string, dbmsCloseQuoteChar: string): number { 35 | let isInsideStringQuote = false; 36 | let isInsideDBMSQuote = false; 37 | let index = value.length - 1; 38 | const isWhitespaceRegex = /\s/; 39 | while (index >= 0) { 40 | const currentChar = value[index]; 41 | if (currentChar === "'" && value[index - 1] !== '\\') { 42 | isInsideStringQuote = !isInsideStringQuote; 43 | } 44 | if (currentChar === dbmsCloseQuoteChar && value[index - 1] !== '\\') { 45 | isInsideDBMSQuote = true; 46 | } else if (currentChar === dbmsOpenQuoteChar && value[index - 1] !== '\\') { 47 | isInsideDBMSQuote = false; 48 | } 49 | if (currentChar === ')' && !isInsideStringQuote && !isInsideDBMSQuote) { 50 | // Subquery without alias 51 | return null; 52 | } 53 | if (isWhitespaceRegex.test(currentChar) && !isInsideStringQuote && !isInsideDBMSQuote) { 54 | if (value.substring(index + 1).toUpperCase() === 'END') { 55 | // Reserved keyword, not an alias (CASE statement, BEGIN...END transaction, etc) 56 | return null; 57 | } 58 | return index + 1; 59 | } 60 | index--; 61 | } 62 | return null; 63 | } 64 | 65 | _getTableAliasEndLocation(value: string, dbmsOpenQuoteChar: string, dbmsCloseQuoteChar: string): number { 66 | let isInsideStringQuote = false; 67 | let isInsideDBMSQuote = false; 68 | let index = 0; 69 | const isWhitespaceRegex = /\s/; 70 | let potentialTableAliasIndex = null; 71 | while (index < value.length) { 72 | const currentChar = value[index]; 73 | if (currentChar === "'" && value[index - 1] !== '\\') { 74 | isInsideStringQuote = !isInsideStringQuote; 75 | } 76 | if (currentChar === dbmsCloseQuoteChar && value[index - 1] !== '\\') { 77 | isInsideDBMSQuote = true; 78 | } else if (currentChar === dbmsOpenQuoteChar && value[index - 1] !== '\\') { 79 | isInsideDBMSQuote = false; 80 | } 81 | if (isWhitespaceRegex.test(currentChar) && !isInsideStringQuote && !isInsideDBMSQuote) { 82 | // Either we're in a subquery, or have reached the column alias 83 | return potentialTableAliasIndex; 84 | } 85 | if (currentChar === '.' && !isInsideStringQuote && !isInsideDBMSQuote) { 86 | potentialTableAliasIndex = index; 87 | } 88 | index++; 89 | } 90 | return potentialTableAliasIndex; 91 | } 92 | 93 | _getFunctionArgumentLocation(ctx: any, columnLocation: TokenLocation, functionRules: any[], argumentRules: any[]): TokenLocation { 94 | while (ctx !== undefined && ctx.childCount > 0) { 95 | let currentChild = ctx.children[0]; 96 | if (currentChild._start.start === columnLocation.startIndex 97 | && currentChild._stop.stop === columnLocation.stopIndex) { 98 | for (const functionRule of functionRules) { 99 | if (currentChild instanceof functionRule && currentChild.childCount > 0) { 100 | for (const argumentRule of argumentRules) { 101 | for (const child of currentChild.children) { 102 | if (child instanceof argumentRule) { 103 | return new TokenLocation((child._start as any)._line, (child._stop as any)._line, (child._start as any).start, (child._stop as any).stop); 104 | } 105 | } 106 | } 107 | } 108 | } 109 | } else { 110 | return null; 111 | } 112 | ctx = currentChild; 113 | } 114 | return null; 115 | } 116 | 117 | _handleError(error: Error) { 118 | if (this.options && this.options.logErrors) { 119 | if (error.stack) { 120 | console.error(error.stack); 121 | } else { 122 | console.error(error.name + ': ' + error.message); 123 | } 124 | } 125 | if (this.options && !this.options.throwErrors) { 126 | return; 127 | } 128 | throw error; 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /src/SQLSurveyor.ts: -------------------------------------------------------------------------------- 1 | import { ParsedSql } from "./models/ParsedSql"; 2 | import { TSqlQueryListener } from "./parsing/TSQLQueryListener"; 3 | import { TokenLocation } from "./models/TokenLocation"; 4 | import { TrackingErrorStrategy } from "./parsing/TrackingErrorStrategy"; 5 | import { PlSqlQueryListener } from "./parsing/PlSqlQueryListener"; 6 | import { MySQLQueryListener } from "./parsing/MySQLQueryListener"; 7 | import { BaseSqlQueryListener } from "./parsing/BaseSqlQueryListener"; 8 | import { PLpgSQLQueryListener } from "./parsing/PLpgSQLQueryListener"; 9 | import { antlr4tsSQL, SQLDialect, ParseTreeWalker, PredictionMode, CommonTokenStream, Parser, Token } from 'antlr4ts-sql'; 10 | import { TokenTypeIdentifier } from "./lexing/TokenTypeIdentifier"; 11 | import { SQLSurveyorOptions } from "./SQLSurveyorOptions"; 12 | 13 | export class SQLSurveyor { 14 | 15 | _dialect: SQLDialect; 16 | _antlr4tssql: antlr4tsSQL; 17 | _options: SQLSurveyorOptions; 18 | 19 | constructor(dialect: SQLDialect, options?: SQLSurveyorOptions) { 20 | this._dialect = dialect; 21 | if (this._dialect === null || this._dialect === undefined) { 22 | this._dialect = SQLDialect.TSQL; 23 | } 24 | this._antlr4tssql = new antlr4tsSQL(this._dialect); 25 | this._options = null; 26 | if (options !== null && options !== undefined) { 27 | this._options = options; 28 | } 29 | } 30 | 31 | setDialect(dialect: SQLDialect): void { 32 | this._dialect = dialect; 33 | this._antlr4tssql = new antlr4tsSQL(this._dialect); 34 | } 35 | 36 | survey(sqlScript: string): ParsedSql { 37 | let removedTrailingPeriod: boolean = false; 38 | if (sqlScript.endsWith('.') && this._dialect === SQLDialect.MYSQL) { 39 | // The MySQL Parser struggles with trailing '.' on incomplete SQL statements 40 | sqlScript = sqlScript.substring(0, sqlScript.length - 1); 41 | removedTrailingPeriod = true; 42 | } 43 | const tokens = this._getTokens(sqlScript); 44 | const parser = this._getParser(tokens); 45 | const parsedTree = this._antlr4tssql.getParseTree(parser); 46 | const listener = this._getListener(sqlScript); 47 | 48 | // Populate the parsedSql object on the listener 49 | try { 50 | // @ts-ignore Weak Type Detection 51 | ParseTreeWalker.DEFAULT.walk(listener, parsedTree); 52 | } catch (e) { 53 | // We'll attempt to complete surveying, don't throw 54 | if (this._options && this._options.logErrors) { 55 | console.error(e); 56 | } 57 | } 58 | for (const parsedQuery of Object.values(listener.parsedSql.parsedQueries)) { 59 | parsedQuery._consolidateTables(); 60 | } 61 | 62 | // Load the tokens 63 | const tokenTypeIdentifier = new TokenTypeIdentifier(this._dialect, parser); 64 | for (const commonToken of tokens.getTokens() as any[]) { 65 | if (commonToken.channel !== Token.HIDDEN_CHANNEL) { 66 | const tokenLocation: TokenLocation = new TokenLocation(commonToken._line, commonToken._line, commonToken.start, commonToken.stop); 67 | let parsedQuery = listener.parsedSql.getQueryAtLocation(commonToken.start); 68 | if (parsedQuery !== null) { 69 | const token = tokenLocation.getToken(sqlScript); 70 | while (parsedQuery !== null) { 71 | if (token.length > 0) { 72 | parsedQuery._addToken(tokenLocation, tokenTypeIdentifier.getTokenType(commonToken.type), token); 73 | } 74 | let subParsedQuery = parsedQuery._getCommonTableExpressionAtLocation(commonToken.start); 75 | if (subParsedQuery === null) { 76 | subParsedQuery = parsedQuery._getSubqueryAtLocation(commonToken.start); 77 | } 78 | parsedQuery = subParsedQuery; 79 | } 80 | } 81 | } 82 | } 83 | 84 | if (removedTrailingPeriod) { 85 | let parsedQuery = listener.parsedSql.getQueryAtLocation(sqlScript.length); 86 | if (parsedQuery !== null && Object.keys(parsedQuery.tokens).length > 0) { 87 | const queryTokens = Object.values(parsedQuery.tokens); 88 | const lastToken = queryTokens[queryTokens.length - 1]; 89 | parsedQuery._addToken(new TokenLocation(lastToken.location.lineStart, lastToken.location.lineEnd, lastToken.location.stopIndex + 1, lastToken.location.stopIndex + 1), lastToken.type, '.'); 90 | } 91 | } 92 | 93 | // Load the names of any Common Table Expressions 94 | Object.values(listener.parsedSql.parsedQueries).forEach(parsedQuery => parsedQuery._setCommonTableExpressionNames()); 95 | 96 | // Set any errors 97 | for (const error of (parser.errorHandler as TrackingErrorStrategy).errors) { 98 | error.token.setValue(sqlScript); 99 | const parsedQuery = listener.parsedSql.getQueryAtLocation(error.token.location.startIndex); 100 | if (parsedQuery === null) { 101 | // Nothing to add the error to 102 | continue; 103 | } 104 | parsedQuery.queryErrors.push(error); 105 | } 106 | 107 | return listener.parsedSql; 108 | } 109 | 110 | _getTokens(sqlScript: string): CommonTokenStream { 111 | const tokens = this._antlr4tssql.getTokens(sqlScript, []); 112 | return tokens; 113 | } 114 | 115 | _getParser(tokens: CommonTokenStream): Parser { 116 | let parser = this._antlr4tssql.getParser(tokens, []); 117 | parser.errorHandler = new TrackingErrorStrategy(); 118 | parser.interpreter.setPredictionMode(PredictionMode.LL); 119 | return parser; 120 | } 121 | 122 | _getListener(sqlScript: string): BaseSqlQueryListener { 123 | if (this._dialect === SQLDialect.TSQL) { 124 | return new TSqlQueryListener(sqlScript, this._options); 125 | } else if (this._dialect === SQLDialect.PLSQL) { 126 | return new PlSqlQueryListener(sqlScript, this._options); 127 | } else if (this._dialect === SQLDialect.PLpgSQL) { 128 | return new PLpgSQLQueryListener(sqlScript, this._options); 129 | } else if (this._dialect === SQLDialect.MYSQL) { 130 | return new MySQLQueryListener(sqlScript, this._options); 131 | } 132 | return null; 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sql-surveyor 2 | 3 | SQL Surveyor is a zero configuration, high-level SQL parser. Existing parsers are low-level, giving you parse trees instead of 4 | easy access to query details. So we built a high-level parser that handles all the parse tree analysis and 5 | provides you with an easy to consume object representing your query. Identify tables, columns, aliases 6 | and more in your SQL script in one easy to consume object. See the [full API](https://modeldba.com/sql-surveyor/api) for details. 7 | 8 | Parse one query or entire SQL scripts at once. 9 | Supports MySQL, T-SQL (SQL Server), PL/pgSQL (PostgreSQL) and PL/SQL (Oracle) dialects. 10 | 11 | ## Install 12 | ```shell 13 | npm install sql-surveyor 14 | ``` 15 | 16 | ## [Full documentation can be found here](https://modeldba.com/sql-surveyor/docs/) 17 | 18 | ## Get Started 19 | 20 | ```typescript 21 | import { SQLSurveyor, SQLDialect } from 'sql-surveyor'; 22 | 23 | const sql = 'SELECT t1.columnA, t2.columnB FROM table1 t1 JOIN table2 t2 ON t1.id = t2.table1_id'; 24 | const surveyor = new SQLSurveyor(SQLDialect.PLSQL); 25 | const parsedSql = surveyor.survey(sql); 26 | console.dir(parsedSql, { depth: null }); 27 | 28 | // ParsedSql { 29 | // parsedQueries: { 30 | // '0': ParsedQuery { 31 | // outputColumns: [ 32 | // OutputColumn { columnName: 'columnA', columnAlias: null, tableName: 'table1', tableAlias: 't1'}, 33 | // OutputColumn { columnName: 'columnB', columnAlias: null, tableName: 'table2', tableAlias: 't2'} 34 | // ], 35 | // referencedColumns: [ 36 | // ReferencedColumn { 37 | // columnName: 'id', tableName: 'table1', tableAlias: 't1', 38 | // locations: Set { 39 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 63, stopIndex: 67 } 40 | // } 41 | // }, 42 | // ReferencedColumn { 43 | // columnName: 'table1_id', tableName: 'table2', tableAlias: 't2', 44 | // locations: Set { 45 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 71, stopIndex: 82 } 46 | // } 47 | // } 48 | // ], 49 | // referencedTables: { 50 | // table1: ReferencedTable { 51 | // tableName: 'table1', schemaName: null, databaseName: null, aliases: Set { 't1' }, 52 | // locations: Set { 53 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 35, stopIndex: 40 }, 54 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 63, stopIndex: 64 }, 55 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 7, stopIndex: 8 } } 56 | // }, 57 | // table2: ReferencedTable { 58 | // tableName: 'table2', schemaName: null, databaseName: null, aliases: Set { 't2' }, 59 | // locations: Set { 60 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 50, stopIndex: 55 }, 61 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 71, stopIndex: 72 }, 62 | // TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 19, stopIndex: 20 } 63 | // } 64 | // } 65 | // }, 66 | // tokens: { 67 | // '0': Token { value: 'SELECT', type: 'KEYWORD', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 0, stopIndex: 5 }}, 68 | // '7': Token { value: 't1', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 7, stopIndex: 8 }}, 69 | // '9': Token { value: '.', type: 'OPERATOR', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 9, stopIndex: 9 }}, 70 | // '10': Token { value: 'columnA', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 10, stopIndex: 16 }}, 71 | // '17': Token { value: ',', type: 'OPERATOR', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 17, stopIndex: 17 }}, 72 | // '19': Token { value: 't2', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 19, stopIndex: 20 }}, 73 | // '21': Token { value: '.', type: 'OPERATOR', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 21, stopIndex: 21 }}, 74 | // '22': Token { value: 'columnB', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 22, stopIndex: 28 }}, 75 | // '30': Token { value: 'FROM', type: 'KEYWORD', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 30, stopIndex: 33 }}, 76 | // '35': Token { value: 'table1', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 35, stopIndex: 40 }}, 77 | // '42': Token { value: 't1', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 42, stopIndex: 43 }}, 78 | // '45': Token { value: 'JOIN', type: 'KEYWORD', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 45, stopIndex: 48 }}, 79 | // '50': Token { value: 'table2', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 50, stopIndex: 55 }}, 80 | // '57': Token { value: 't2', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 57, stopIndex: 58 }}, 81 | // '60': Token { value: 'ON', type: 'KEYWORD', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 60, stopIndex: 61 }}, 82 | // '63': Token { value: 't1', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 63, stopIndex: 64 }}, 83 | // '65': Token { value: '.', type: 'OPERATOR', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 65, stopIndex: 65 }}, 84 | // '66': Token { value: 'id', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 66, stopIndex: 67 }}, 85 | // '69': Token { value: '=', type: 'OPERATOR', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 69, stopIndex: 69 }}, 86 | // '71': Token { value: 't2', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 71, stopIndex: 72 }}, 87 | // '73': Token { value: '.', type: 'OPERATOR', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 73, stopIndex: 73 }}, 88 | // '74': Token { value: 'table1_id', type: 'IDENTIFIER', location: TokenLocation { lineStart: 1, lineEnd: 1, startIndex: 74, stopIndex: 82 }} 89 | // }, 90 | // query: 'SELECT t1.columnA, t2.columnB FROM table1 t1 JOIN table2 t2 ON t1.id = t2.table1_id', 91 | // queryType: 'DML', 92 | // queryLocation: TokenLocation { lineStart: 1,lineEnd: 1,startIndex: 0,stopIndex: 82 }, 93 | // queryErrors: [], 94 | // subqueries: {}, 95 | // commonTableExpressions: {} 96 | // } 97 | // } 98 | // } 99 | ``` 100 | 101 | ## Created By 102 | 103 | [![modelDBA logo](https://modeldba.com/sql-surveyor/modelDBA128x128.png "modelDBA")](https://modeldba.com) 104 | 105 | sql-surveyor is a project created and maintained by [modelDBA](https://modeldba.com), a database IDE for modern developers. 106 | modelDBA lets you visualize SQL as you type and edit tables easily with a no-code table editor. 107 | 108 | -------------------------------------------------------------------------------- /test/equivalence.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect, ParsedSql, TokenLocation, TokenType } from '../dist/index'; 2 | 3 | let mysqlSurveyor: SQLSurveyor = null; 4 | let plsqlSurveyor: SQLSurveyor = null; 5 | let plpgsqlSurveyor: SQLSurveyor = null; 6 | let tsqlSurveyor: SQLSurveyor = null; 7 | beforeAll(() => { 8 | mysqlSurveyor = new SQLSurveyor(SQLDialect.MYSQL); 9 | plsqlSurveyor = new SQLSurveyor(SQLDialect.PLSQL); 10 | plpgsqlSurveyor = new SQLSurveyor(SQLDialect.PLpgSQL); 11 | tsqlSurveyor = new SQLSurveyor(SQLDialect.TSQL); 12 | }); 13 | 14 | test('equivalence for a select query with a JOIN', () => { 15 | const sql = 'SELECT * FROM tableName t1 \r\n JOIN tableName2 t2 ON t1.id = t2.id'; 16 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 17 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 18 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 19 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 20 | expect(parsedMySql).toStrictEqual(parsedPlSql); 21 | expect(parsedMySql).toStrictEqual(parsedTSql); 22 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 23 | }); 24 | 25 | test('equivalence for a select query with a subquery', () => { 26 | const sql = 'SELECT t1.val FROM tableName t1 where t1.col1 = 1 and (select 1 from tableName2) > 0'; 27 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 28 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 29 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 30 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 31 | expect(parsedMySql).toStrictEqual(parsedTSql); 32 | expect(parsedMySql).toStrictEqual(parsedPlSql); 33 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 34 | }); 35 | 36 | test('equivalence for a select query with a multiple output columns and a subquery', () => { 37 | const sql = 'SELECT t.column1, t.* FROM table1 t WHERE t.col in (select col from table2 where col3 = col4)'; 38 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 39 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 40 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 41 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 42 | expect(parsedMySql).toStrictEqual(parsedPlSql); 43 | expect(parsedMySql).toStrictEqual(parsedTSql); 44 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 45 | }); 46 | 47 | test('equivalence for a query with a common table expression', () => { 48 | const sql = 'with my_depts as (select dept_num from departments where department_name = \'test\') select * from my_depts ;'; 49 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 50 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 51 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 52 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 53 | expect(parsedMySql).toStrictEqual(parsedPlSql); 54 | expect(parsedMySql).toStrictEqual(parsedTSql); 55 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 56 | }); 57 | 58 | test('equivalence for a query with multiple common table expressions', () => { 59 | const sql = 'with my_depts as (select dept_num from departments where department_name = \'test\'), my_emps as (select emp_num from employees) select * from my_depts;'; 60 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 61 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 62 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 63 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 64 | expect(parsedMySql).toStrictEqual(parsedPlSql); 65 | expect(parsedMySql).toStrictEqual(parsedTSql); 66 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 67 | }); 68 | 69 | test('equivalence for a query with multiple common table expressions and a subquery', () => { 70 | const sql = 'with my_depts as (select dept_num from departments where department_name = \'test\'), my_emps as (select emp_num from employees) select * from my_depts where dept_num in (select * from departmentemployees);'; 71 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 72 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 73 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 74 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 75 | expect(parsedMySql).toStrictEqual(parsedPlSql); 76 | expect(parsedMySql).toStrictEqual(parsedTSql); 77 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 78 | }); 79 | 80 | test('equivalence for a query with a subquery', () => { 81 | const sql = 'SELECT t1.val FROM tableName t1 where t1.col1 = 1 and (select 1 from tableName2) > 0'; 82 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 83 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 84 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 85 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 86 | expect(parsedMySql).toStrictEqual(parsedPlSql); 87 | expect(parsedMySql).toStrictEqual(parsedTSql); 88 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 89 | }); 90 | 91 | test('equivalence for an update query', () => { 92 | const sql = 'UPDATE departments SET department_name = \'lol\' WHERE dept_num = 1'; 93 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 94 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 95 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 96 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 97 | expect(parsedMySql).toStrictEqual(parsedPlSql); 98 | expect(parsedMySql).toStrictEqual(parsedTSql); 99 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 100 | }); 101 | 102 | test('equivalence for a select query with a column name in the SELECT list', () => { 103 | const sql = 'SELECT t1.val FROM tableName t1'; 104 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 105 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 106 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 107 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 108 | expect(parsedMySql).toStrictEqual(parsedPlSql); 109 | expect(parsedMySql).toStrictEqual(parsedTSql); 110 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 111 | }); 112 | 113 | test('equivalence for a select query with an aggregated column name in the SELECT list', () => { 114 | const sql = 'SELECT MAX(t1.val) FROM tableName t1'; 115 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 116 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 117 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 118 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 119 | expect(parsedMySql).toStrictEqual(parsedPlSql); 120 | expect(parsedMySql).toStrictEqual(parsedTSql); 121 | 122 | // Known bug: PL/pgSQL grammar does not identify aggregations functions 123 | // TODO: Remove this after the grammar adds support for aggregation functions 124 | parsedPLpgSql.parsedQueries[0].tokens[7].type = TokenType.KEYWORD; 125 | 126 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 127 | }); 128 | 129 | test('equivalence for a subquery in the SELECT list', () => { 130 | const sql = 'SELECT (SELECT MAX(d.dept_num) FROM departments d JOIN departmentemployees de ON d.dept_num = de.dept_num) AS maxval, e.employee_num FROM employees e '; 131 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 132 | const parsedPlSql: ParsedSql = plsqlSurveyor.survey(sql); 133 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 134 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 135 | expect(parsedMySql).toStrictEqual(parsedTSql); 136 | expect(parsedMySql).toStrictEqual(parsedPlSql); 137 | 138 | // Known bug: PL/pgSQL grammar does not identify aggregations functions 139 | // TODO: Remove this after the grammar adds support for aggregation functions 140 | parsedPLpgSql.parsedQueries[0].tokens[15].type = TokenType.KEYWORD; 141 | parsedPLpgSql.parsedQueries[0].subqueries[8].tokens[15].type = TokenType.KEYWORD; 142 | 143 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 144 | }); 145 | 146 | test('equivalence for "tableName AS alias" syntax', () => { 147 | const sql = 'select f.id from foo as f'; 148 | const parsedMySql: ParsedSql = mysqlSurveyor.survey(sql); 149 | const parsedPLpgSql: ParsedSql = plpgsqlSurveyor.survey(sql); 150 | // Skip PL/SQL - Does not support "table AS alias" syntax 151 | const parsedTSql: ParsedSql = tsqlSurveyor.survey(sql); 152 | expect(parsedMySql).toStrictEqual(parsedTSql); 153 | expect(parsedMySql).toStrictEqual(parsedPLpgSql); 154 | }); -------------------------------------------------------------------------------- /src/parsing/PLpgSQLQueryListener.ts: -------------------------------------------------------------------------------- 1 | import { PLpgSQLParserListener, PLpgSQLGrammar } from 'antlr4ts-sql'; 2 | import { BaseSqlQueryListener } from './BaseSqlQueryListener'; 3 | import { TokenLocation } from '../models/TokenLocation'; 4 | import { ParsedQuery } from '../models/ParsedQuery'; 5 | import { QueryType } from '../models/QueryType'; 6 | import { ReferencedTable } from '../models/ReferencedTable'; 7 | 8 | export class PLpgSQLQueryListener extends BaseSqlQueryListener implements PLpgSQLParserListener { 9 | 10 | unquote(value: string) { 11 | if (value.startsWith('"') && value.endsWith('"')) { 12 | return value.slice(1, value.length - 1); 13 | } 14 | return value; 15 | } 16 | 17 | _getAliasStartIndex(value: string): number { 18 | return super._getAliasStartIndex(value, '"', '"'); 19 | } 20 | 21 | _getTableAliasEndLocation(value: string): number { 22 | return super._getTableAliasEndLocation(value, '"', '"'); 23 | } 24 | 25 | parseContextToReferencedTable(ctx: any) { 26 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 27 | const tableText = tableLocation.getToken(this.input); 28 | let tableNameOrAlias = tableText; 29 | let schemaName = null; 30 | if (tableText.includes('.')) { 31 | const columnTextSplit: string[] = tableText.split('.'); 32 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 33 | schemaName = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 34 | } else { 35 | tableNameOrAlias = this.unquote(tableNameOrAlias); 36 | } 37 | const referencedTable = new ReferencedTable(tableNameOrAlias); 38 | referencedTable.schemaName = schemaName; 39 | return referencedTable; 40 | } 41 | 42 | enterData_statement(ctx: any) { 43 | try { 44 | if (!(ctx._parent instanceof PLpgSQLGrammar.With_queryContext)) { // Ignore the trailing portion of a CTE query 45 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 46 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DML, queryLocation.getToken(this.input), queryLocation)); 47 | } 48 | } catch (err) { 49 | this._handleError(err); 50 | } 51 | } 52 | 53 | enterSchema_statement(ctx: any) { 54 | try { 55 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 56 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DDL, queryLocation.getToken(this.input), queryLocation)); 57 | } catch (err) { 58 | this._handleError(err); 59 | } 60 | } 61 | 62 | enterScript_statement(ctx: any) { 63 | try { 64 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 65 | this.parsedSql._addQuery(new ParsedQuery(null, queryLocation.getToken(this.input), queryLocation)); 66 | } catch (err) { 67 | this._handleError(err); 68 | } 69 | } 70 | 71 | enterSelect_stmt_no_parens(ctx: any) { 72 | try { 73 | const subqueryLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 74 | let parsedQuery = this.parsedSql.getQueryAtLocation(subqueryLocation.startIndex); 75 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(subqueryLocation.startIndex); 76 | parsedQuery._addSubQuery(new ParsedQuery(QueryType.DML, subqueryLocation.getToken(this.input), subqueryLocation)); 77 | } catch (err) { 78 | this._handleError(err); 79 | } 80 | } 81 | 82 | enterWith_query(ctx: any) { 83 | try { 84 | const cteLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 85 | let parsedQuery = this.parsedSql.getQueryAtLocation(cteLocation.startIndex); 86 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(cteLocation.startIndex); 87 | parsedQuery._addCommonTableExpression(new ParsedQuery(QueryType.DML, cteLocation.getToken(this.input), cteLocation)); 88 | } catch (err) { 89 | this._handleError(err); 90 | } 91 | } 92 | 93 | exitSchema_qualified_name(ctx: any) { 94 | try { 95 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 96 | const referencedTable = this.parseContextToReferencedTable(ctx); 97 | let parsedQuery = this.parsedSql.getQueryAtLocation(tableLocation.startIndex); 98 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(tableLocation.startIndex); 99 | parsedQuery._addTableNameLocation(referencedTable.tableName, tableLocation, referencedTable.schemaName, referencedTable.databaseName); 100 | } catch (err) { 101 | this._handleError(err); 102 | } 103 | } 104 | 105 | exitAlias_clause(ctx: any) { 106 | try { 107 | if (ctx._alias !== null && ctx._alias !== undefined) { 108 | const aliasLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 109 | const referencedTable = this.parseContextToReferencedTable(ctx._parent.children[0]); 110 | let parsedQuery = this.parsedSql.getQueryAtLocation(aliasLocation.startIndex); 111 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(aliasLocation.startIndex); 112 | let aliasName = this.unquote(aliasLocation.getToken(this.input)); 113 | const aliasStartIndex = this._getAliasStartIndex(aliasName); 114 | if (aliasStartIndex !== null) { 115 | // alias is in the format 'AS alias', ignore the 'AS ' 116 | aliasName = aliasName.substring(aliasStartIndex); 117 | } 118 | parsedQuery._addAliasForTable(aliasName, referencedTable.tableName); 119 | } 120 | } catch (err) { 121 | this._handleError(err); 122 | } 123 | } 124 | 125 | exitIndirection_var(ctx: any) { 126 | try { 127 | let parentContext = ctx._parent; 128 | while (parentContext !== undefined) { 129 | if (parentContext instanceof PLpgSQLGrammar.Select_sublistContext) { 130 | // This is an output column, don't record it as a referenced column 131 | return; 132 | } else if (parentContext instanceof PLpgSQLGrammar.Select_stmt_no_parensContext) { 133 | // This is a subquery in the SELECT list, add the referenced column 134 | break; 135 | } 136 | parentContext = parentContext._parent; 137 | } 138 | const columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 139 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 140 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 141 | const columnText = columnLocation.getToken(this.input); 142 | let columnName = columnText; 143 | let tableNameOrAlias = null; 144 | if (columnText.includes('.')) { 145 | const columnTextSplit: string[] = columnText.split('.'); 146 | columnName = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 147 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 148 | let tableNameOrAliasStartIndex = columnLocation.stopIndex - columnTextSplit[columnTextSplit.length - 1].length - columnTextSplit[columnTextSplit.length - 2].length; 149 | let tableNameOrAliasStopIndex = tableNameOrAliasStartIndex + columnTextSplit[columnTextSplit.length - 2].length - 1; 150 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, tableNameOrAliasStartIndex, tableNameOrAliasStopIndex); 151 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 152 | } else { 153 | columnName = this.unquote(columnName); 154 | } 155 | parsedQuery._addReferencedColumn(columnName, tableNameOrAlias, columnLocation); 156 | } catch (err) { 157 | this._handleError(err); 158 | } 159 | } 160 | 161 | exitIndirection_identifier(ctx: any) { 162 | try { 163 | this.exitIndirection_var(ctx); 164 | } catch (err) { 165 | this._handleError(err); 166 | } 167 | } 168 | 169 | exitSelect_sublist(ctx: any) { 170 | try { 171 | let columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 172 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 173 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 174 | let columnText = columnLocation.getToken(this.input); 175 | let columnName = columnText; 176 | let columnAlias = null; 177 | let tableNameOrAlias = null; 178 | if (columnText.includes('.')) { 179 | // Column may have a table alias 180 | const functionArgumentLocation = this._getFunctionArgumentLocation(ctx, columnLocation); 181 | if (functionArgumentLocation !== null) { 182 | columnText = functionArgumentLocation.getToken(this.input); 183 | columnLocation = functionArgumentLocation; 184 | } 185 | const tableNameOrAliasStopIndex = this._getTableAliasEndLocation(columnText); 186 | if (tableNameOrAliasStopIndex !== null) { 187 | tableNameOrAlias = this.unquote(columnText.substring(0, tableNameOrAliasStopIndex)); 188 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, columnLocation.startIndex, columnLocation.startIndex + tableNameOrAliasStopIndex - 1); 189 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 190 | } 191 | } 192 | columnName = columnName.trim(); 193 | const aliasStartIndex = this._getAliasStartIndex(columnName); 194 | if (aliasStartIndex !== null) { 195 | // Column has an alias 196 | columnAlias = columnName.substring(aliasStartIndex); 197 | columnName = columnName.substring(0, aliasStartIndex - 1).trimEnd(); 198 | if (columnName.toUpperCase().endsWith('AS')) { 199 | columnName = columnName.substring(0, columnName.length - 2).trimEnd(); 200 | } 201 | } 202 | columnName = this.unquote(columnName); 203 | if (columnAlias !== null) { 204 | columnAlias = this.unquote(columnAlias); 205 | } 206 | parsedQuery._addOutputColumn(columnName, columnAlias, tableNameOrAlias); 207 | } catch (err) { 208 | this._handleError(err); 209 | } 210 | } 211 | 212 | _getFunctionArgumentLocation(ctx: any, columnLocation: TokenLocation): TokenLocation { 213 | const functionRules = [PLpgSQLGrammar.Function_callContext]; 214 | const argumentRules = [PLpgSQLGrammar.Vex_or_named_notationContext]; 215 | return super._getFunctionArgumentLocation(ctx, columnLocation, functionRules, argumentRules); 216 | } 217 | 218 | } -------------------------------------------------------------------------------- /src/parsing/PlSqlQueryListener.ts: -------------------------------------------------------------------------------- 1 | import { TokenLocation } from '../models/TokenLocation'; 2 | import { ParsedQuery } from '../models/ParsedQuery'; 3 | import { QueryType } from '../models/QueryType'; 4 | import { PlSqlParserListener, PlSQLGrammar } from 'antlr4ts-sql'; 5 | import { BaseSqlQueryListener } from './BaseSqlQueryListener'; 6 | import { ReferencedTable } from '../models/ReferencedTable'; 7 | 8 | export class PlSqlQueryListener extends BaseSqlQueryListener implements PlSqlParserListener { 9 | 10 | unquote(value: string) { 11 | if (value.startsWith('"') && value.endsWith('"')) { 12 | return value.slice(1, value.length - 1); 13 | } 14 | return value; 15 | } 16 | 17 | _getAliasStartIndex(value: string): number { 18 | return super._getAliasStartIndex(value, '"', '"'); 19 | } 20 | 21 | _getTableAliasEndLocation(value: string): number { 22 | return super._getTableAliasEndLocation(value, '"', '"'); 23 | } 24 | 25 | parseContextToReferencedTable(ctx: any) { 26 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 27 | const tableText = tableLocation.getToken(this.input); 28 | let tableNameOrAlias = tableText; 29 | let schemaName = null; 30 | if (tableText.includes('.')) { 31 | const columnTextSplit: string[] = tableText.split('.'); 32 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 33 | schemaName = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 34 | } else { 35 | tableNameOrAlias = this.unquote(tableNameOrAlias); 36 | } 37 | const referencedTable = new ReferencedTable(tableNameOrAlias); 38 | referencedTable.schemaName = schemaName; 39 | return referencedTable; 40 | } 41 | 42 | enterData_manipulation_language_statements(ctx: any) { 43 | try { 44 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 45 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DML, queryLocation.getToken(this.input), queryLocation)); 46 | } catch (err) { 47 | this._handleError(err); 48 | } 49 | } 50 | 51 | enterUnit_statement(ctx: any) { 52 | try { 53 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 54 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DDL, queryLocation.getToken(this.input), queryLocation)); 55 | } catch (err) { 56 | this._handleError(err); 57 | } 58 | } 59 | 60 | enterSubquery(ctx: any) { 61 | try { 62 | const subqueryLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 63 | let parsedQuery = this.parsedSql.getQueryAtLocation(subqueryLocation.startIndex); 64 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(subqueryLocation.startIndex); 65 | if (!(ctx._parent instanceof PlSQLGrammar.Factoring_elementContext) // Ignore the "WITH name AS (query)" portion of CTE 66 | && !(ctx._parent.children[0] instanceof PlSQLGrammar.Subquery_factoring_clauseContext) // Ignore the trailing portion of a CTE query 67 | && !(parsedQuery.queryLocation.startIndex === subqueryLocation.startIndex 68 | && parsedQuery.queryLocation.stopIndex === subqueryLocation.stopIndex)) { // PLSQL grammar has EVERY query as a subquery, prevent repeating the same query 69 | parsedQuery._addSubQuery(new ParsedQuery(QueryType.DML, subqueryLocation.getToken(this.input), subqueryLocation)); 70 | } 71 | } catch (err) { 72 | this._handleError(err); 73 | } 74 | } 75 | 76 | enterFactoring_element(ctx: any) { 77 | try { 78 | const cteLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 79 | let parsedQuery = this.parsedSql.getQueryAtLocation(cteLocation.startIndex); 80 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(cteLocation.startIndex); 81 | parsedQuery._addCommonTableExpression(new ParsedQuery(QueryType.DML, cteLocation.getToken(this.input), cteLocation)); 82 | } catch (err) { 83 | this._handleError(err); 84 | } 85 | } 86 | 87 | exitTable_alias(ctx: any) { 88 | try { 89 | const aliasLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 90 | const referencedTable = this.parseContextToReferencedTable(ctx._parent.children[0].children[0].children[0]); 91 | let parsedQuery = this.parsedSql.getQueryAtLocation(aliasLocation.startIndex); 92 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(aliasLocation.startIndex); 93 | parsedQuery._addAliasForTable(this.unquote(aliasLocation.getToken(this.input)), referencedTable.tableName); 94 | } catch (err) { 95 | this._handleError(err); 96 | } 97 | } 98 | 99 | exitVariable_name(ctx: any) { 100 | // Handle any variables as columns 101 | this.exitColumn_name(ctx); 102 | } 103 | 104 | exitTableview_name(ctx: any) { 105 | try { 106 | if (!(ctx._parent instanceof PlSQLGrammar.Select_list_elementsContext)) { // We already detect this context separately 107 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 108 | const referencedTable = this.parseContextToReferencedTable(ctx); 109 | let parsedQuery = this.parsedSql.getQueryAtLocation(tableLocation.startIndex); 110 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(tableLocation.startIndex); 111 | parsedQuery._addTableNameLocation(referencedTable.tableName, tableLocation, referencedTable.schemaName, referencedTable.databaseName); 112 | } 113 | } catch (err) { 114 | this._handleError(err); 115 | } 116 | } 117 | 118 | exitColumn_name(ctx: any) { 119 | try { 120 | let parentContext = ctx._parent; 121 | while (parentContext !== undefined) { 122 | if (parentContext instanceof PlSQLGrammar.Select_list_elementsContext) { 123 | // This is an output column, don't record it as a referenced column 124 | return; 125 | } else if (parentContext instanceof PlSQLGrammar.SubqueryContext) { 126 | // This is a subquery in the SELECT list, add the referenced column 127 | break; 128 | } 129 | parentContext = parentContext._parent; 130 | } 131 | const columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 132 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 133 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 134 | const columnText = columnLocation.getToken(this.input); 135 | let columnName = columnText; 136 | let tableNameOrAlias = null; 137 | if (columnText.includes('.')) { 138 | const columnTextSplit: string[] = columnText.split('.'); 139 | columnName = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 140 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 141 | let tableNameOrAliasStartIndex = columnLocation.stopIndex - columnTextSplit[columnTextSplit.length - 1].length - columnTextSplit[columnTextSplit.length - 2].length; 142 | let tableNameOrAliasStopIndex = tableNameOrAliasStartIndex + columnTextSplit[columnTextSplit.length - 2].length - 1; 143 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, tableNameOrAliasStartIndex, tableNameOrAliasStopIndex); 144 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 145 | } else { 146 | columnName = this.unquote(columnName); 147 | } 148 | parsedQuery._addReferencedColumn(columnName, tableNameOrAlias, columnLocation); 149 | } catch (err) { 150 | this._handleError(err); 151 | } 152 | } 153 | 154 | exitGeneral_element(ctx: any) { 155 | try { 156 | let parentContext = ctx._parent; 157 | while (parentContext !== undefined) { 158 | if (parentContext instanceof PlSQLGrammar.Select_list_elementsContext) { 159 | // This is an output column, don't record it as a referenced column 160 | return; 161 | } else if (parentContext instanceof PlSQLGrammar.SubqueryContext) { 162 | // This is a subquery in the SELECT list, add the referenced column 163 | break; 164 | } 165 | parentContext = parentContext._parent; 166 | } 167 | this.exitColumn_name(ctx); 168 | } catch (err) { 169 | this._handleError(err); 170 | } 171 | } 172 | 173 | exitSelect_list_elements(ctx: any) { 174 | try { 175 | let columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 176 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 177 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 178 | let columnText = columnLocation.getToken(this.input); 179 | let columnName = columnText; 180 | let columnAlias = null; 181 | let tableNameOrAlias = null; 182 | if (columnText.includes('.')) { 183 | // Column may have a table alias 184 | const functionArgumentLocation = this._getFunctionArgumentLocation(ctx, columnLocation); 185 | if (functionArgumentLocation !== null) { 186 | columnText = functionArgumentLocation.getToken(this.input); 187 | columnLocation = functionArgumentLocation; 188 | } 189 | const tableNameOrAliasStopIndex = this._getTableAliasEndLocation(columnText); 190 | if (tableNameOrAliasStopIndex !== null) { 191 | tableNameOrAlias = this.unquote(columnText.substring(0, tableNameOrAliasStopIndex)); 192 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, columnLocation.startIndex, columnLocation.startIndex + tableNameOrAliasStopIndex - 1); 193 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 194 | } 195 | } 196 | columnName = columnName.trim(); 197 | const lastUnquotedSpaceIndex = this._getAliasStartIndex(columnName); 198 | if (lastUnquotedSpaceIndex !== null) { 199 | // Column has an alias 200 | columnAlias = columnName.substring(lastUnquotedSpaceIndex); 201 | columnName = columnName.substring(0, lastUnquotedSpaceIndex - 1).trimEnd(); 202 | if (columnName.toUpperCase().endsWith('AS')) { 203 | columnName = columnName.substring(0, columnName.length - 2).trimEnd(); 204 | } 205 | } 206 | columnName = this.unquote(columnName); 207 | if (columnAlias !== null) { 208 | columnAlias = this.unquote(columnAlias); 209 | } 210 | parsedQuery._addOutputColumn(columnName, columnAlias, tableNameOrAlias); 211 | } catch (err) { 212 | this._handleError(err); 213 | } 214 | } 215 | 216 | _getFunctionArgumentLocation(ctx: any, columnLocation: TokenLocation): TokenLocation { 217 | const functionRules = [PlSQLGrammar.Numeric_functionContext, PlSQLGrammar.String_functionContext, PlSQLGrammar.Other_functionContext]; 218 | const argumentRules = [PlSQLGrammar.ExpressionContext]; 219 | return super._getFunctionArgumentLocation(ctx, columnLocation, functionRules, argumentRules); 220 | } 221 | 222 | exitSelected_list(ctx: any) { 223 | try { 224 | const columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 225 | if (columnLocation.getToken(this.input) === '*') { 226 | // Otherwise, the columns will be picked up by exitSelected_list_elements on their own 227 | this.exitSelect_list_elements(ctx); 228 | } 229 | } catch (err) { 230 | this._handleError(err); 231 | } 232 | } 233 | 234 | } -------------------------------------------------------------------------------- /test/mysql.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect, ParsedSql, TokenLocation } from '../dist/index'; 2 | 3 | let surveyor: SQLSurveyor = null; 4 | beforeAll(() => { 5 | surveyor = new SQLSurveyor(SQLDialect.MYSQL); 6 | }); 7 | 8 | test('that table names and aliases are correctly parsed', () => { 9 | const sql = 'SELECT * FROM tableName t1 \r\n JOIN tableName2 t2 ON t1.id = t2.id'; 10 | const parsedSql: ParsedSql = surveyor.survey(sql); 11 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 12 | const parsedQuery = parsedSql.getQueryAtLocation(0); 13 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 14 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 15 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 16 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 17 | expect(parsedQuery.queryLocation.stopIndex).toBe(64); // 0-based indices, \r and \n are one character each 18 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 19 | 20 | const tableName = parsedQuery.referencedTables['tableName']; 21 | expect(tableName).toBeTruthy(); 22 | expect(tableName.aliases.size).toBe(1); 23 | expect(tableName.aliases.has('t1')).toBeTruthy(); 24 | expect(tableName.locations.size).toBe(2); 25 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 26 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 27 | expect(tableName.schemaName).toBeNull(); 28 | expect(tableName.databaseName).toBeNull(); 29 | 30 | const tableName2 = parsedQuery.referencedTables['tableName2']; 31 | expect(tableName2).toBeTruthy(); 32 | expect(tableName2.aliases.size).toBe(1); 33 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 34 | expect(tableName2.locations.size).toBe(2); 35 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 36 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 37 | expect(tableName2.schemaName).toBeNull(); 38 | expect(tableName2.databaseName).toBeNull(); 39 | 40 | expect(parsedQuery.outputColumns.length).toBe(1); 41 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 42 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 43 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 44 | }); 45 | 46 | 47 | test('that quote characters are removed from databases, schemas, tables, and columns', () => { 48 | const sql = 'SELECT * FROM `tableName` t1 \r\n JOIN tableName2 `t2` ON `t1`.id = t2.id'; 49 | const parsedSql: ParsedSql = surveyor.survey(sql); 50 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 51 | const parsedQuery = parsedSql.getQueryAtLocation(0); 52 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 53 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 54 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 55 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 56 | expect(parsedQuery.queryLocation.stopIndex).toBe(70); // 0-based indices, \r and \n are one character each 57 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 58 | 59 | const tableName = parsedQuery.referencedTables['tableName']; 60 | expect(tableName).toBeTruthy(); 61 | expect(tableName.aliases.size).toBe(1); 62 | expect(tableName.aliases.has('t1')).toBeTruthy(); 63 | expect(tableName.locations.size).toBe(2); 64 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 65 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 66 | expect(tableName.schemaName).toBeNull(); 67 | expect(tableName.databaseName).toBeNull(); 68 | 69 | const tableName2 = parsedQuery.referencedTables['tableName2']; 70 | expect(tableName2).toBeTruthy(); 71 | expect(tableName2.aliases.size).toBe(1); 72 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 73 | expect(tableName2.locations.size).toBe(2); 74 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 75 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 76 | expect(tableName2.schemaName).toBeNull(); 77 | expect(tableName2.databaseName).toBeNull(); 78 | 79 | expect(parsedQuery.outputColumns.length).toBe(1); 80 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 81 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 82 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 83 | }); 84 | 85 | test('that output column names and aliases are correctly parsed', () => { 86 | let sql = 'SELECT col1 as c1, avg(col2) as average, t1.col3 as c3, col4 c4, (select count(*) from test), (select count(*) from test) as numRows, (select max(col1) from t1) maxval, ' 87 | sql += ' `space column`, `space column 2` as space2, `space column 3` space3, \'a string value\', \'another string value\' another, \'final string value\' as final FROM tableName t1;'; 88 | 89 | const parsedSql: ParsedSql = surveyor.survey(sql); 90 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 91 | const parsedQuery = parsedSql.getQueryAtLocation(0); 92 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(13); 93 | 94 | const column = parsedQuery.outputColumns[0]; 95 | expect(column.columnName).toBe('col1'); 96 | expect(column.columnAlias).toBe('c1'); 97 | expect(column.tableName).toBeNull(); 98 | expect(column.tableAlias).toBeNull(); 99 | 100 | const column2 = parsedQuery.outputColumns[1]; 101 | expect(column2.columnName).toBe('avg(col2)'); 102 | expect(column2.columnAlias).toBe('average'); 103 | expect(column2.tableName).toBeNull(); 104 | expect(column2.tableAlias).toBeNull(); 105 | 106 | const column3 = parsedQuery.outputColumns[2]; 107 | expect(column3.columnName).toBe('t1.col3'); 108 | expect(column3.columnAlias).toBe('c3'); 109 | expect(column3.tableName).toBe('tableName'); 110 | expect(column3.tableAlias).toBe('t1'); 111 | 112 | const column4 = parsedQuery.outputColumns[3]; 113 | expect(column4.columnName).toBe('col4'); 114 | expect(column4.columnAlias).toBe('c4'); 115 | expect(column4.tableName).toBeNull(); 116 | expect(column4.tableAlias).toBeNull(); 117 | 118 | const column5 = parsedQuery.outputColumns[4]; 119 | expect(column5.columnName).toBe('(select count(*) from test)'); 120 | expect(column5.columnAlias).toBeNull(); 121 | expect(column5.tableName).toBeNull(); 122 | expect(column5.tableAlias).toBeNull(); 123 | 124 | const column6 = parsedQuery.outputColumns[5]; 125 | expect(column6.columnName).toBe('(select count(*) from test)'); 126 | expect(column6.columnAlias).toBe('numRows'); 127 | expect(column6.tableName).toBeNull(); 128 | expect(column6.tableAlias).toBeNull(); 129 | 130 | const column7 = parsedQuery.outputColumns[6]; 131 | expect(column7.columnName).toBe('(select max(col1) from t1)'); 132 | expect(column7.columnAlias).toBe('maxval'); 133 | expect(column7.tableName).toBeNull(); 134 | expect(column7.tableAlias).toBeNull(); 135 | 136 | const column8 = parsedQuery.outputColumns[7]; 137 | expect(column8.columnName).toBe('space column'); 138 | expect(column8.columnAlias).toBeNull(); 139 | expect(column8.tableName).toBeNull(); 140 | expect(column8.tableAlias).toBeNull(); 141 | 142 | const column9 = parsedQuery.outputColumns[8]; 143 | expect(column9.columnName).toBe('space column 2'); 144 | expect(column9.columnAlias).toBe('space2'); 145 | expect(column9.tableName).toBeNull(); 146 | expect(column9.tableAlias).toBeNull(); 147 | 148 | const column10 = parsedQuery.outputColumns[9]; 149 | expect(column10.columnName).toBe('space column 3'); 150 | expect(column10.columnAlias).toBe('space3'); 151 | expect(column10.tableName).toBeNull(); 152 | expect(column10.tableAlias).toBeNull(); 153 | 154 | const column11 = parsedQuery.outputColumns[10]; 155 | expect(column11.columnName).toBe("'a string value'"); 156 | expect(column11.columnAlias).toBeNull(); 157 | expect(column11.tableName).toBeNull(); 158 | expect(column11.tableAlias).toBeNull(); 159 | 160 | const column12 = parsedQuery.outputColumns[11]; 161 | expect(column12.columnName).toBe("'another string value'"); 162 | expect(column12.columnAlias).toBe('another'); 163 | expect(column12.tableName).toBeNull(); 164 | expect(column12.tableAlias).toBeNull(); 165 | 166 | const column13 = parsedQuery.outputColumns[12]; 167 | expect(column13.columnName).toBe("'final string value'"); 168 | expect(column13.columnAlias).toBe('final'); 169 | expect(column13.tableName).toBeNull(); 170 | expect(column13.tableAlias).toBeNull(); 171 | }); 172 | 173 | test('that output column names and aliases are correctly parsed when subquery has a period', () => { 174 | let sql = 'select e2.employee_num, (select max(e.employee_num) from employees e) as counter from employees e2;'; 175 | 176 | const parsedSql: ParsedSql = surveyor.survey(sql); 177 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 178 | const parsedQuery = parsedSql.getQueryAtLocation(0); 179 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(2); 180 | 181 | const column = parsedQuery.outputColumns[0]; 182 | expect(column.columnName).toBe('e2.employee_num'); 183 | expect(column.columnAlias).toBeNull(); 184 | expect(column.tableName).toBe('employees'); 185 | expect(column.tableAlias).toBe('e2'); 186 | 187 | const column2 = parsedQuery.outputColumns[1]; 188 | expect(column2.columnName).toBe('(select max(e.employee_num) from employees e)'); 189 | expect(column2.columnAlias).toBe('counter'); 190 | expect(column2.tableName).toBeNull(); 191 | expect(column2.tableAlias).toBeNull(); 192 | }); 193 | 194 | test('that output column names and aliases are correctly parsed for a CASE statement', () => { 195 | let sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END FROM table1;'; 196 | 197 | let parsedSql: ParsedSql = surveyor.survey(sql); 198 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 199 | let parsedQuery = parsedSql.getQueryAtLocation(0); 200 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 201 | 202 | let column = parsedQuery.outputColumns[0]; 203 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 204 | expect(column.columnAlias).toBeNull(); 205 | expect(column.tableName).toBeNull(); 206 | expect(column.tableAlias).toBeNull(); 207 | 208 | sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END csstmt FROM table1;'; 209 | parsedSql = surveyor.survey(sql); 210 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 211 | parsedQuery = parsedSql.getQueryAtLocation(0); 212 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 213 | 214 | column = parsedQuery.outputColumns[0]; 215 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 216 | expect(column.columnAlias).toBe('csstmt'); 217 | expect(column.tableName).toBeNull(); 218 | expect(column.tableAlias).toBeNull(); 219 | 220 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) FROM table1;'; 221 | parsedSql = surveyor.survey(sql); 222 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 223 | parsedQuery = parsedSql.getQueryAtLocation(0); 224 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 225 | 226 | column = parsedQuery.outputColumns[0]; 227 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 228 | expect(column.columnAlias).toBeNull(); 229 | expect(column.tableName).toBeNull(); 230 | expect(column.tableAlias).toBeNull(); 231 | 232 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) csstmt FROM table1;'; 233 | parsedSql = surveyor.survey(sql); 234 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 235 | parsedQuery = parsedSql.getQueryAtLocation(0); 236 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 237 | 238 | column = parsedQuery.outputColumns[0]; 239 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 240 | expect(column.columnAlias).toBe('csstmt'); 241 | expect(column.tableName).toBeNull(); 242 | expect(column.tableAlias).toBeNull(); 243 | }); -------------------------------------------------------------------------------- /test/plsql.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect, ParsedSql, TokenLocation } from '../dist/index'; 2 | 3 | let surveyor: SQLSurveyor = null; 4 | beforeAll(() => { 5 | surveyor = new SQLSurveyor(SQLDialect.PLSQL); 6 | }); 7 | 8 | test('that table names and aliases are correctly parsed', () => { 9 | const sql = 'SELECT * FROM tableName t1 \r\n JOIN tableName2 t2 ON t1.id = t2.id'; 10 | const parsedSql: ParsedSql = surveyor.survey(sql); 11 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 12 | const parsedQuery = parsedSql.getQueryAtLocation(0); 13 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 14 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 15 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 16 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 17 | expect(parsedQuery.queryLocation.stopIndex).toBe(64); // 0-based indices, \r and \n are one character each 18 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 19 | 20 | const tableName = parsedQuery.referencedTables['tableName']; 21 | expect(tableName).toBeTruthy(); 22 | expect(tableName.aliases.size).toBe(1); 23 | expect(tableName.aliases.has('t1')).toBeTruthy(); 24 | expect(tableName.locations.size).toBe(2); 25 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 26 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 27 | expect(tableName.schemaName).toBeNull(); 28 | expect(tableName.databaseName).toBeNull(); 29 | 30 | const tableName2 = parsedQuery.referencedTables['tableName2']; 31 | expect(tableName2).toBeTruthy(); 32 | expect(tableName2.aliases.size).toBe(1); 33 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 34 | expect(tableName2.locations.size).toBe(2); 35 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 36 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 37 | expect(tableName2.schemaName).toBeNull(); 38 | expect(tableName2.databaseName).toBeNull(); 39 | 40 | expect(parsedQuery.outputColumns.length).toBe(1); 41 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 42 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 43 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 44 | }); 45 | 46 | 47 | test('that quote characters are removed from databases, schemas, tables, and columns', () => { 48 | const sql = 'SELECT * FROM "tableName" t1 \r\n JOIN tableName2 "t2" ON "t1".id = t2.id'; 49 | const parsedSql: ParsedSql = surveyor.survey(sql); 50 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 51 | const parsedQuery = parsedSql.getQueryAtLocation(0); 52 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 53 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 54 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 55 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 56 | expect(parsedQuery.queryLocation.stopIndex).toBe(70); // 0-based indices, \r and \n are one character each 57 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 58 | 59 | const tableName = parsedQuery.referencedTables['tableName']; 60 | expect(tableName).toBeTruthy(); 61 | expect(tableName.aliases.size).toBe(1); 62 | expect(tableName.aliases.has('t1')).toBeTruthy(); 63 | expect(tableName.locations.size).toBe(2); 64 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 65 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 66 | expect(tableName.schemaName).toBeNull(); 67 | expect(tableName.databaseName).toBeNull(); 68 | 69 | const tableName2 = parsedQuery.referencedTables['tableName2']; 70 | expect(tableName2).toBeTruthy(); 71 | expect(tableName2.aliases.size).toBe(1); 72 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 73 | expect(tableName2.locations.size).toBe(2); 74 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 75 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 76 | expect(tableName2.schemaName).toBeNull(); 77 | expect(tableName2.databaseName).toBeNull(); 78 | 79 | expect(parsedQuery.outputColumns.length).toBe(1); 80 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 81 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 82 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 83 | }); 84 | 85 | test('that output column names and aliases are correctly parsed', () => { 86 | let sql = 'SELECT col1 as c1, avg(col2) as average, t1.col3 as c3, col4 c4, (select count(*) from test), (select count(*) from test) as numRows, (select max(col1) from t1) maxval, ' 87 | sql += ' "space column", "space column 2" as space2, "space column 3" space3, \'a string value\', \'another string value\' another, \'final string value\' as final FROM tableName t1;'; 88 | 89 | const parsedSql: ParsedSql = surveyor.survey(sql); 90 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 91 | const parsedQuery = parsedSql.getQueryAtLocation(0); 92 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(13); 93 | 94 | const column = parsedQuery.outputColumns[0]; 95 | expect(column.columnName).toBe('col1'); 96 | expect(column.columnAlias).toBe('c1'); 97 | expect(column.tableName).toBeNull(); 98 | expect(column.tableAlias).toBeNull(); 99 | 100 | const column2 = parsedQuery.outputColumns[1]; 101 | expect(column2.columnName).toBe('avg(col2)'); 102 | expect(column2.columnAlias).toBe('average'); 103 | expect(column2.tableName).toBeNull(); 104 | expect(column2.tableAlias).toBeNull(); 105 | 106 | const column3 = parsedQuery.outputColumns[2]; 107 | expect(column3.columnName).toBe('t1.col3'); 108 | expect(column3.columnAlias).toBe('c3'); 109 | expect(column3.tableName).toBe('tableName'); 110 | expect(column3.tableAlias).toBe('t1'); 111 | 112 | const column4 = parsedQuery.outputColumns[3]; 113 | expect(column4.columnName).toBe('col4'); 114 | expect(column4.columnAlias).toBe('c4'); 115 | expect(column4.tableName).toBeNull(); 116 | expect(column4.tableAlias).toBeNull(); 117 | 118 | const column5 = parsedQuery.outputColumns[4]; 119 | expect(column5.columnName).toBe('(select count(*) from test)'); 120 | expect(column5.columnAlias).toBeNull(); 121 | expect(column5.tableName).toBeNull(); 122 | expect(column5.tableAlias).toBeNull(); 123 | 124 | const column6 = parsedQuery.outputColumns[5]; 125 | expect(column6.columnName).toBe('(select count(*) from test)'); 126 | expect(column6.columnAlias).toBe('numRows'); 127 | expect(column6.tableName).toBeNull(); 128 | expect(column6.tableAlias).toBeNull(); 129 | 130 | const column7 = parsedQuery.outputColumns[6]; 131 | expect(column7.columnName).toBe('(select max(col1) from t1)'); 132 | expect(column7.columnAlias).toBe('maxval'); 133 | expect(column7.tableName).toBeNull(); 134 | expect(column7.tableAlias).toBeNull(); 135 | 136 | const column8 = parsedQuery.outputColumns[7]; 137 | expect(column8.columnName).toBe('space column'); 138 | expect(column8.columnAlias).toBeNull(); 139 | expect(column8.tableName).toBeNull(); 140 | expect(column8.tableAlias).toBeNull(); 141 | 142 | const column9 = parsedQuery.outputColumns[8]; 143 | expect(column9.columnName).toBe('space column 2'); 144 | expect(column9.columnAlias).toBe('space2'); 145 | expect(column9.tableName).toBeNull(); 146 | expect(column9.tableAlias).toBeNull(); 147 | 148 | const column10 = parsedQuery.outputColumns[9]; 149 | expect(column10.columnName).toBe('space column 3'); 150 | expect(column10.columnAlias).toBe('space3'); 151 | expect(column10.tableName).toBeNull(); 152 | expect(column10.tableAlias).toBeNull(); 153 | 154 | const column11 = parsedQuery.outputColumns[10]; 155 | expect(column11.columnName).toBe("'a string value'"); 156 | expect(column11.columnAlias).toBeNull(); 157 | expect(column11.tableName).toBeNull(); 158 | expect(column11.tableAlias).toBeNull(); 159 | 160 | const column12 = parsedQuery.outputColumns[11]; 161 | expect(column12.columnName).toBe("'another string value'"); 162 | expect(column12.columnAlias).toBe('another'); 163 | expect(column12.tableName).toBeNull(); 164 | expect(column12.tableAlias).toBeNull(); 165 | 166 | const column13 = parsedQuery.outputColumns[12]; 167 | expect(column13.columnName).toBe("'final string value'"); 168 | expect(column13.columnAlias).toBe('final'); 169 | expect(column13.tableName).toBeNull(); 170 | expect(column13.tableAlias).toBeNull(); 171 | }); 172 | 173 | test('that output column names and aliases are correctly parsed when subquery has a period', () => { 174 | let sql = 'select e2.employee_num, (select max(e.employee_num) from employees e) as counter, schemaName.employees.employee_id as eid from employees e2;'; 175 | 176 | const parsedSql: ParsedSql = surveyor.survey(sql); 177 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 178 | const parsedQuery = parsedSql.getQueryAtLocation(0); 179 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(3); 180 | 181 | const column = parsedQuery.outputColumns[0]; 182 | expect(column.columnName).toBe('e2.employee_num'); 183 | expect(column.columnAlias).toBeNull(); 184 | expect(column.tableName).toBe('employees'); 185 | expect(column.tableAlias).toBe('e2'); 186 | 187 | const column2 = parsedQuery.outputColumns[1]; 188 | expect(column2.columnName).toBe('(select max(e.employee_num) from employees e)'); 189 | expect(column2.columnAlias).toBe('counter'); 190 | expect(column2.tableName).toBeNull(); 191 | expect(column2.tableAlias).toBeNull(); 192 | 193 | const column3 = parsedQuery.outputColumns[2]; 194 | expect(column3.columnName).toBe('schemaName.employees.employee_id'); 195 | expect(column3.columnAlias).toBe('eid'); 196 | expect(column3.tableName).toBe('schemaName.employees'); 197 | expect(column3.tableAlias).toBeNull(); 198 | }); 199 | 200 | test('that output column names and aliases are correctly parsed for a CASE statement', () => { 201 | let sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END FROM table1;'; 202 | 203 | let parsedSql: ParsedSql = surveyor.survey(sql); 204 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 205 | let parsedQuery = parsedSql.getQueryAtLocation(0); 206 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 207 | 208 | let column = parsedQuery.outputColumns[0]; 209 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 210 | expect(column.columnAlias).toBeNull(); 211 | expect(column.tableName).toBeNull(); 212 | expect(column.tableAlias).toBeNull(); 213 | 214 | sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END csstmt FROM table1;'; 215 | parsedSql = surveyor.survey(sql); 216 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 217 | parsedQuery = parsedSql.getQueryAtLocation(0); 218 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 219 | 220 | column = parsedQuery.outputColumns[0]; 221 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 222 | expect(column.columnAlias).toBe('csstmt'); 223 | expect(column.tableName).toBeNull(); 224 | expect(column.tableAlias).toBeNull(); 225 | 226 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) FROM table1;'; 227 | parsedSql = surveyor.survey(sql); 228 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 229 | parsedQuery = parsedSql.getQueryAtLocation(0); 230 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 231 | 232 | column = parsedQuery.outputColumns[0]; 233 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 234 | expect(column.columnAlias).toBeNull(); 235 | expect(column.tableName).toBeNull(); 236 | expect(column.tableAlias).toBeNull(); 237 | 238 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) csstmt FROM table1;'; 239 | parsedSql = surveyor.survey(sql); 240 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 241 | parsedQuery = parsedSql.getQueryAtLocation(0); 242 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 243 | 244 | column = parsedQuery.outputColumns[0]; 245 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 246 | expect(column.columnAlias).toBe('csstmt'); 247 | expect(column.tableName).toBeNull(); 248 | expect(column.tableAlias).toBeNull(); 249 | }); -------------------------------------------------------------------------------- /test/tsql.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect, ParsedSql, TokenLocation } from '../dist/index'; 2 | 3 | let surveyor: SQLSurveyor = null; 4 | beforeAll(() => { 5 | surveyor = new SQLSurveyor(SQLDialect.TSQL); 6 | }); 7 | 8 | test('that table names and aliases are correctly parsed', () => { 9 | const sql = 'SELECT * FROM tableName t1 \r\n JOIN tableName2 t2 ON t1.id = t2.id'; 10 | const parsedSql: ParsedSql = surveyor.survey(sql); 11 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 12 | const parsedQuery = parsedSql.getQueryAtLocation(0); 13 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 14 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 15 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 16 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 17 | expect(parsedQuery.queryLocation.stopIndex).toBe(64); // 0-based indices, \r and \n are one character each 18 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 19 | 20 | const tableName = parsedQuery.referencedTables['tableName']; 21 | expect(tableName).toBeTruthy(); 22 | expect(tableName.aliases.size).toBe(1); 23 | expect(tableName.aliases.has('t1')).toBeTruthy(); 24 | expect(tableName.locations.size).toBe(2); 25 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 26 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 27 | expect(tableName.schemaName).toBeNull(); 28 | expect(tableName.databaseName).toBeNull(); 29 | 30 | const tableName2 = parsedQuery.referencedTables['tableName2']; 31 | expect(tableName2).toBeTruthy(); 32 | expect(tableName2.aliases.size).toBe(1); 33 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 34 | expect(tableName2.locations.size).toBe(2); 35 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 36 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 37 | expect(tableName2.schemaName).toBeNull(); 38 | expect(tableName2.databaseName).toBeNull(); 39 | 40 | expect(parsedQuery.outputColumns.length).toBe(1); 41 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 42 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 43 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 44 | }); 45 | 46 | 47 | test('that quote characters are removed from databases, schemas, tables, and columns', () => { 48 | const sql = 'SELECT * FROM [tableName] t1 \r\n JOIN tableName2 [t2] ON [t1].id = t2.id'; 49 | const parsedSql: ParsedSql = surveyor.survey(sql); 50 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 51 | const parsedQuery = parsedSql.getQueryAtLocation(0); 52 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 53 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 54 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 55 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 56 | expect(parsedQuery.queryLocation.stopIndex).toBe(70); // 0-based indices, \r and \n are one character each 57 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 58 | 59 | const tableName = parsedQuery.referencedTables['tableName']; 60 | expect(tableName).toBeTruthy(); 61 | expect(tableName.aliases.size).toBe(1); 62 | expect(tableName.aliases.has('t1')).toBeTruthy(); 63 | expect(tableName.locations.size).toBe(2); 64 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 65 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 66 | expect(tableName.schemaName).toBeNull(); 67 | expect(tableName.databaseName).toBeNull(); 68 | 69 | const tableName2 = parsedQuery.referencedTables['tableName2']; 70 | expect(tableName2).toBeTruthy(); 71 | expect(tableName2.aliases.size).toBe(1); 72 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 73 | expect(tableName2.locations.size).toBe(2); 74 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 75 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 76 | expect(tableName2.schemaName).toBeNull(); 77 | expect(tableName2.databaseName).toBeNull(); 78 | 79 | expect(parsedQuery.outputColumns.length).toBe(1); 80 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 81 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 82 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 83 | }); 84 | 85 | 86 | test('that output column names and aliases are correctly parsed', () => { 87 | let sql = 'SELECT col1 as c1, avg(col2) as average, t1.col3 as c3, col4 c4, (select count(*) from test), (select count(*) from test) as numRows, (select max(col1) from t1) maxval, ' 88 | sql += ' [space column], [space column 2] as space2, [space column 3] space3, \'a string value\', \'another string value\' another, \'final string value\' as final FROM tableName t1;'; 89 | 90 | const parsedSql: ParsedSql = surveyor.survey(sql); 91 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 92 | const parsedQuery = parsedSql.getQueryAtLocation(0); 93 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(13); 94 | 95 | const column = parsedQuery.outputColumns[0]; 96 | expect(column.columnName).toBe('col1'); 97 | expect(column.columnAlias).toBe('c1'); 98 | expect(column.tableName).toBeNull(); 99 | expect(column.tableAlias).toBeNull(); 100 | 101 | const column2 = parsedQuery.outputColumns[1]; 102 | expect(column2.columnName).toBe('avg(col2)'); 103 | expect(column2.columnAlias).toBe('average'); 104 | expect(column2.tableName).toBeNull(); 105 | expect(column2.tableAlias).toBeNull(); 106 | 107 | const column3 = parsedQuery.outputColumns[2]; 108 | expect(column3.columnName).toBe('t1.col3'); 109 | expect(column3.columnAlias).toBe('c3'); 110 | expect(column3.tableName).toBe('tableName'); 111 | expect(column3.tableAlias).toBe('t1'); 112 | 113 | const column4 = parsedQuery.outputColumns[3]; 114 | expect(column4.columnName).toBe('col4'); 115 | expect(column4.columnAlias).toBe('c4'); 116 | expect(column4.tableName).toBeNull(); 117 | expect(column4.tableAlias).toBeNull(); 118 | 119 | const column5 = parsedQuery.outputColumns[4]; 120 | expect(column5.columnName).toBe('(select count(*) from test)'); 121 | expect(column5.columnAlias).toBeNull(); 122 | expect(column5.tableName).toBeNull(); 123 | expect(column5.tableAlias).toBeNull(); 124 | 125 | const column6 = parsedQuery.outputColumns[5]; 126 | expect(column6.columnName).toBe('(select count(*) from test)'); 127 | expect(column6.columnAlias).toBe('numRows'); 128 | expect(column6.tableName).toBeNull(); 129 | expect(column6.tableAlias).toBeNull(); 130 | 131 | const column7 = parsedQuery.outputColumns[6]; 132 | expect(column7.columnName).toBe('(select max(col1) from t1)'); 133 | expect(column7.columnAlias).toBe('maxval'); 134 | expect(column7.tableName).toBeNull(); 135 | expect(column7.tableAlias).toBeNull(); 136 | 137 | const column8 = parsedQuery.outputColumns[7]; 138 | expect(column8.columnName).toBe('space column'); 139 | expect(column8.columnAlias).toBeNull(); 140 | expect(column8.tableName).toBeNull(); 141 | expect(column8.tableAlias).toBeNull(); 142 | 143 | const column9 = parsedQuery.outputColumns[8]; 144 | expect(column9.columnName).toBe('space column 2'); 145 | expect(column9.columnAlias).toBe('space2'); 146 | expect(column9.tableName).toBeNull(); 147 | expect(column9.tableAlias).toBeNull(); 148 | 149 | const column10 = parsedQuery.outputColumns[9]; 150 | expect(column10.columnName).toBe('space column 3'); 151 | expect(column10.columnAlias).toBe('space3'); 152 | expect(column10.tableName).toBeNull(); 153 | expect(column10.tableAlias).toBeNull(); 154 | 155 | const column11 = parsedQuery.outputColumns[10]; 156 | expect(column11.columnName).toBe("'a string value'"); 157 | expect(column11.columnAlias).toBeNull(); 158 | expect(column11.tableName).toBeNull(); 159 | expect(column11.tableAlias).toBeNull(); 160 | 161 | const column12 = parsedQuery.outputColumns[11]; 162 | expect(column12.columnName).toBe("'another string value'"); 163 | expect(column12.columnAlias).toBe('another'); 164 | expect(column12.tableName).toBeNull(); 165 | expect(column12.tableAlias).toBeNull(); 166 | 167 | const column13 = parsedQuery.outputColumns[12]; 168 | expect(column13.columnName).toBe("'final string value'"); 169 | expect(column13.columnAlias).toBe('final'); 170 | expect(column13.tableName).toBeNull(); 171 | expect(column13.tableAlias).toBeNull(); 172 | }); 173 | 174 | test('that output column names and aliases are correctly parsed when subquery has a period', () => { 175 | let sql = 'select e2.employee_num, (select max(e.employee_num) from employees e) as counter, schemaName.employees.employee_id as eid from employees e2;'; 176 | 177 | const parsedSql: ParsedSql = surveyor.survey(sql); 178 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 179 | const parsedQuery = parsedSql.getQueryAtLocation(0); 180 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(3); 181 | 182 | const column = parsedQuery.outputColumns[0]; 183 | expect(column.columnName).toBe('e2.employee_num'); 184 | expect(column.columnAlias).toBeNull(); 185 | expect(column.tableName).toBe('employees'); 186 | expect(column.tableAlias).toBe('e2'); 187 | 188 | const column2 = parsedQuery.outputColumns[1]; 189 | expect(column2.columnName).toBe('(select max(e.employee_num) from employees e)'); 190 | expect(column2.columnAlias).toBe('counter'); 191 | expect(column2.tableName).toBeNull(); 192 | expect(column2.tableAlias).toBeNull(); 193 | 194 | const column3 = parsedQuery.outputColumns[2]; 195 | expect(column3.columnName).toBe('schemaName.employees.employee_id'); 196 | expect(column3.columnAlias).toBe('eid'); 197 | expect(column3.tableName).toBe('schemaName.employees'); 198 | expect(column3.tableAlias).toBeNull(); 199 | }); 200 | 201 | test('that output column names and aliases are correctly parsed for a CASE statement', () => { 202 | let sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END FROM table1;'; 203 | 204 | let parsedSql: ParsedSql = surveyor.survey(sql); 205 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 206 | let parsedQuery = parsedSql.getQueryAtLocation(0); 207 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 208 | 209 | let column = parsedQuery.outputColumns[0]; 210 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 211 | expect(column.columnAlias).toBeNull(); 212 | expect(column.tableName).toBeNull(); 213 | expect(column.tableAlias).toBeNull(); 214 | 215 | sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END csstmt FROM table1;'; 216 | parsedSql = surveyor.survey(sql); 217 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 218 | parsedQuery = parsedSql.getQueryAtLocation(0); 219 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 220 | 221 | column = parsedQuery.outputColumns[0]; 222 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 223 | expect(column.columnAlias).toBe('csstmt'); 224 | expect(column.tableName).toBeNull(); 225 | expect(column.tableAlias).toBeNull(); 226 | 227 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) FROM table1;'; 228 | parsedSql = surveyor.survey(sql); 229 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 230 | parsedQuery = parsedSql.getQueryAtLocation(0); 231 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 232 | 233 | column = parsedQuery.outputColumns[0]; 234 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 235 | expect(column.columnAlias).toBeNull(); 236 | expect(column.tableName).toBeNull(); 237 | expect(column.tableAlias).toBeNull(); 238 | 239 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) csstmt FROM table1;'; 240 | parsedSql = surveyor.survey(sql); 241 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 242 | parsedQuery = parsedSql.getQueryAtLocation(0); 243 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 244 | 245 | column = parsedQuery.outputColumns[0]; 246 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 247 | expect(column.columnAlias).toBe('csstmt'); 248 | expect(column.tableName).toBeNull(); 249 | expect(column.tableAlias).toBeNull(); 250 | }); -------------------------------------------------------------------------------- /test/plpgsql.test.ts: -------------------------------------------------------------------------------- 1 | import { SQLSurveyor, SQLDialect, ParsedSql, TokenLocation } from '../dist/index'; 2 | 3 | let surveyor: SQLSurveyor = null; 4 | beforeAll(() => { 5 | surveyor = new SQLSurveyor(SQLDialect.PLpgSQL); 6 | }); 7 | 8 | test('that table names and aliases are correctly parsed', () => { 9 | const sql = 'SELECT * FROM tableName t1 \r\n JOIN tableName2 t2 ON t1.id = t2.id'; 10 | const parsedSql: ParsedSql = surveyor.survey(sql); 11 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 12 | const parsedQuery = parsedSql.getQueryAtLocation(0); 13 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 14 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 15 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 16 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 17 | expect(parsedQuery.queryLocation.stopIndex).toBe(64); // 0-based indices, \r and \n are one character each 18 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 19 | 20 | const tableName = parsedQuery.referencedTables['tableName']; 21 | expect(tableName).toBeTruthy(); 22 | expect(tableName.aliases.size).toBe(1); 23 | expect(tableName.aliases.has('t1')).toBeTruthy(); 24 | expect(tableName.locations.size).toBe(2); 25 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 26 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 27 | expect(tableName.schemaName).toBeNull(); 28 | expect(tableName.databaseName).toBeNull(); 29 | 30 | const tableName2 = parsedQuery.referencedTables['tableName2']; 31 | expect(tableName2).toBeTruthy(); 32 | expect(tableName2.aliases.size).toBe(1); 33 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 34 | expect(tableName2.locations.size).toBe(2); 35 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 36 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 37 | expect(tableName2.schemaName).toBeNull(); 38 | expect(tableName2.databaseName).toBeNull(); 39 | 40 | expect(parsedQuery.outputColumns.length).toBe(1); 41 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 42 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 43 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 44 | }); 45 | 46 | 47 | test('that quote characters are removed from databases, schemas, tables, and columns', () => { 48 | const sql = 'SELECT * FROM "tableName" t1 \r\n JOIN tableName2 "t2" ON "t1".id = t2.id'; 49 | const parsedSql: ParsedSql = surveyor.survey(sql); 50 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 51 | const parsedQuery = parsedSql.getQueryAtLocation(0); 52 | expect(Object.keys(parsedQuery.referencedTables).length).toBe(2); 53 | expect(parsedQuery.queryLocation.lineStart).toBe(1); 54 | expect(parsedQuery.queryLocation.lineEnd).toBe(2); 55 | expect(parsedQuery.queryLocation.startIndex).toBe(0); 56 | expect(parsedQuery.queryLocation.stopIndex).toBe(70); // 0-based indices, \r and \n are one character each 57 | expect(Object.keys(parsedQuery.tokens).length).toBe(16); 58 | 59 | const tableName = parsedQuery.referencedTables['tableName']; 60 | expect(tableName).toBeTruthy(); 61 | expect(tableName.aliases.size).toBe(1); 62 | expect(tableName.aliases.has('t1')).toBeTruthy(); 63 | expect(tableName.locations.size).toBe(2); 64 | expect(tableName.locations.has(new TokenLocation(1, 1, 14, 23))); 65 | expect(tableName.locations.has(new TokenLocation(1, 1, 54, 56))); 66 | expect(tableName.schemaName).toBeNull(); 67 | expect(tableName.databaseName).toBeNull(); 68 | 69 | const tableName2 = parsedQuery.referencedTables['tableName2']; 70 | expect(tableName2).toBeTruthy(); 71 | expect(tableName2.aliases.size).toBe(1); 72 | expect(tableName2.aliases.has('t2')).toBeTruthy(); 73 | expect(tableName2.locations.size).toBe(2); 74 | expect(tableName2.locations.has(new TokenLocation(1, 1, 37, 47))); 75 | expect(tableName2.locations.has(new TokenLocation(1, 1, 62, 64))); 76 | expect(tableName2.schemaName).toBeNull(); 77 | expect(tableName2.databaseName).toBeNull(); 78 | 79 | expect(parsedQuery.outputColumns.length).toBe(1); 80 | expect(parsedQuery.outputColumns[0].columnName).toBe('*'); 81 | expect(parsedQuery.outputColumns[0].tableName).toBeNull(); 82 | expect(parsedQuery.outputColumns[0].tableAlias).toBeNull(); 83 | }); 84 | 85 | 86 | test('that output column names and aliases are correctly parsed', () => { 87 | let sql = 'SELECT col1 as c1, avg(col2) as average, t1.col3 as c3, col4 c4, (select count(*) from test), (select count(*) from test) as numRows, (select max(col1) from t1) maxval, ' 88 | sql += ' "space column", "space column 2" as space2, "space column 3" space3, \'a string value\', \'another string value\' another, \'final string value\' as final FROM tableName t1;'; 89 | 90 | const parsedSql: ParsedSql = surveyor.survey(sql); 91 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 92 | const parsedQuery = parsedSql.getQueryAtLocation(0); 93 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(13); 94 | 95 | const column = parsedQuery.outputColumns[0]; 96 | expect(column.columnName).toBe('col1'); 97 | expect(column.columnAlias).toBe('c1'); 98 | expect(column.tableName).toBeNull(); 99 | expect(column.tableAlias).toBeNull(); 100 | 101 | const column2 = parsedQuery.outputColumns[1]; 102 | expect(column2.columnName).toBe('avg(col2)'); 103 | expect(column2.columnAlias).toBe('average'); 104 | expect(column2.tableName).toBeNull(); 105 | expect(column2.tableAlias).toBeNull(); 106 | 107 | const column3 = parsedQuery.outputColumns[2]; 108 | expect(column3.columnName).toBe('t1.col3'); 109 | expect(column3.columnAlias).toBe('c3'); 110 | expect(column3.tableName).toBe('tableName'); 111 | expect(column3.tableAlias).toBe('t1'); 112 | 113 | const column4 = parsedQuery.outputColumns[3]; 114 | expect(column4.columnName).toBe('col4'); 115 | expect(column4.columnAlias).toBe('c4'); 116 | expect(column4.tableName).toBeNull(); 117 | expect(column4.tableAlias).toBeNull(); 118 | 119 | const column5 = parsedQuery.outputColumns[4]; 120 | expect(column5.columnName).toBe('(select count(*) from test)'); 121 | expect(column5.columnAlias).toBeNull(); 122 | expect(column5.tableName).toBeNull(); 123 | expect(column5.tableAlias).toBeNull(); 124 | 125 | const column6 = parsedQuery.outputColumns[5]; 126 | expect(column6.columnName).toBe('(select count(*) from test)'); 127 | expect(column6.columnAlias).toBe('numRows'); 128 | expect(column6.tableName).toBeNull(); 129 | expect(column6.tableAlias).toBeNull(); 130 | 131 | const column7 = parsedQuery.outputColumns[6]; 132 | expect(column7.columnName).toBe('(select max(col1) from t1)'); 133 | expect(column7.columnAlias).toBe('maxval'); 134 | expect(column7.tableName).toBeNull(); 135 | expect(column7.tableAlias).toBeNull(); 136 | 137 | const column8 = parsedQuery.outputColumns[7]; 138 | expect(column8.columnName).toBe('space column'); 139 | expect(column8.columnAlias).toBeNull(); 140 | expect(column8.tableName).toBeNull(); 141 | expect(column8.tableAlias).toBeNull(); 142 | 143 | const column9 = parsedQuery.outputColumns[8]; 144 | expect(column9.columnName).toBe('space column 2'); 145 | expect(column9.columnAlias).toBe('space2'); 146 | expect(column9.tableName).toBeNull(); 147 | expect(column9.tableAlias).toBeNull(); 148 | 149 | const column10 = parsedQuery.outputColumns[9]; 150 | expect(column10.columnName).toBe('space column 3'); 151 | expect(column10.columnAlias).toBe('space3'); 152 | expect(column10.tableName).toBeNull(); 153 | expect(column10.tableAlias).toBeNull(); 154 | 155 | const column11 = parsedQuery.outputColumns[10]; 156 | expect(column11.columnName).toBe("'a string value'"); 157 | expect(column11.columnAlias).toBeNull(); 158 | expect(column11.tableName).toBeNull(); 159 | expect(column11.tableAlias).toBeNull(); 160 | 161 | const column12 = parsedQuery.outputColumns[11]; 162 | expect(column12.columnName).toBe("'another string value'"); 163 | expect(column12.columnAlias).toBe('another'); 164 | expect(column12.tableName).toBeNull(); 165 | expect(column12.tableAlias).toBeNull(); 166 | 167 | const column13 = parsedQuery.outputColumns[12]; 168 | expect(column13.columnName).toBe("'final string value'"); 169 | expect(column13.columnAlias).toBe('final'); 170 | expect(column13.tableName).toBeNull(); 171 | expect(column13.tableAlias).toBeNull(); 172 | }); 173 | 174 | test('that output column names and aliases are correctly parsed when subquery has a period', () => { 175 | let sql = 'select e2.employee_num, (select max(e.employee_num) from employees e) as counter, schemaName.employees.employee_id as eid from employees e2;'; 176 | 177 | const parsedSql: ParsedSql = surveyor.survey(sql); 178 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 179 | const parsedQuery = parsedSql.getQueryAtLocation(0); 180 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(3); 181 | 182 | const column = parsedQuery.outputColumns[0]; 183 | expect(column.columnName).toBe('e2.employee_num'); 184 | expect(column.columnAlias).toBeNull(); 185 | expect(column.tableName).toBe('employees'); 186 | expect(column.tableAlias).toBe('e2'); 187 | 188 | const column2 = parsedQuery.outputColumns[1]; 189 | expect(column2.columnName).toBe('(select max(e.employee_num) from employees e)'); 190 | expect(column2.columnAlias).toBe('counter'); 191 | expect(column2.tableName).toBeNull(); 192 | expect(column2.tableAlias).toBeNull(); 193 | 194 | const column3 = parsedQuery.outputColumns[2]; 195 | expect(column3.columnName).toBe('schemaName.employees.employee_id'); 196 | expect(column3.columnAlias).toBe('eid'); 197 | expect(column3.tableName).toBe('schemaName.employees'); 198 | expect(column3.tableAlias).toBeNull(); 199 | }); 200 | 201 | test('that output column names and aliases are correctly parsed for a CASE statement', () => { 202 | let sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END FROM table1;'; 203 | 204 | let parsedSql: ParsedSql = surveyor.survey(sql); 205 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 206 | let parsedQuery = parsedSql.getQueryAtLocation(0); 207 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 208 | 209 | let column = parsedQuery.outputColumns[0]; 210 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 211 | expect(column.columnAlias).toBeNull(); 212 | expect(column.tableName).toBeNull(); 213 | expect(column.tableAlias).toBeNull(); 214 | 215 | sql = 'SELECT CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END csstmt FROM table1;'; 216 | parsedSql = surveyor.survey(sql); 217 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 218 | parsedQuery = parsedSql.getQueryAtLocation(0); 219 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 220 | 221 | column = parsedQuery.outputColumns[0]; 222 | expect(column.columnName).toBe('CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END'); 223 | expect(column.columnAlias).toBe('csstmt'); 224 | expect(column.tableName).toBeNull(); 225 | expect(column.tableAlias).toBeNull(); 226 | 227 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) FROM table1;'; 228 | parsedSql = surveyor.survey(sql); 229 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 230 | parsedQuery = parsedSql.getQueryAtLocation(0); 231 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 232 | 233 | column = parsedQuery.outputColumns[0]; 234 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 235 | expect(column.columnAlias).toBeNull(); 236 | expect(column.tableName).toBeNull(); 237 | expect(column.tableAlias).toBeNull(); 238 | 239 | sql = 'SELECT (CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END) csstmt FROM table1;'; 240 | parsedSql = surveyor.survey(sql); 241 | expect(Object.keys(parsedSql.parsedQueries).length).toBe(1); 242 | parsedQuery = parsedSql.getQueryAtLocation(0); 243 | expect(Object.keys(parsedQuery.outputColumns).length).toBe(1); 244 | 245 | column = parsedQuery.outputColumns[0]; 246 | expect(column.columnName).toBe('(CASE WHEN a > 1 THEN 100 WHEN a < 1 THEN -100 ELSE 0 END)'); 247 | expect(column.columnAlias).toBe('csstmt'); 248 | expect(column.tableName).toBeNull(); 249 | expect(column.tableAlias).toBeNull(); 250 | }); -------------------------------------------------------------------------------- /src/parsing/MySQLQueryListener.ts: -------------------------------------------------------------------------------- 1 | import { MultiQueryMySQLParserListener, MySQLGrammar } from 'antlr4ts-sql'; 2 | import { BaseSqlQueryListener } from './BaseSqlQueryListener'; 3 | import { TokenLocation } from '../models/TokenLocation'; 4 | import { ParsedQuery } from '../models/ParsedQuery'; 5 | import { QueryType } from '../models/QueryType'; 6 | import { ReferencedTable } from '../models/ReferencedTable'; 7 | 8 | export class MySQLQueryListener extends BaseSqlQueryListener implements MultiQueryMySQLParserListener { 9 | 10 | unquote(value: string) { 11 | if (value.startsWith('`') && value.endsWith('`')) { 12 | return value.slice(1, value.length - 1); 13 | } 14 | return value; 15 | } 16 | 17 | _getAliasStartIndex(value: string): number { 18 | return super._getAliasStartIndex(value, '`', '`'); 19 | } 20 | 21 | _getTableAliasEndLocation(value: string): number { 22 | return super._getTableAliasEndLocation(value, '`', '`'); 23 | } 24 | 25 | parseContextToReferencedTable(ctx: any) { 26 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 27 | const tableText = tableLocation.getToken(this.input); 28 | let tableNameOrAlias = tableText; 29 | let schemaName = null; 30 | if (tableText.includes('.')) { 31 | const columnTextSplit: string[] = tableText.split('.'); 32 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 33 | schemaName = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 34 | } else { 35 | tableNameOrAlias = this.unquote(tableNameOrAlias); 36 | } 37 | const referencedTable = new ReferencedTable(tableNameOrAlias); 38 | referencedTable.schemaName = schemaName; 39 | return referencedTable; 40 | } 41 | 42 | _addDMLQuery(ctx) { 43 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 44 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DML, queryLocation.getToken(this.input), queryLocation)); 45 | } 46 | 47 | enterSimpleStatement(ctx) { 48 | try { 49 | const queryLocation: TokenLocation = this._getClauseLocation(ctx); 50 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DDL, queryLocation.getToken(this.input), queryLocation)); 51 | } catch (err) { 52 | this._handleError(err); 53 | } 54 | } 55 | 56 | // DML 57 | enterCallStatement(ctx) { 58 | try { 59 | this._addDMLQuery(ctx); 60 | } catch (err) { 61 | this._handleError(err); 62 | } 63 | } 64 | 65 | enterDeleteStatement(ctx) { 66 | try { 67 | this._addDMLQuery(ctx); 68 | } catch (err) { 69 | this._handleError(err); 70 | } 71 | } 72 | 73 | enterDoStatement(ctx) { 74 | try { 75 | this._addDMLQuery(ctx); 76 | } catch (err) { 77 | this._handleError(err); 78 | } 79 | } 80 | 81 | enterHandlerStatement(ctx) { 82 | try { 83 | this._addDMLQuery(ctx); 84 | } catch (err) { 85 | this._handleError(err); 86 | } 87 | } 88 | 89 | enterInsertStatement(ctx) { 90 | try { 91 | this._addDMLQuery(ctx); 92 | } catch (err) { 93 | this._handleError(err); 94 | } 95 | } 96 | 97 | enterLoadStatement(ctx) { 98 | try { 99 | this._addDMLQuery(ctx); 100 | } catch (err) { 101 | this._handleError(err); 102 | } 103 | } 104 | 105 | enterReplaceStatement(ctx) { 106 | try { 107 | this._addDMLQuery(ctx); 108 | } catch (err) { 109 | this._handleError(err); 110 | } 111 | } 112 | 113 | enterSelectStatement(ctx) { 114 | try { 115 | this._addDMLQuery(ctx); 116 | } catch (err) { 117 | this._handleError(err); 118 | } 119 | } 120 | 121 | enterUpdateStatement(ctx) { 122 | try { 123 | this._addDMLQuery(ctx); 124 | } catch (err) { 125 | this._handleError(err); 126 | } 127 | } 128 | 129 | enterTransactionOrLockingStatement(ctx) { 130 | try { 131 | this._addDMLQuery(ctx); 132 | } catch (err) { 133 | this._handleError(err); 134 | } 135 | } 136 | 137 | enterReplicationStatement(ctx) { 138 | try { 139 | this._addDMLQuery(ctx); 140 | } catch (err) { 141 | this._handleError(err); 142 | } 143 | } 144 | 145 | enterPreparedStatement(ctx) { 146 | try { 147 | this._addDMLQuery(ctx); 148 | } catch (err) { 149 | this._handleError(err); 150 | } 151 | } 152 | 153 | enterCommonTableExpression(ctx: any) { 154 | try { 155 | const cteLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 156 | let parsedQuery = this.parsedSql.getQueryAtLocation(cteLocation.startIndex); 157 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(cteLocation.startIndex); 158 | parsedQuery._addCommonTableExpression(new ParsedQuery(QueryType.DML, cteLocation.getToken(this.input), cteLocation)); 159 | } catch (err) { 160 | this._handleError(err); 161 | } 162 | } 163 | 164 | enterSubquery(ctx: any) { 165 | try { 166 | // Don't include opening and closing parentheses 167 | const subqueryLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start + 1, ctx._stop.stop - 1); 168 | if (!(ctx._parent instanceof MySQLGrammar.CommonTableExpressionContext)) { 169 | let parsedQuery = this.parsedSql.getQueryAtLocation(subqueryLocation.startIndex); 170 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(subqueryLocation.startIndex); 171 | parsedQuery._addSubQuery(new ParsedQuery(QueryType.DML, subqueryLocation.getToken(this.input), subqueryLocation)); 172 | } 173 | } catch (err) { 174 | this._handleError(err); 175 | } 176 | } 177 | 178 | exitSelectItem(ctx: any) { 179 | try { 180 | let columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 181 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 182 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 183 | let columnText = columnLocation.getToken(this.input); 184 | let columnName = columnText; 185 | let columnAlias = null; 186 | let tableNameOrAlias = null; 187 | if (columnText.includes('.')) { 188 | // Column may have a table alias 189 | const functionArgumentLocation = this._getFunctionArgumentLocation(ctx, columnLocation); 190 | if (functionArgumentLocation !== null) { 191 | columnText = functionArgumentLocation.getToken(this.input); 192 | columnLocation = functionArgumentLocation; 193 | } 194 | const tableNameOrAliasStopIndex = this._getTableAliasEndLocation(columnText); 195 | if (tableNameOrAliasStopIndex !== null) { 196 | tableNameOrAlias = this.unquote(columnText.substring(0, tableNameOrAliasStopIndex)); 197 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, columnLocation.startIndex, columnLocation.startIndex + tableNameOrAliasStopIndex - 1); 198 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 199 | } 200 | } 201 | columnName = columnName.trim(); 202 | const lastUnquotedSpaceIndex = this._getAliasStartIndex(columnName); 203 | if (lastUnquotedSpaceIndex !== null) { 204 | // Column has an alias 205 | columnAlias = columnName.substring(lastUnquotedSpaceIndex); 206 | columnName = columnName.substring(0, lastUnquotedSpaceIndex - 1).trimEnd(); 207 | if (columnName.toUpperCase().endsWith('AS')) { 208 | columnName = columnName.substring(0, columnName.length - 2).trimEnd(); 209 | } 210 | } 211 | columnName = this.unquote(columnName); 212 | if (columnAlias !== null) { 213 | columnAlias = this.unquote(columnAlias); 214 | } 215 | parsedQuery._addOutputColumn(columnName, columnAlias, tableNameOrAlias); 216 | } catch (err) { 217 | this._handleError(err); 218 | } 219 | } 220 | 221 | _getFunctionArgumentLocation(ctx: any, columnLocation: TokenLocation): TokenLocation { 222 | const functionRules = [MySQLGrammar.SumExprContext]; 223 | const argumentRules = [MySQLGrammar.InSumExprContext]; 224 | return super._getFunctionArgumentLocation(ctx, columnLocation, functionRules, argumentRules); 225 | } 226 | 227 | exitSelectItemList(ctx: any) { 228 | try { 229 | const columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 230 | if (columnLocation.getToken(this.input) === '*') { 231 | // Otherwise, the columns will be picked up by exitSelectItem on their own 232 | this.exitSelectItem(ctx); 233 | } 234 | } catch (err) { 235 | this._handleError(err); 236 | } 237 | } 238 | 239 | exitTableRef(ctx: any) { 240 | try { 241 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 242 | const referencedTable = this.parseContextToReferencedTable(ctx); 243 | let parsedQuery = this.parsedSql.getQueryAtLocation(tableLocation.startIndex); 244 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(tableLocation.startIndex); 245 | parsedQuery._addTableNameLocation(referencedTable.tableName, tableLocation, referencedTable.schemaName, referencedTable.databaseName); 246 | } catch (err) { 247 | this._handleError(err); 248 | } 249 | } 250 | 251 | exitTableAlias(ctx: any) { 252 | try { 253 | const aliasLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 254 | const referencedTable = this.parseContextToReferencedTable(ctx._parent.children[0]); 255 | let parsedQuery = this.parsedSql.getQueryAtLocation(aliasLocation.startIndex); 256 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(aliasLocation.startIndex); 257 | let aliasName = this.unquote(aliasLocation.getToken(this.input)); 258 | const aliasStartIndex = this._getAliasStartIndex(aliasName); 259 | if (aliasStartIndex !== null) { 260 | // alias is in the format 'AS alias', ignore the 'AS ' 261 | aliasName = aliasName.substring(aliasStartIndex); 262 | } 263 | parsedQuery._addAliasForTable(aliasName, referencedTable.tableName); 264 | } catch (err) { 265 | this._handleError(err); 266 | } 267 | } 268 | 269 | exitColumnRef(ctx: any) { 270 | try { 271 | let parentContext = ctx._parent; 272 | while (parentContext !== undefined) { 273 | if (parentContext instanceof MySQLGrammar.SelectItemContext) { 274 | // This is an output column, don't record it as a referenced column 275 | return; 276 | } else if (parentContext instanceof MySQLGrammar.SubqueryContext) { 277 | // This is a subquery in the SELECT list, add the referenced column 278 | break; 279 | } 280 | parentContext = parentContext._parent; 281 | } 282 | const columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 283 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 284 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 285 | const columnText = columnLocation.getToken(this.input); 286 | let columnName = columnText; 287 | let tableNameOrAlias = null; 288 | if (columnText.includes('.')) { 289 | const columnTextSplit: string[] = columnText.split('.'); 290 | columnName = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 291 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 292 | let tableNameOrAliasStartIndex = columnLocation.stopIndex - columnTextSplit[columnTextSplit.length - 1].length - columnTextSplit[columnTextSplit.length - 2].length; 293 | let tableNameOrAliasStopIndex = tableNameOrAliasStartIndex + columnTextSplit[columnTextSplit.length - 2].length - 1; 294 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, tableNameOrAliasStartIndex, tableNameOrAliasStopIndex); 295 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 296 | } else { 297 | columnName = this.unquote(columnName); 298 | } 299 | parsedQuery._addReferencedColumn(columnName, tableNameOrAlias, columnLocation); 300 | } catch (err) { 301 | this._handleError(err); 302 | } 303 | } 304 | 305 | } -------------------------------------------------------------------------------- /src/parsing/TSQLQueryListener.ts: -------------------------------------------------------------------------------- 1 | import { TSqlParserListener, TSQLGrammar } from 'antlr4ts-sql'; 2 | import { TokenLocation } from '../models/TokenLocation'; 3 | import { ParsedQuery } from '../models/ParsedQuery'; 4 | import { QueryType } from '../models/QueryType'; 5 | import { BaseSqlQueryListener } from './BaseSqlQueryListener'; 6 | import { ReferencedTable } from '../models/ReferencedTable'; 7 | 8 | export class TSqlQueryListener extends BaseSqlQueryListener implements TSqlParserListener { 9 | 10 | unquote(value: string) { 11 | if (value.startsWith('[') && value.endsWith(']')) { 12 | return value.slice(1, value.length - 1); 13 | } 14 | return value; 15 | } 16 | 17 | _getAliasStartIndex(value: string): number { 18 | return super._getAliasStartIndex(value, '[', ']'); 19 | } 20 | 21 | _getTableAliasEndLocation(value: string): number { 22 | return super._getTableAliasEndLocation(value, '[', ']'); 23 | } 24 | 25 | parseContextToReferencedTable(ctx: any) { 26 | let databaseName: string = null; 27 | if (ctx._database !== undefined) { 28 | const databaseLocation: TokenLocation = new TokenLocation(ctx._database._start._line, ctx._database._stop._line, ctx._database._start.start, ctx._database._stop.stop); 29 | databaseName = this.unquote(databaseLocation.getToken(this.input)); 30 | } 31 | let schemaName: string = null; 32 | if (ctx._schema !== undefined) { 33 | const schemaLocation: TokenLocation = new TokenLocation(ctx._schema._start._line, ctx._schema._stop._line, ctx._schema._start.start, ctx._schema._stop.stop); 34 | schemaName = this.unquote(schemaLocation.getToken(this.input)); 35 | } 36 | let tableNameOrAlias: string = null; 37 | let tableLocation: TokenLocation; 38 | if (ctx._table !== undefined) { 39 | tableLocation = new TokenLocation(ctx._table._start._line, ctx._table._stop._line, ctx._table._start.start, ctx._table._stop.stop); 40 | tableNameOrAlias = this.unquote(tableLocation.getToken(this.input)); 41 | } else { 42 | tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 43 | tableNameOrAlias = this.unquote(tableLocation.getToken(this.input)); 44 | } 45 | const referencedTable = new ReferencedTable(tableNameOrAlias); 46 | referencedTable.schemaName = schemaName; 47 | referencedTable.databaseName = databaseName; 48 | return referencedTable; 49 | } 50 | 51 | _getClauseLocationWithoutTrailingSemicolon(queryLocation: TokenLocation): TokenLocation { 52 | const whitespaceRegex = /[\s]/; 53 | let newStopIndex = queryLocation.stopIndex; 54 | while (this.input[newStopIndex] !== undefined 55 | && (whitespaceRegex.test(this.input[newStopIndex]) 56 | || this.input[newStopIndex] === ';')) { 57 | newStopIndex--; 58 | } 59 | if (newStopIndex !== queryLocation.stopIndex) { 60 | queryLocation.stopIndex = newStopIndex; 61 | } 62 | return queryLocation; 63 | } 64 | 65 | enterDml_clause(ctx: any) { 66 | try { 67 | let queryLocation: TokenLocation = this._getClauseLocation(ctx); 68 | // Remove trailing ; and whitespace if it exists to match other SQL dialects 69 | queryLocation = this._getClauseLocationWithoutTrailingSemicolon(queryLocation); 70 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DML, queryLocation.getToken(this.input), queryLocation)); 71 | } catch (err) { 72 | this._handleError(err); 73 | } 74 | } 75 | 76 | enterDdl_clause(ctx: any) { 77 | try { 78 | let queryLocation: TokenLocation = this._getClauseLocation(ctx); 79 | // Remove trailing ; and whitespace if it exists to match other SQL dialects 80 | queryLocation = this._getClauseLocationWithoutTrailingSemicolon(queryLocation); 81 | this.parsedSql._addQuery(new ParsedQuery(QueryType.DDL, queryLocation.getToken(this.input), queryLocation)); 82 | } catch (err) { 83 | this._handleError(err); 84 | } 85 | } 86 | 87 | enterSubquery(ctx: any) { 88 | try { 89 | const subqueryLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 90 | let parsedQuery = this.parsedSql.getQueryAtLocation(subqueryLocation.startIndex); 91 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(subqueryLocation.startIndex); 92 | parsedQuery._addSubQuery(new ParsedQuery(QueryType.DML, subqueryLocation.getToken(this.input), subqueryLocation)); 93 | } catch (err) { 94 | this._handleError(err); 95 | } 96 | } 97 | 98 | enterCommon_table_expression(ctx: any) { 99 | try { 100 | const cteLocation: TokenLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 101 | let parsedQuery = this.parsedSql.getQueryAtLocation(cteLocation.startIndex); 102 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(cteLocation.startIndex); 103 | parsedQuery._addCommonTableExpression(new ParsedQuery(QueryType.DML, cteLocation.getToken(this.input), cteLocation)); 104 | } catch (err) { 105 | this._handleError(err); 106 | } 107 | } 108 | 109 | exitFull_table_name(ctx: any) { 110 | try { 111 | this.exitTable_name(ctx); 112 | } catch (err) { 113 | this._handleError(err); 114 | } 115 | } 116 | 117 | exitTable_name(ctx: any) { 118 | try { 119 | let parentContext = ctx._parent; 120 | while (parentContext !== undefined) { 121 | if (parentContext instanceof TSQLGrammar.Select_list_elemContext) { 122 | // This is part of an output column, don't record it as a referenced table 123 | return; 124 | } else if (parentContext instanceof TSQLGrammar.SubqueryContext) { 125 | // This is a subquery in the SELECT list, add the referenced table 126 | break; 127 | } 128 | parentContext = parentContext._parent; 129 | } 130 | const tableLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 131 | const referencedTable = this.parseContextToReferencedTable(ctx); 132 | let parsedQuery = this.parsedSql.getQueryAtLocation(tableLocation.startIndex); 133 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(tableLocation.startIndex); 134 | parsedQuery._addTableNameLocation(referencedTable.tableName, tableLocation, referencedTable.schemaName, referencedTable.databaseName); 135 | } catch (err) { 136 | this._handleError(err); 137 | } 138 | } 139 | 140 | exitTable_alias(ctx: any) { 141 | try { 142 | const aliasLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 143 | const referencedTable = this.parseContextToReferencedTable(ctx._parent._parent.children[0].children[0]); 144 | let parsedQuery = this.parsedSql.getQueryAtLocation(aliasLocation.startIndex); 145 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(aliasLocation.startIndex); 146 | parsedQuery._addAliasForTable(this.unquote(aliasLocation.getToken(this.input)), referencedTable.tableName); 147 | } catch (err) { 148 | this._handleError(err); 149 | } 150 | } 151 | 152 | exitColumn_elem(ctx) { 153 | try { 154 | let columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 155 | if (ctx._parent.children[1] instanceof TSQLGrammar.As_column_aliasContext) { 156 | columnLocation.lineEnd = (ctx._parent.children[1]._stop as any)._line; 157 | columnLocation.stopIndex = (ctx._parent.children[1]._stop as any).stop; 158 | } 159 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 160 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 161 | let columnText = columnLocation.getToken(this.input); 162 | let columnName = columnText; 163 | let columnAlias = null; 164 | let tableNameOrAlias = null; 165 | if (columnText.includes('.')) { 166 | // Column may have a table alias 167 | const functionArgumentLocation = this._getFunctionArgumentLocation(ctx, columnLocation); 168 | if (functionArgumentLocation !== null) { 169 | columnText = functionArgumentLocation.getToken(this.input); 170 | columnLocation = functionArgumentLocation; 171 | } 172 | const tableNameOrAliasStopIndex = this._getTableAliasEndLocation(columnText); 173 | if (tableNameOrAliasStopIndex !== null) { 174 | tableNameOrAlias = this.unquote(columnText.substring(0, tableNameOrAliasStopIndex)); 175 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, columnLocation.startIndex, columnLocation.startIndex + tableNameOrAliasStopIndex - 1); 176 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 177 | } 178 | } 179 | columnName = columnName.trim(); 180 | const aliasStartIndex = this._getAliasStartIndex(columnName); 181 | if (aliasStartIndex !== null) { 182 | // Column has an alias 183 | columnAlias = columnName.substring(aliasStartIndex); 184 | columnName = columnName.substring(0, aliasStartIndex - 1).trimEnd(); 185 | if (columnName.toUpperCase().endsWith('AS')) { 186 | columnName = columnName.substring(0, columnName.length - 2).trimEnd(); 187 | } 188 | } 189 | columnName = this.unquote(columnName); 190 | if (columnAlias !== null) { 191 | columnAlias = this.unquote(columnAlias); 192 | } 193 | parsedQuery._addOutputColumn(columnName, columnAlias, tableNameOrAlias); 194 | } catch (err) { 195 | this._handleError(err); 196 | } 197 | } 198 | 199 | _getFunctionArgumentLocation(ctx: any, columnLocation: TokenLocation): TokenLocation { 200 | const functionRules = [TSQLGrammar.Aggregate_windowed_functionContext, TSQLGrammar.Analytic_windowed_functionContext, TSQLGrammar.Ranking_windowed_functionContext]; 201 | const argumentRules = [TSQLGrammar.ExpressionContext, TSQLGrammar.All_distinct_expressionContext]; 202 | return super._getFunctionArgumentLocation(ctx, columnLocation, functionRules, argumentRules); 203 | } 204 | 205 | exitFull_column_name(ctx) { 206 | try { 207 | let parentContext = ctx._parent; 208 | while (parentContext !== undefined) { 209 | if (parentContext instanceof TSQLGrammar.Column_elemContext) { 210 | // Column_elem will already handle this token 211 | // (not all Full_column_name are children of Column_elem) 212 | return; 213 | } else if (parentContext instanceof TSQLGrammar.Select_list_elemContext) { 214 | // This is an output column, don't record it as a referenced column 215 | return; 216 | } else if (parentContext instanceof TSQLGrammar.SubqueryContext) { 217 | // This is a subquery in the SELECT list, add the referenced column 218 | break; 219 | } 220 | parentContext = parentContext._parent; 221 | } 222 | const columnLocation = new TokenLocation(ctx._start._line, ctx._stop._line, ctx._start.start, ctx._stop.stop); 223 | let parsedQuery = this.parsedSql.getQueryAtLocation(columnLocation.startIndex); 224 | parsedQuery = parsedQuery.getSmallestQueryAtLocation(columnLocation.startIndex); 225 | const columnText = columnLocation.getToken(this.input); 226 | let columnName = columnText; 227 | let tableNameOrAlias = null; 228 | if (columnText.includes('.')) { 229 | const columnTextSplit: string[] = columnText.split('.'); 230 | columnName = this.unquote(columnTextSplit[columnTextSplit.length - 1]); 231 | tableNameOrAlias = this.unquote(columnTextSplit[columnTextSplit.length - 2]); 232 | let tableNameOrAliasStartIndex = columnLocation.stopIndex - columnTextSplit[columnTextSplit.length - 1].length - columnTextSplit[columnTextSplit.length - 2].length; 233 | let tableNameOrAliasStopIndex = tableNameOrAliasStartIndex + columnTextSplit[columnTextSplit.length - 2].length - 1; 234 | const tableNameOrAliasLocation = new TokenLocation(columnLocation.lineStart, columnLocation.lineEnd, tableNameOrAliasStartIndex, tableNameOrAliasStopIndex); 235 | parsedQuery._addTableNameLocation(tableNameOrAlias, tableNameOrAliasLocation, null, null); 236 | } else { 237 | columnName = this.unquote(columnName); 238 | } 239 | parsedQuery._addReferencedColumn(columnName, tableNameOrAlias, columnLocation); 240 | } catch (err) { 241 | this._handleError(err); 242 | } 243 | } 244 | 245 | exitAsterisk(ctx) { 246 | try { 247 | this.exitColumn_elem(ctx); 248 | } catch (err) { 249 | this._handleError(err); 250 | } 251 | } 252 | 253 | exitExpression_elem(ctx) { 254 | try { 255 | if (ctx.children[0] instanceof TSQLGrammar.ExpressionContext) { 256 | return this.exitColumn_elem(ctx.children[0]); 257 | } else if (ctx.children.length > 1) { 258 | return this.exitColumn_elem(ctx.children[ctx.children.length - 1]); 259 | } 260 | } catch (err) { 261 | this._handleError(err); 262 | } 263 | } 264 | 265 | } -------------------------------------------------------------------------------- /src/models/ParsedQuery.ts: -------------------------------------------------------------------------------- 1 | import { QueryType } from "./QueryType"; 2 | import { OutputColumn } from "./OutputColumn"; 3 | import { ReferencedColumn } from "./ReferencedColumn"; 4 | import { ReferencedTable } from "./ReferencedTable"; 5 | import { TokenLocation } from "./TokenLocation"; 6 | import { Token } from "./Token"; 7 | import { ParsingError } from "./ParsingError"; 8 | import { TokenType } from "./TokenType"; 9 | 10 | export class ParsedQuery { 11 | 12 | query: string; 13 | queryType: QueryType; 14 | outputColumns: OutputColumn[]; 15 | referencedColumns: ReferencedColumn[]; 16 | referencedTables: { [tableName: string]: ReferencedTable }; 17 | 18 | tokens: { [queryStartIndex: number]: Token }; 19 | 20 | queryLocation: TokenLocation; 21 | queryErrors: ParsingError[]; 22 | 23 | commonTableExpressionName: string; 24 | 25 | subqueries: { [subqueryStartIndex: number]: ParsedQuery }; 26 | commonTableExpressions: { [cteStartIndex: number]: ParsedQuery }; 27 | 28 | constructor(queryType: QueryType, query: string, queryLocation: TokenLocation) { 29 | this.outputColumns = []; 30 | this.referencedColumns = []; 31 | this.referencedTables = {}; 32 | 33 | this.tokens = {}; 34 | 35 | this.query = query; 36 | this.queryType = queryType; 37 | this.queryLocation = queryLocation; 38 | 39 | this.queryErrors = []; 40 | this.subqueries = {}; 41 | this.commonTableExpressions = {}; 42 | } 43 | 44 | getAllReferencedTables(): { [tableName: string]: ReferencedTable } { 45 | const tables: { [tableName: string]: ReferencedTable } = {}; 46 | for (const referencedTableName in this.referencedTables) { 47 | tables[referencedTableName] = ReferencedTable.clone(this.referencedTables[referencedTableName]); 48 | } 49 | for (const query of [...Object.values(this.subqueries), ...Object.values(this.commonTableExpressions)]) { 50 | const queryReferencedTables: { [tableName: string]: ReferencedTable } = query.getAllReferencedTables(); 51 | for (const referencedTableName in queryReferencedTables) { 52 | if (tables[referencedTableName] === undefined) { 53 | tables[referencedTableName] = ReferencedTable.clone(queryReferencedTables[referencedTableName]); 54 | } else { 55 | const table = tables[referencedTableName]; 56 | if (table.schemaName === null && queryReferencedTables[referencedTableName].schemaName !== null) { 57 | table.schemaName = queryReferencedTables[referencedTableName].schemaName; 58 | } 59 | if (table.databaseName === null && queryReferencedTables[referencedTableName].databaseName !== null) { 60 | table.databaseName = queryReferencedTables[referencedTableName].databaseName; 61 | } 62 | queryReferencedTables[referencedTableName].aliases.forEach(alias => table.aliases.add(alias)); 63 | queryReferencedTables[referencedTableName].locations.forEach(location => table.locations.add(TokenLocation.clone(location))); 64 | } 65 | } 66 | } 67 | return tables; 68 | } 69 | 70 | getAllReferencedColumns(): ReferencedColumn[] { 71 | const columns: ReferencedColumn[] = [...this.referencedColumns]; 72 | for (const query of [...Object.values(this.subqueries), ...Object.values(this.commonTableExpressions)]) { 73 | const queryReferencedColumns: ReferencedColumn[] = query.getAllReferencedColumns(); 74 | for (const referencedColumn of queryReferencedColumns) { 75 | const existingReferencedColumnCandidates: ReferencedColumn[] = columns.filter(column => column.columnName === referencedColumn.columnName); 76 | let matchedCandidate: boolean = false; 77 | for (const existingReferencedColumnCandidate of existingReferencedColumnCandidates) { 78 | if (existingReferencedColumnCandidate.tableAlias === referencedColumn.tableAlias 79 | || existingReferencedColumnCandidate.tableName === referencedColumn.tableName) { 80 | matchedCandidate = true; 81 | if (existingReferencedColumnCandidate.tableName === null && referencedColumn.tableName !== null) { 82 | existingReferencedColumnCandidate.tableName = referencedColumn.tableName; 83 | } 84 | if (existingReferencedColumnCandidate.tableAlias === null && referencedColumn.tableAlias !== null) { 85 | existingReferencedColumnCandidate.tableAlias = referencedColumn.tableAlias; 86 | } 87 | referencedColumn.locations.forEach(location => existingReferencedColumnCandidate.locations.add(TokenLocation.clone(location))); 88 | break; 89 | } 90 | } 91 | if (!matchedCandidate) { 92 | columns.push(referencedColumn); 93 | } 94 | } 95 | } 96 | return columns; 97 | } 98 | 99 | getTableFromAlias(alias: string): string { 100 | for (const table of Object.values(this.referencedTables)) { 101 | if (table.aliases.has(alias)) { 102 | return table.tableName; 103 | } 104 | } 105 | for (const query of Object.values(this.subqueries)) { 106 | const subqueryTable = query.getTableFromAlias(alias); 107 | if (subqueryTable !== null) { 108 | return subqueryTable; 109 | } 110 | } 111 | for (const cte of Object.values(this.commonTableExpressions)) { 112 | const cteTable = cte.getTableFromAlias(alias); 113 | if (cteTable !== null) { 114 | return cteTable; 115 | } 116 | } 117 | return null; 118 | } 119 | 120 | getAliasesForTable(tableName: string): string[] { 121 | if (this.referencedTables[tableName] !== undefined) { 122 | return Array.from(this.referencedTables[tableName].aliases); 123 | } 124 | for (const query of Object.values(this.subqueries)) { 125 | if (query.referencedTables[tableName] !== undefined) { 126 | return Array.from(query.referencedTables[tableName].aliases); 127 | } 128 | } 129 | for (const cte of Object.values(this.commonTableExpressions)) { 130 | if (cte.referencedTables[tableName] !== undefined) { 131 | return Array.from(cte.referencedTables[tableName].aliases); 132 | } 133 | } 134 | return null; 135 | } 136 | 137 | getTokenAtLocation(stringIndex: number): Token { 138 | if (stringIndex === undefined || stringIndex === null) { 139 | return null; 140 | } 141 | const tokenStartIndices: string[] = Object.keys(this.tokens); 142 | for (let i = 0; i < tokenStartIndices.length; i++) { 143 | const currentTokenStartIndex: number = Number(tokenStartIndices[i]); 144 | let nextTokenStartIndex: number = null; 145 | if (tokenStartIndices[i + 1] !== undefined) { 146 | nextTokenStartIndex = Number(tokenStartIndices[i + 1]); 147 | } 148 | if (stringIndex >= currentTokenStartIndex 149 | && (nextTokenStartIndex === null || stringIndex < nextTokenStartIndex)) { 150 | return this.tokens[currentTokenStartIndex]; 151 | } 152 | } 153 | return null; 154 | } 155 | 156 | getPreviousTokenFromLocation(stringIndex: number): Token { 157 | if (stringIndex === undefined || stringIndex === null) { 158 | return null; 159 | } 160 | const tokenStartIndices: string[] = Object.keys(this.tokens); 161 | const lastIndex = tokenStartIndices[tokenStartIndices.length - 1]; 162 | if (tokenStartIndices.length > 0 && stringIndex >= (parseInt(lastIndex) + this.tokens[lastIndex].value.length)) { 163 | // Index is past the tokens in this query, previous token is the last token 164 | return this.tokens[lastIndex]; 165 | } 166 | let previousTokenStartIndex: number = null; 167 | for (let i = 0; i < tokenStartIndices.length; i++) { 168 | const currentTokenStartIndex: number = Number(tokenStartIndices[i]); 169 | let nextTokenStartIndex: number = null; 170 | if (tokenStartIndices[i + 1] !== undefined) { 171 | nextTokenStartIndex = Number(tokenStartIndices[i + 1]); 172 | } 173 | const currentTokenStopIndex = this.tokens[tokenStartIndices[i]].location.stopIndex; 174 | if (stringIndex > currentTokenStopIndex 175 | && nextTokenStartIndex !== null 176 | && stringIndex < nextTokenStartIndex) { 177 | // We're past the current token, but before the next token 178 | return this.tokens[currentTokenStartIndex]; 179 | } 180 | if (stringIndex >= currentTokenStartIndex 181 | && (nextTokenStartIndex === null || stringIndex < nextTokenStartIndex)) { 182 | if (previousTokenStartIndex === null) { 183 | return null; // No previous token, at the first token 184 | } 185 | return this.tokens[previousTokenStartIndex]; 186 | } 187 | previousTokenStartIndex = currentTokenStartIndex; 188 | } 189 | return null; 190 | } 191 | 192 | getNextTokenFromLocation(stringIndex: number): Token { 193 | const previousToken = this.getPreviousTokenFromLocation(stringIndex); 194 | if (previousToken === null) { 195 | return null; 196 | } 197 | const tokenStartIndices: string[] = Object.keys(this.tokens); 198 | const previousTokenIndex = tokenStartIndices.indexOf(previousToken.location.startIndex.toString()); 199 | const nextToken = this.tokens[tokenStartIndices[previousTokenIndex + 2]]; 200 | if (nextToken !== undefined && nextToken !== null) { 201 | return nextToken; 202 | } 203 | return null; 204 | } 205 | 206 | getReferencedColumn(columnName: string, tableName?: string, tableAlias?: string): ReferencedColumn { 207 | for (const referencedColumn of this.referencedColumns) { 208 | if (referencedColumn.columnName === columnName) { 209 | if ((tableName === null || tableName === undefined || referencedColumn.tableName === tableName) 210 | && (tableAlias === null || tableAlias === undefined || referencedColumn.tableAlias === tableAlias)) { 211 | return referencedColumn; 212 | } 213 | } 214 | } 215 | return null; 216 | } 217 | 218 | /** 219 | * Gets the smallest query at a given location 220 | * i.e. the smallest subquery or common table expression that encapsulates 221 | * the specified index 222 | * @param stringIndex 223 | */ 224 | getSmallestQueryAtLocation(stringIndex: number): ParsedQuery { 225 | let smallestQuery: ParsedQuery = this; 226 | let smallerQuery: ParsedQuery = this; 227 | while (smallerQuery !== null) { 228 | smallerQuery = smallestQuery._getCommonTableExpressionAtLocation(stringIndex); 229 | if (smallerQuery === null) { 230 | smallerQuery = smallestQuery._getSubqueryAtLocation(stringIndex); 231 | } 232 | if (smallerQuery !== null) { 233 | smallestQuery = smallerQuery; 234 | } 235 | } 236 | return smallestQuery; 237 | } 238 | 239 | _getSubqueryAtLocation(stringIndex: number): ParsedQuery { 240 | const subqueryIndex = this._getParsedQueryIndexAtLocation(stringIndex, this.subqueries); 241 | if (subqueryIndex !== null) { 242 | const subqueryIndices = Object.keys(this.subqueries); 243 | return this.subqueries[subqueryIndices[subqueryIndex]]; 244 | } 245 | return null; 246 | } 247 | 248 | _getCommonTableExpressionAtLocation(stringIndex: number): ParsedQuery { 249 | const cteIndex = this._getParsedQueryIndexAtLocation(stringIndex, this.commonTableExpressions); 250 | if (cteIndex !== null) { 251 | const cteIndices = Object.keys(this.commonTableExpressions); 252 | return this.commonTableExpressions[cteIndices[cteIndex]]; 253 | } 254 | return null; 255 | } 256 | 257 | _getParsedQueryIndexAtLocation(stringIndex: number, queries: { [queryStartIndex: number]: ParsedQuery }): number { 258 | if (stringIndex === undefined || stringIndex === null 259 | || queries === undefined || queries === null) { 260 | return null; 261 | } 262 | const queryStartIndices = Object.keys(queries); 263 | for (let i = 0; i < queryStartIndices.length; i++) { 264 | const currentQueryStartIndex: number = Number(queryStartIndices[i]); 265 | if (stringIndex >= currentQueryStartIndex 266 | && stringIndex <= queries[queryStartIndices[i]].queryLocation.stopIndex) { 267 | return i; 268 | } 269 | } 270 | return null; 271 | } 272 | 273 | _addAliasForTable(aliasName: string, tableName: string): void { 274 | this.referencedTables[tableName].aliases.add(aliasName); 275 | } 276 | 277 | _addCommonTableExpression(parsedQuery: ParsedQuery): void { 278 | this.commonTableExpressions[parsedQuery.queryLocation.startIndex] = parsedQuery; 279 | } 280 | 281 | _addOutputColumn(columnName: string, columnAlias: string, tableNameOrAlias: string): void { 282 | let tableName = null; 283 | let tableAlias = null; 284 | if (tableNameOrAlias !== null) { 285 | tableName = this.getTableFromAlias(tableNameOrAlias); 286 | if (tableName !== null) { 287 | tableAlias = tableNameOrAlias; 288 | } else { 289 | tableName = tableNameOrAlias; 290 | } 291 | } 292 | const outputColumn = new OutputColumn(columnName, columnAlias, tableName, tableAlias); 293 | this.outputColumns.push(outputColumn); 294 | } 295 | 296 | _addReferencedColumn(columnName: string, tableNameOrAlias: string, location: TokenLocation): void { 297 | let tableName = null; 298 | let tableAlias = null; 299 | if (tableNameOrAlias !== null) { 300 | tableName = this.getTableFromAlias(tableNameOrAlias); 301 | if (tableName !== null) { 302 | tableAlias = tableNameOrAlias; 303 | } else { 304 | tableName = tableNameOrAlias; 305 | } 306 | } 307 | const existingReferencedColumn = this.getReferencedColumn(columnName, tableName, tableAlias); 308 | if (existingReferencedColumn !== null && existingReferencedColumn !== undefined) { 309 | existingReferencedColumn.locations.add(location); 310 | } else { 311 | this.referencedColumns.push(new ReferencedColumn(columnName, tableName, tableAlias, location)); 312 | } 313 | } 314 | 315 | _addSubQuery(parsedQuery: ParsedQuery): void { 316 | this.subqueries[parsedQuery.queryLocation.startIndex] = parsedQuery; 317 | } 318 | 319 | _addTableNameLocation(tableName: string, location: TokenLocation, schemaName: string, databaseName: string): void { 320 | const subquery = this._getSubqueryAtLocation(location.startIndex); 321 | if (subquery !== null) { 322 | subquery._addTableNameLocation(tableName, location, schemaName, databaseName); 323 | return; 324 | } 325 | const cte = this._getCommonTableExpressionAtLocation(location.startIndex); 326 | if (cte !== null) { 327 | cte._addTableNameLocation(tableName, location, schemaName, databaseName); 328 | return; 329 | } 330 | const aliasTableName = this.getTableFromAlias(tableName); 331 | if (aliasTableName) { 332 | tableName = aliasTableName; 333 | } 334 | if (this.referencedTables[tableName] === undefined) { 335 | this.referencedTables[tableName] = new ReferencedTable(tableName); 336 | this.referencedTables[tableName].schemaName = schemaName; 337 | this.referencedTables[tableName].databaseName = databaseName; 338 | } 339 | this.referencedTables[tableName].locations.add(location); 340 | } 341 | 342 | _addToken(location: TokenLocation, type: TokenType, token: string): void { 343 | this.tokens[location.startIndex] = new Token(token, type, location); 344 | } 345 | 346 | /** 347 | * Aliases can be added to the ParsedQuery before the table itself 348 | * This method merges the aliases into the appropriate ReferencedTable 349 | */ 350 | _consolidateTables(): void { 351 | Object.values(this.commonTableExpressions).forEach(cte => cte._consolidateTables()); 352 | Object.values(this.subqueries).forEach(subquery => subquery._consolidateTables()); 353 | const tableKeysToRemove: string[] = []; 354 | for (const tableName in this.referencedTables) { 355 | const realTableName = this.getTableFromAlias(tableName); 356 | if (realTableName !== null) { 357 | tableKeysToRemove.push(tableName); 358 | for (const location of this.referencedTables[tableName].locations) { 359 | this._addTableNameLocation(realTableName, location, this.referencedTables[tableName].schemaName, this.referencedTables[tableName].databaseName); 360 | } 361 | } 362 | } 363 | for (const key of tableKeysToRemove) { 364 | delete this.referencedTables[key]; 365 | } 366 | 367 | for (const outputColumn of this.outputColumns) { 368 | if (outputColumn.tableName !== null) { 369 | const realTableName = this.getTableFromAlias(outputColumn.tableName); 370 | if (realTableName !== null) { 371 | outputColumn.tableAlias = outputColumn.tableName; 372 | outputColumn.tableName = realTableName; 373 | } 374 | } 375 | } 376 | } 377 | 378 | /** 379 | * Set the common table expression names for any CTEs. 380 | * Must be called after tokens have been added to the CTEs. 381 | */ 382 | _setCommonTableExpressionNames(): void { 383 | for (const cte of Object.values(this.commonTableExpressions)) { 384 | let cteName: string = null; 385 | for (const token of Object.values(cte.tokens)) { 386 | if (token.value.toUpperCase() !== 'WITH') { 387 | cteName = token.value; 388 | break; 389 | } 390 | } 391 | cte.commonTableExpressionName = cteName; 392 | cte._setCommonTableExpressionNames(); 393 | } 394 | for (const subquery of Object.values(this.subqueries)) { 395 | subquery._setCommonTableExpressionNames(); 396 | } 397 | } 398 | } --------------------------------------------------------------------------------