From c1b619ce63efb364b0afe60ceed2b8f8b88b2525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=90=89=E6=98=93?= Date: Fri, 15 Nov 2024 10:05:53 +0800 Subject: [PATCH 1/3] fix: export sql parser EntityContext types (#157) --- package.json | 2 +- pnpm-lock.yaml | 15 ++++++++------- src/main.ts | 5 ++++- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 08757eff..fc4a88ba 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "pre-commit": "npx pretty-quick --staged" }, "dependencies": { - "dt-sql-parser": "4.0.2" + "dt-sql-parser": "4.1.0-beta.4" }, "peerDependencies": { "monaco-editor": ">=0.31.0" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d53512fd..1c3b1e83 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: dt-sql-parser: - specifier: 4.0.2 - version: 4.0.2(antlr4ng-cli@1.0.7) + specifier: 4.1.0-beta.4 + version: 4.1.0-beta.4(antlr4ng-cli@1.0.7) devDependencies: '@commitlint/cli': specifier: ^17.7.2 @@ -714,8 +714,9 @@ packages: resolution: {integrity: sha512-sCm11ak2oY6DglEPpCB8TixLjWAxd3kJTs6UIcSasNYxXdFPV+YKlye92c8H4kKFqV5qYMIh7d+cYecEg0dIkA==} engines: {node: '>=6'} - dt-sql-parser@4.0.2: - resolution: {integrity: sha512-8D/kfYLW+wgz7Cwf5K+OCtex7QHiCyIuI18pw0a5vjSXRKCpfQqNQeG7tU5vp4D0RQEZJiMBuKJPBYwoqWxoAA==} + dt-sql-parser@4.1.0-beta.4: + resolution: {integrity: sha512-L+Qsw+lv7enkMuhy0XXOm7H63gaajwX7X0RUGCNU8h5xw9Pj5DEWvLcKTS0R+YmO4FzVXOpEzH9e1KkqQaKFaQ==} + engines: {node: '>=18'} email-addresses@3.1.0: resolution: {integrity: sha512-k0/r7GrWVL32kZlGwfPNgB2Y/mMXVTq/decgLczm/j34whdaspNrZO8CnXPf1laaHxI6ptUlsnAxN+UAPw+fzg==} @@ -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.14.14)(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.5.1)(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.14.14)(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.5.1)(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.0.2(antlr4ng-cli@1.0.7): + dt-sql-parser@4.1.0-beta.4(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/main.ts b/src/main.ts index f743ce30..45036c2a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -13,6 +13,9 @@ export type { Suggestions, TextSlice, ParseError, + StmtContext, EntityContext, - StmtContext + CommonEntityContext, + ColumnEntityContext, + FuncEntityContext } from 'dt-sql-parser'; From 4020720e1f1a8f854dc16ad948e237574f906afb Mon Sep 17 00:00:00 2001 From: jialan Date: Wed, 8 Jan 2025 11:57:34 +0800 Subject: [PATCH 2/3] 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 b5138627e5f9bb2ac9f36d9d5fec4a91f8c43e19 Mon Sep 17 00:00:00 2001 From: jialan Date: Wed, 8 Jan 2025 14:52:59 +0800 Subject: [PATCH 3/3] 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;