diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cf4c20..c159b3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.15.1](https://github.com/DTStack/monaco-sql-languages/compare/v0.15.0...v0.15.1) (2025-06-06) + + +### Bug Fixes + +* [#166](https://github.com/DTStack/monaco-sql-languages/issues/166) upgrade dt-sql-parser version ([#183](https://github.com/DTStack/monaco-sql-languages/issues/183)) ([fea3c3c](https://github.com/DTStack/monaco-sql-languages/commit/fea3c3c79682eed760dd7c00a4a5b830a60d9565)) + +## [0.15.0](https://github.com/DTStack/monaco-sql-languages/compare/v0.14.0...v0.15.0) (2025-05-16) + + +### Features + +* **sqlParser:** upgrade dt-sql-parser@4.3.0 ([58a4304](https://github.com/DTStack/monaco-sql-languages/commit/58a430403d2f3a97e91c500e14d9fd3f4be7427c)) + +## [0.14.0](https://github.com/DTStack/monaco-sql-languages/compare/v0.13.1...v0.14.0) (2025-05-09) + + +### Features + +* **dependency:** upgrade dt-sql-parser@4.2.0 ([6d0d6c2](https://github.com/DTStack/monaco-sql-languages/commit/6d0d6c26c80a31325dd8c400b43ab331dab9ac95)) +* support built-in sql snippets ([#154](https://github.com/DTStack/monaco-sql-languages/issues/154)) ([a5d68bb](https://github.com/DTStack/monaco-sql-languages/commit/a5d68bbb32d219715d4caf6abcb9b98cc754b861)) + + +### Bug Fixes + +* **scripts:** resolve configuration conflicts ([76d728c](https://github.com/DTStack/monaco-sql-languages/commit/76d728ce75c75dea0e0eaf69b2b9bba44c644952)) + +### [0.13.1](https://github.com/DTStack/monaco-sql-languages/compare/v0.13.0...v0.13.1) (2025-02-17) + + +### Features + +* [#167](https://github.com/DTStack/monaco-sql-languages/issues/167) support dt highlight style ([#168](https://github.com/DTStack/monaco-sql-languages/issues/168)) ([7beb537](https://github.com/DTStack/monaco-sql-languages/commit/7beb5373a40317a7ecd18a9cc66f5f133e27345b)) +* update dt-sql-parser's version ([#174](https://github.com/DTStack/monaco-sql-languages/issues/174)) ([159df20](https://github.com/DTStack/monaco-sql-languages/commit/159df20946198d549705afb61cbbdc7ef72e7d88)) + ## [0.13.0](https://github.com/DTStack/monaco-sql-languages/compare/v0.12.1...v0.13.0) (2025-02-13) diff --git a/package.json b/package.json index 4ed60883..82f749ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monaco-sql-languages", - "version": "0.13.0", + "version": "0.15.1", "description": "SQL languages for the Monaco Editor, based on monaco-languages.", "scripts": { "prepublishOnly": "npm run build", @@ -66,9 +66,6 @@ "simple-git-hooks": { "pre-commit": "npx pretty-quick --staged" }, - "dependencies": { - "dt-sql-parser": "4.2.0" - }, "peerDependencies": { "monaco-editor": ">=0.31.0" }, @@ -85,5 +82,8 @@ "*": [ "prettier --write --ignore-unknown" ] + }, + "dependencies": { + "dt-sql-parser": "4.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 986d2d9f..c8202e3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: dt-sql-parser: - specifier: 4.2.0 - version: 4.2.0(antlr4ng-cli@1.0.7) + specifier: 4.3.1 + version: 4.3.1(antlr4ng-cli@1.0.7) devDependencies: '@commitlint/cli': specifier: ^17.7.2 @@ -715,8 +715,8 @@ packages: resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} engines: {node: '>=6'} - dt-sql-parser@4.2.0: - resolution: {integrity: sha512-tsTHGNGIeTd7xACh8FNzSCaQHYyITJeSTMZPxGFkROiccPxj82uEqOeUgJ+bYof9hEacf4h61LsiMyxPy+tl7g==} + dt-sql-parser@4.3.1: + resolution: {integrity: sha512-WlFB9of+ChwWtc5M222jHGIpzqHx51szLe/11GAwwbA+4hRaVkMpWMf2bbYj4i855edSoTQ52zyLJVOpe+4OVg==} engines: {node: '>=18'} email-addresses@3.1.0: @@ -1497,7 +1497,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) querystringify@2.2.0: @@ -2062,7 +2061,7 @@ snapshots: '@types/node': 20.5.1 chalk: 4.1.2 cosmiconfig: 8.3.6(typescript@5.5.4) - cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.4))(typescript@5.5.4) + cosmiconfig-typescript-loader: 4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4))(typescript@5.5.4) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -2578,7 +2577,7 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.5.1)(typescript@5.5.4))(typescript@5.5.4): + cosmiconfig-typescript-loader@4.4.0(@types/node@20.5.1)(cosmiconfig@8.3.6(typescript@5.5.4))(ts-node@10.9.2(@types/node@20.14.14)(typescript@5.5.4))(typescript@5.5.4): dependencies: '@types/node': 20.5.1 cosmiconfig: 8.3.6(typescript@5.5.4) @@ -2704,7 +2703,7 @@ snapshots: find-up: 3.0.0 minimatch: 3.1.2 - dt-sql-parser@4.2.0(antlr4ng-cli@1.0.7): + dt-sql-parser@4.3.1(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/languages/flink/flink.ts b/src/languages/flink/flink.ts index c3d0b5f2..dec89534 100644 --- a/src/languages/flink/flink.ts +++ b/src/languages/flink/flink.ts @@ -489,13 +489,14 @@ export const language = { { include: '@comments' }, { include: '@whitespace' }, { include: '@pseudoColumns' }, + { include: '@customParams' }, { include: '@numbers' }, { include: '@strings' }, { include: '@complexIdentifiers' }, { include: '@scopes' }, { include: '@complexDataTypes' }, { include: '@complexFunctions' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@#$]+/, @@ -535,6 +536,10 @@ export const language = { } ] ], + customParams: [ + [/\${[A-Za-z0-9._-]*}/, TokenClassConsts.VARIABLE], + [/\@\@{[A-Za-z0-9._-]*}/, TokenClassConsts.VARIABLE] + ], numbers: [ [/0[xX][0-9a-fA-F]*/, TokenClassConsts.NUMBER_HEX], [/[$][+-]*\d*(\.\d*)?/, TokenClassConsts.NUMBER], diff --git a/src/languages/hive/hive.ts b/src/languages/hive/hive.ts index 9f18e6a3..151a1bfd 100644 --- a/src/languages/hive/hive.ts +++ b/src/languages/hive/hive.ts @@ -515,7 +515,7 @@ export const language = { { include: '@complexIdentifiers' }, { include: '@scopes' }, { include: '@complexDataTypes' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@#$]+/, diff --git a/src/languages/impala/impala.ts b/src/languages/impala/impala.ts index 92b71bd2..0991684a 100644 --- a/src/languages/impala/impala.ts +++ b/src/languages/impala/impala.ts @@ -474,7 +474,7 @@ export const language = { { include: '@scopes' }, { include: '@complexDataTypes' }, { include: '@complexOperators' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@#$]+/, diff --git a/src/languages/mysql/mysql.ts b/src/languages/mysql/mysql.ts index 4545430d..616438c9 100644 --- a/src/languages/mysql/mysql.ts +++ b/src/languages/mysql/mysql.ts @@ -911,7 +911,7 @@ export const language = { { include: '@complexIdentifiers' }, { include: '@scopes' }, { include: '@complexOperators' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@]+/, diff --git a/src/languages/pgsql/pgsql.ts b/src/languages/pgsql/pgsql.ts index acfb0d69..7c49cd23 100644 --- a/src/languages/pgsql/pgsql.ts +++ b/src/languages/pgsql/pgsql.ts @@ -980,7 +980,7 @@ export const language = { { include: '@complexIdentifiers' }, { include: '@scopes' }, { include: '@complexDataTypes' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@#$]+/, diff --git a/src/languages/spark/spark.ts b/src/languages/spark/spark.ts index 3d5689df..b5440e90 100644 --- a/src/languages/spark/spark.ts +++ b/src/languages/spark/spark.ts @@ -699,7 +699,7 @@ export const language = { { include: '@complexIdentifiers' }, { include: '@scopes' }, { include: '@complexDataTypes' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@#$]+/, diff --git a/src/languages/trino/trino.ts b/src/languages/trino/trino.ts index 93c9b936..3d90a391 100644 --- a/src/languages/trino/trino.ts +++ b/src/languages/trino/trino.ts @@ -636,7 +636,7 @@ export const language = { { include: '@complexIdentifiers' }, { include: '@scopes' }, { include: '@complexDataTypes' }, - [/[;,.]/, TokenClassConsts.DELIMITER], + [/[:;,.]/, TokenClassConsts.DELIMITER], [/[\(\)\[\]\{\}]/, '@brackets'], [ /[\w@$-]+/, // https://trino.io/docs/current/language/reserved.html#language-identifiers diff --git a/src/theme/vs-plus/light.ts b/src/theme/vs-plus/light.ts index 78539917..1da1bce6 100644 --- a/src/theme/vs-plus/light.ts +++ b/src/theme/vs-plus/light.ts @@ -8,31 +8,31 @@ export const lightThemeData: editor.IStandaloneThemeData = { base: 'vs', inherit: true, rules: [ - { token: postfixTokenClass(TokenClassConsts.BINARY), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.BINARY_ESCAPE), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.COMMENT), foreground: '008000' }, - { token: postfixTokenClass(TokenClassConsts.COMMENT_QUOTE), foreground: '008000' }, - { token: postfixTokenClass(TokenClassConsts.DELIMITER), foreground: '000000' }, - { token: postfixTokenClass(TokenClassConsts.DELIMITER_CURLY), foreground: '319331' }, - { token: postfixTokenClass(TokenClassConsts.DELIMITER_PAREN), foreground: '0431fa' }, - { token: postfixTokenClass(TokenClassConsts.DELIMITER_SQUARE), foreground: '0431fa' }, - { token: postfixTokenClass(TokenClassConsts.IDENTIFIER), foreground: '001080' }, - { token: postfixTokenClass(TokenClassConsts.IDENTIFIER_QUOTE), foreground: '001080' }, - { token: postfixTokenClass(TokenClassConsts.KEYWORD), foreground: '0000ff' }, - { token: postfixTokenClass(TokenClassConsts.KEYWORD_SCOPE), foreground: 'af00db' }, - { token: postfixTokenClass(TokenClassConsts.NUMBER), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.NUMBER_FLOAT), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.NUMBER_BINARY), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.NUMBER_OCTAL), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.NUMBER_HEX), foreground: '098658' }, - { token: postfixTokenClass(TokenClassConsts.OPERATOR), foreground: '000000' }, - { token: postfixTokenClass(TokenClassConsts.OPERATOR_KEYWORD), foreground: '0000ff' }, - { token: postfixTokenClass(TokenClassConsts.OPERATOR_SYMBOL), foreground: '000000' }, - { token: postfixTokenClass(TokenClassConsts.PREDEFINED), foreground: '795e26' }, - { token: postfixTokenClass(TokenClassConsts.STRING), foreground: 'a31515' }, - { token: postfixTokenClass(TokenClassConsts.STRING_ESCAPE), foreground: 'a31515' }, - { token: postfixTokenClass(TokenClassConsts.TYPE), foreground: '267f99' }, - { token: postfixTokenClass(TokenClassConsts.VARIABLE), foreground: '4fc1ff' } + { token: postfixTokenClass(TokenClassConsts.BINARY), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.BINARY_ESCAPE), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.NUMBER), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.NUMBER_FLOAT), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.NUMBER_BINARY), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.NUMBER_OCTAL), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.NUMBER_HEX), foreground: '45AB5A' }, + { token: postfixTokenClass(TokenClassConsts.COMMENT), foreground: 'B1B4C5' }, + { token: postfixTokenClass(TokenClassConsts.COMMENT_QUOTE), foreground: 'B1B4C5' }, + { token: postfixTokenClass(TokenClassConsts.DELIMITER), foreground: '7D98B1' }, + { token: postfixTokenClass(TokenClassConsts.OPERATOR), foreground: '7D98B1' }, + { token: postfixTokenClass(TokenClassConsts.OPERATOR_SYMBOL), foreground: '7D98B1' }, + { token: postfixTokenClass(TokenClassConsts.DELIMITER_CURLY), foreground: 'B1BB86' }, + { token: postfixTokenClass(TokenClassConsts.DELIMITER_PAREN), foreground: 'B1BB86' }, + { token: postfixTokenClass(TokenClassConsts.DELIMITER_SQUARE), foreground: 'B1BB86' }, + { token: postfixTokenClass(TokenClassConsts.IDENTIFIER), foreground: '201A1A' }, + { token: postfixTokenClass(TokenClassConsts.IDENTIFIER_QUOTE), foreground: '201A1A' }, + { token: postfixTokenClass(TokenClassConsts.KEYWORD), foreground: '3300FF' }, + { token: postfixTokenClass(TokenClassConsts.OPERATOR_KEYWORD), foreground: '3300FF' }, + { token: postfixTokenClass(TokenClassConsts.KEYWORD_SCOPE), foreground: 'E221DA' }, + { token: postfixTokenClass(TokenClassConsts.PREDEFINED), foreground: 'C3771C' }, + { token: postfixTokenClass(TokenClassConsts.STRING), foreground: 'BC1313' }, + { token: postfixTokenClass(TokenClassConsts.STRING_ESCAPE), foreground: 'BC1313' }, + { token: postfixTokenClass(TokenClassConsts.TYPE), foreground: '256FC6' }, + { token: postfixTokenClass(TokenClassConsts.VARIABLE), foreground: '00AD84' } ], colors: {} }; diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index c060536c..35da2e43 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -1,8 +1,33 @@ 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 { + CommonEntityContext, + CompletionService, + ICompletionItem, + Suggestions, + WordRange +} from 'monaco-sql-languages/esm/languageService'; +import { EntityContextType, StmtContextType } from 'monaco-sql-languages/esm/main'; -import { getCatalogs, getDataBases, getSchemas, getTables, getViews } from './dbMetaProvider'; +import { + getCatalogs, + getDataBases, + getSchemas, + getTables, + getViews, + getColumns +} from './dbMetaProvider'; +import { + AttrName, + ColumnDeclareType, + EntityContext, + TableDeclareType +} from 'dt-sql-parser/dist/parser/common/entityCollector'; + +// 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,179 +37,701 @@ 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[] = []; + + // 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) + if (wordRanges.length <= 1) { + 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 (wordRanges.length === 2 && words[1] === '.') { + // Table.column format completion + const tbNameOrAlias = words[0]; + + 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 (handles partial schema references) + const matchingTable = sourceTables.find((tb) => { + const parsedTable = parseEntityText(tb.text); + // Check if input matches table name or schema.table pattern + 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) { + // Check if this table is locally created + 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)) + ); + + // Only fetch from remote if table is not locally created + 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; - } - } + return result; +}; + +/** + * Get columns from a specific table + */ +const getSpecificTableColumns = ( + sourceTableDefinitionEntities: CommonEntityContext[], + realTablePath: string, + displayAlias?: string +): ICompletionItem[] => { + 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); + 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(); +}; + +/** + * Get columns from a specific derived table (subquery) + */ +const getSpecificDerivedTableColumns = ( + derivedTableEntities: CommonEntityContext[], + displayAlias?: string +): ICompletionItem[] => { + 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); + 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(); +}; + +/** + * Get columns from a specific CTAS table + */ +const getSpecificCTASTableColumns = ( + ctasTableEntities: CommonEntityContext[], + realTablePath: string, + displayAlias?: string +): ICompletionItem[] => { + 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); + 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(); +}; + +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; + console.log('syntax', syntax); + console.log('entities', entities); + + 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) => ({ diff --git a/website/src/languages/helpers/dbMetaProvider.ts b/website/src/languages/helpers/dbMetaProvider.ts index 881dfd23..9fd8b599 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 { ICompletionItem } from 'monaco-sql-languages/esm/languageService'; 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); +}