diff --git a/README-zh_CN.md b/README-zh_CN.md
index aab75a00..fec76452 100644
--- a/README-zh_CN.md
+++ b/README-zh_CN.md
@@ -39,6 +39,7 @@ Monaco SQL Languages 是一个基于 Monaco Editor 的 SQL 语言项目,从 [m
- Trino (Presto)
- PostgreSQL
- Impala
+- GenericSQL
@@ -72,6 +73,7 @@ npm install monaco-sql-languages
import 'monaco-sql-languages/esm/languages/trino/trino.contribution';
import 'monaco-sql-languages/esm/languages/pgsql/pgsql.contribution';
import 'monaco-sql-languages/esm/languages/impala/impala.contribution';
+ import 'monaco-sql-languages/esm/languages/generic/generic.contribution';
// 或者你可以通过下面的方式一次性导入所有的语言功能
// import 'monaco-sql-languages/esm/all.contributions';
diff --git a/README.md b/README.md
index 49268932..e7264c32 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,7 @@ This project is based on the SQL language project of Monaco Editor, which was fo
- Trino (Presto)
- PostgreSQL
- Impala
+- GenericSQL
@@ -71,6 +72,7 @@ npm install monaco-sql-languages
import 'monaco-sql-languages/esm/languages/trino/trino.contribution';
import 'monaco-sql-languages/esm/languages/pgsql/pgsql.contribution';
import 'monaco-sql-languages/esm/languages/impala/impala.contribution';
+ import 'monaco-sql-languages/esm/languages/generic/generic.contribution';
// Or you can import all language contributions at once.
// import 'monaco-sql-languages/esm/all.contributions';
diff --git a/package.json b/package.json
index ee3ccfe1..ef4d847d 100644
--- a/package.json
+++ b/package.json
@@ -34,7 +34,8 @@
"postgresql",
"flink",
"trino",
- "impala"
+ "impala",
+ "generic"
],
"repository": {
"type": "git",
@@ -84,6 +85,6 @@
]
},
"dependencies": {
- "dt-sql-parser": "4.4.2"
+ "dt-sql-parser": "4.5.0"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8ce50c9d..d2dc1700 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -9,8 +9,8 @@ importers:
.:
dependencies:
dt-sql-parser:
- specifier: 4.4.2
- version: 4.4.2(antlr4ng-cli@1.0.7)
+ specifier: 4.5.0
+ version: 4.5.0(antlr4ng-cli@1.0.7)
devDependencies:
'@commitlint/cli':
specifier: ^17.7.2
@@ -714,8 +714,8 @@ packages:
resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==}
engines: {node: '>=6'}
- dt-sql-parser@4.4.2:
- resolution: {integrity: sha512-NpfSeCetxDICs5NNOHKO/e/PTQ/EFnGW0QBo11e5nmuWqVRACZkArcBH9/LKnoe1Mdtql45U5h0aU9GOqhBLWg==}
+ dt-sql-parser@4.5.0:
+ resolution: {integrity: sha512-x12CX4vqeJKUZzCDjVBlG2DLQZ7ORnqVYYmnOFiz1IMBCOuamjHUeU05IGiRbRO+VqQSoN5RasiFJAjqzZ1WJw==}
engines: {node: '>=18'}
eastasianwidth@0.2.0:
@@ -2729,7 +2729,7 @@ snapshots:
find-up: 3.0.0
minimatch: 3.1.2
- dt-sql-parser@4.4.2(antlr4ng-cli@1.0.7):
+ dt-sql-parser@4.5.0(antlr4ng-cli@1.0.7):
dependencies:
antlr4-c3: 3.3.7(antlr4ng-cli@1.0.7)
antlr4ng: 2.0.11(antlr4ng-cli@1.0.7)
diff --git a/src/all.contributions.ts b/src/all.contributions.ts
index 8989152f..6b43abe3 100644
--- a/src/all.contributions.ts
+++ b/src/all.contributions.ts
@@ -5,3 +5,4 @@ import './languages/trino/trino.contribution';
import './languages/mysql/mysql.contribution';
import './languages/pgsql/pgsql.contribution';
import './languages/impala/impala.contribution';
+import './languages/generic/generic.contribution';
diff --git a/src/common/constants.ts b/src/common/constants.ts
index c8f7d8ee..eb8ab9ea 100644
--- a/src/common/constants.ts
+++ b/src/common/constants.ts
@@ -37,5 +37,6 @@ export enum LanguageIdEnum {
PG = 'pgsql',
SPARK = 'sparksql',
TRINO = 'trinosql',
- IMPALA = 'impalasql'
+ IMPALA = 'impalasql',
+ GENERIC = 'genericsql'
}
diff --git a/src/languageFeatures.ts b/src/languageFeatures.ts
index d19a1960..d3877b37 100644
--- a/src/languageFeatures.ts
+++ b/src/languageFeatures.ts
@@ -255,7 +255,9 @@ export class DefinitionAdapter implements languages.Def
startIndex: -1,
endIndex: -1,
startColumn: -1,
- endColumn: -1
+ endColumn: -1,
+ startTokenIndex: -1,
+ endTokenIndex: -1
};
const curEntity = entities?.find((entity: EntityContext) => {
const entityPosition = entity.position;
diff --git a/src/languages/generic/generic.contribution.ts b/src/languages/generic/generic.contribution.ts
new file mode 100644
index 00000000..f474843d
--- /dev/null
+++ b/src/languages/generic/generic.contribution.ts
@@ -0,0 +1,18 @@
+import { registerLanguage } from '../../_.contribution';
+import { LanguageIdEnum } from '../../common/constants';
+import { setupLanguageFeatures } from '../../setupLanguageFeatures';
+
+registerLanguage({
+ id: LanguageIdEnum.GENERIC,
+ extensions: ['.genericsql'],
+ aliases: ['GenericSQL', 'generic', 'Generic'],
+ loader: () => import('./generic')
+});
+
+setupLanguageFeatures(LanguageIdEnum.GENERIC, {
+ completionItems: true,
+ diagnostics: false,
+ references: false,
+ definitions: false,
+ hover: false
+});
diff --git a/src/languages/generic/generic.snippet.ts b/src/languages/generic/generic.snippet.ts
new file mode 100644
index 00000000..5c803966
--- /dev/null
+++ b/src/languages/generic/generic.snippet.ts
@@ -0,0 +1,76 @@
+import type { CompletionSnippetOption } from 'src/monaco.contribution';
+
+export const genericSnippets: CompletionSnippetOption[] = [
+ {
+ label: 'select',
+ prefix: 'SELECT',
+ body: ['SELECT ${2:column1}, ${3:column2} FROM ${1:table_name};\n$4']
+ },
+ {
+ label: 'select-join',
+ prefix: 'SELECT-JOIN',
+ body: [
+ 'SELECT ${8:column1} FROM ${1:table_name1} ${2:t1}',
+ '${3:LEFT} JOIN ${4:table_name2} ${5:t2} ON ${2:t1}.${6:column1} = ${5:t2}.${7:column2};\n$9'
+ ]
+ },
+ {
+ label: 'select-order-by',
+ prefix: 'SELECT-ORDER-BY',
+ body: [
+ 'SELECT ${2:column1}, ${3:column2} FROM ${1:table_name} ORDER BY ${4:column1} ${5:desc};\n$6'
+ ]
+ },
+ {
+ label: 'select-group-by',
+ prefix: 'SELECT-GROUP-BY',
+ body: ['SELECT ${2:column1}, COUNT(*) FROM ${1:table_name} GROUP BY ${2:column1};\n$3']
+ },
+ {
+ label: 'insert',
+ prefix: 'INSERT-INTO',
+ body: [
+ 'INSERT INTO ${1:table_name} (${2:column1}, ${3:column2})',
+ 'SELECT ${4:column1}, ${5:column2} FROM ${6:source_table};\n$7'
+ ]
+ },
+ {
+ label: 'update',
+ prefix: 'UPDATE',
+ body: [
+ 'UPDATE ${1:table_name}',
+ 'SET ${2:column1} = ${3:value1}',
+ 'WHERE ${4:column2} = ${5:value2};\n$6'
+ ]
+ },
+ {
+ label: 'delete',
+ prefix: 'DELETE',
+ body: ['DELETE FROM ${1:table_name}', 'WHERE ${2:column1} = ${3:value1};\n$4']
+ },
+ {
+ label: 'create-table',
+ prefix: 'CREATE-TABLE',
+ body: [
+ 'CREATE TABLE IF NOT EXISTS ${1:table_name} (',
+ '\t${2:column1} ${3:INT} PRIMARY KEY,',
+ '\t${4:column2} ${5:VARCHAR(100)} NOT NULL',
+ ');\n$6'
+ ]
+ },
+ {
+ label: 'alter-table-add',
+ prefix: 'ALTER-TABLE-ADD',
+ body: ['ALTER TABLE ${1:table_name} ADD COLUMN ${2:column_name} ${3:INT};\n$4']
+ },
+ {
+ label: 'alter-table-drop',
+ prefix: 'ALTER-TABLE-DROP',
+ body: ['ALTER TABLE ${1:table_name} DROP COLUMN ${2:column_name};\n$3']
+ },
+ {
+ label: 'drop-table',
+ prefix: 'DROP-TABLE',
+ body: ['DROP TABLE IF EXISTS ${1:table_name};\n$2']
+ }
+];
diff --git a/src/languages/generic/generic.ts b/src/languages/generic/generic.ts
new file mode 100644
index 00000000..ccf3a548
--- /dev/null
+++ b/src/languages/generic/generic.ts
@@ -0,0 +1,266 @@
+import type { languages } from '../../fillers/monaco-editor-core';
+import { TokenClassConsts } from '../../common/constants';
+
+export const conf: languages.LanguageConfiguration = {
+ comments: {
+ lineComment: '--',
+ blockComment: ['/*', '*/']
+ },
+ brackets: [
+ ['{', '}'],
+ ['[', ']'],
+ ['(', ')']
+ ],
+ autoClosingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ { open: '`', close: '`' }
+ ],
+ surroundingPairs: [
+ { open: '{', close: '}' },
+ { open: '[', close: ']' },
+ { open: '(', close: ')' },
+ { open: '"', close: '"' },
+ { open: "'", close: "'" },
+ { open: '`', close: '`' }
+ ]
+};
+
+export const language = {
+ defaultToken: '',
+ tokenPostfix: '.sql',
+ ignoreCase: true,
+ brackets: [
+ { open: '[', close: ']', token: TokenClassConsts.DELIMITER_SQUARE },
+ { open: '(', close: ')', token: TokenClassConsts.DELIMITER_PAREN },
+ { open: '{', close: '}', token: TokenClassConsts.DELIMITER_CURLY }
+ ],
+ keywords: [
+ 'ADD',
+ 'ALL',
+ 'ALTER',
+ 'AND',
+ 'AS',
+ 'ASC',
+ 'BETWEEN',
+ 'BY',
+ 'CASE',
+ 'CAST',
+ 'CHECK',
+ 'COALESCE',
+ 'COLUMN',
+ 'CONSTRAINT',
+ 'CREATE',
+ 'CROSS',
+ 'DEFAULT',
+ 'DELETE',
+ 'DESC',
+ 'DISTINCT',
+ 'DROP',
+ 'ELSE',
+ 'END',
+ 'ESCAPE',
+ 'EXCEPT',
+ 'EXISTS',
+ 'FALSE',
+ 'FIRST',
+ 'FOREIGN',
+ 'FROM',
+ 'FULL',
+ 'GROUP',
+ 'HAVING',
+ 'IF',
+ 'IN',
+ 'INNER',
+ 'INSERT',
+ 'INTERSECT',
+ 'INTO',
+ 'IS',
+ 'JOIN',
+ 'KEY',
+ 'LAST',
+ 'LEFT',
+ 'LIKE',
+ 'LIMIT',
+ 'NOT',
+ 'NULL',
+ 'NULLIF',
+ 'NULLS',
+ 'OFFSET',
+ 'ON',
+ 'OR',
+ 'ORDER',
+ 'OUTER',
+ 'PRIMARY',
+ 'RECURSIVE',
+ 'REFERENCES',
+ 'RENAME',
+ 'RIGHT',
+ 'SELECT',
+ 'SET',
+ 'TABLE',
+ 'THEN',
+ 'TO',
+ 'TRUE',
+ 'UNION',
+ 'UNIQUE',
+ 'UPDATE',
+ 'VALUES',
+ 'WHEN',
+ 'WHERE',
+ 'WITH'
+ ],
+ operators: [
+ 'AND',
+ 'BETWEEN',
+ 'IN',
+ 'LIKE',
+ 'NOT',
+ 'EXISTS',
+ 'OR',
+ 'IS',
+ 'UNION',
+ 'INTERSECT',
+ 'EXCEPT',
+ 'JOIN',
+ 'CROSS',
+ 'INNER',
+ 'OUTER',
+ 'FULL',
+ 'LEFT',
+ 'RIGHT'
+ ],
+ builtinFunctions: [
+ 'AVG',
+ 'COUNT',
+ 'FIRST_VALUE',
+ 'LAG',
+ 'LAST_VALUE',
+ 'LEAD',
+ 'MAX',
+ 'MIN',
+ 'NTH_VALUE',
+ 'NTILE',
+ 'PERCENT_RANK',
+ 'RANK',
+ 'ROW_NUMBER',
+ 'SUM',
+ 'STDDEV',
+ 'STDDEV_POP',
+ 'STDDEV_SAMP',
+ 'VAR_POP',
+ 'VAR_SAMP',
+ 'VARIANCE'
+ ],
+ builtinVariables: [
+ // Not supported
+ ],
+ typeKeywords: [
+ 'BOOLEAN',
+ 'TINYINT',
+ 'SMALLINT',
+ 'INT',
+ 'INTEGER',
+ 'BIGINT',
+ 'FLOAT',
+ 'DOUBLE',
+ 'DECIMAL',
+ 'NUMERIC',
+ 'VARCHAR',
+ 'CHAR',
+ 'TEXT',
+ 'DATE',
+ 'TIME',
+ 'TIMESTAMP',
+ 'BINARY',
+ 'VARBINARY'
+ ],
+ scopeKeywords: ['CASE', 'END', 'WHEN', 'THEN', 'ELSE'],
+ pseudoColumns: [
+ // Not supported
+ ],
+ tokenizer: {
+ root: [
+ { include: '@comments' },
+ { include: '@whitespace' },
+ { include: '@pseudoColumns' },
+ { include: '@customParams' },
+ { include: '@numbers' },
+ { include: '@strings' },
+ { include: '@complexIdentifiers' },
+ { include: '@scopes' },
+ [/[:;,.]/, TokenClassConsts.DELIMITER],
+ [/[\(\)\[\]\{\}]/, '@brackets'],
+ [
+ /[\w@]+/,
+ {
+ cases: {
+ '@scopeKeywords': TokenClassConsts.KEYWORD_SCOPE,
+ '@operators': TokenClassConsts.OPERATOR_KEYWORD,
+ '@typeKeywords': TokenClassConsts.TYPE,
+ '@builtinVariables': TokenClassConsts.VARIABLE,
+ '@builtinFunctions': TokenClassConsts.PREDEFINED,
+ '@keywords': TokenClassConsts.KEYWORD,
+ '@default': TokenClassConsts.IDENTIFIER
+ }
+ }
+ ],
+ [/[<>=!%&+\-*/|~^]/, TokenClassConsts.OPERATOR_SYMBOL]
+ ],
+ whitespace: [[/[\s\t\r\n]+/, TokenClassConsts.WHITE]],
+ comments: [
+ [/--+.*/, TokenClassConsts.COMMENT],
+ [/\/\*/, { token: TokenClassConsts.COMMENT_QUOTE, next: '@comment' }]
+ ],
+ comment: [
+ [/[^*/]+/, TokenClassConsts.COMMENT],
+ [/\*\//, { token: TokenClassConsts.COMMENT_QUOTE, next: '@pop' }],
+ [/./, TokenClassConsts.COMMENT]
+ ],
+ pseudoColumns: [
+ [
+ /[$][A-Za-z_][\w@#$]*/,
+ {
+ cases: {
+ '@pseudoColumns': TokenClassConsts.PREDEFINED,
+ '@default': TokenClassConsts.IDENTIFIER
+ }
+ }
+ ]
+ ],
+ customParams: [
+ [/\${[A-Za-z0-9._-]*}/, TokenClassConsts.VARIABLE],
+ [/\@\@{[A-Za-z0-9._-]*}/, TokenClassConsts.VARIABLE]
+ ],
+ numbers: [
+ [/[$][+-]*\d*(\.\d*)?/, TokenClassConsts.NUMBER],
+ [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, TokenClassConsts.NUMBER]
+ ],
+ strings: [[/'/, { token: TokenClassConsts.STRING, next: '@string' }]],
+ string: [
+ [/[^']+/, TokenClassConsts.STRING_ESCAPE],
+ [/''/, TokenClassConsts.STRING],
+ [/'/, { token: TokenClassConsts.STRING, next: '@pop' }]
+ ],
+ complexIdentifiers: [
+ [/`/, { token: TokenClassConsts.IDENTIFIER_QUOTE, next: '@quotedIdentifier' }],
+ [/"/, { token: TokenClassConsts.IDENTIFIER_QUOTE, next: '@doubleQuotedIdentifier' }]
+ ],
+ quotedIdentifier: [
+ [/[^`]+/, TokenClassConsts.IDENTIFIER_QUOTE],
+ [/``/, TokenClassConsts.IDENTIFIER_QUOTE],
+ [/`/, { token: TokenClassConsts.IDENTIFIER_QUOTE, next: '@pop' }]
+ ],
+ doubleQuotedIdentifier: [
+ [/[^"]+/, TokenClassConsts.IDENTIFIER_QUOTE],
+ [/""/, TokenClassConsts.IDENTIFIER_QUOTE],
+ [/"/, { token: TokenClassConsts.IDENTIFIER_QUOTE, next: '@pop' }]
+ ],
+ scopes: [
+ // Not supported
+ ]
+ }
+};
diff --git a/src/languages/generic/generic.worker.ts b/src/languages/generic/generic.worker.ts
new file mode 100644
index 00000000..e5b8289c
--- /dev/null
+++ b/src/languages/generic/generic.worker.ts
@@ -0,0 +1,11 @@
+import { worker } from '../../fillers/monaco-editor-core';
+import * as EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker.js';
+import { ICreateData } from '../../baseSQLWorker';
+import { GenericSQLWorker } from './genericWorker';
+
+self.onmessage = () => {
+ // ignore the first message
+ EditorWorker.initialize((ctx: worker.IWorkerContext, createData: ICreateData) => {
+ return new GenericSQLWorker(ctx, createData);
+ });
+};
diff --git a/src/languages/generic/genericWorker.ts b/src/languages/generic/genericWorker.ts
new file mode 100644
index 00000000..776f95e6
--- /dev/null
+++ b/src/languages/generic/genericWorker.ts
@@ -0,0 +1,17 @@
+import { worker } from '../../fillers/monaco-editor-core';
+import { GenericSQL } from 'dt-sql-parser/dist/parser/generic';
+import { BaseSQLWorker, ICreateData } from '../../baseSQLWorker';
+
+export class GenericSQLWorker extends BaseSQLWorker {
+ protected _ctx: worker.IWorkerContext;
+ protected parser: GenericSQL;
+ constructor(ctx: worker.IWorkerContext, createData: ICreateData) {
+ super(ctx, createData);
+ this._ctx = ctx;
+ this.parser = new GenericSQL();
+ }
+}
+
+export function create(ctx: worker.IWorkerContext, createData: ICreateData): GenericSQLWorker {
+ return new GenericSQLWorker(ctx, createData);
+}
diff --git a/src/setupLanguageFeatures.ts b/src/setupLanguageFeatures.ts
index 6e965958..3ef5ef18 100644
--- a/src/setupLanguageFeatures.ts
+++ b/src/setupLanguageFeatures.ts
@@ -109,6 +109,8 @@ function getDefaultSnippets(languageId: LanguageIdEnum) {
return snippets.sparkSnippets;
case LanguageIdEnum.TRINO:
return snippets.trinoSnippets;
+ case LanguageIdEnum.GENERIC:
+ return snippets.genericSnippets;
default:
return [];
}
diff --git a/src/snippets.ts b/src/snippets.ts
index 997e8f28..4174ed3f 100644
--- a/src/snippets.ts
+++ b/src/snippets.ts
@@ -5,3 +5,4 @@ export { pgsqlSnippets } from './languages/pgsql/pgsql.snippet';
export { sparkSnippets } from './languages/spark/spark.snippet';
export { mysqlSnippets } from './languages/mysql/mysql.snippet';
export { impalaSnippets } from './languages/impala/impala.snippet';
+export { genericSnippets } from './languages/generic/generic.snippet';
diff --git a/website/src/consts/index.ts b/website/src/consts/index.ts
index 1cb3fe23..02d38d6e 100644
--- a/website/src/consts/index.ts
+++ b/website/src/consts/index.ts
@@ -38,7 +38,8 @@ export const SQL_LANGUAGES = [
'MySQL',
'PGSQL',
'TrinoSQL',
- 'ImpalaSQL'
+ 'ImpalaSQL',
+ 'GenericSQL'
];
export const defaultLanguage = SQL_LANGUAGES[0];
diff --git a/website/src/extensions/main/index.tsx b/website/src/extensions/main/index.tsx
index 83682bf3..a554ea4e 100644
--- a/website/src/extensions/main/index.tsx
+++ b/website/src/extensions/main/index.tsx
@@ -1,37 +1,36 @@
-import {
- IContributeType,
- IEditorTab,
- IExtension,
- IMoleculeContext,
- TabGroup
-} from '@dtinsight/molecule';
-
+import { ParseError } from 'dt-sql-parser';
import * as monaco from 'monaco-editor';
import { vsPlusTheme } from 'monaco-sql-languages/esm/main';
+import { LanguageService, type SerializedTreeNode } from 'monaco-sql-languages/esm/languageService';
-import Welcome from '@/workbench/welcome';
+import TreeVisualizerPanel from '@/components/treeVisualizerPanel';
import {
- FILE_PATH,
- QUICK_GITHUB,
- PARSE_LANGUAGE,
+ ACTIVITY_API,
ACTIVITY_FOLDER,
ACTIVITY_SQL,
- ACTIVITY_API,
- SQL_LANGUAGES,
- PARSE_TREE
+ FILE_PATH,
+ PARSE_LANGUAGE,
+ PARSE_TREE,
+ QUICK_GITHUB,
+ SQL_LANGUAGES
} from '@/consts';
-import QuickGithub from '@/workbench/quickGithub';
-import SourceSpace from '@/workbench/sourceSpace';
-import UnitTest from '@/workbench/unitTest';
-import ApiDocPage from '@/workbench/apiDocPage';
import { debounce } from '@/utils/tool';
-import { LanguageService } from '../../../../esm/languageService';
-import { ParseError } from 'dt-sql-parser';
+import ApiDocPage from '@/workbench/apiDocPage';
import { ProblemsPaneView } from '@/workbench/problems';
import ProblemStore from '@/workbench/problems/clients/problemStore';
-import { ProblemsService } from '@/workbench/problems/services';
import { ProblemsController } from '@/workbench/problems/controllers';
-import TreeVisualizerPanel from '@/components/treeVisualizerPanel';
+import { ProblemsService } from '@/workbench/problems/services';
+import QuickGithub from '@/workbench/quickGithub';
+import SourceSpace from '@/workbench/sourceSpace';
+import UnitTest from '@/workbench/unitTest';
+import Welcome from '@/workbench/welcome';
+import {
+ IContributeType,
+ IEditorTab,
+ IExtension,
+ IMoleculeContext,
+ TabGroup
+} from '@dtinsight/molecule';
const problemsService = new ProblemsService();
@@ -499,12 +498,14 @@ const updateParseTree = (molecule: IMoleculeContext, languageService: LanguageSe
sql = '';
}
- languageService.getSerializedParseTree(language, sql).then((tree) => {
- molecule.panel.update({
- id: PARSE_TREE,
- data: tree
+ languageService
+ .getSerializedParseTree(language, sql)
+ .then((tree: SerializedTreeNode | null) => {
+ molecule.panel.update({
+ id: PARSE_TREE,
+ data: tree
+ });
});
- });
};
const debounceUpdateParseTree = debounce(updateParseTree, 400);
diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts
index c060536c..f1a8f922 100644
--- a/website/src/languages/helpers/completionService.ts
+++ b/website/src/languages/helpers/completionService.ts
@@ -1,8 +1,31 @@
+import type { CommonEntityContext, Suggestions, WordRange } from 'dt-sql-parser';
+import {
+ AttrName,
+ ColumnDeclareType,
+ EntityContext,
+ TableDeclareType
+} from 'dt-sql-parser/dist/parser/common/entityCollector';
import { languages } from 'monaco-editor/esm/vs/editor/editor.api';
-import { CompletionService, ICompletionItem } from 'monaco-sql-languages/esm/languageService';
-import { EntityContextType } from 'monaco-sql-languages/esm/main';
+import { EntityContextType, StmtContextType } from 'monaco-sql-languages/esm/main';
+import type {
+ CompletionService,
+ ICompletionItem
+} from 'monaco-sql-languages/esm/monaco.contribution';
-import { getCatalogs, getDataBases, getSchemas, getTables, getViews } from './dbMetaProvider';
+import {
+ getCatalogs,
+ getColumns,
+ getDataBases,
+ getSchemas,
+ getTables,
+ getViews
+} from './dbMetaProvider';
+
+// Custom completion item interface, extending ICompletionItem to support additional properties
+interface EnhancedCompletionItem extends ICompletionItem {
+ _tableName?: string;
+ _columnText?: string;
+}
const haveCatalogSQLType = (languageId: string) => {
return ['flinksql', 'trinosql'].includes(languageId.toLowerCase());
@@ -12,180 +35,740 @@ const namedSchemaSQLType = (languageId: string) => {
return ['trinosql', 'hivesql', 'sparksql'].includes(languageId);
};
-export const completionService: CompletionService = async function (
- model,
- _position,
- _completionContext,
- suggestions,
- _entities,
- snippets
-) {
- if (!suggestions) {
- return Promise.resolve([]);
+const isWordRangesEndWithWhiteSpace = (wordRanges: WordRange[]) => {
+ return wordRanges.length > 1 && wordRanges.at(-1)?.text === ' ';
+};
+
+// Completion tracker class, used to track already added completion types
+class CompletionTracker {
+ private completionTypes = new Set();
+
+ hasCompletionType(type: string): boolean {
+ return this.completionTypes.has(type);
}
- const languageId = model.getLanguageId();
+
+ markAsCompleted(type: string): void {
+ this.completionTypes.add(type);
+ }
+}
+
+/**
+ * Get database object completion items (catalog, database, table, etc.)
+ */
+const getDatabaseObjectCompletions = async (
+ tracker: CompletionTracker,
+ languageId: string,
+ contextType: EntityContextType | StmtContextType,
+ words: string[]
+): Promise => {
const haveCatalog = haveCatalogSQLType(languageId);
const getDBOrSchema = namedSchemaSQLType(languageId) ? getSchemas : getDataBases;
+ const wordCount = words.length;
+ const result: ICompletionItem[] = [];
- const { keywords, syntax } = suggestions;
+ // Complete Catalog
+ if (wordCount <= 1 && haveCatalog && !tracker.hasCompletionType('catalog')) {
+ if (
+ [EntityContextType.CATALOG, EntityContextType.DATABASE_CREATE].includes(
+ contextType as EntityContextType
+ )
+ ) {
+ result.push(...(await getCatalogs(languageId)));
+ tracker.markAsCompleted('catalog');
+ }
+ }
- const keywordsCompletionItems: ICompletionItem[] = keywords.map((kw) => ({
- label: kw,
- kind: languages.CompletionItemKind.Keyword,
- detail: '关键字',
- sortText: '2' + kw
- }));
+ // Complete Database
+ if (wordCount <= 1 && !tracker.hasCompletionType('database')) {
+ if (
+ [
+ EntityContextType.DATABASE,
+ EntityContextType.TABLE,
+ EntityContextType.TABLE_CREATE,
+ EntityContextType.VIEW,
+ EntityContextType.VIEW_CREATE
+ ].includes(contextType as EntityContextType)
+ ) {
+ result.push(...(await getDBOrSchema(languageId)));
+ tracker.markAsCompleted('database');
+ }
+ }
- let syntaxCompletionItems: ICompletionItem[] = [];
+ // Complete Database under Catalog
+ if (
+ wordCount >= 2 &&
+ wordCount <= 3 &&
+ haveCatalog &&
+ !tracker.hasCompletionType('database_in_catalog')
+ ) {
+ if (
+ [
+ EntityContextType.DATABASE,
+ EntityContextType.TABLE,
+ EntityContextType.TABLE_CREATE,
+ EntityContextType.VIEW,
+ EntityContextType.VIEW_CREATE
+ ].includes(contextType as EntityContextType)
+ ) {
+ result.push(...(await getDBOrSchema(languageId, words[0])));
+ tracker.markAsCompleted('database_in_catalog');
+ }
+ }
- /** 是否已经存在 catalog 补全项 */
- let existCatalogCompletions = false;
- /** 是否已经存在 database 补全项 tmpDatabase */
- let existDatabaseCompletions = false;
- /** 是否已经存在 database 补全项 */
- let existDatabaseInCatCompletions = false;
- /** 是否已经存在 table 补全项 tmpTable */
- let existTableCompletions = false;
- /** 是否已经存在 tableInDb 补全项 (cat.db.table) */
- let existTableInDbCompletions = false;
- /** 是否已经存在 view 补全项 tmpDb */
- let existViewCompletions = false;
- /** 是否已经存在 viewInDb 补全项 */
- let existViewInDbCompletions = false;
+ // Complete Table
+ if (
+ contextType === EntityContextType.TABLE &&
+ wordCount <= 1 &&
+ !tracker.hasCompletionType('table')
+ ) {
+ result.push(...(await getTables(languageId)));
+ tracker.markAsCompleted('table');
+ }
- for (let i = 0; i < syntax.length; i++) {
- const { syntaxContextType, wordRanges } = syntax[i];
+ // Complete Tables under Database
+ if (
+ contextType === EntityContextType.TABLE &&
+ wordCount >= 2 &&
+ wordCount <= 3 &&
+ !tracker.hasCompletionType('table_in_database')
+ ) {
+ result.push(...(await getTables(languageId, undefined, words[0])));
+ tracker.markAsCompleted('table_in_database');
+ }
- // e.g. words -> ['cat', '.', 'database', '.', 'table']
- const words = wordRanges.map((wr) => wr.text);
- const wordCount = words.length;
+ // Complete Tables under Catalog.Database
+ if (
+ contextType === EntityContextType.TABLE &&
+ wordCount >= 4 &&
+ wordCount <= 5 &&
+ haveCatalog &&
+ !tracker.hasCompletionType('table_in_catalog_database')
+ ) {
+ result.push(...(await getTables(languageId, words[0], words[2])));
+ tracker.markAsCompleted('table_in_catalog_database');
+ }
- if (
- syntaxContextType === EntityContextType.CATALOG ||
- syntaxContextType === EntityContextType.DATABASE_CREATE
- ) {
- if (!existCatalogCompletions && wordCount <= 1) {
- syntaxCompletionItems = syntaxCompletionItems.concat(await getCatalogs(languageId));
- existCatalogCompletions = true;
+ // Complete View
+ if (
+ contextType === EntityContextType.VIEW &&
+ wordCount <= 1 &&
+ !tracker.hasCompletionType('view')
+ ) {
+ result.push(...(await getViews(languageId)));
+ tracker.markAsCompleted('view');
+ }
+
+ // Complete Views under Database
+ if (
+ contextType === EntityContextType.VIEW &&
+ wordCount >= 2 &&
+ wordCount <= 3 &&
+ !tracker.hasCompletionType('view_in_database')
+ ) {
+ result.push(...(await getViews(languageId, undefined, words[0])));
+ tracker.markAsCompleted('view_in_database');
+ }
+
+ // Complete Views under Catalog.Database
+ if (
+ contextType === EntityContextType.VIEW &&
+ wordCount >= 4 &&
+ wordCount <= 5 &&
+ !tracker.hasCompletionType('view_in_catalog_database')
+ ) {
+ result.push(...(await getViews(languageId, words[0], words[2])));
+ tracker.markAsCompleted('view_in_catalog_database');
+ }
+
+ return result;
+};
+
+/**
+ * Parse entity text and extract different parts
+ * @param originEntityText - The origin entity text
+ * @returns Parsed entity information
+ * @example
+ * parseEntityText('catalog.database.table') => { catalog: 'catalog', schema: 'database', table: 'table', fullPath: 'catalog.database.table' }
+ * parseEntityText('schema.table') => { catalog: null, schema: 'schema', table: 'table', fullPath: 'schema.table' }
+ * parseEntityText('table') => { catalog: null, schema: null, table: 'table', fullPath: 'table' }
+ */
+const parseEntityText = (originEntityText: string) => {
+ // Use regex to split correctly, keeping backtick-wrapped parts as a whole.
+ // Match: backtick-wrapped content (including internal dots) or regular non-dot characters.
+ // '`xx.xx`' should be treated as a whole word `xx.xx`.
+ const regex = /`[^`]+`|[^.]+/g;
+ const matches = originEntityText.match(regex) || [];
+
+ const words = matches.map((word) => {
+ if (word.startsWith('`') && word.endsWith('`') && word.length >= 3) {
+ const content = word.slice(1, -1);
+ // Only remove backticks when content contains only letters, numbers, and underscores
+ if (/^[a-zA-Z0-9_]+$/.test(content)) {
+ return content;
}
}
+ return word;
+ });
- if (
- syntaxContextType === EntityContextType.DATABASE ||
- syntaxContextType === EntityContextType.TABLE_CREATE ||
- syntaxContextType === EntityContextType.VIEW_CREATE
- ) {
- if (!existCatalogCompletions && haveCatalog && wordCount <= 1) {
- syntaxCompletionItems = syntaxCompletionItems.concat(await getCatalogs(languageId));
- existCatalogCompletions = true;
+ const length = words.length;
+ if (length >= 3) {
+ // catalog.schema.table format
+ return {
+ catalog: words[0],
+ schema: words[1],
+ table: words[2],
+ fullPath: words.join('.'),
+ pureEntityText: words[2]
+ };
+ } else if (length === 2) {
+ // schema.table format
+ return {
+ catalog: null,
+ schema: words[0],
+ table: words[1],
+ fullPath: words.join('.'),
+ pureEntityText: words[1]
+ };
+ } else {
+ // table format
+ return {
+ catalog: null,
+ schema: null,
+ table: words[0],
+ fullPath: words[0],
+ pureEntityText: words[0]
+ };
+ }
+};
+
+/**
+ * Get the pure entity text from the origin entity text
+ * @param originEntityText - The origin entity text
+ * @returns The pure entity text
+ * @example
+ * getPureEntityText('catalog.database.table') => 'table'
+ * getPureEntityText('tb.id') => 'id'
+ * getPureEntityText('`a1`') => 'a1'
+ */
+const getPureEntityText = (originEntityText: string) => {
+ return parseEntityText(originEntityText).pureEntityText;
+};
+
+/**
+ * Remove backticks from text for filter matching
+ * @param text - The text that may contain backticks
+ * @returns The text without backticks
+ */
+const removeBackticks = (text: string): string => {
+ return text.replace(/`/g, '');
+};
+
+/**
+ * Check if two entity paths match, considering schema information
+ * @param createTablePath - The path from CREATE TABLE statement
+ * @param referenceTablePath - The path from table reference
+ * @returns Whether the paths match
+ */
+const isEntityPathMatch = (createTablePath: string, referenceTablePath: string): boolean => {
+ const createInfo = parseEntityText(createTablePath);
+ const refInfo = parseEntityText(referenceTablePath);
+
+ // Exact match
+ if (createInfo.fullPath === refInfo.fullPath) {
+ return true;
+ }
+
+ // If reference has no schema but table name matches
+ if (!refInfo.schema && createInfo.table === refInfo.table) {
+ return true;
+ }
+
+ // If both have schema and table, they must match exactly
+ if (createInfo.schema && refInfo.schema) {
+ return createInfo.schema === refInfo.schema && createInfo.table === refInfo.table;
+ }
+
+ return false;
+};
+
+/**
+ * Process column completions, including regular columns and table.column format
+ */
+const getColumnCompletions = async (
+ languageId: string,
+ wordRanges: WordRange[],
+ entities: EntityContext[] | null
+): Promise => {
+ if (!entities) return [];
+
+ const words = wordRanges.map((wr) => wr.text);
+ const result: ICompletionItem[] = [];
+
+ // Extract already selected columns from QUERY_RESULT to filter them out
+ const queryResultEntity = entities.find(
+ (entity) => entity.entityContextType === EntityContextType.QUERY_RESULT
+ ) as CommonEntityContext | undefined;
+
+ const selectedColumns = new Set();
+ queryResultEntity?.columns?.forEach((col) => {
+ const columnName = col[AttrName.alias]?.text || getPureEntityText(col.text);
+ if (columnName) {
+ selectedColumns.add(columnName);
+ }
+ });
+
+ // All tables defined in the context
+ const allTableDefinitionEntities =
+ (entities?.filter(
+ (entity) => entity.entityContextType === EntityContextType.TABLE_CREATE
+ ) as CommonEntityContext[]) || [];
+
+ // Source tables in the SELECT statement
+ const sourceTables =
+ (entities?.filter(
+ (entity) => entity.entityContextType === EntityContextType.TABLE && entity.isAccessible
+ ) as CommonEntityContext[]) || [];
+
+ // Find table definitions from source tables (regular CREATE TABLE with explicit columns)
+ const sourceTableDefinitionEntities = allTableDefinitionEntities.filter((createTable) =>
+ sourceTables?.some(
+ (sourceTable) =>
+ sourceTable.declareType === TableDeclareType.LITERAL &&
+ isEntityPathMatch(createTable.text, sourceTable.text)
+ )
+ );
+
+ // Find CTAS table definitions from source tables (CREATE TABLE AS SELECT)
+ const ctasTableDefinitionEntities = allTableDefinitionEntities.filter((createTable) =>
+ sourceTables?.some(
+ (sourceTable) =>
+ sourceTable.declareType === TableDeclareType.LITERAL &&
+ // Check if the CREATE TABLE has relatedEntities with QUERY_RESULT (indicates CTAS)
+ createTable.relatedEntities?.some(
+ (relatedEntity) =>
+ relatedEntity.entityContextType === EntityContextType.QUERY_RESULT
+ ) &&
+ isEntityPathMatch(createTable.text, sourceTable.text)
+ )
+ );
+
+ const derivedTableEntities =
+ sourceTables?.filter((entity) => entity.declareType === TableDeclareType.EXPRESSION) || [];
+
+ const tableNameAliasMap: Record = sourceTables.reduce(
+ (acc: Record, tb) => {
+ const alias = tb[AttrName.alias]?.text;
+ if (alias) {
+ acc[tb.text] = alias;
}
+ return acc;
+ },
+ {}
+ );
- if (!existDatabaseCompletions && wordCount <= 1) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getDBOrSchema(languageId)
- );
- existDatabaseCompletions = true;
+ // alias to full table path
+ const aliasToTableMap: Record = Object.fromEntries(
+ Object.entries(tableNameAliasMap).map(([tablePath, alias]) => [alias, tablePath])
+ );
+
+ // When not typing a dot, suggest all source tables and columns (if source tables are directly created in local context)
+ // Handle the case where input is like 't1.' -> wordRanges could be ['t1', '.'] or ['t1.']
+ const currentText = words[words.length - 1] || '';
+ const isDotContext =
+ (wordRanges.length === 2 && words[1] === '.') ||
+ (wordRanges.length === 1 && currentText.endsWith('.'));
+ const tbNameOrAliasForDotContext = isDotContext
+ ? wordRanges.length === 2
+ ? words[0]
+ : currentText.slice(0, -1)
+ : null;
+
+ if (wordRanges.length <= 1 && !currentText.endsWith('.')) {
+ const columnRepeatCountMap = new Map();
+
+ // Get columns from local tables
+ let sourceTableColumns: EnhancedCompletionItem[] = [];
+
+ sourceTables.forEach((sourceTable) => {
+ const realTablePath = sourceTable.text;
+ const displayAlias = tableNameAliasMap[sourceTable.text];
+
+ const tableColumns = [
+ ...getSpecificTableColumns(
+ sourceTableDefinitionEntities,
+ realTablePath,
+ displayAlias
+ ),
+ ...getSpecificDerivedTableColumns(derivedTableEntities, displayAlias),
+ ...getSpecificCTASTableColumns(
+ ctasTableDefinitionEntities,
+ realTablePath,
+ displayAlias
+ )
+ ];
+
+ sourceTableColumns.push(...tableColumns);
+ });
+
+ // Count duplicate column names
+ sourceTableColumns.forEach((col) => {
+ if (col._columnText) {
+ const repeatCount = columnRepeatCountMap.get(col._columnText) || 0;
+ columnRepeatCountMap.set(col._columnText, repeatCount + 1);
+ }
+ });
+
+ // If there are columns with the same name, automatically include table name in inserted text
+ sourceTableColumns = sourceTableColumns.map((column) => {
+ const columnRepeatCount = columnRepeatCountMap.get(column._columnText as string) || 0;
+ const isIncludeInMultipleTables = sourceTables.length > 1;
+ if (columnRepeatCount > 1 && isIncludeInMultipleTables) {
+ const newLabel = `${column._tableName}.${column.label}`;
+ return {
+ ...column,
+ label: newLabel,
+ filterText: removeBackticks(newLabel),
+ insertText: `${column._tableName}.${column._columnText}`
+ };
}
- if (!existDatabaseInCatCompletions && haveCatalog && wordCount >= 2 && wordCount <= 3) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getDBOrSchema(languageId, words[0])
+ return column;
+ });
+
+ result.push(...sourceTableColumns);
+
+ // Also suggest tables when inputting column
+ const tableCompletionItems =
+ sourceTables.length > 1
+ ? sourceTables.map((tb) => {
+ const tableName = tb[AttrName.alias]?.text ?? getPureEntityText(tb.text);
+ return {
+ label: tableName,
+ filterText: removeBackticks(tableName),
+ kind: languages.CompletionItemKind.Field,
+ detail:
+ tb.declareType === TableDeclareType.LITERAL
+ ? 'table'
+ : 'derived table',
+ sortText: '1' + tableName
+ };
+ })
+ : [];
+
+ result.push(...tableCompletionItems);
+ } else if (isDotContext && tbNameOrAliasForDotContext) {
+ // Table.column format completion when input is like 't1.'
+ // wordRanges could be ['t1', '.'] or ['t1.']
+ const tbNameOrAlias = tbNameOrAliasForDotContext;
+
+ let realTablePath = tbNameOrAlias;
+
+ // Check if the input is an alias and resolve to full table path
+ if (aliasToTableMap[tbNameOrAlias]) {
+ realTablePath = aliasToTableMap[tbNameOrAlias];
+ } else {
+ // Try to find matching table in source tables
+ const matchingTable = sourceTables.find((tb) => {
+ const parsedTable = parseEntityText(tb.text);
+ return (
+ parsedTable.table === tbNameOrAlias ||
+ parsedTable.fullPath === tbNameOrAlias ||
+ tb.text === tbNameOrAlias
);
- existDatabaseInCatCompletions = true;
+ });
+
+ if (matchingTable) {
+ realTablePath = matchingTable.text;
}
}
- if (syntaxContextType === EntityContextType.TABLE) {
- if (wordCount <= 1) {
- if (!existCatalogCompletions && haveCatalog) {
- const ctas = await getCatalogs(languageId);
- syntaxCompletionItems = syntaxCompletionItems.concat(ctas);
- existCatalogCompletions = true;
- }
-
- if (!existDatabaseCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getDBOrSchema(languageId)
- );
- existDatabaseCompletions = true;
- }
-
- if (!existTableCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getTables(languageId)
- );
- existTableCompletions = true;
- }
- } else if (wordCount >= 2 && wordCount <= 3) {
- if (!existDatabaseInCatCompletions && haveCatalog) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getDBOrSchema(languageId, words[0])
- );
- existDatabaseInCatCompletions = true;
- }
-
- if (!existTableInDbCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getTables(languageId, undefined, words[0])
- );
- existTableInDbCompletions = true;
- }
- } else if (wordCount >= 4 && wordCount <= 5) {
- if (!existTableInDbCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getTables(languageId, words[0], words[2])
- );
- existTableInDbCompletions = true;
- }
+ // Find columns in local table definitions
+ const displayAlias = aliasToTableMap[tbNameOrAlias] ? tbNameOrAlias : undefined;
+
+ const localTableColumns = [
+ ...getSpecificTableColumns(sourceTableDefinitionEntities, realTablePath, displayAlias),
+ ...getSpecificDerivedTableColumns(derivedTableEntities, displayAlias),
+ ...getSpecificCTASTableColumns(ctasTableDefinitionEntities, realTablePath, displayAlias)
+ ];
+
+ result.push(...localTableColumns);
+
+ // If no local table columns found, try to fetch from cloud
+ if (localTableColumns.length === 0) {
+ const isLocallyCreatedTable = allTableDefinitionEntities.some((createTable) => {
+ return isEntityPathMatch(createTable.text, realTablePath);
+ });
+
+ const isLiteralTable = sourceTables.some(
+ (tb) =>
+ tb.declareType === TableDeclareType.LITERAL &&
+ (tb.text === realTablePath || isEntityPathMatch(tb.text, realTablePath))
+ );
+
+ if (!isLocallyCreatedTable && isLiteralTable) {
+ const remoteColumns = await getColumns(languageId, realTablePath);
+ result.push(...remoteColumns);
}
}
+ }
- if (syntaxContextType === EntityContextType.VIEW) {
- if (wordCount <= 1) {
- if (!existCatalogCompletions && haveCatalog) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getCatalogs(languageId)
- );
- existCatalogCompletions = true;
- }
-
- if (!existDatabaseCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getDBOrSchema(languageId)
- );
- existDatabaseCompletions = true;
- }
-
- if (!existViewCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getViews(languageId)
- );
- existViewCompletions = true;
- }
- } else if (wordCount >= 2 && wordCount <= 3) {
- if (!existDatabaseInCatCompletions && haveCatalog) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getDBOrSchema(languageId, words[0])
- );
- existDatabaseInCatCompletions = true;
- }
-
- if (!existViewInDbCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getViews(languageId, undefined, words[0])
- );
- existViewInDbCompletions = true;
- }
- } else if (wordCount >= 4 && wordCount <= 5) {
- if (!existViewInDbCompletions) {
- syntaxCompletionItems = syntaxCompletionItems.concat(
- await getViews(languageId, words[0], words[2])
- );
- existViewInDbCompletions = true;
- }
- }
+ // Filter out already selected columns in QUERY_RESULT
+ if (selectedColumns.size > 0) {
+ return result.filter((item) => {
+ const columnName =
+ (item as EnhancedCompletionItem)._columnText ||
+ (typeof item.label === 'string'
+ ? item.label
+ : (item.label as EnhancedCompletionItem).label);
+ return !selectedColumns.has(columnName);
+ });
+ }
+
+ return result;
+};
+
+/**
+ * Get columns from a specific table
+ */
+const getSpecificTableColumns = (
+ sourceTableDefinitionEntities: CommonEntityContext[],
+ realTablePath: string,
+ displayAlias?: string
+): any[] => {
+ return sourceTableDefinitionEntities
+ .filter((tb) => {
+ return (
+ tb.text === realTablePath ||
+ isEntityPathMatch(tb.text, realTablePath) ||
+ getPureEntityText(tb.text) === getPureEntityText(realTablePath)
+ );
+ })
+ .map((tb) => {
+ const tableName = displayAlias || getPureEntityText(tb.text);
+ return (
+ tb.columns?.map((column) => {
+ const columnName =
+ column[AttrName.alias]?.text || getPureEntityText(column.text);
+ if (!columnName) return null;
+ const label =
+ columnName +
+ (column[AttrName.colType]?.text
+ ? `(${column[AttrName.colType].text})`
+ : '');
+ return {
+ label,
+ filterText: removeBackticks(label),
+ insertText: columnName,
+ kind: languages.CompletionItemKind.EnumMember,
+ detail: `\`${tableName}\`'s column`,
+ sortText: '0' + tableName + columnName,
+ _columnText: columnName,
+ _tableName: tableName
+ };
+ }) || []
+ );
+ })
+ .flat()
+ .filter(Boolean);
+};
+
+/**
+ * Get columns from a specific derived table (subquery)
+ */
+const getSpecificDerivedTableColumns = (
+ derivedTableEntities: CommonEntityContext[],
+ displayAlias?: string
+): any[] => {
+ return derivedTableEntities
+ .filter((tb) => {
+ return displayAlias ? tb[AttrName.alias]?.text === displayAlias : false;
+ })
+ .map((tb) => {
+ const derivedTableQueryResult = tb.relatedEntities?.find(
+ (entity) => entity.entityContextType === EntityContextType.QUERY_RESULT
+ ) as CommonEntityContext | undefined;
+
+ const tableName =
+ displayAlias || tb[AttrName.alias]?.text || getPureEntityText(tb.text);
+
+ return (
+ derivedTableQueryResult?.columns
+ ?.filter((column) => column.declareType !== ColumnDeclareType.ALL)
+ .map((column) => {
+ const columnName =
+ column[AttrName.alias]?.text || getPureEntityText(column.text);
+ if (!columnName) return null;
+ return {
+ label: columnName,
+ filterText: removeBackticks(columnName),
+ insertText: columnName,
+ kind: languages.CompletionItemKind.EnumMember,
+ detail: `\`${tableName}\`'s column`,
+ sortText: '0' + tableName + columnName,
+ _columnText: columnName,
+ _tableName: tableName
+ };
+ }) || []
+ );
+ })
+ .flat()
+ .filter(Boolean);
+};
+
+/**
+ * Get columns from a specific CTAS table
+ */
+const getSpecificCTASTableColumns = (
+ ctasTableEntities: CommonEntityContext[],
+ realTablePath: string,
+ displayAlias?: string
+): any[] => {
+ return ctasTableEntities
+ .filter((tb) => {
+ return (
+ tb.text === realTablePath ||
+ isEntityPathMatch(tb.text, realTablePath) ||
+ getPureEntityText(tb.text) === getPureEntityText(realTablePath)
+ );
+ })
+ .map((tb) => {
+ const ctasQueryResult = tb.relatedEntities?.find(
+ (entity) => entity.entityContextType === EntityContextType.QUERY_RESULT
+ ) as CommonEntityContext | undefined;
+
+ const tableName = displayAlias || getPureEntityText(tb.text);
+
+ return (
+ ctasQueryResult?.columns
+ ?.filter((column) => column.declareType !== ColumnDeclareType.ALL)
+ .map((column) => {
+ const columnName =
+ column[AttrName.alias]?.text || getPureEntityText(column.text);
+ if (!columnName) return null;
+ const label =
+ columnName +
+ (column[AttrName.colType]?.text
+ ? `(${column[AttrName.colType].text})`
+ : '');
+ return {
+ label,
+ filterText: removeBackticks(label),
+ insertText: columnName,
+ kind: languages.CompletionItemKind.EnumMember,
+ detail: `\`${tableName}\`'s column`,
+ sortText: '0' + tableName + columnName,
+ _columnText: columnName,
+ _tableName: tableName
+ };
+ }) || []
+ );
+ })
+ .flat()
+ .filter(Boolean);
+};
+
+const getSyntaxCompletionItems = async (
+ languageId: string,
+ syntax: Suggestions['syntax'],
+ entities: EntityContext[] | null
+): Promise => {
+ const tracker = new CompletionTracker();
+ let syntaxCompletionItems: ICompletionItem[] = [];
+
+ for (let i = 0; i < syntax.length; i++) {
+ const { syntaxContextType, wordRanges } = syntax[i];
+ const words = wordRanges.map((wr) => wr.text);
+
+ // If already typed a space, we've left that context
+ if (isWordRangesEndWithWhiteSpace(wordRanges)) continue;
+
+ if (
+ [
+ EntityContextType.CATALOG,
+ EntityContextType.DATABASE,
+ EntityContextType.DATABASE_CREATE,
+ EntityContextType.TABLE,
+ EntityContextType.TABLE_CREATE,
+ EntityContextType.VIEW,
+ EntityContextType.VIEW_CREATE
+ ].includes(syntaxContextType as EntityContextType) &&
+ !tracker.hasCompletionType('db_objects')
+ ) {
+ // Get database object completions (catalog, database, table, etc.)
+ const dbObjectCompletions = await getDatabaseObjectCompletions(
+ tracker,
+ languageId,
+ syntaxContextType,
+ words
+ );
+
+ syntaxCompletionItems = syntaxCompletionItems.concat(dbObjectCompletions);
+ tracker.markAsCompleted('db_objects');
+ }
+
+ // Add table completions from table entities created in context
+ if (
+ syntaxContextType === EntityContextType.TABLE &&
+ words.length <= 1 &&
+ !tracker.hasCompletionType('created_tables')
+ ) {
+ const createTables =
+ entities
+ ?.filter(
+ (entity) => entity.entityContextType === EntityContextType.TABLE_CREATE
+ )
+ .map((tb) => {
+ const tableName = getPureEntityText(tb.text);
+ return {
+ label: tableName,
+ filterText: removeBackticks(tableName),
+ kind: languages.CompletionItemKind.Field,
+ detail: 'table',
+ sortText: '1' + tableName
+ };
+ }) || [];
+
+ syntaxCompletionItems = syntaxCompletionItems.concat(createTables);
+ tracker.markAsCompleted('created_tables');
+ }
+
+ // Process column completions
+ if (
+ syntaxContextType === EntityContextType.COLUMN &&
+ !tracker.hasCompletionType('columns')
+ ) {
+ const columnCompletions = await getColumnCompletions(languageId, wordRanges, entities);
+ syntaxCompletionItems = syntaxCompletionItems.concat(columnCompletions);
+ tracker.markAsCompleted('columns');
}
}
+ return syntaxCompletionItems;
+};
+
+export const completionService: CompletionService = async function (
+ model,
+ _position,
+ _completionContext,
+ suggestions,
+ entities,
+ snippets
+) {
+ if (!suggestions) {
+ return Promise.resolve([]);
+ }
+ const languageId = model.getLanguageId();
+
+ const { keywords, syntax } = suggestions;
+
+ const keywordsCompletionItems: ICompletionItem[] = keywords.map((kw) => ({
+ label: kw,
+ kind: languages.CompletionItemKind.Keyword,
+ detail: 'keyword',
+ sortText: '2' + kw
+ }));
+
+ const syntaxCompletionItems = await getSyntaxCompletionItems(languageId, syntax, entities);
+
const snippetCompletionItems: ICompletionItem[] =
snippets?.map((item) => ({
label: item.label || item.prefix,
diff --git a/website/src/languages/helpers/dbMetaProvider.ts b/website/src/languages/helpers/dbMetaProvider.ts
index 881dfd23..fe06b4fb 100644
--- a/website/src/languages/helpers/dbMetaProvider.ts
+++ b/website/src/languages/helpers/dbMetaProvider.ts
@@ -1,4 +1,5 @@
import { languages } from 'monaco-editor/esm/vs/editor/editor.api';
+import type { ICompletionItem } from 'monaco-sql-languages/esm/monaco.contribution';
const catalogList = ['mock_catalog_1', 'mock_catalog_2', 'mock_catalog_3'];
const schemaList = ['mock_schema_1', 'mock_schema_2', 'mock_schema_3'];
@@ -21,78 +22,136 @@ const prefixLabel = (languageId: string, text: string) => {
};
/**
- * 获取所有的 catalog
+ * Remove backticks from text for filter matching
+ */
+const removeBackticks = (text: string): string => {
+ return text.replace(/`/g, '');
+};
+
+/**
+ * Get all catalogs
*/
export function getCatalogs(languageId: string) {
- const catCompletions = catalogList.map((cat) => ({
- label: prefixLabel(languageId, cat),
- kind: languages.CompletionItemKind.Field,
- detail: 'catalog',
- sortText: '1' + prefixLabel(languageId, cat)
- }));
+ const catCompletions = catalogList.map((cat) => {
+ const label = prefixLabel(languageId, cat);
+ return {
+ label,
+ filterText: removeBackticks(label),
+ kind: languages.CompletionItemKind.Field,
+ detail: 'Remote: catalog',
+ sortText: '1' + label
+ };
+ });
return Promise.resolve(catCompletions);
}
/**
- * 根据catalog 获取 database
+ * Get databases based on catalog
*/
export function getDataBases(languageId: string, catalog?: string) {
const databases = catalog ? databaseList : tmpDatabaseList;
- const databaseCompletions = databases.map((db) => ({
- label: prefixLabel(languageId, db),
- kind: languages.CompletionItemKind.Field,
- detail: 'database',
- sortText: '1' + prefixLabel(languageId, db)
- }));
+ const databaseCompletions = databases.map((db) => {
+ const label = prefixLabel(languageId, db);
+ return {
+ label,
+ filterText: removeBackticks(label),
+ kind: languages.CompletionItemKind.Field,
+ detail: 'Remote: database',
+ sortText: '1' + label
+ };
+ });
return Promise.resolve(databaseCompletions);
}
/**
- * 根据catalog 获取 schema
+ * Get schemas based on catalog
*/
export function getSchemas(languageId: string, catalog?: string) {
const schemas = catalog ? schemaList : tmpSchemaList;
- const schemaCompletions = schemas.map((sc) => ({
- label: prefixLabel(languageId, sc),
- kind: languages.CompletionItemKind.Field,
- detail: 'schema',
- sortText: '1' + prefixLabel(languageId, sc)
- }));
+ const schemaCompletions = schemas.map((sc) => {
+ const label = prefixLabel(languageId, sc);
+ return {
+ label,
+ filterText: removeBackticks(label),
+ kind: languages.CompletionItemKind.Field,
+ detail: 'Remote: schema',
+ sortText: '1' + label
+ };
+ });
return Promise.resolve(schemaCompletions);
}
/**
- * 根据 catalog 和 database 获取 table
+ * Get tables based on catalog and database
*/
export function getTables(languageId: string, catalog?: string, database?: string) {
const tables = catalog && database ? tableList : tmpTableList;
- const tableCompletions = tables.map((tb) => ({
- label: prefixLabel(languageId, tb),
- kind: languages.CompletionItemKind.Field,
- detail: 'table',
- sortText: '1' + prefixLabel(languageId, tb)
- }));
+ const tableCompletions = tables.map((tb) => {
+ const label = prefixLabel(languageId, tb);
+ return {
+ label,
+ filterText: removeBackticks(label),
+ kind: languages.CompletionItemKind.Field,
+ detail: 'Remote: table',
+ sortText: '1' + label
+ };
+ });
return Promise.resolve(tableCompletions);
}
/**
- * 根据 catalog 和 database 获取 view
+ * Get views based on catalog and database
*/
export function getViews(languageId: string, catalog?: string, database?: string) {
const views = catalog && database ? viewList : tmpViewList;
- const viewCompletions = views.map((v) => ({
- label: prefixLabel(languageId, v),
- kind: languages.CompletionItemKind.Field,
- detail: 'view',
- sortText: '1' + prefixLabel(languageId, v)
- }));
+ const viewCompletions = views.map((v) => {
+ const label = prefixLabel(languageId, v);
+ return {
+ label,
+ filterText: removeBackticks(label),
+ kind: languages.CompletionItemKind.Field,
+ detail: 'Remote: view',
+ sortText: '1' + label
+ };
+ });
return Promise.resolve(viewCompletions);
}
+
+/**
+ * Get column information for a specific table
+ * @param languageId Language ID
+ * @param tableName Table name
+ * @returns Column completion items
+ */
+export function getColumns(languageId: string, tableName: string): Promise {
+ // Mock column data, should fetch from cloud in real environment
+ const mockColumns = [
+ { name: 'id', type: 'INT' },
+ { name: 'name', type: 'VARCHAR' },
+ { name: 'age', type: 'INT' },
+ { name: 'created_at', type: 'TIMESTAMP' },
+ { name: 'updated_at', type: 'TIMESTAMP' }
+ ];
+
+ const columnCompletions = mockColumns.map((col) => {
+ const label = `${col.name}(${col.type})`;
+ return {
+ label,
+ filterText: removeBackticks(label),
+ insertText: col.name,
+ kind: languages.CompletionItemKind.EnumMember,
+ detail: `Remote: \`${tableName}\`'s column`,
+ sortText: '0' + tableName + col.name
+ };
+ });
+
+ return Promise.resolve(columnCompletions);
+}
diff --git a/website/src/languages/index.ts b/website/src/languages/index.ts
index fc52add3..685edca9 100644
--- a/website/src/languages/index.ts
+++ b/website/src/languages/index.ts
@@ -137,3 +137,15 @@ setupLanguageFeatures(LanguageIdEnum.IMPALA, {
hover: true,
preprocessCode
});
+
+setupLanguageFeatures(LanguageIdEnum.GENERIC, {
+ completionItems: {
+ enable: true,
+ completionService
+ },
+ diagnostics: false,
+ references: true,
+ definitions: true,
+ hover: true,
+ preprocessCode
+});
diff --git a/website/src/languages/languageWorker.ts b/website/src/languages/languageWorker.ts
index 53bb10bf..688550bc 100644
--- a/website/src/languages/languageWorker.ts
+++ b/website/src/languages/languageWorker.ts
@@ -9,6 +9,7 @@ import PGSQLWorker from 'monaco-sql-languages/esm/languages/pgsql/pgsql.worker?w
import MySQLWorker from 'monaco-sql-languages/esm/languages/mysql/mysql.worker?worker';
import TrinoSQLWorker from 'monaco-sql-languages/esm/languages/trino/trino.worker?worker';
import ImpalaSQLWorker from 'monaco-sql-languages/esm/languages/impala/impala.worker?worker';
+import GenericSQLWorker from 'monaco-sql-languages/esm/languages/generic/generic.worker?worker';
(globalThis as any).MonacoEnvironment = {
getWorker(_: any, label: string) {
@@ -33,6 +34,9 @@ import ImpalaSQLWorker from 'monaco-sql-languages/esm/languages/impala/impala.wo
if (label === LanguageIdEnum.IMPALA) {
return new ImpalaSQLWorker();
}
+ if (label === LanguageIdEnum.GENERIC) {
+ return new GenericSQLWorker();
+ }
return new EditorWorker();
}
};