From 7beb5373a40317a7ecd18a9cc66f5f133e27345b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=90=89=E6=98=93?= Date: Fri, 14 Feb 2025 16:14:24 +0800 Subject: [PATCH 01/14] feat: #167 support dt highlight style (#168) --- package.json | 2 +- pnpm-lock.yaml | 10 +++---- src/languages/flink/flink.ts | 7 ++++- src/languages/hive/hive.ts | 2 +- src/languages/impala/impala.ts | 2 +- src/languages/mysql/mysql.ts | 2 +- src/languages/pgsql/pgsql.ts | 2 +- src/languages/spark/spark.ts | 2 +- src/languages/trino/trino.ts | 2 +- src/theme/vs-plus/light.ts | 50 +++++++++++++++++----------------- 10 files changed, 43 insertions(+), 38 deletions(-) diff --git a/package.json b/package.json index dc75bc76..b83e1d9a 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "pre-commit": "npx pretty-quick --staged" }, "dependencies": { - "dt-sql-parser": "4.1.0-beta.4" + "dt-sql-parser": "4.1.0" }, "peerDependencies": { "monaco-editor": ">=0.31.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1c3b1e83..2e5fad93 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: dt-sql-parser: - specifier: 4.1.0-beta.4 - version: 4.1.0-beta.4(antlr4ng-cli@1.0.7) + specifier: 4.1.0 + version: 4.1.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.1.0-beta.4: - resolution: {integrity: sha512-L+Qsw+lv7enkMuhy0XXOm7H63gaajwX7X0RUGCNU8h5xw9Pj5DEWvLcKTS0R+YmO4FzVXOpEzH9e1KkqQaKFaQ==} + dt-sql-parser@4.1.0: + resolution: {integrity: sha512-0DJI0jXSd53S5oVyG9u3bm8rZ1+lmZ9mhLQwxpK6Po6tv3833k8IqobaTEPwN62xJx+y/wGlOjiqorRXEMDkJQ==} engines: {node: '>=18'} email-addresses@3.1.0: @@ -2703,7 +2703,7 @@ snapshots: find-up: 3.0.0 minimatch: 3.1.2 - dt-sql-parser@4.1.0-beta.4(antlr4ng-cli@1.0.7): + dt-sql-parser@4.1.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/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: {} }; From 159df20946198d549705afb61cbbdc7ef72e7d88 Mon Sep 17 00:00:00 2001 From: XCynthia <942884029@qq.com> Date: Mon, 17 Feb 2025 15:47:57 +0800 Subject: [PATCH 02/14] feat: update dt-sql-parser's version (#174) Co-authored-by: zhaoge <> --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index b83e1d9a..8b0fa3a2 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "pre-commit": "npx pretty-quick --staged" }, "dependencies": { - "dt-sql-parser": "4.1.0" + "dt-sql-parser": "4.1.1" }, "peerDependencies": { "monaco-editor": ">=0.31.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2e5fad93..247ac3be 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: dt-sql-parser: - specifier: 4.1.0 - version: 4.1.0(antlr4ng-cli@1.0.7) + specifier: 4.1.1 + version: 4.1.1(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.1.0: - resolution: {integrity: sha512-0DJI0jXSd53S5oVyG9u3bm8rZ1+lmZ9mhLQwxpK6Po6tv3833k8IqobaTEPwN62xJx+y/wGlOjiqorRXEMDkJQ==} + dt-sql-parser@4.1.1: + resolution: {integrity: sha512-9f4m+CxIzhIrFqW8g95bCRQd+B+WeiC+RgDinBL5hwxa9OXrC6iIbaUtmiMDLO3GDBONn9ybgc+kQzv+G7EKcg==} engines: {node: '>=18'} email-addresses@3.1.0: @@ -2703,7 +2703,7 @@ snapshots: find-up: 3.0.0 minimatch: 3.1.2 - dt-sql-parser@4.1.0(antlr4ng-cli@1.0.7): + dt-sql-parser@4.1.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) From cf3fb21b6addfba42e4fede60a4a816240aa069b Mon Sep 17 00:00:00 2001 From: mumiao <1270865802zl@gmail.com> Date: Mon, 17 Feb 2025 15:51:22 +0800 Subject: [PATCH 03/14] chore(release): 0.13.1 --- CHANGELOG.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cf4c20..64b178d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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.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 8b0fa3a2..3cfaaaa3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monaco-sql-languages", - "version": "0.13.0", + "version": "0.13.1", "description": "SQL languages for the Monaco Editor, based on monaco-languages.", "scripts": { "prepublishOnly": "npm run build", From 25f5afa76e6190838d43b6c453f6b32bc2aaf436 Mon Sep 17 00:00:00 2001 From: mumiao <1270865802zl@gmail.com> Date: Fri, 9 May 2025 16:06:16 +0800 Subject: [PATCH 04/14] chore(release): 0.14.0 --- CHANGELOG.md | 13 +++++++++++++ package.json | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64b178d4..ac681daf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ 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.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) diff --git a/package.json b/package.json index 99be846d..c111379c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monaco-sql-languages", - "version": "0.13.1", + "version": "0.14.0", "description": "SQL languages for the Monaco Editor, based on monaco-languages.", "scripts": { "prepublishOnly": "npm run build", From 3bc2fa71cd7f4e13c5878f19a25a2283caed30a9 Mon Sep 17 00:00:00 2001 From: liuxy0551 Date: Tue, 13 May 2025 10:06:55 +0800 Subject: [PATCH 05/14] ci: pnpm-lock conflict --- pnpm-lock.yaml | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 682442f7..d0b4ac21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,13 +9,8 @@ importers: .: dependencies: dt-sql-parser: -<<<<<<< HEAD - specifier: 4.1.1 - version: 4.1.1(antlr4ng-cli@1.0.7) -======= specifier: 4.2.0 version: 4.2.0(antlr4ng-cli@1.0.7) ->>>>>>> next devDependencies: '@commitlint/cli': specifier: ^17.7.2 @@ -313,7 +308,6 @@ packages: antlr4ng-cli@1.0.7: resolution: {integrity: sha512-qN2FsDBmLvsQcA5CWTrPz8I8gNXeS1fgXBBhI78VyxBSBV/EJgqy8ks6IDTC9jyugpl40csCQ4sL5K4i2YZ/2w==} - deprecated: 'This package is deprecated and will no longer be updated. Please use the new antlr-ng package instead: https://github.com/mike-lischke/antlr-ng' hasBin: true antlr4ng@2.0.11: @@ -720,13 +714,8 @@ packages: resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} engines: {node: '>=6'} -<<<<<<< HEAD - dt-sql-parser@4.1.1: - resolution: {integrity: sha512-9f4m+CxIzhIrFqW8g95bCRQd+B+WeiC+RgDinBL5hwxa9OXrC6iIbaUtmiMDLO3GDBONn9ybgc+kQzv+G7EKcg==} -======= dt-sql-parser@4.2.0: resolution: {integrity: sha512-tsTHGNGIeTd7xACh8FNzSCaQHYyITJeSTMZPxGFkROiccPxj82uEqOeUgJ+bYof9hEacf4h61LsiMyxPy+tl7g==} ->>>>>>> next engines: {node: '>=18'} email-addresses@3.1.0: @@ -1507,7 +1496,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: @@ -2714,11 +2702,7 @@ snapshots: find-up: 3.0.0 minimatch: 3.1.2 -<<<<<<< HEAD - dt-sql-parser@4.1.1(antlr4ng-cli@1.0.7): -======= dt-sql-parser@4.2.0(antlr4ng-cli@1.0.7): ->>>>>>> next dependencies: antlr4-c3: 3.3.7(antlr4ng-cli@1.0.7) antlr4ng: 2.0.11(antlr4ng-cli@1.0.7) From 58a430403d2f3a97e91c500e14d9fd3f4be7427c Mon Sep 17 00:00:00 2001 From: mumiao <1270865802zl@gmail.com> Date: Fri, 16 May 2025 14:32:09 +0800 Subject: [PATCH 06/14] feat(sqlParser): upgrade dt-sql-parser@4.3.0 --- package.json | 6 +++--- pnpm-lock.yaml | 15 ++++++++------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index c111379c..c2cd689f 100644 --- a/package.json +++ b/package.json @@ -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.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0b4ac21..bf57305d 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.0 + version: 4.3.0(antlr4ng-cli@1.0.7) devDependencies: '@commitlint/cli': specifier: ^17.7.2 @@ -308,6 +308,7 @@ packages: antlr4ng-cli@1.0.7: resolution: {integrity: sha512-qN2FsDBmLvsQcA5CWTrPz8I8gNXeS1fgXBBhI78VyxBSBV/EJgqy8ks6IDTC9jyugpl40csCQ4sL5K4i2YZ/2w==} + deprecated: 'This package is deprecated and will no longer be updated. Please use the new antlr-ng package instead: https://github.com/mike-lischke/antlr-ng' hasBin: true antlr4ng@2.0.11: @@ -714,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.0: + resolution: {integrity: sha512-k3G9xga1F4Y4KKj+6Up9G/4BK4dM6rjISISln+s8FaV8qmMfmCJOmzfJQkY9/9xrqLFKCesBi1HYDK9FcIHoKA==} engines: {node: '>=18'} email-addresses@3.1.0: @@ -2060,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 @@ -2576,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) @@ -2702,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.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) From 57c7be846b5b1636b30adae0d9e01a2de9015263 Mon Sep 17 00:00:00 2001 From: mumiao <1270865802zl@gmail.com> Date: Fri, 16 May 2025 14:33:23 +0800 Subject: [PATCH 07/14] chore(release): 0.15.0 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ac681daf..226b5373 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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.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) diff --git a/package.json b/package.json index c2cd689f..c525e05a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monaco-sql-languages", - "version": "0.14.0", + "version": "0.15.0", "description": "SQL languages for the Monaco Editor, based on monaco-languages.", "scripts": { "prepublishOnly": "npm run build", From fea3c3c79682eed760dd7c00a4a5b830a60d9565 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=90=89=E6=98=93?= Date: Fri, 6 Jun 2025 09:40:55 +0800 Subject: [PATCH 08/14] fix: #166 upgrade dt-sql-parser version (#183) --- package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index c525e05a..88f52c70 100644 --- a/package.json +++ b/package.json @@ -84,6 +84,6 @@ ] }, "dependencies": { - "dt-sql-parser": "4.3.0" + "dt-sql-parser": "4.3.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf57305d..c8202e3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: dt-sql-parser: - specifier: 4.3.0 - version: 4.3.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.3.0: - resolution: {integrity: sha512-k3G9xga1F4Y4KKj+6Up9G/4BK4dM6rjISISln+s8FaV8qmMfmCJOmzfJQkY9/9xrqLFKCesBi1HYDK9FcIHoKA==} + dt-sql-parser@4.3.1: + resolution: {integrity: sha512-WlFB9of+ChwWtc5M222jHGIpzqHx51szLe/11GAwwbA+4hRaVkMpWMf2bbYj4i855edSoTQ52zyLJVOpe+4OVg==} engines: {node: '>=18'} email-addresses@3.1.0: @@ -2703,7 +2703,7 @@ snapshots: find-up: 3.0.0 minimatch: 3.1.2 - dt-sql-parser@4.3.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) From 991bd2d8e32cad73c27148794945f4aeef78fdf9 Mon Sep 17 00:00:00 2001 From: mumiao <1270865802zl@gmail.com> Date: Fri, 6 Jun 2025 09:41:46 +0800 Subject: [PATCH 09/14] chore(release): 0.15.1 --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 226b5373..c159b3e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ 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) diff --git a/package.json b/package.json index 88f52c70..82f749ab 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "monaco-sql-languages", - "version": "0.15.0", + "version": "0.15.1", "description": "SQL languages for the Monaco Editor, based on monaco-languages.", "scripts": { "prepublishOnly": "npm run build", From 3909897896a7b251746b2e4cbe29f9b72d76d8df Mon Sep 17 00:00:00 2001 From: jialan Date: Wed, 8 Jan 2025 11:57:34 +0800 Subject: [PATCH 10/14] docs: add website column completions demo --- .../languages/helpers/completionService.ts | 221 ++++++++++++++++-- 1 file changed, 200 insertions(+), 21 deletions(-) diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index c6fde960..f3df3469 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -1,8 +1,15 @@ import { languages } from 'monaco-editor/esm/vs/editor/editor.api'; -import { CompletionService, ICompletionItem } from 'monaco-sql-languages/esm/languageService'; +import { + CommonEntityContext, + CompletionService, + ICompletionItem, + Suggestions, + WordRange +} from 'monaco-sql-languages/esm/languageService'; import { EntityContextType } from 'monaco-sql-languages/esm/main'; import { getCatalogs, getDataBases, getSchemas, getTables, getViews } from './dbMetaProvider'; +import { AttrName, EntityContext } from 'dt-sql-parser/dist/parser/common/entityCollector'; const haveCatalogSQLType = (languageId: string) => { return ['flinksql', 'trinosql'].includes(languageId.toLowerCase()); @@ -12,28 +19,18 @@ const namedSchemaSQLType = (languageId: string) => { return ['trinosql', 'hivesql', 'sparksql'].includes(languageId); }; -export const completionService: CompletionService = async function ( - model, - _position, - _completionContext, - suggestions -) { - if (!suggestions) { - return Promise.resolve([]); - } - const languageId = model.getLanguageId(); +const isWordRangesEndWithWhiteSpace = (wordRanges: WordRange[]) => { + return wordRanges.length > 1 && wordRanges.at(-1)?.text === ' '; +}; + +const getSyntaxCompletionItems = async ( + languageId: string, + syntax: Suggestions['syntax'], + entities: EntityContext[] | null +): Promise => { const haveCatalog = haveCatalogSQLType(languageId); const getDBOrSchema = namedSchemaSQLType(languageId) ? getSchemas : getDataBases; - const { keywords, syntax } = suggestions; - - const keywordsCompletionItems: ICompletionItem[] = keywords.map((kw) => ({ - label: kw, - kind: languages.CompletionItemKind.Keyword, - detail: '关键字', - sortText: '2' + kw - })); - let syntaxCompletionItems: ICompletionItem[] = []; /** 是否已经存在 catalog 补全项 */ @@ -58,6 +55,13 @@ export const completionService: CompletionService = async function ( const words = wordRanges.map((wr) => wr.text); const wordCount = words.length; + /** + * 在做上下文判断时,如果已经键入了空格,则表示已经离开了该上下文。 + * 如: SELECT id | FROM t1 + * 光标所处位置在id后且键入了空格,虽然收集到的上下文信息中包含了`EntityContextType.COLUMN`,但不应该继续补全字段, table同理 + */ + if (isWordRangesEndWithWhiteSpace(wordRanges)) continue; + if ( syntaxContextType === EntityContextType.CATALOG || syntaxContextType === EntityContextType.DATABASE_CREATE @@ -108,8 +112,21 @@ export const completionService: CompletionService = async function ( } if (!existTableCompletions) { + const createTables = + entities + ?.filter( + (entity) => + entity.entityContextType === EntityContextType.TABLE_CREATE + ) + .map((tb) => ({ + label: tb.text, + kind: languages.CompletionItemKind.Field, + detail: 'table', + sortText: '1' + tb.text + })) || []; syntaxCompletionItems = syntaxCompletionItems.concat( - await getTables(languageId) + await getTables(languageId), + createTables ); existTableCompletions = true; } @@ -182,6 +199,168 @@ export const completionService: CompletionService = async function ( } } } + + if (syntaxContextType === EntityContextType.COLUMN) { + const inSelectStmtContext = entities?.some( + (entity) => + entity.entityContextType === EntityContextType.TABLE && + entity.belongStmt.isContainCaret + ); + // 上下文中建的所有表 + const allCreateTables = + (entities?.filter( + (entity) => entity.entityContextType === EntityContextType.TABLE_CREATE + ) as CommonEntityContext[]) || []; + + if (inSelectStmtContext) { + // select语句中的来源表 + // todo filter 子查询中的表 + const fromTables = + entities?.filter( + (entity) => + entity.entityContextType === EntityContextType.TABLE && + entity.belongStmt.isContainCaret + ) || []; + // 从上下文中找到来源表的定义信息 + const fromTableDefinitionEntities = allCreateTables.filter((tb) => + fromTables?.some((ft) => ft.text === tb.text) + ); + const tableNameAliasMap = fromTableDefinitionEntities.reduce( + (acc: Record, tb) => { + acc[tb.text] = + fromTables?.find((ft) => ft.text === tb.text)?.[AttrName.alias]?.text || + tb.text; + return acc; + }, + {} + ); + + let fromTableColumns: (ICompletionItem & { + _tableName?: string; + _columnText?: string; + })[] = []; + + if (wordRanges.length <= 1) { + const columnRepeatCountMap = new Map(); + fromTableColumns = fromTableDefinitionEntities + .map((tb) => { + const displayTbName = + tableNameAliasMap[tb.text] === tb.text + ? tb.text + : tableNameAliasMap[tb.text]; + return ( + tb.columns?.map((column) => { + const columnName = column.text; + const repeatCount = columnRepeatCountMap.get(columnName) || 0; + columnRepeatCountMap.set(columnName, repeatCount + 1); + return { + label: + column.text + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''), + insertText: column.text, + kind: languages.CompletionItemKind.EnumMember, + detail: `来源表 ${displayTbName} 的字段`, + sortText: '0' + displayTbName + column.text + repeatCount, + _tableName: displayTbName, + _columnText: column.text + }; + }) || [] + ); + }) + .flat(); + + // 如果有多个重名字段,则插入的字段自动包含表名 + fromTableColumns = fromTableColumns.map((column) => { + const columnRepeatCount = + columnRepeatCountMap.get(column.label as string) || 0; + const isFromMultipleTables = fromTables.length > 1; + return columnRepeatCount > 1 && isFromMultipleTables + ? { + ...column, + insertText: `${column._tableName}.${column._columnText}` + } + : column; + }); + + // 输入字段时提供可选表 + const tableOrAliasCompletionItems = fromTables.map((tb) => { + const displayTbName = tableNameAliasMap[tb.text] + ? tableNameAliasMap[tb.text] + : tb.text; + return { + label: displayTbName, + kind: languages.CompletionItemKind.Field, + detail: `table`, + sortText: '1' + displayTbName + }; + }); + + syntaxCompletionItems = syntaxCompletionItems.concat( + tableOrAliasCompletionItems + ); + } else if (wordRanges.length >= 2 && words[1] === '.') { + const tbNameOrAlias = words[0]; + fromTableColumns = fromTableDefinitionEntities + .filter( + (tb) => + tb.text === tbNameOrAlias || + tableNameAliasMap[tb.text] === tbNameOrAlias + ) + .map((tb) => { + const displayTbName = tableNameAliasMap[tb.text] + ? tableNameAliasMap[tb.text] + : tb.text; + return ( + tb.columns?.map((column) => ({ + label: + column.text + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''), + insertText: column.text, + kind: languages.CompletionItemKind.EnumMember, + detail: `来源表 ${displayTbName} 的字段`, + sortText: '0' + displayTbName + column.text + })) || [] + ); + }) + .flat(); + } + + syntaxCompletionItems = syntaxCompletionItems.concat(fromTableColumns); + } + } } + + return syntaxCompletionItems; +}; + +export const completionService: CompletionService = async function ( + model, + _position, + _completionContext, + suggestions, + entities +) { + 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: '关键字', + sortText: '2' + kw + })); + + const syntaxCompletionItems = await getSyntaxCompletionItems(languageId, syntax, entities); + return [...syntaxCompletionItems, ...keywordsCompletionItems]; }; From 9db2d62e4d60edd39fc4b3914b2245ba797af77c Mon Sep 17 00:00:00 2001 From: jialan Date: Wed, 8 Jan 2025 14:52:59 +0800 Subject: [PATCH 11/14] docs: display table name if column duplicated --- website/src/languages/helpers/completionService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index f3df3469..3c012662 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -250,9 +250,8 @@ const getSyntaxCompletionItems = async ( : tableNameAliasMap[tb.text]; return ( tb.columns?.map((column) => { - const columnName = column.text; - const repeatCount = columnRepeatCountMap.get(columnName) || 0; - columnRepeatCountMap.set(columnName, repeatCount + 1); + const repeatCount = columnRepeatCountMap.get(column.text) || 0; + columnRepeatCountMap.set(column.text, repeatCount + 1); return { label: column.text + @@ -274,11 +273,12 @@ const getSyntaxCompletionItems = async ( // 如果有多个重名字段,则插入的字段自动包含表名 fromTableColumns = fromTableColumns.map((column) => { const columnRepeatCount = - columnRepeatCountMap.get(column.label as string) || 0; + columnRepeatCountMap.get(column._columnText as string) || 0; const isFromMultipleTables = fromTables.length > 1; return columnRepeatCount > 1 && isFromMultipleTables ? { ...column, + label: `${column._tableName}.${column.label}`, insertText: `${column._tableName}.${column._columnText}` } : column; From e60976da530bf1391e791b5580201535d51ec158 Mon Sep 17 00:00:00 2001 From: JackWang032 <--global> Date: Mon, 16 Jun 2025 14:12:45 +0800 Subject: [PATCH 12/14] refactor: extract completion methods and add column completion demo --- .../languages/helpers/completionService.ts | 804 +++++++++++------- .../src/languages/helpers/dbMetaProvider.ts | 38 +- 2 files changed, 547 insertions(+), 295 deletions(-) diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index b991a0a5..5f1b050e 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -6,10 +6,29 @@ import { Suggestions, WordRange } from 'monaco-sql-languages/esm/languageService'; -import { EntityContextType } from 'monaco-sql-languages/esm/main'; +import { EntityContextType, StmtContextType } from 'monaco-sql-languages/esm/main'; -import { getCatalogs, getDataBases, getSchemas, getTables, getViews } from './dbMetaProvider'; -import { AttrName, EntityContext } from 'dt-sql-parser/dist/parser/common/entityCollector'; +import { + getCatalogs, + getDataBases, + getSchemas, + getTables, + getViews, + getColumns +} from './dbMetaProvider'; +import { + AttrName, + ColumnDeclareType, + EntityContext, + isCommonEntityContext, + 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()); @@ -23,314 +42,519 @@ const isWordRangesEndWithWhiteSpace = (wordRanges: WordRange[]) => { return wordRanges.length > 1 && wordRanges.at(-1)?.text === ' '; }; -const getSyntaxCompletionItems = async ( +// 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); + } + + markAsCompleted(type: string): void { + this.completionTypes.add(type); + } +} + +/** + * Get database object completion items (catalog, database, table, etc.) + */ +const getDatabaseObjectCompletions = async ( + tracker: CompletionTracker, languageId: string, - syntax: Suggestions['syntax'], - entities: EntityContext[] | null + contextType: EntityContextType | StmtContextType, + words: string[] ): Promise => { const haveCatalog = haveCatalogSQLType(languageId); const getDBOrSchema = namedSchemaSQLType(languageId) ? getSchemas : getDataBases; + const wordCount = words.length; + const result: ICompletionItem[] = []; - let syntaxCompletionItems: ICompletionItem[] = []; - - /** 是否已经存在 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; - - for (let i = 0; i < syntax.length; i++) { - const { syntaxContextType, wordRanges } = syntax[i]; - - // e.g. words -> ['cat', '.', 'database', '.', 'table'] - const words = wordRanges.map((wr) => wr.text); - const wordCount = words.length; - - /** - * 在做上下文判断时,如果已经键入了空格,则表示已经离开了该上下文。 - * 如: SELECT id | FROM t1 - * 光标所处位置在id后且键入了空格,虽然收集到的上下文信息中包含了`EntityContextType.COLUMN`,但不应该继续补全字段, table同理 - */ - if (isWordRangesEndWithWhiteSpace(wordRanges)) continue; - + // Complete Catalog + if (wordCount <= 1 && haveCatalog && !tracker.hasCompletionType('catalog')) { if ( - syntaxContextType === EntityContextType.CATALOG || - syntaxContextType === EntityContextType.DATABASE_CREATE + [EntityContextType.CATALOG, EntityContextType.DATABASE_CREATE].includes( + contextType as EntityContextType + ) ) { - if (!existCatalogCompletions && wordCount <= 1) { - syntaxCompletionItems = syntaxCompletionItems.concat(await getCatalogs(languageId)); - existCatalogCompletions = true; - } + result.push(...(await getCatalogs(languageId))); + tracker.markAsCompleted('catalog'); } + } + // Complete Database + if (wordCount <= 1 && !tracker.hasCompletionType('database')) { if ( - syntaxContextType === EntityContextType.DATABASE || - syntaxContextType === EntityContextType.TABLE_CREATE || - syntaxContextType === EntityContextType.VIEW_CREATE + [ + EntityContextType.DATABASE, + EntityContextType.TABLE, + EntityContextType.TABLE_CREATE, + EntityContextType.VIEW, + EntityContextType.VIEW_CREATE + ].includes(contextType as EntityContextType) ) { - if (!existCatalogCompletions && haveCatalog && wordCount <= 1) { - syntaxCompletionItems = syntaxCompletionItems.concat(await getCatalogs(languageId)); - existCatalogCompletions = true; - } - - if (!existDatabaseCompletions && wordCount <= 1) { - syntaxCompletionItems = syntaxCompletionItems.concat( - await getDBOrSchema(languageId) - ); - existDatabaseCompletions = true; - } - if (!existDatabaseInCatCompletions && haveCatalog && wordCount >= 2 && wordCount <= 3) { - syntaxCompletionItems = syntaxCompletionItems.concat( - await getDBOrSchema(languageId, words[0]) - ); - existDatabaseInCatCompletions = true; - } + result.push(...(await getDBOrSchema(languageId))); + tracker.markAsCompleted('database'); } + } - 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) { - const createTables = - entities - ?.filter( - (entity) => - entity.entityContextType === EntityContextType.TABLE_CREATE - ) - .map((tb) => ({ - label: tb.text, - kind: languages.CompletionItemKind.Field, - detail: 'table', - sortText: '1' + tb.text - })) || []; - syntaxCompletionItems = syntaxCompletionItems.concat( - await getTables(languageId), - createTables - ); - 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; - } - } + // 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'); } + } + + // Complete Table + if ( + contextType === EntityContextType.TABLE && + wordCount <= 1 && + !tracker.hasCompletionType('table') + ) { + result.push(...(await getTables(languageId))); + tracker.markAsCompleted('table'); + } + + // 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'); + } + + // 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'); + } + + // 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; +}; + +/** + * Get columns from locally defined tables + */ +const getLocalTableColumns = ( + sourceTableDefinitionEntities: CommonEntityContext[], + tableNameAliasMap: Record = {} +): EnhancedCompletionItem[] => { + return sourceTableDefinitionEntities + .map((tb) => { + const tableName = tableNameAliasMap[tb.text] || getPureEntityText(tb.text); + return ( + tb.columns?.map((column) => { + const columnName = + column[AttrName.alias]?.text || getPureEntityText(column.text); + return { + label: + columnName + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''), + insertText: columnName, + kind: languages.CompletionItemKind.EnumMember, + detail: `\`${tableName}\`'s column`, + sortText: '0' + tableName + columnName, + _tableName: tableName, + _columnText: columnName + }; + }) || [] + ); + }) + .flat(); +}; - 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; - } +/** + * Get columns from derived tables (subqueries) + */ +const getDerivedTableColumns = ( + derivedTableEntities: CommonEntityContext[] +): EnhancedCompletionItem[] => { + return derivedTableEntities + .map((tb: CommonEntityContext) => { + const derivedTableQueryResult = tb.relatedEntities?.find( + (entity) => entity.entityContextType === EntityContextType.QUERY_RESULT + ) as CommonEntityContext | undefined; + + const tableName = 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, + insertText: columnName, + kind: languages.CompletionItemKind.EnumMember, + detail: `\`${tableName}\`'s column`, + sortText: '0' + tableName + columnName, + _tableName: tableName, + _columnText: columnName + }; + }) || [] + ); + }) + .flat(); +}; + +/** + * 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) => { + const words = originEntityText + .split('.') + .map((word) => + word.startsWith('`') && word.endsWith('`') && word.length >= 3 + ? word.slice(1, -1) + : word + ); + return words[words.length - 1]; +}; + +/** + * 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 + const sourceTableDefinitionEntities = allTableDefinitionEntities.filter((createTable) => + sourceTables?.some( + (sourceTable) => + sourceTable.declareType === TableDeclareType.COMMON && + // You can also check schema name here + getPureEntityText(sourceTable.text) === getPureEntityText(createTable.text) && + sourceTable.isAccessible + ) + ); + + const derivedTableEntities = + (entities?.filter( + (entity) => + isCommonEntityContext(entity) && + entity.entityContextType === EntityContextType.TABLE && + entity.isAccessible && + entity.declareType === TableDeclareType.EXPRESSION + ) as CommonEntityContext[]) || []; + + const tableNameAliasMap: Record = sourceTables.reduce( + (acc: Record, tb) => { + acc[tb.text] = tb[AttrName.alias]?.text || ''; + return acc; + }, + {} + ); + console.log(wordRanges); + + // 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 = [ + ...getLocalTableColumns(sourceTableDefinitionEntities, tableNameAliasMap), + ...getDerivedTableColumns(derivedTableEntities) + ]; + + // 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; + return columnRepeatCount > 1 && isIncludeInMultipleTables + ? { + ...column, + label: `${column._tableName}.${column.label}`, + insertText: `${column._tableName}.${column._columnText}` + } + : column; + }); + + result.push(...sourceTableColumns); + + // Also suggest tables when inputting column + const tableCompletionItems = sourceTables.map((tb) => { + const tableName = tb[AttrName.alias]?.text ?? getPureEntityText(tb.text); + return { + label: tableName, + kind: languages.CompletionItemKind.Field, + detail: tb.declareType === TableDeclareType.COMMON ? 'table' : 'derived table', + sortText: '1' + tableName + }; + }); + + result.push(...tableCompletionItems); + } else if (wordRanges.length === 2 && words[1] === '.') { + // Table.column format completion + const tbNameOrAlias = words[0]; + + // Find columns in local table definitions + const localTableColumns = [ + ...getSpecificTableColumns( + sourceTableDefinitionEntities, + tbNameOrAlias, + tableNameAliasMap + ), + ...getSpecificDerivedTableColumns( + derivedTableEntities, + tbNameOrAlias, + tableNameAliasMap + ) + ]; + + result.push(...localTableColumns); + + // If no local table columns found, try to fetch from cloud + if (localTableColumns.length === 0) { + // Find the real table name for the alias + const realTableName = + Object.entries(tableNameAliasMap).find( + ([_table, alias]) => alias === tbNameOrAlias + )?.[0] || tbNameOrAlias; + + // Get columns from cloud + const remoteColumns = await getColumns(languageId, realTableName); + result.push(...remoteColumns); } + } + + return result; +}; - if (syntaxContextType === EntityContextType.COLUMN) { - const inSelectStmtContext = entities?.some( - (entity) => - entity.entityContextType === EntityContextType.TABLE && - entity.belongStmt.isContainCaret +/** + * Get columns from a specific table + */ +const getSpecificTableColumns = ( + sourceTableDefinitionEntities: CommonEntityContext[], + tableNameOrAlias: string, + tableNameAliasMap: Record = {} +): ICompletionItem[] => { + return sourceTableDefinitionEntities + .filter((tb) => { + return tb.text === tableNameOrAlias || tableNameAliasMap[tb.text] === tableNameOrAlias; + }) + .map((tb) => { + const tableName = tableNameAliasMap[tb.text] || getPureEntityText(tb.text); + return ( + tb.columns?.map((column) => { + const columnName = + column[AttrName.alias]?.text || getPureEntityText(column.text); + return { + label: + columnName + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''), + insertText: columnName, + kind: languages.CompletionItemKind.EnumMember, + detail: `\`${tableName}\`'s column`, + sortText: '0' + tableName + columnName + }; + }) || [] ); - // 上下文中建的所有表 - const allCreateTables = - (entities?.filter( - (entity) => entity.entityContextType === EntityContextType.TABLE_CREATE - ) as CommonEntityContext[]) || []; - - if (inSelectStmtContext) { - // select语句中的来源表 - // todo filter 子查询中的表 - const fromTables = - entities?.filter( - (entity) => - entity.entityContextType === EntityContextType.TABLE && - entity.belongStmt.isContainCaret - ) || []; - // 从上下文中找到来源表的定义信息 - const fromTableDefinitionEntities = allCreateTables.filter((tb) => - fromTables?.some((ft) => ft.text === tb.text) - ); - const tableNameAliasMap = fromTableDefinitionEntities.reduce( - (acc: Record, tb) => { - acc[tb.text] = - fromTables?.find((ft) => ft.text === tb.text)?.[AttrName.alias]?.text || - tb.text; - return acc; - }, - {} - ); - - let fromTableColumns: (ICompletionItem & { - _tableName?: string; - _columnText?: string; - })[] = []; - - if (wordRanges.length <= 1) { - const columnRepeatCountMap = new Map(); - fromTableColumns = fromTableDefinitionEntities - .map((tb) => { - const displayTbName = - tableNameAliasMap[tb.text] === tb.text - ? tb.text - : tableNameAliasMap[tb.text]; - return ( - tb.columns?.map((column) => { - const repeatCount = columnRepeatCountMap.get(column.text) || 0; - columnRepeatCountMap.set(column.text, repeatCount + 1); - return { - label: - column.text + - (column[AttrName.colType]?.text - ? `(${column[AttrName.colType].text})` - : ''), - insertText: column.text, - kind: languages.CompletionItemKind.EnumMember, - detail: `来源表 ${displayTbName} 的字段`, - sortText: '0' + displayTbName + column.text + repeatCount, - _tableName: displayTbName, - _columnText: column.text - }; - }) || [] - ); - }) - .flat(); - - // 如果有多个重名字段,则插入的字段自动包含表名 - fromTableColumns = fromTableColumns.map((column) => { - const columnRepeatCount = - columnRepeatCountMap.get(column._columnText as string) || 0; - const isFromMultipleTables = fromTables.length > 1; - return columnRepeatCount > 1 && isFromMultipleTables - ? { - ...column, - label: `${column._tableName}.${column.label}`, - insertText: `${column._tableName}.${column._columnText}` - } - : column; - }); - - // 输入字段时提供可选表 - const tableOrAliasCompletionItems = fromTables.map((tb) => { - const displayTbName = tableNameAliasMap[tb.text] - ? tableNameAliasMap[tb.text] - : tb.text; + }) + .flat(); +}; + +/** + * Get columns from a specific derived table (subquery) + */ +const getSpecificDerivedTableColumns = ( + derivedTableEntities: CommonEntityContext[], + tableNameOrAlias: string, + tableNameAliasMap: Record = {} +): ICompletionItem[] => { + return derivedTableEntities + .filter((tb) => { + return tb.text === tableNameOrAlias || tableNameAliasMap[tb.text] === tableNameOrAlias; + }) + .map((tb) => { + const derivedTableQueryResult = tb.relatedEntities?.find( + (entity) => entity.entityContextType === EntityContextType.QUERY_RESULT + ) as CommonEntityContext | undefined; + + const tableName = tb[AttrName.alias]?.text || tb.text; + + return ( + derivedTableQueryResult?.columns + ?.filter((column) => column.declareType !== ColumnDeclareType.ALL) + .map((column) => { + const columnName = + column[AttrName.alias]?.text || getPureEntityText(column.text); return { - label: displayTbName, + label: columnName, + insertText: columnName, + kind: languages.CompletionItemKind.EnumMember, + detail: `\`${tableName}\`'s column`, + sortText: '0' + tableName + columnName + }; + }) || [] + ); + }) + .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, kind: languages.CompletionItemKind.Field, - detail: `table`, - sortText: '1' + displayTbName + detail: 'table', + sortText: '1' + tableName }; - }); - - syntaxCompletionItems = syntaxCompletionItems.concat( - tableOrAliasCompletionItems - ); - } else if (wordRanges.length >= 2 && words[1] === '.') { - const tbNameOrAlias = words[0]; - fromTableColumns = fromTableDefinitionEntities - .filter( - (tb) => - tb.text === tbNameOrAlias || - tableNameAliasMap[tb.text] === tbNameOrAlias - ) - .map((tb) => { - const displayTbName = tableNameAliasMap[tb.text] - ? tableNameAliasMap[tb.text] - : tb.text; - return ( - tb.columns?.map((column) => ({ - label: - column.text + - (column[AttrName.colType]?.text - ? `(${column[AttrName.colType].text})` - : ''), - insertText: column.text, - kind: languages.CompletionItemKind.EnumMember, - detail: `来源表 ${displayTbName} 的字段`, - sortText: '0' + displayTbName + column.text - })) || [] - ); - }) - .flat(); - } - - syntaxCompletionItems = syntaxCompletionItems.concat(fromTableColumns); - } + }) || []; + + 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'); } } @@ -357,7 +581,7 @@ export const completionService: CompletionService = async function ( const keywordsCompletionItems: ICompletionItem[] = keywords.map((kw) => ({ label: kw, kind: languages.CompletionItemKind.Keyword, - detail: '关键字', + detail: 'keyword', sortText: '2' + kw })); diff --git a/website/src/languages/helpers/dbMetaProvider.ts b/website/src/languages/helpers/dbMetaProvider.ts index 881dfd23..253c8822 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,7 +22,7 @@ const prefixLabel = (languageId: string, text: string) => { }; /** - * 获取所有的 catalog + * Get all catalogs */ export function getCatalogs(languageId: string) { const catCompletions = catalogList.map((cat) => ({ @@ -34,7 +35,7 @@ export function getCatalogs(languageId: string) { } /** - * 根据catalog 获取 database + * Get databases based on catalog */ export function getDataBases(languageId: string, catalog?: string) { const databases = catalog ? databaseList : tmpDatabaseList; @@ -50,7 +51,7 @@ export function getDataBases(languageId: string, catalog?: string) { } /** - * 根据catalog 获取 schema + * Get schemas based on catalog */ export function getSchemas(languageId: string, catalog?: string) { const schemas = catalog ? schemaList : tmpSchemaList; @@ -66,7 +67,7 @@ export function getSchemas(languageId: string, catalog?: string) { } /** - * 根据 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; @@ -82,7 +83,7 @@ export function getTables(languageId: string, catalog?: string, database?: strin } /** - * 根据 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; @@ -96,3 +97,30 @@ export function getViews(languageId: string, catalog?: string, database?: string 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) => ({ + label: `${col.name}(${col.type})`, + insertText: col.name, + kind: languages.CompletionItemKind.EnumMember, + detail: `Remote: \`${tableName}\`'s column`, + sortText: '0' + tableName + col.name + })); + + return Promise.resolve(columnCompletions); +} From 7310e9de6aa8b189227034bf12b5f8b775e128e7 Mon Sep 17 00:00:00 2001 From: JackWang032 <--global> Date: Mon, 1 Sep 2025 13:57:37 +0800 Subject: [PATCH 13/14] feat: enhance entity completion --- .../languages/helpers/completionService.ts | 342 ++++++++++++------ 1 file changed, 224 insertions(+), 118 deletions(-) diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index 5f1b050e..11bde685 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -20,7 +20,6 @@ import { AttrName, ColumnDeclareType, EntityContext, - isCommonEntityContext, TableDeclareType } from 'dt-sql-parser/dist/parser/common/entityCollector'; @@ -187,71 +186,52 @@ const getDatabaseObjectCompletions = async ( }; /** - * Get columns from locally defined tables - */ -const getLocalTableColumns = ( - sourceTableDefinitionEntities: CommonEntityContext[], - tableNameAliasMap: Record = {} -): EnhancedCompletionItem[] => { - return sourceTableDefinitionEntities - .map((tb) => { - const tableName = tableNameAliasMap[tb.text] || getPureEntityText(tb.text); - return ( - tb.columns?.map((column) => { - const columnName = - column[AttrName.alias]?.text || getPureEntityText(column.text); - return { - label: - columnName + - (column[AttrName.colType]?.text - ? `(${column[AttrName.colType].text})` - : ''), - insertText: columnName, - kind: languages.CompletionItemKind.EnumMember, - detail: `\`${tableName}\`'s column`, - sortText: '0' + tableName + columnName, - _tableName: tableName, - _columnText: columnName - }; - }) || [] - ); - }) - .flat(); -}; - -/** - * Get columns from derived tables (subqueries) + * 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 getDerivedTableColumns = ( - derivedTableEntities: CommonEntityContext[] -): EnhancedCompletionItem[] => { - return derivedTableEntities - .map((tb: CommonEntityContext) => { - const derivedTableQueryResult = tb.relatedEntities?.find( - (entity) => entity.entityContextType === EntityContextType.QUERY_RESULT - ) as CommonEntityContext | undefined; - - const tableName = tb[AttrName.alias]?.text || getPureEntityText(tb.text); +const parseEntityText = (originEntityText: string) => { + const words = originEntityText + .split('.') + .map((word) => + word.startsWith('`') && word.endsWith('`') && word.length >= 3 + ? word.slice(1, -1) + : word + ); - return ( - derivedTableQueryResult?.columns - ?.filter((column) => column.declareType !== ColumnDeclareType.ALL) - .map((column) => { - const columnName = - column[AttrName.alias]?.text || getPureEntityText(column.text); - return { - label: columnName, - insertText: columnName, - kind: languages.CompletionItemKind.EnumMember, - detail: `\`${tableName}\`'s column`, - sortText: '0' + tableName + columnName, - _tableName: tableName, - _columnText: columnName - }; - }) || [] - ); - }) - .flat(); + 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] + }; + } }; /** @@ -264,14 +244,35 @@ const getDerivedTableColumns = ( * getPureEntityText('`a1`') => 'a1' */ const getPureEntityText = (originEntityText: string) => { - const words = originEntityText - .split('.') - .map((word) => - word.startsWith('`') && word.endsWith('`') && word.length >= 3 - ? word.slice(1, -1) - : word - ); - return words[words.length - 1]; + return parseEntityText(originEntityText).pureEntityText; +}; + +/** + * 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; }; /** @@ -299,44 +300,75 @@ const getColumnCompletions = async ( (entity) => entity.entityContextType === EntityContextType.TABLE && entity.isAccessible ) as CommonEntityContext[]) || []; - // Find table definitions from source tables + // Find table definitions from source tables (regular CREATE TABLE with explicit columns) const sourceTableDefinitionEntities = allTableDefinitionEntities.filter((createTable) => sourceTables?.some( (sourceTable) => - sourceTable.declareType === TableDeclareType.COMMON && - // You can also check schema name here - getPureEntityText(sourceTable.text) === getPureEntityText(createTable.text) && - sourceTable.isAccessible + 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 = - (entities?.filter( - (entity) => - isCommonEntityContext(entity) && - entity.entityContextType === EntityContextType.TABLE && - entity.isAccessible && - entity.declareType === TableDeclareType.EXPRESSION - ) as CommonEntityContext[]) || []; + sourceTables?.filter((entity) => entity.declareType === TableDeclareType.EXPRESSION) || []; const tableNameAliasMap: Record = sourceTables.reduce( (acc: Record, tb) => { - acc[tb.text] = tb[AttrName.alias]?.text || ''; + const alias = tb[AttrName.alias]?.text; + if (alias) { + acc[tb.text] = alias; + } return acc; }, {} ); - console.log(wordRanges); + + // 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 = [ - ...getLocalTableColumns(sourceTableDefinitionEntities, tableNameAliasMap), - ...getDerivedTableColumns(derivedTableEntities) - ]; + 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) => { @@ -367,7 +399,7 @@ const getColumnCompletions = async ( return { label: tableName, kind: languages.CompletionItemKind.Field, - detail: tb.declareType === TableDeclareType.COMMON ? 'table' : 'derived table', + detail: tb.declareType === TableDeclareType.LITERAL ? 'table' : 'derived table', sortText: '1' + tableName }; }); @@ -377,33 +409,57 @@ const getColumnCompletions = async ( // 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 + ); + }); + + if (matchingTable) { + realTablePath = matchingTable.text; + } + } + // Find columns in local table definitions + const displayAlias = aliasToTableMap[tbNameOrAlias] ? tbNameOrAlias : undefined; + const localTableColumns = [ - ...getSpecificTableColumns( - sourceTableDefinitionEntities, - tbNameOrAlias, - tableNameAliasMap - ), - ...getSpecificDerivedTableColumns( - derivedTableEntities, - tbNameOrAlias, - tableNameAliasMap - ) + ...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) { - // Find the real table name for the alias - const realTableName = - Object.entries(tableNameAliasMap).find( - ([_table, alias]) => alias === tbNameOrAlias - )?.[0] || tbNameOrAlias; - - // Get columns from cloud - const remoteColumns = await getColumns(languageId, realTableName); - result.push(...remoteColumns); + // 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); + } } } @@ -415,15 +471,19 @@ const getColumnCompletions = async ( */ const getSpecificTableColumns = ( sourceTableDefinitionEntities: CommonEntityContext[], - tableNameOrAlias: string, - tableNameAliasMap: Record = {} + realTablePath: string, + displayAlias?: string ): ICompletionItem[] => { return sourceTableDefinitionEntities .filter((tb) => { - return tb.text === tableNameOrAlias || tableNameAliasMap[tb.text] === tableNameOrAlias; + return ( + tb.text === realTablePath || + isEntityPathMatch(tb.text, realTablePath) || + getPureEntityText(tb.text) === getPureEntityText(realTablePath) + ); }) .map((tb) => { - const tableName = tableNameAliasMap[tb.text] || getPureEntityText(tb.text); + const tableName = displayAlias || getPureEntityText(tb.text); return ( tb.columns?.map((column) => { const columnName = @@ -450,19 +510,19 @@ const getSpecificTableColumns = ( */ const getSpecificDerivedTableColumns = ( derivedTableEntities: CommonEntityContext[], - tableNameOrAlias: string, - tableNameAliasMap: Record = {} + displayAlias?: string ): ICompletionItem[] => { return derivedTableEntities .filter((tb) => { - return tb.text === tableNameOrAlias || tableNameAliasMap[tb.text] === tableNameOrAlias; + 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 = tb[AttrName.alias]?.text || tb.text; + const tableName = + displayAlias || tb[AttrName.alias]?.text || getPureEntityText(tb.text); return ( derivedTableQueryResult?.columns @@ -483,6 +543,52 @@ const getSpecificDerivedTableColumns = ( .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); + return { + label: + columnName + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''), + insertText: columnName, + kind: languages.CompletionItemKind.EnumMember, + detail: `\`${tableName}\`'s column`, + sortText: '0' + tableName + columnName + }; + }) || [] + ); + }) + .flat(); +}; + const getSyntaxCompletionItems = async ( languageId: string, syntax: Suggestions['syntax'], From c7d7309d5e6e37d15075763a1dc4f4a153c1f766 Mon Sep 17 00:00:00 2001 From: JackWang032 <--global> Date: Mon, 1 Sep 2025 16:54:53 +0800 Subject: [PATCH 14/14] feat: optimize completion display --- website/src/languages/helpers/completionService.ts | 12 +++++++++--- website/src/languages/helpers/dbMetaProvider.ts | 10 +++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index 11bde685..a96e5333 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -497,7 +497,9 @@ const getSpecificTableColumns = ( insertText: columnName, kind: languages.CompletionItemKind.EnumMember, detail: `\`${tableName}\`'s column`, - sortText: '0' + tableName + columnName + sortText: '0' + tableName + columnName, + _columnText: columnName, + _tableName: tableName }; }) || [] ); @@ -535,7 +537,9 @@ const getSpecificDerivedTableColumns = ( insertText: columnName, kind: languages.CompletionItemKind.EnumMember, detail: `\`${tableName}\`'s column`, - sortText: '0' + tableName + columnName + sortText: '0' + tableName + columnName, + _columnText: columnName, + _tableName: tableName }; }) || [] ); @@ -581,7 +585,9 @@ const getSpecificCTASTableColumns = ( insertText: columnName, kind: languages.CompletionItemKind.EnumMember, detail: `\`${tableName}\`'s column`, - sortText: '0' + tableName + columnName + sortText: '0' + tableName + columnName, + _columnText: columnName, + _tableName: tableName }; }) || [] ); diff --git a/website/src/languages/helpers/dbMetaProvider.ts b/website/src/languages/helpers/dbMetaProvider.ts index 253c8822..4a47a011 100644 --- a/website/src/languages/helpers/dbMetaProvider.ts +++ b/website/src/languages/helpers/dbMetaProvider.ts @@ -28,7 +28,7 @@ export function getCatalogs(languageId: string) { const catCompletions = catalogList.map((cat) => ({ label: prefixLabel(languageId, cat), kind: languages.CompletionItemKind.Field, - detail: 'catalog', + detail: 'Remote: catalog', sortText: '1' + prefixLabel(languageId, cat) })); return Promise.resolve(catCompletions); @@ -43,7 +43,7 @@ export function getDataBases(languageId: string, catalog?: string) { const databaseCompletions = databases.map((db) => ({ label: prefixLabel(languageId, db), kind: languages.CompletionItemKind.Field, - detail: 'database', + detail: 'Remote: database', sortText: '1' + prefixLabel(languageId, db) })); @@ -59,7 +59,7 @@ export function getSchemas(languageId: string, catalog?: string) { const schemaCompletions = schemas.map((sc) => ({ label: prefixLabel(languageId, sc), kind: languages.CompletionItemKind.Field, - detail: 'schema', + detail: 'Remote: schema', sortText: '1' + prefixLabel(languageId, sc) })); @@ -75,7 +75,7 @@ export function getTables(languageId: string, catalog?: string, database?: strin const tableCompletions = tables.map((tb) => ({ label: prefixLabel(languageId, tb), kind: languages.CompletionItemKind.Field, - detail: 'table', + detail: 'Remote: table', sortText: '1' + prefixLabel(languageId, tb) })); @@ -91,7 +91,7 @@ export function getViews(languageId: string, catalog?: string, database?: string const viewCompletions = views.map((v) => ({ label: prefixLabel(languageId, v), kind: languages.CompletionItemKind.Field, - detail: 'view', + detail: 'Remote: view', sortText: '1' + prefixLabel(languageId, v) }));