From 3909897896a7b251746b2e4cbe29f9b72d76d8df Mon Sep 17 00:00:00 2001 From: jialan Date: Wed, 8 Jan 2025 11:57:34 +0800 Subject: [PATCH 1/6] 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 2/6] 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 3/6] 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 4/6] 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 5/6] 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) })); From 6a8d3a5d00ad90d3d22bd9cad9d7dd5efb8ec131 Mon Sep 17 00:00:00 2001 From: JackWang032 <--global> Date: Tue, 28 Oct 2025 19:27:44 +0800 Subject: [PATCH 6/6] feat: improve completion filtering and backtick handling --- .../languages/helpers/completionService.ts | 100 +++++++++++------ .../src/languages/helpers/dbMetaProvider.ts | 105 ++++++++++++------ 2 files changed, 135 insertions(+), 70 deletions(-) diff --git a/website/src/languages/helpers/completionService.ts b/website/src/languages/helpers/completionService.ts index a96e5333..35da2e43 100644 --- a/website/src/languages/helpers/completionService.ts +++ b/website/src/languages/helpers/completionService.ts @@ -195,13 +195,22 @@ const getDatabaseObjectCompletions = async ( * parseEntityText('table') => { catalog: null, schema: null, table: 'table', fullPath: 'table' } */ const parseEntityText = (originEntityText: string) => { - const words = originEntityText - .split('.') - .map((word) => - word.startsWith('`') && word.endsWith('`') && word.length >= 3 - ? word.slice(1, -1) - : word - ); + // Use regex to split correctly, keeping backtick-wrapped parts as a whole. + // Match: backtick-wrapped content (including internal dots) or regular non-dot characters. + // '`xx.xx`' should be treated as a whole word `xx.xx`. + const regex = /`[^`]+`|[^.]+/g; + const matches = originEntityText.match(regex) || []; + + const words = matches.map((word) => { + if (word.startsWith('`') && word.endsWith('`') && word.length >= 3) { + const content = word.slice(1, -1); + // Only remove backticks when content contains only letters, numbers, and underscores + if (/^[a-zA-Z0-9_]+$/.test(content)) { + return content; + } + } + return word; + }); const length = words.length; if (length >= 3) { @@ -247,6 +256,15 @@ const getPureEntityText = (originEntityText: string) => { return parseEntityText(originEntityText).pureEntityText; }; +/** + * Remove backticks from text for filter matching + * @param text - The text that may contain backticks + * @returns The text without backticks + */ +const removeBackticks = (text: string): string => { + return text.replace(/`/g, ''); +}; + /** * Check if two entity paths match, considering schema information * @param createTablePath - The path from CREATE TABLE statement @@ -382,27 +400,37 @@ const getColumnCompletions = async ( 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; + if (columnRepeatCount > 1 && isIncludeInMultipleTables) { + const newLabel = `${column._tableName}.${column.label}`; + return { + ...column, + label: newLabel, + filterText: removeBackticks(newLabel), + insertText: `${column._tableName}.${column._columnText}` + }; + } + return 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.LITERAL ? 'table' : 'derived table', - sortText: '1' + tableName - }; - }); + const tableCompletionItems = + sourceTables.length > 1 + ? sourceTables.map((tb) => { + const tableName = tb[AttrName.alias]?.text ?? getPureEntityText(tb.text); + return { + label: tableName, + filterText: removeBackticks(tableName), + kind: languages.CompletionItemKind.Field, + detail: + tb.declareType === TableDeclareType.LITERAL + ? 'table' + : 'derived table', + sortText: '1' + tableName + }; + }) + : []; result.push(...tableCompletionItems); } else if (wordRanges.length === 2 && words[1] === '.') { @@ -488,12 +516,14 @@ const getSpecificTableColumns = ( tb.columns?.map((column) => { const columnName = column[AttrName.alias]?.text || getPureEntityText(column.text); + const label = + columnName + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''); return { - label: - columnName + - (column[AttrName.colType]?.text - ? `(${column[AttrName.colType].text})` - : ''), + label, + filterText: removeBackticks(label), insertText: columnName, kind: languages.CompletionItemKind.EnumMember, detail: `\`${tableName}\`'s column`, @@ -534,6 +564,7 @@ const getSpecificDerivedTableColumns = ( column[AttrName.alias]?.text || getPureEntityText(column.text); return { label: columnName, + filterText: removeBackticks(columnName), insertText: columnName, kind: languages.CompletionItemKind.EnumMember, detail: `\`${tableName}\`'s column`, @@ -576,12 +607,14 @@ const getSpecificCTASTableColumns = ( .map((column) => { const columnName = column[AttrName.alias]?.text || getPureEntityText(column.text); + const label = + columnName + + (column[AttrName.colType]?.text + ? `(${column[AttrName.colType].text})` + : ''); return { - label: - columnName + - (column[AttrName.colType]?.text - ? `(${column[AttrName.colType].text})` - : ''), + label, + filterText: removeBackticks(label), insertText: columnName, kind: languages.CompletionItemKind.EnumMember, detail: `\`${tableName}\`'s column`, @@ -649,6 +682,7 @@ const getSyntaxCompletionItems = async ( const tableName = getPureEntityText(tb.text); return { label: tableName, + filterText: removeBackticks(tableName), kind: languages.CompletionItemKind.Field, detail: 'table', sortText: '1' + tableName diff --git a/website/src/languages/helpers/dbMetaProvider.ts b/website/src/languages/helpers/dbMetaProvider.ts index 4a47a011..9fd8b599 100644 --- a/website/src/languages/helpers/dbMetaProvider.ts +++ b/website/src/languages/helpers/dbMetaProvider.ts @@ -21,16 +21,27 @@ const prefixLabel = (languageId: string, text: string) => { return prefix ? `${prefix}_${text}` : text; }; +/** + * Remove backticks from text for filter matching + */ +const removeBackticks = (text: string): string => { + return text.replace(/`/g, ''); +}; + /** * Get all catalogs */ export function getCatalogs(languageId: string) { - const catCompletions = catalogList.map((cat) => ({ - label: prefixLabel(languageId, cat), - kind: languages.CompletionItemKind.Field, - detail: 'Remote: catalog', - sortText: '1' + prefixLabel(languageId, cat) - })); + const catCompletions = catalogList.map((cat) => { + const label = prefixLabel(languageId, cat); + return { + label, + filterText: removeBackticks(label), + kind: languages.CompletionItemKind.Field, + detail: 'Remote: catalog', + sortText: '1' + label + }; + }); return Promise.resolve(catCompletions); } @@ -40,12 +51,16 @@ export function getCatalogs(languageId: string) { export function getDataBases(languageId: string, catalog?: string) { const databases = catalog ? databaseList : tmpDatabaseList; - const databaseCompletions = databases.map((db) => ({ - label: prefixLabel(languageId, db), - kind: languages.CompletionItemKind.Field, - detail: 'Remote: database', - sortText: '1' + prefixLabel(languageId, db) - })); + const databaseCompletions = databases.map((db) => { + const label = prefixLabel(languageId, db); + return { + label, + filterText: removeBackticks(label), + kind: languages.CompletionItemKind.Field, + detail: 'Remote: database', + sortText: '1' + label + }; + }); return Promise.resolve(databaseCompletions); } @@ -56,12 +71,16 @@ export function getDataBases(languageId: string, catalog?: string) { export function getSchemas(languageId: string, catalog?: string) { const schemas = catalog ? schemaList : tmpSchemaList; - const schemaCompletions = schemas.map((sc) => ({ - label: prefixLabel(languageId, sc), - kind: languages.CompletionItemKind.Field, - detail: 'Remote: schema', - sortText: '1' + prefixLabel(languageId, sc) - })); + const schemaCompletions = schemas.map((sc) => { + const label = prefixLabel(languageId, sc); + return { + label, + filterText: removeBackticks(label), + kind: languages.CompletionItemKind.Field, + detail: 'Remote: schema', + sortText: '1' + label + }; + }); return Promise.resolve(schemaCompletions); } @@ -72,12 +91,16 @@ export function getSchemas(languageId: string, catalog?: string) { export function getTables(languageId: string, catalog?: string, database?: string) { const tables = catalog && database ? tableList : tmpTableList; - const tableCompletions = tables.map((tb) => ({ - label: prefixLabel(languageId, tb), - kind: languages.CompletionItemKind.Field, - detail: 'Remote: table', - sortText: '1' + prefixLabel(languageId, tb) - })); + const tableCompletions = tables.map((tb) => { + const label = prefixLabel(languageId, tb); + return { + label, + filterText: removeBackticks(label), + kind: languages.CompletionItemKind.Field, + detail: 'Remote: table', + sortText: '1' + label + }; + }); return Promise.resolve(tableCompletions); } @@ -88,12 +111,16 @@ export function getTables(languageId: string, catalog?: string, database?: strin export function getViews(languageId: string, catalog?: string, database?: string) { const views = catalog && database ? viewList : tmpViewList; - const viewCompletions = views.map((v) => ({ - label: prefixLabel(languageId, v), - kind: languages.CompletionItemKind.Field, - detail: 'Remote: view', - sortText: '1' + prefixLabel(languageId, v) - })); + const viewCompletions = views.map((v) => { + const label = prefixLabel(languageId, v); + return { + label, + filterText: removeBackticks(label), + kind: languages.CompletionItemKind.Field, + detail: 'Remote: view', + sortText: '1' + label + }; + }); return Promise.resolve(viewCompletions); } @@ -114,13 +141,17 @@ export function getColumns(languageId: string, tableName: string): Promise ({ - label: `${col.name}(${col.type})`, - insertText: col.name, - kind: languages.CompletionItemKind.EnumMember, - detail: `Remote: \`${tableName}\`'s column`, - sortText: '0' + tableName + col.name - })); + const columnCompletions = mockColumns.map((col) => { + const label = `${col.name}(${col.type})`; + return { + label, + filterText: removeBackticks(label), + insertText: col.name, + kind: languages.CompletionItemKind.EnumMember, + detail: `Remote: \`${tableName}\`'s column`, + sortText: '0' + tableName + col.name + }; + }); return Promise.resolve(columnCompletions); }