mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
310 lines
7.2 KiB
TypeScript
310 lines
7.2 KiB
TypeScript
/*
|
|
* CloudBeaver - Cloud Database Manager
|
|
* Copyright (C) 2020-2022 DBeaver Corp and others
|
|
*
|
|
* Licensed under the Apache License, Version 2.0.
|
|
* you may not use this file except in compliance with the License.
|
|
*/
|
|
|
|
import type { SqlDialectInfo } from '@cloudbeaver/core-sdk';
|
|
|
|
type RequireOne<T, K extends keyof T> = {
|
|
[X in Exclude<keyof T, K>]?: T[X]
|
|
} & {
|
|
[P in K]-?: T[P]
|
|
};
|
|
|
|
export interface IQueryInfo {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
export interface ISQLScriptSegment {
|
|
query: string;
|
|
|
|
/** query begin index in script */
|
|
begin: number;
|
|
/** query end index in script */
|
|
end: number;
|
|
|
|
/** query begin line */
|
|
from: number;
|
|
/** query end line */
|
|
to: number;
|
|
/** query begin index in line */
|
|
fromPosition: number;
|
|
/** query end index in line */
|
|
toPosition: number;
|
|
}
|
|
|
|
export interface ISQLScriptLine {
|
|
index: number;
|
|
begin: number;
|
|
end: number;
|
|
}
|
|
|
|
const defaultDialect: RequireOne<
|
|
SqlDialectInfo,
|
|
'scriptDelimiter' | 'quoteStrings' | 'singleLineComments' | 'multiLineComments'
|
|
> = {
|
|
scriptDelimiter: ';',
|
|
quoteStrings: [['"', '"']],
|
|
singleLineComments: ['--'],
|
|
multiLineComments: [['/*', '*/']],
|
|
};
|
|
|
|
export class SQLParser {
|
|
get scriptDelimiters(): string[] {
|
|
return [...this.customScriptDelimiters, this.dialect?.scriptDelimiter || defaultDialect.scriptDelimiter];
|
|
}
|
|
|
|
get quoteStrings(): string[][] {
|
|
return [...this.customQuotes, ...(this.dialect?.quoteStrings || defaultDialect.quoteStrings)];
|
|
}
|
|
|
|
get singleLineComments(): string[] {
|
|
return this.dialect?.singleLineComments || defaultDialect.singleLineComments;
|
|
}
|
|
|
|
get multiLineComments(): string[][] {
|
|
return this.dialect?.multiLineComments || defaultDialect.multiLineComments;
|
|
}
|
|
|
|
get scripts(): ISQLScriptSegment[] {
|
|
this.update();
|
|
return this._scripts;
|
|
}
|
|
|
|
get lineCount(): number {
|
|
this.update();
|
|
return this.lines.length;
|
|
}
|
|
|
|
get actualScript(): string {
|
|
return this.script;
|
|
}
|
|
|
|
private dialect: SqlDialectInfo | null;
|
|
private _scripts: ISQLScriptSegment[];
|
|
private script: string;
|
|
private parsedScript: string;
|
|
private lines: ISQLScriptLine[];
|
|
private customScriptDelimiters: string[];
|
|
private readonly customQuotes: string[][];
|
|
|
|
constructor() {
|
|
this.dialect = null;
|
|
this._scripts = [];
|
|
this.script = '';
|
|
this.parsedScript = '';
|
|
this.lines = [];
|
|
this.customScriptDelimiters = [];
|
|
this.customQuotes = [["'", "'"]];
|
|
}
|
|
|
|
getSegment(begin: number, end: number): ISQLScriptSegment | undefined {
|
|
this.update();
|
|
const from = this.getScriptLineAtPos(begin);
|
|
const to = this.getScriptLineAtPos(end);
|
|
|
|
if (!from || !to) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
query: this.parsedScript.substring(begin, end),
|
|
begin,
|
|
end,
|
|
from: from.index,
|
|
to: to.index,
|
|
fromPosition: begin - from.begin,
|
|
toPosition: end - to.begin,
|
|
};
|
|
}
|
|
|
|
getQueryAtPos(position: number): ISQLScriptSegment | undefined {
|
|
this.update();
|
|
const script = this._scripts.find(script => script.begin <= position && script.end > position);
|
|
|
|
if (script) {
|
|
return script;
|
|
}
|
|
|
|
const line = this.getLineAtPos(position);
|
|
|
|
const closestScripts = this._scripts.filter(
|
|
script => script.begin <= position && (script.to === line || script.to === line - 1)
|
|
);
|
|
|
|
if (closestScripts.length > 0) {
|
|
return closestScripts[closestScripts.length - 1];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
getLineAtPos(position: number): number {
|
|
return this.lines.find(line => line.begin <= position && line.end > position)?.index ?? 0;
|
|
}
|
|
|
|
getScriptLineAtPos(position: number): ISQLScriptLine | undefined {
|
|
return this.lines.find(line => line.begin <= position && line.end > position);
|
|
}
|
|
|
|
setScript(script: string): void {
|
|
this.script = script;
|
|
}
|
|
|
|
setDialect(dialect: SqlDialectInfo | null): void {
|
|
this.dialect = dialect;
|
|
this.parsedScript = '';
|
|
}
|
|
|
|
setCustomDelimiters(delimiters: string[]): void {
|
|
this.customScriptDelimiters = delimiters;
|
|
}
|
|
|
|
parse(script: string): void {
|
|
this._scripts = [];
|
|
this.parsedScript = script;
|
|
this.lines = [];
|
|
|
|
let ignore = false;
|
|
let releaseChar = '';
|
|
let currentSegment = '';
|
|
|
|
let position = 0;
|
|
|
|
let begin = 0;
|
|
for (const line of script.split('\n')) {
|
|
const end = begin + line.length + 1;
|
|
|
|
this.lines.push({
|
|
index: this.lines.length,
|
|
begin,
|
|
end,
|
|
});
|
|
|
|
begin = end;
|
|
}
|
|
|
|
for (const char of script) {
|
|
currentSegment += char;
|
|
position++;
|
|
|
|
if (ignore) {
|
|
if (currentSegment.endsWith(releaseChar)) {
|
|
ignore = false;
|
|
}
|
|
|
|
if (position < script.length) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
const scriptDelimiter = this.scriptDelimiters.find(scriptDelimiter => currentSegment.endsWith(scriptDelimiter));
|
|
|
|
if (scriptDelimiter || position === script.length) {
|
|
let query = currentSegment;
|
|
|
|
if (scriptDelimiter) {
|
|
query = query.substring(0, query.length - scriptDelimiter.length);
|
|
}
|
|
|
|
query = query.trim();
|
|
|
|
if (query) {
|
|
const begin = script.indexOf(query, position - currentSegment.length);
|
|
const end = begin + query.length;
|
|
const from = this.getScriptLineAtPos(begin);
|
|
const to = this.getScriptLineAtPos(end);
|
|
|
|
if (from && to) {
|
|
this._scripts.push({
|
|
query,
|
|
begin,
|
|
end,
|
|
from: from.index,
|
|
to: to.index,
|
|
fromPosition: begin - from.begin,
|
|
toPosition: end - to.begin,
|
|
});
|
|
}
|
|
}
|
|
|
|
currentSegment = '';
|
|
}
|
|
|
|
for (const singleLineComment of this.singleLineComments) {
|
|
if (currentSegment.endsWith(singleLineComment)) {
|
|
ignore = true;
|
|
releaseChar = '\n';
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ignore) {
|
|
continue;
|
|
}
|
|
|
|
for (const quote of this.quoteStrings) {
|
|
if (currentSegment.endsWith(getBlockChar(quote, true))) {
|
|
ignore = true;
|
|
releaseChar = getBlockChar(quote, false);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ignore) {
|
|
continue;
|
|
}
|
|
|
|
for (const comment of this.multiLineComments) {
|
|
if (currentSegment.endsWith(getBlockChar(comment, true))) {
|
|
ignore = true;
|
|
releaseChar = getBlockChar(comment, false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setQueries(queries: IQueryInfo[]): this {
|
|
this.update();
|
|
this._scripts = [];
|
|
|
|
for (const query of queries) {
|
|
const from = this.getScriptLineAtPos(query.start);
|
|
const to = this.getScriptLineAtPos(query.end);
|
|
|
|
if (from && to) {
|
|
this._scripts.push({
|
|
query: this.script.substring(query.start, query.end),
|
|
begin: query.start,
|
|
end: query.end,
|
|
from: from.index,
|
|
to: to.index,
|
|
fromPosition: query.start - from.begin,
|
|
toPosition: query.end - to.begin,
|
|
});
|
|
}
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
private update() {
|
|
if (this.parsedScript !== this.script) {
|
|
this.parse(this.script);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getBlockChar(chars: string[], openChar: boolean) {
|
|
if (chars.length === 1) {
|
|
return chars[0];
|
|
}
|
|
|
|
return openChar ? chars[0] : chars[1];
|
|
}
|