feat(editor): 支持 SELECT * 展开为字段列表
* ✨ feat(editor): 支持将 SELECT * 展开为字段列表
- 新增右键菜单及 `Mod+Shift+X` 快捷键,可展开 `*` 和 `别名.*`
- 基于语义分析识别单表来源、别名及原始引用符,并按数据库方言处理字段名转义
- 优先读取本地或远程表字段元数据,单表纯星号查询可回退使用执行结果字段
- 增加字段加载失败提示及多语言文案
- 补充星号展开、结果匹配和快捷键注册测试
* fix(editor): selectStar 回退加词边界检查并记录加载失败
---------
Co-authored-by: t8y2 <t8y2@users.noreply.github.com>
Co-authored-by: skyler <1156263951@qq.com>
This commit is contained in:
parent
fa6540b739
commit
5c281b1653
|
|
@ -0,0 +1 @@
|
|||
/Users/skyler/VsCodeProjects/dbx-workspace/dbx/apps/desktop/node_modules
|
||||
|
|
@ -32,6 +32,7 @@ import { useSettingsStore } from "@/stores/settingsStore";
|
|||
import { useTheme } from "@/composables/useTheme";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import {
|
||||
buildSelectStarExpansion,
|
||||
buildSqlCompletionItemsFromContext,
|
||||
getSqlFunctionSignatureHelp,
|
||||
getSqlCompletionContext,
|
||||
|
|
@ -39,11 +40,12 @@ import {
|
|||
isSqlCompletionSuppressedContext,
|
||||
isSqlLikeCompletionStatement,
|
||||
recordCompletionSelection,
|
||||
selectStarResultColumnsMatch,
|
||||
shouldAutoOpenSqlCompletion,
|
||||
shouldChainSqlCompletionAfterAccept,
|
||||
extractCteDefinitions,
|
||||
} from "@/lib/sql/sqlCompletion";
|
||||
import { sqlCompletionContextFromSemantic } from "@/lib/sql/semantic/completion";
|
||||
import { sqlCompletionContextFromSemantic, sqlSemanticSelectStarIsOnlyProjection, sqlSemanticSelectStarQualifierSql, sqlSemanticSelectStarTableSource } from "@/lib/sql/semantic/completion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
import { mergeSqlSemanticReferenceAnalysis, resolveSqlSemanticNavigationTarget } from "@/lib/sql/semantic/references";
|
||||
import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearch/elasticsearchCompletion";
|
||||
|
|
@ -130,7 +132,7 @@ import {
|
|||
import { sqlReferenceAnalysisDialectFor } from "@/lib/sql/semantic/dialect";
|
||||
import { buildRedisSyntaxDiagnostics, shouldRunRedisDiagnostics } from "@/lib/redis/redisSyntaxDiagnostics";
|
||||
import { buildRedisCompletionItemsFromContext, getRedisCompletionContext, getRedisCompletionResultValidFor, shouldAutoOpenRedisCompletion, takesKeyArgument, type RedisCompletionItem } from "@/lib/redis/redisCompletion";
|
||||
import type { SqlCompletionColumn, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject, SqlCompletionReferencedTable, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionForeignKey, SqlCompletionItem, SqlCompletionObject, SqlCompletionReferencedTable, SqlCompletionTable } from "@/lib/sql/sqlCompletion";
|
||||
import type { CompletionAssistantObjectKind, ColumnInfo, DatabaseType, IndexInfo, SqlReferenceAnalysis, SqlServerCompletionContext, SqlTableReference, SqlTextSpan } from "@/types/database";
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -149,6 +151,10 @@ const props = defineProps<{
|
|||
compressRequestId?: number;
|
||||
executionError?: string;
|
||||
executionErrorSql?: string;
|
||||
resultColumns?: string[];
|
||||
resultSourceStatement?: string;
|
||||
resultSourceFrom?: number;
|
||||
resultSourceTo?: number;
|
||||
readOnly?: boolean;
|
||||
autoFocus?: boolean;
|
||||
forceWordWrap?: boolean;
|
||||
|
|
@ -314,6 +320,18 @@ const selectedSql = ref("");
|
|||
const executableSql = ref("");
|
||||
const contextObjectTarget = ref<SqlObjectNavigationTarget | null>(null);
|
||||
|
||||
interface SelectStarExpansionTarget {
|
||||
from: number;
|
||||
to: number;
|
||||
reference: SqlCompletionReferencedTable;
|
||||
context: SqlCompletionContext;
|
||||
qualifierSql?: string;
|
||||
statementSql: string;
|
||||
allowResultColumnsFallback: boolean;
|
||||
}
|
||||
|
||||
const selectStarExpansionTarget = ref<SelectStarExpansionTarget | null>(null);
|
||||
|
||||
const hasSelectedSql = computed(() => selectedSql.value.trim().length > 0);
|
||||
const canCopySelectedSql = computed(() => selectedSql.value.length > 0);
|
||||
const canExecuteContextSql = computed(() => executableSql.value.trim().length > 0);
|
||||
|
|
@ -792,14 +810,91 @@ function closePicker() {
|
|||
view.value?.focus();
|
||||
}
|
||||
|
||||
function syncContextMenuState(currentView: EditorViewType) {
|
||||
function syncContextMenuState(currentView: EditorViewType, starPosition?: number) {
|
||||
selectedSql.value = selectedSqlFromView(currentView);
|
||||
executableSql.value = executableSqlFromView(currentView);
|
||||
selectStarExpansionTarget.value = selectStarExpansionTargetForView(currentView, starPosition);
|
||||
}
|
||||
|
||||
function selectStarExpansionTargetForView(currentView: EditorViewType, position?: number): SelectStarExpansionTarget | null {
|
||||
if (!props.connectionId || props.database == null || props.readOnly || !SEMANTIC_SQL_COMPLETION_ENABLED) return null;
|
||||
|
||||
const sql = currentView.state.doc.toString();
|
||||
const selection = currentView.state.selection.main;
|
||||
let cursor: number;
|
||||
if (position != null) {
|
||||
if (sql[position] === "*") {
|
||||
cursor = position + 1;
|
||||
} else if (sql[position - 1] === "*") {
|
||||
cursor = position;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (!selection.empty) {
|
||||
if (currentView.state.sliceDoc(selection.from, selection.to) !== "*") return null;
|
||||
cursor = selection.to;
|
||||
} else if (sql[selection.head] === "*") {
|
||||
cursor = selection.head + 1;
|
||||
} else if (sql[selection.head - 1] === "*") {
|
||||
cursor = selection.head;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = buildSqlSemanticModel(sql, cursor, sqlCompletionDialectOptions());
|
||||
const intent = model.cursorIntent;
|
||||
if (intent.kind !== "star" || intent.confidence !== "high" || intent.replacementRange.end - intent.replacementRange.start !== 1 || sql.slice(intent.replacementRange.start, intent.replacementRange.end) !== "*") return null;
|
||||
if (position == null && !selection.empty && (selection.from !== intent.replacementRange.start || selection.to !== intent.replacementRange.end)) return null;
|
||||
|
||||
const starToken = model.tokens.find((token) => token.span.start === intent.replacementRange.start && token.span.end === intent.replacementRange.end && token.text === "*");
|
||||
if (!starToken) return null;
|
||||
let isSelectProjection = false;
|
||||
for (let index = model.tokens.length - 1; index >= 0; index -= 1) {
|
||||
const token = model.tokens[index];
|
||||
if (!token || token.span.end > starToken.span.start || token.depth !== starToken.depth || token.kind !== "word") continue;
|
||||
if (token.normalized === "from") return null;
|
||||
if (token.normalized === "select") {
|
||||
isSelectProjection = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isSelectProjection) return null;
|
||||
|
||||
const source = sqlSemanticSelectStarTableSource(model);
|
||||
if (!source) return null;
|
||||
|
||||
const sourceTarget = queryTableCandidateAtSqlPosition({
|
||||
connectionId: props.connectionId,
|
||||
database: props.database,
|
||||
schema: props.schema,
|
||||
databaseType: props.databaseType,
|
||||
sql,
|
||||
position: source.qualifiedName?.span.start ?? source.sourceSpan.start,
|
||||
});
|
||||
const reference: SqlCompletionReferencedTable = {
|
||||
name: sourceTarget?.tableName ?? source.metadataTarget?.table ?? source.name,
|
||||
database: sourceTarget?.database ?? source.metadataTarget?.database,
|
||||
schema: sourceTarget?.schema ?? source.metadataTarget?.schema,
|
||||
alias: source.alias,
|
||||
};
|
||||
const legacyContext = getSqlCompletionContext(sql, cursor, sqlCompletionDialectOptions());
|
||||
const context = sqlCompletionContextFromSemantic(model, legacyContext);
|
||||
if (context.statementKind !== "select" || !context.onStar) return null;
|
||||
|
||||
return {
|
||||
from: intent.replacementRange.start,
|
||||
to: intent.replacementRange.end,
|
||||
reference,
|
||||
context: { ...context, referencedTables: [reference] },
|
||||
qualifierSql: sqlSemanticSelectStarQualifierSql(model),
|
||||
statementSql: model.statement.text,
|
||||
allowResultColumnsFallback: model.rowSources.length === 1 && sqlSemanticSelectStarIsOnlyProjection(model),
|
||||
};
|
||||
}
|
||||
|
||||
function syncContextMenuStateAtEvent(currentView: EditorViewType, event: MouseEvent) {
|
||||
syncContextMenuState(currentView);
|
||||
const pos = currentView.posAtCoords({ x: event.clientX, y: event.clientY });
|
||||
syncContextMenuState(currentView, pos ?? undefined);
|
||||
if (pos == null) {
|
||||
contextObjectTarget.value = null;
|
||||
return;
|
||||
|
|
@ -1327,6 +1422,9 @@ function selectSqlLineFromGutter(currentView: EditorViewType, line: { from: numb
|
|||
|
||||
const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
const shortcuts = normalizeShortcutSettings(settingsStore.editorSettings.shortcuts);
|
||||
// The menu closes before running its action, so retain this right-click's
|
||||
// resolved target instead of reading state after the close handler runs.
|
||||
const starExpansionTarget = selectStarExpansionTarget.value;
|
||||
return [
|
||||
...(props.hideExecutionControls
|
||||
? []
|
||||
|
|
@ -1357,6 +1455,13 @@ const contextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
},
|
||||
]),
|
||||
...queryContextObjectActions(contextObjectTarget.value?.type).map(contextObjectMenuItem),
|
||||
{
|
||||
label: t("editor.contextMenu.expandSelectStar"),
|
||||
action: () => void expandSelectStar(starExpansionTarget),
|
||||
disabled: !starExpansionTarget,
|
||||
icon: Table2,
|
||||
shortcut: shortcuts.expandSelectStar,
|
||||
},
|
||||
{ label: "", separator: true },
|
||||
{
|
||||
label: t("editor.contextMenu.copySelection"),
|
||||
|
|
@ -1445,6 +1550,12 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
|||
void formatCurrentSql();
|
||||
return true;
|
||||
}),
|
||||
...binding(shortcuts.expandSelectStar, (currentView) => {
|
||||
const target = selectStarExpansionTargetForView(currentView);
|
||||
if (!target) return false;
|
||||
void expandSelectStar(target);
|
||||
return true;
|
||||
}),
|
||||
...binding(shortcuts.indentMore, (view) => codeMirrorIndentMore?.(view) ?? false),
|
||||
...binding(shortcuts.indentLess, (view) => codeMirrorIndentLess?.(view) ?? false),
|
||||
...binding(shortcuts.duplicateLine, (view) => codeMirrorCopyLineDown?.(view) ?? false),
|
||||
|
|
@ -1788,19 +1899,79 @@ async function findExactSemanticDiagnosticTable(table: SqlTableReference): Promi
|
|||
return remoteMatches.find((item) => completionTablesMatch(item, table)) ?? null;
|
||||
}
|
||||
|
||||
async function ensureColumnsForTable(table: { name: string; database?: string | null; schema?: string | null }): Promise<boolean> {
|
||||
async function ensureColumnsForTable(table: { name: string; database?: string | null; schema?: string | null }, reference?: Pick<SqlCompletionReferencedTable, "nameQuoted" | "schemaQuoted">): Promise<boolean> {
|
||||
if (isVirtualCompletionTableReference(table)) return false;
|
||||
const cacheKey = completionCacheKey(table);
|
||||
if (cachedColumnsByTable.has(cacheKey)) return true;
|
||||
if (!props.connectionId || props.database == null) return false;
|
||||
const target = completionMetadataTarget(table);
|
||||
if (!target) return false;
|
||||
const columns = await listCompletionColumnsForEditor(props.connectionId, target.database, table.name, target.schema, target.catalog);
|
||||
const localColumns = connectionStore.lookupLocalCompletionColumns(props.connectionId, target.database, table.name, target.schema, target.catalog, completionColumnRequestContext(reference));
|
||||
if (localColumns.length > 0) {
|
||||
cachedColumnsByTable.set(cacheKey, localColumns);
|
||||
loadedColumnsByTable.add(cacheKey.toLowerCase());
|
||||
return true;
|
||||
}
|
||||
const columns = await listCompletionColumnsForEditor(props.connectionId, target.database, table.name, target.schema, target.catalog, reference);
|
||||
cachedColumnsByTable.set(cacheKey, columns);
|
||||
loadedColumnsByTable.add(cacheKey.toLowerCase());
|
||||
return true;
|
||||
}
|
||||
|
||||
function resultColumnsForSelectStar(target: SelectStarExpansionTarget, sql: string): SqlCompletionColumn[] {
|
||||
if (
|
||||
!target.allowResultColumnsFallback ||
|
||||
!selectStarResultColumnsMatch({
|
||||
currentSql: sql,
|
||||
targetFrom: target.from,
|
||||
targetTo: target.to,
|
||||
statementSql: target.statementSql,
|
||||
sourceStatement: props.resultSourceStatement,
|
||||
sourceFrom: props.resultSourceFrom,
|
||||
sourceTo: props.resultSourceTo,
|
||||
})
|
||||
)
|
||||
return [];
|
||||
return (props.resultColumns ?? [])
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean)
|
||||
.map((name) => ({ name, table: target.reference.name, schema: target.reference.schema }));
|
||||
}
|
||||
|
||||
async function expandSelectStar(target = selectStarExpansionTarget.value) {
|
||||
const currentView = view.value;
|
||||
if (!currentView || props.readOnly) return;
|
||||
if (!target) return;
|
||||
|
||||
const originalDocument = currentView.state.doc.toString();
|
||||
try {
|
||||
await ensureColumnsForTable(target.reference, target.reference);
|
||||
} catch (error) {
|
||||
console.warn("expandSelectStar: failed to load columns", error);
|
||||
}
|
||||
|
||||
if (view.value !== currentView || currentView.state.doc.toString() !== originalDocument || currentView.state.sliceDoc(target.from, target.to) !== "*") return;
|
||||
const columns = cachedColumnsByTable.get(completionCacheKey(target.reference));
|
||||
const expansionColumns = columns?.length ? columns : resultColumnsForSelectStar(target, originalDocument);
|
||||
if (expansionColumns.length === 0) {
|
||||
toast(t("editor.contextMenu.expandSelectStarUnavailable"), 3000);
|
||||
return;
|
||||
}
|
||||
const expansion = buildSelectStarExpansion(target.context, new Map([[completionCacheKey(target.reference), expansionColumns]]), props.dialect, target.qualifierSql, props.databaseType);
|
||||
if (!expansion) {
|
||||
toast(t("editor.contextMenu.expandSelectStarUnavailable"), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
currentView.dispatch({
|
||||
changes: { from: target.from, to: target.to, insert: expansion },
|
||||
selection: { anchor: target.from + expansion.length },
|
||||
scrollIntoView: true,
|
||||
userEvent: "input.expandSelectStar",
|
||||
});
|
||||
currentView.focus();
|
||||
}
|
||||
|
||||
function isMissingTableMetadataError(error: unknown) {
|
||||
const message = String(error instanceof Error ? error.message : error).toLowerCase();
|
||||
return message.includes("42s02") || message.includes("1146") || message.includes("doesn't exist") || message.includes("does not exist") || message.includes("unknown table");
|
||||
|
|
|
|||
|
|
@ -972,6 +972,10 @@ defineExpose({ focusSearch, refreshData, refreshQueryEditorCompletionCache, hand
|
|||
:compress-request-id="compressSqlRequest?.tabId === activeTab.id ? compressSqlRequest.id : undefined"
|
||||
:execution-error="activeQueryError"
|
||||
:execution-error-sql="activeTab.lastExecutedSql"
|
||||
:result-columns="activeTab.result?.columns"
|
||||
:result-source-statement="activeTab.result?.sourceStatement"
|
||||
:result-source-from="activeTab.result?.sourceFrom"
|
||||
:result-source-to="activeTab.result?.sourceTo"
|
||||
:statement-execution-markers="activeStatementExecutionMarkers"
|
||||
:initial-viewport="activeTab.editorViewport"
|
||||
:initial-selection="activeTab.editorSelection"
|
||||
|
|
|
|||
|
|
@ -947,6 +947,8 @@ export default {
|
|||
uppercaseSelection: "Convert to uppercase",
|
||||
lowercaseSelection: "Convert to lowercase",
|
||||
delimitedList: "Convert to delimited list",
|
||||
expandSelectStar: "Expand * to columns",
|
||||
expandSelectStarUnavailable: "Unable to load columns for this table",
|
||||
findReplace: "Find/Replace",
|
||||
deleteEmptyLines: "Delete empty lines",
|
||||
selectAll: "Select all",
|
||||
|
|
@ -5256,6 +5258,7 @@ export default {
|
|||
shortcutSaveSql: "Save SQL",
|
||||
shortcutAcceptCompletion: "Accept completion",
|
||||
shortcutFormatSql: "Format SQL",
|
||||
shortcutExpandSelectStar: "Expand * to columns",
|
||||
shortcutToggleLineComment: "Toggle line comment",
|
||||
shortcutIndentMore: "Indent more",
|
||||
shortcutIndentLess: "Indent less",
|
||||
|
|
|
|||
|
|
@ -927,6 +927,8 @@ export default withEnglishFallback({
|
|||
lowercaseSelection: "Convertir a minusculas",
|
||||
selectAll: "Seleccionar todo",
|
||||
delimitedList: "Convertir a lista delimitada",
|
||||
expandSelectStar: "Expandir * a columnas",
|
||||
expandSelectStarUnavailable: "No se pudieron cargar las columnas de esta tabla",
|
||||
findReplace: "Buscar/Reemplazar",
|
||||
deleteEmptyLines: "Eliminar líneas vacías",
|
||||
},
|
||||
|
|
@ -4991,6 +4993,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "Guardar SQL",
|
||||
shortcutAcceptCompletion: "Aceptar completado",
|
||||
shortcutFormatSql: "Formatear SQL",
|
||||
shortcutExpandSelectStar: "Expandir * a columnas",
|
||||
shortcutToggleLineComment: "Alternar comentario de línea",
|
||||
shortcutIndentMore: "Aumentar sangría",
|
||||
shortcutIndentLess: "Reducir sangría",
|
||||
|
|
|
|||
|
|
@ -925,6 +925,8 @@ export default withEnglishFallback({
|
|||
lowercaseSelection: "Converti in minuscolo",
|
||||
selectAll: "Seleziona tutto",
|
||||
delimitedList: "Converti in lista delimitata",
|
||||
expandSelectStar: "Espandi * in colonne",
|
||||
expandSelectStarUnavailable: "Impossibile caricare le colonne di questa tabella",
|
||||
findReplace: "Trova/Sostituisci",
|
||||
deleteEmptyLines: "Elimina righe vuote",
|
||||
},
|
||||
|
|
@ -4991,6 +4993,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "Salva SQL",
|
||||
shortcutAcceptCompletion: "Accetta completamento",
|
||||
shortcutFormatSql: "Formatta SQL",
|
||||
shortcutExpandSelectStar: "Espandi * in colonne",
|
||||
shortcutToggleLineComment: "Attiva/disattiva commento di riga",
|
||||
shortcutIndentMore: "Aumenta rientro",
|
||||
shortcutIndentLess: "Riduci rientro",
|
||||
|
|
|
|||
|
|
@ -945,6 +945,8 @@ export default withEnglishFallback({
|
|||
lowercaseSelection: "小文字に変換",
|
||||
selectAll: "すべて選択",
|
||||
delimitedList: "区切り文字付きリストに変換",
|
||||
expandSelectStar: "* を列に展開",
|
||||
expandSelectStarUnavailable: "このテーブルの列を読み込めません",
|
||||
findReplace: "検索/置換",
|
||||
deleteEmptyLines: "空行を削除",
|
||||
},
|
||||
|
|
@ -5013,6 +5015,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "SQLを保存",
|
||||
shortcutAcceptCompletion: "補完を受け入れる",
|
||||
shortcutFormatSql: "SQLをフォーマット",
|
||||
shortcutExpandSelectStar: "* を列に展開",
|
||||
shortcutToggleLineComment: "行コメントを切り替え",
|
||||
shortcutIndentMore: "インデントを増やす",
|
||||
shortcutIndentLess: "インデントを減らす",
|
||||
|
|
|
|||
|
|
@ -867,6 +867,8 @@ export default withEnglishFallback({
|
|||
uppercaseSelection: "대문자로 변환",
|
||||
lowercaseSelection: "소문자로 변환",
|
||||
delimitedList: "구분자 목록으로 변환",
|
||||
expandSelectStar: "*을(를) 열로 확장",
|
||||
expandSelectStarUnavailable: "이 테이블의 열을 불러올 수 없습니다",
|
||||
findReplace: "찾기/바꾸기",
|
||||
deleteEmptyLines: "빈 줄 삭제",
|
||||
selectAll: "모두 선택",
|
||||
|
|
@ -4784,6 +4786,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "SQL 저장",
|
||||
shortcutAcceptCompletion: "완성 수락",
|
||||
shortcutFormatSql: "SQL 서식",
|
||||
shortcutExpandSelectStar: "*을(를) 열로 확장",
|
||||
shortcutToggleLineComment: "줄 주석 전환",
|
||||
shortcutIndentMore: "들여쓰기 늘리기",
|
||||
shortcutIndentLess: "들여쓰기 줄이기",
|
||||
|
|
|
|||
|
|
@ -926,6 +926,8 @@ export default withEnglishFallback({
|
|||
lowercaseSelection: "Converter para minúsculas",
|
||||
selectAll: "Selecionar tudo",
|
||||
delimitedList: "Converter para lista delimitada",
|
||||
expandSelectStar: "Expandir * em colunas",
|
||||
expandSelectStarUnavailable: "Não foi possível carregar as colunas desta tabela",
|
||||
findReplace: "Localizar/substituir",
|
||||
deleteEmptyLines: "Excluir linhas vazias",
|
||||
},
|
||||
|
|
@ -4993,6 +4995,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "Salvar SQL",
|
||||
shortcutAcceptCompletion: "Aceitar autocompletar",
|
||||
shortcutFormatSql: "Formatar SQL",
|
||||
shortcutExpandSelectStar: "Expandir * em colunas",
|
||||
shortcutToggleLineComment: "Alternar comentário de linha",
|
||||
shortcutIndentMore: "Aumentar recuo",
|
||||
shortcutIndentLess: "Reduzir recuo",
|
||||
|
|
|
|||
|
|
@ -948,6 +948,8 @@ export default withEnglishFallback({
|
|||
uppercaseSelection: "转为大写",
|
||||
lowercaseSelection: "转为小写",
|
||||
delimitedList: "转换为带分隔符的列表",
|
||||
expandSelectStar: "将 * 展开为字段",
|
||||
expandSelectStarUnavailable: "无法读取该表的字段信息",
|
||||
findReplace: "查找/替换",
|
||||
deleteEmptyLines: "删除空行",
|
||||
selectAll: "全选",
|
||||
|
|
@ -5253,6 +5255,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "保存 SQL",
|
||||
shortcutAcceptCompletion: "接受补全",
|
||||
shortcutFormatSql: "格式化 SQL",
|
||||
shortcutExpandSelectStar: "将 * 展开为字段",
|
||||
shortcutToggleLineComment: "切换行注释",
|
||||
shortcutIndentMore: "增加缩进",
|
||||
shortcutIndentLess: "减少缩进",
|
||||
|
|
|
|||
|
|
@ -925,6 +925,8 @@ export default withEnglishFallback({
|
|||
lowercaseSelection: "轉為小寫",
|
||||
selectAll: "全選",
|
||||
delimitedList: "轉換為帶分隔符的清單",
|
||||
expandSelectStar: "將 * 展開為欄位",
|
||||
expandSelectStarUnavailable: "無法讀取此資料表的欄位資訊",
|
||||
findReplace: "查找/取代",
|
||||
deleteEmptyLines: "刪除空行",
|
||||
},
|
||||
|
|
@ -4443,6 +4445,7 @@ export default withEnglishFallback({
|
|||
shortcutSaveSql: "儲存 SQL",
|
||||
shortcutAcceptCompletion: "接受補全",
|
||||
shortcutFormatSql: "格式化 SQL",
|
||||
shortcutExpandSelectStar: "將 * 展開為欄位",
|
||||
shortcutToggleLineComment: "切換行註解",
|
||||
shortcutIndentMore: "增加縮排",
|
||||
shortcutIndentLess: "減少縮排",
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ describe("shortcutRegistry editor actions", () => {
|
|||
expect(findShortcutConflict("executeSqlInNewResultTab", DEFAULT_SHORTCUT_SETTINGS.executeSqlInNewResultTab, DEFAULT_SHORTCUT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it("registers a conflict-free shortcut for expanding SELECT stars", () => {
|
||||
const definition = SHORTCUT_DEFINITIONS.find((item) => item.id === "expandSelectStar");
|
||||
|
||||
expect(definition).toMatchObject({ scope: "editor", defaultShortcut: "Mod+Shift+X" });
|
||||
expect(shortcutToCodeMirrorKey(DEFAULT_SHORTCUT_SETTINGS.expandSelectStar)).toBe("Mod-Shift-x");
|
||||
expect(findShortcutConflict("expandSelectStar", DEFAULT_SHORTCUT_SETTINGS.expandSelectStar, DEFAULT_SHORTCUT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it("resolves the close-other-tabs default per platform and heals cross-platform synced defaults", () => {
|
||||
// 本测试环境(darwin):默认应为 macOS 组合
|
||||
expect(DEFAULT_SHORTCUT_SETTINGS.closeOtherTabs).toBe(closeOtherTabsDefaultShortcut());
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { sqlSemanticCompletionScope, sqlSemanticLocalColumnsByTable, sqlSemanticProjectionAliasColumns } from "@/lib/sql/semantic/completion";
|
||||
import { sqlSemanticCompletionScope, sqlSemanticLocalColumnsByTable, sqlSemanticProjectionAliasColumns, sqlSemanticSelectStarIsOnlyProjection, sqlSemanticSelectStarQualifierSql, sqlSemanticSelectStarTableSource } from "@/lib/sql/semantic/completion";
|
||||
import { SQL_SEMANTIC_BASELINE_FIXTURES, sqlFixtureCursor } from "@/lib/sql/semantic/fixtures";
|
||||
import { buildSqlSemanticModel, sqlSemanticTableNameSpans } from "@/lib/sql/semantic/model";
|
||||
|
||||
|
|
@ -29,6 +29,46 @@ describe("sqlSemanticModel baseline fixtures", () => {
|
|||
});
|
||||
}
|
||||
|
||||
it("does not treat SELECT as the qualifier of an unqualified star", () => {
|
||||
const { sql, cursor } = sqlFixtureCursor("select *| from apis as ap");
|
||||
const model = buildSqlSemanticModel(sql, cursor);
|
||||
|
||||
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "star", qualifierParts: [] }));
|
||||
});
|
||||
|
||||
it("retains the qualifier of a qualified star", () => {
|
||||
const { sql, cursor } = sqlFixtureCursor("select ap.*| from apis as ap");
|
||||
const model = buildSqlSemanticModel(sql, cursor);
|
||||
|
||||
expect(model.cursorIntent).toEqual(expect.objectContaining({ kind: "star", qualifierParts: ["ap"], targetSourceId: model.rowSources[0]?.id }));
|
||||
});
|
||||
|
||||
it("does not resolve an unqualified star to the only physical table among multiple row sources", () => {
|
||||
const { sql, cursor } = sqlFixtureCursor("select *| from users u join (select 1 as x) s on 1 = 1");
|
||||
const model = buildSqlSemanticModel(sql, cursor);
|
||||
|
||||
expect(model.rowSources.map((source) => source.kind)).toEqual(["table", "subquery"]);
|
||||
expect(sqlSemanticSelectStarTableSource(model)).toBeUndefined();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["select *| from apis", true],
|
||||
["select distinct *| from apis", true],
|
||||
["select ap.*| from apis ap", true],
|
||||
["select *, 1 as extra| from apis", false],
|
||||
["select ap.*, 1 as extra| from apis ap", false],
|
||||
] as const)("detects whether the star is the only projection in %s", (markedSql, expected) => {
|
||||
const { sql, cursor } = sqlFixtureCursor(markedSql);
|
||||
|
||||
expect(sqlSemanticSelectStarIsOnlyProjection(buildSqlSemanticModel(sql, cursor))).toBe(expected);
|
||||
});
|
||||
|
||||
it("retains the original quoting of a star qualifier", () => {
|
||||
const { sql, cursor } = sqlFixtureCursor('select "Order Alias".*| from orders as "Order Alias"');
|
||||
|
||||
expect(sqlSemanticSelectStarQualifierSql(buildSqlSemanticModel(sql, cursor, { databaseType: "postgres", dialect: "postgres" }))).toBe('"Order Alias"');
|
||||
});
|
||||
|
||||
it("does not mix row sources from inactive statements", () => {
|
||||
const { sql, cursor } = sqlFixtureCursor("select * from users u; select * from orders o where o.|");
|
||||
const model = buildSqlSemanticModel(sql, cursor);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { buildSqlCompletionItems, getSqlCompletionContext, shouldAutoOpenSqlCompletion } from "@/lib/sql/sqlCompletion";
|
||||
import { buildSelectStarExpansion, buildSqlCompletionItems, getSqlCompletionContext, selectStarResultColumnsMatch, shouldAutoOpenSqlCompletion } from "@/lib/sql/sqlCompletion";
|
||||
import { sqlCompletionContextFromSemantic } from "@/lib/sql/semantic/completion";
|
||||
import { buildSqlSemanticModel } from "@/lib/sql/semantic/model";
|
||||
|
||||
describe("sqlCompletion keyword snippets", () => {
|
||||
it("auto-opens and suggests SELECT when typing sel", () => {
|
||||
|
|
@ -14,6 +16,118 @@ describe("sqlCompletion keyword snippets", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("SELECT star expansion", () => {
|
||||
it("reuses completion column ordering for an unqualified star", () => {
|
||||
const sql = "SELECT * FROM apis";
|
||||
const context = getSqlCompletionContext(sql, "SELECT *".length);
|
||||
|
||||
expect(
|
||||
buildSelectStarExpansion(
|
||||
context,
|
||||
new Map([
|
||||
[
|
||||
"apis",
|
||||
[
|
||||
{ name: "id", table: "apis" },
|
||||
{ name: "created_at", table: "apis" },
|
||||
{ name: "method", table: "apis" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
),
|
||||
).toBe("id, created_at, method");
|
||||
});
|
||||
|
||||
it("preserves an alias while replacing only the star", () => {
|
||||
const sql = "SELECT ap.* FROM apis AS ap";
|
||||
const cursor = "SELECT ap.*".length;
|
||||
const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor), getSqlCompletionContext(sql, cursor));
|
||||
|
||||
expect(
|
||||
buildSelectStarExpansion(
|
||||
context,
|
||||
new Map([
|
||||
[
|
||||
"apis",
|
||||
[
|
||||
{ name: "id", table: "apis" },
|
||||
{ name: "created_at", table: "apis" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
),
|
||||
).toBe("id, ap.created_at");
|
||||
});
|
||||
|
||||
it.each([
|
||||
["postgres", "postgres", '"Order Alias"', '"created at"'],
|
||||
["mysql", "mysql", "`Order Alias`", "`created at`"],
|
||||
["sqlserver", "sqlserver", "[Order Alias]", "[created at]"],
|
||||
["oracle", "mysql", '"Order Alias"', '"created at"'],
|
||||
] as const)("preserves a quoted %s alias for every expanded column", (databaseType, dialect, qualifierSql, quotedColumn) => {
|
||||
const sql = `SELECT ${qualifierSql}.* FROM orders AS ${qualifierSql}`;
|
||||
const cursor = sql.indexOf("*") + 1;
|
||||
const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor, { databaseType, dialect }), getSqlCompletionContext(sql, cursor, { databaseType, dialect }));
|
||||
|
||||
expect(
|
||||
buildSelectStarExpansion(
|
||||
context,
|
||||
new Map([
|
||||
[
|
||||
"orders",
|
||||
[
|
||||
{ name: "id", table: "orders" },
|
||||
{ name: "created at", table: "orders" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
dialect,
|
||||
qualifierSql,
|
||||
databaseType,
|
||||
),
|
||||
).toBe(`id, ${qualifierSql}.${quotedColumn}`);
|
||||
});
|
||||
|
||||
it("expands an unqualified star from result columns when the table has an alias", () => {
|
||||
const sql = "select *\nfrom apis as ap\nlimit 100;";
|
||||
const cursor = "select *".length;
|
||||
const context = sqlCompletionContextFromSemantic(buildSqlSemanticModel(sql, cursor), getSqlCompletionContext(sql, cursor));
|
||||
|
||||
expect(
|
||||
buildSelectStarExpansion(
|
||||
context,
|
||||
new Map([
|
||||
[
|
||||
"apis",
|
||||
[
|
||||
{ name: "id", table: "apis" },
|
||||
{ name: "created_at", table: "apis" },
|
||||
{ name: "updated_at", table: "apis" },
|
||||
{ name: "deleted_at", table: "apis" },
|
||||
{ name: "method", table: "apis" },
|
||||
],
|
||||
],
|
||||
]),
|
||||
),
|
||||
).toBe("id, created_at, updated_at, deleted_at, method");
|
||||
});
|
||||
|
||||
it("accepts result columns only when their source still contains the target star", () => {
|
||||
const currentSql = "select * from apis;\nselect * from users;";
|
||||
const sourceStatement = "select * from users";
|
||||
const sourceFrom = currentSql.lastIndexOf("select");
|
||||
const targetFrom = currentSql.lastIndexOf("*");
|
||||
|
||||
expect(selectStarResultColumnsMatch({ currentSql, targetFrom, targetTo: targetFrom + 1, statementSql: sourceStatement, sourceStatement, sourceFrom, sourceTo: sourceFrom + sourceStatement.length })).toBe(true);
|
||||
expect(selectStarResultColumnsMatch({ currentSql, targetFrom: currentSql.indexOf("*"), targetTo: currentSql.indexOf("*") + 1, statementSql: "select * from apis", sourceStatement, sourceFrom, sourceTo: sourceFrom + sourceStatement.length })).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects stale and incomplete result source metadata", () => {
|
||||
expect(selectStarResultColumnsMatch({ currentSql: "select * from users", targetFrom: 7, targetTo: 8, statementSql: "select * from users", sourceStatement: "select * from apis" })).toBe(false);
|
||||
expect(selectStarResultColumnsMatch({ currentSql: "select * from users", targetFrom: 7, targetTo: 8, statementSql: "select * from users", sourceStatement: "select * from users", sourceFrom: 0 })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sqlCompletion database functions", () => {
|
||||
it("suggests ClickHouse functions with canonical casing and preferred placeholders", () => {
|
||||
const sql = "SELECT tostart";
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ export type ShortcutActionId =
|
|||
| "executeSql"
|
||||
| "executeSqlInNewResultTab"
|
||||
| "formatSql"
|
||||
| "expandSelectStar"
|
||||
| "toggleLineComment"
|
||||
| "saveSql"
|
||||
| "acceptCompletion"
|
||||
|
|
@ -103,6 +104,12 @@ export const SHORTCUT_DEFINITIONS: ShortcutDefinition[] = [
|
|||
scope: "editor",
|
||||
defaultShortcut: "Shift+Mod+F",
|
||||
},
|
||||
{
|
||||
id: "expandSelectStar",
|
||||
labelKey: "settings.shortcutExpandSelectStar",
|
||||
scope: "editor",
|
||||
defaultShortcut: "Mod+Shift+X",
|
||||
},
|
||||
{
|
||||
id: "toggleLineComment",
|
||||
labelKey: "settings.shortcutToggleLineComment",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import type { SqlCompletionColumn, SqlCompletionContext, SqlCompletionReferencedTable } from "@/lib/sql/sqlCompletion";
|
||||
import { SQL_SEMANTIC_DIALECTS } from "@/lib/sql/semantic/dialect";
|
||||
import type { SqlSemanticModel, SqlSemanticRowSource } from "@/lib/sql/semantic/types";
|
||||
import type { SqlSemanticModel, SqlSemanticRowSource, SqlSemanticToken } from "@/lib/sql/semantic/types";
|
||||
|
||||
export type SqlSemanticCompletionScopeKind = "keyword" | "table" | "schema" | "catalog" | "routine" | "columns" | "local";
|
||||
|
||||
|
|
@ -13,6 +13,61 @@ export interface SqlSemanticCompletionScope {
|
|||
fallbackReason?: string;
|
||||
}
|
||||
|
||||
function isSemanticIdentifier(token: SqlSemanticToken | undefined): boolean {
|
||||
return token?.kind === "word" || token?.kind === "quoted_identifier";
|
||||
}
|
||||
|
||||
function selectStarToken(model: SqlSemanticModel): SqlSemanticToken | undefined {
|
||||
const range = model.cursorIntent.replacementRange;
|
||||
if (model.cursorIntent.kind !== "star") return undefined;
|
||||
return model.tokens.find((token) => token.text === "*" && token.span.start === range.start && token.span.end === range.end);
|
||||
}
|
||||
|
||||
export function sqlSemanticSelectStarTableSource(model: SqlSemanticModel): SqlSemanticRowSource | undefined {
|
||||
if (model.cursorIntent.kind !== "star") return undefined;
|
||||
const source = model.cursorIntent.targetSourceId ? model.rowSources.find((candidate) => candidate.id === model.cursorIntent.targetSourceId) : model.rowSources.length === 1 ? model.rowSources[0] : undefined;
|
||||
return source?.kind === "table" ? source : undefined;
|
||||
}
|
||||
|
||||
export function sqlSemanticSelectStarQualifierSql(model: SqlSemanticModel): string | undefined {
|
||||
const star = selectStarToken(model);
|
||||
if (!star) return undefined;
|
||||
const starIndex = model.tokens.indexOf(star);
|
||||
let index = starIndex - 1;
|
||||
if (model.tokens[index]?.text !== ".") return undefined;
|
||||
const qualifierEnd = model.tokens[index]!.span.start;
|
||||
index -= 1;
|
||||
if (!isSemanticIdentifier(model.tokens[index])) return undefined;
|
||||
let qualifierStart = model.tokens[index]!.span.start;
|
||||
while (index >= 2 && model.tokens[index - 1]?.text === "." && isSemanticIdentifier(model.tokens[index - 2])) {
|
||||
index -= 2;
|
||||
qualifierStart = model.tokens[index]!.span.start;
|
||||
}
|
||||
return model.sql.slice(qualifierStart, qualifierEnd).trim() || undefined;
|
||||
}
|
||||
|
||||
export function sqlSemanticSelectStarIsOnlyProjection(model: SqlSemanticModel): boolean {
|
||||
const star = selectStarToken(model);
|
||||
if (!star) return false;
|
||||
let selectIndex = -1;
|
||||
for (let index = model.tokens.length - 1; index >= 0; index -= 1) {
|
||||
const token = model.tokens[index];
|
||||
if (!token || token.span.end > star.span.start || token.depth !== star.depth) continue;
|
||||
if (token.kind === "word" && token.normalized === "select") {
|
||||
selectIndex = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (selectIndex < 0) return false;
|
||||
const fromIndex = model.tokens.findIndex((token, index) => index > selectIndex && token.span.start >= star.span.end && token.depth === star.depth && token.kind === "word" && token.normalized === "from");
|
||||
const projectionEnd = fromIndex >= 0 ? model.tokens[fromIndex]!.span.start : model.statement.span.end;
|
||||
const projectionTokens = model.tokens.filter((token) => token.span.start >= model.tokens[selectIndex]!.span.end && token.span.end <= projectionEnd && token.kind !== "comment");
|
||||
if (projectionTokens[0]?.kind === "word" && (projectionTokens[0].normalized === "all" || projectionTokens[0].normalized === "distinct")) projectionTokens.shift();
|
||||
if (projectionTokens.pop() !== star) return false;
|
||||
if (projectionTokens.length === 0) return true;
|
||||
return projectionTokens.length % 2 === 0 && projectionTokens.every((token, index) => (index % 2 === 0 ? isSemanticIdentifier(token) : token.text === "."));
|
||||
}
|
||||
|
||||
export function sqlSemanticReferencedTables(model: SqlSemanticModel): SqlCompletionReferencedTable[] {
|
||||
return model.rowSources
|
||||
.filter((source) => source.kind !== "unknown")
|
||||
|
|
|
|||
|
|
@ -675,7 +675,8 @@ function sourceForQualifier(sources: readonly SqlSemanticRowSource[], qualifierP
|
|||
|
||||
function starQualifierParts(before: readonly SqlSemanticToken[], starIndex: number, dialect: SqlSemanticDialectAdapter): string[] {
|
||||
let index = starIndex - 1;
|
||||
if (before[index]?.text === ".") index -= 1;
|
||||
if (before[index]?.text !== ".") return [];
|
||||
index -= 1;
|
||||
const qualifierParts: string[] = [];
|
||||
while (index >= 0) {
|
||||
const identifier = before[index];
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import type { SqlSemanticBuildOptions, SqlSemanticSpan } from "@/lib/sql/semanti
|
|||
import { DEFAULT_SQL_SNIPPETS, MANTICORESEARCH_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
import { requiresPostgresIdentifierQuote } from "@/lib/sql/sqlIdentifier";
|
||||
import { containsHan, orderedSubsequenceSpan, pinyinFirstLetters } from "@/lib/common/pinyin";
|
||||
import { quoteTableIdentifier } from "@/lib/table/tableSelectSql";
|
||||
|
||||
export { DEFAULT_SQL_SNIPPETS, resolveSqlSnippetBodyForDatabase } from "@/lib/sql/sqlSnippetTemplates";
|
||||
|
||||
|
|
@ -2861,6 +2862,14 @@ export function quoteSqlIdentifier(identifier: string, dialect?: "mysql" | "post
|
|||
|
||||
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()));
|
||||
|
||||
function quoteSelectStarColumnIdentifier(identifier: string, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): string {
|
||||
if (!requiresPostgresIdentifierQuote(identifier, POSTGRES_IDENTIFIER_KEYWORDS)) return identifier;
|
||||
if (databaseType) return quoteTableIdentifier(databaseType, identifier);
|
||||
if (dialect === "mysql") return `\`${identifier.replaceAll("`", "``")}\``;
|
||||
if (dialect === "sqlserver") return `[${identifier.replaceAll("]", "]]")}]`;
|
||||
return quoteSqlIdentifier(identifier, dialect);
|
||||
}
|
||||
|
||||
function buildTableItems(
|
||||
context: Pick<SqlCompletionContext, "prefix" | "qualifier">,
|
||||
tables: SqlCompletionTable[],
|
||||
|
|
@ -3167,16 +3176,38 @@ function buildPreferredKeywordItems(prefix: string, keywords: string[], keywordC
|
|||
}));
|
||||
}
|
||||
|
||||
function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem | null {
|
||||
function selectStarExpansionColumns(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>): SqlCompletionColumn[] {
|
||||
const columns = context.qualifier ? referencedTablesForSelectAllColumns(context).flatMap((ref) => columnsForSelectAllReferencedTable(ref, columnsByTable)) : [...columnsByTable.values()].flat();
|
||||
const uniqueColumns = uniqueColumnsByName(columns);
|
||||
if (uniqueColumns.length === 0) return null;
|
||||
return uniqueColumnsByName(columns);
|
||||
}
|
||||
|
||||
export function selectStarResultColumnsMatch(options: { currentSql: string; targetFrom: number; targetTo: number; statementSql: string; sourceStatement?: string; sourceFrom?: number; sourceTo?: number }): boolean {
|
||||
if (!options.sourceStatement) return false;
|
||||
const hasSourceFrom = typeof options.sourceFrom === "number";
|
||||
const hasSourceTo = typeof options.sourceTo === "number";
|
||||
if (hasSourceFrom !== hasSourceTo) return false;
|
||||
if (!hasSourceFrom || !hasSourceTo) return options.statementSql === options.sourceStatement;
|
||||
// 词边界检查:已执行语句可能是当前内容的真前缀(如 `FROM users` → `FROM users_backup`),
|
||||
// 此时 slice 仍与 sourceStatement 相等,会用旧表列回退到新表。要求 sourceTo 落在标识符边界。
|
||||
const sourceToAtBoundary = !/[\w$]/.test(options.currentSql[options.sourceTo!] ?? "");
|
||||
return options.targetFrom >= options.sourceFrom! && options.targetTo <= options.sourceTo! && sourceToAtBoundary && options.currentSql.slice(options.sourceFrom, options.sourceTo) === options.sourceStatement;
|
||||
}
|
||||
|
||||
export function buildSelectStarExpansion(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, dialect?: "mysql" | "postgres" | "sqlserver", qualifierSql = context.qualifier, databaseType?: DatabaseType): string | null {
|
||||
const columns = selectStarExpansionColumns(context, columnsByTable);
|
||||
if (columns.length === 0) return null;
|
||||
// `alias.*` replaces only the `*`, so the first column must continue the already typed `alias.`.
|
||||
const expansion = context.qualifier ? buildSelectAllColumnExpansion(uniqueColumns, context.qualifier, true, dialect) : uniqueColumns.map((column) => quoteSqlIdentifier(column.name, dialect)).join(", ");
|
||||
return qualifierSql ? buildSelectAllColumnExpansion(columns, qualifierSql, true, dialect, databaseType) : columns.map((column) => quoteSelectStarColumnIdentifier(column.name, dialect, databaseType)).join(", ");
|
||||
}
|
||||
|
||||
function buildStarExpansionItem(context: SqlCompletionContext, columnsByTable: Map<string, SqlCompletionColumn[]>, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem | null {
|
||||
const expansion = buildSelectStarExpansion(context, columnsByTable, dialect);
|
||||
if (!expansion) return null;
|
||||
const columnCount = selectStarExpansionColumns(context, columnsByTable).length;
|
||||
return {
|
||||
label: "* → columns",
|
||||
type: "snippet" as const,
|
||||
detail: `${(t?.starExpansionColumns ?? "{count} columns").replace("{count}", String(uniqueColumns.length))}: ${expansion.length > 60 ? expansion.slice(0, 57) + "..." : expansion}`,
|
||||
detail: `${(t?.starExpansionColumns ?? "{count} columns").replace("{count}", String(columnCount))}: ${expansion.length > 60 ? expansion.slice(0, 57) + "..." : expansion}`,
|
||||
apply: expansion,
|
||||
boost: 1900,
|
||||
};
|
||||
|
|
@ -3248,10 +3279,10 @@ function referencedTablesForSelectAllColumns(context: SqlCompletionContext): Sql
|
|||
return context.referencedTables.filter((table) => referencedTableMatchesColumnQualifier(table, qualifier, qualifierLower, qualifiedTarget));
|
||||
}
|
||||
|
||||
function buildSelectAllColumnExpansion(columns: SqlCompletionColumn[], qualifier: string | undefined, qualifierAlreadyTyped: boolean, dialect?: "mysql" | "postgres" | "sqlserver"): string {
|
||||
function buildSelectAllColumnExpansion(columns: SqlCompletionColumn[], qualifier: string | undefined, qualifierAlreadyTyped: boolean, dialect?: "mysql" | "postgres" | "sqlserver", databaseType?: DatabaseType): string {
|
||||
return columns
|
||||
.map((column, index) => {
|
||||
const columnName = quoteSqlIdentifier(column.name, dialect);
|
||||
const columnName = quoteSelectStarColumnIdentifier(column.name, dialect, databaseType);
|
||||
if (!qualifier || (qualifierAlreadyTyped && index === 0)) return columnName;
|
||||
return `${qualifier}.${columnName}`;
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue