diff --git a/apps/desktop/src/components/editor/EditorSettingsDialog.vue b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
index f4ac9d0503..c2aa6fd20c 100644
--- a/apps/desktop/src/components/editor/EditorSettingsDialog.vue
+++ b/apps/desktop/src/components/editor/EditorSettingsDialog.vue
@@ -103,6 +103,7 @@ const editActiveCustomThemeId = ref(settingsStore.editorSettings.activeCustomThe
const showThemeCustomizer = ref(false);
const editExecuteMode = ref(settingsStore.editorSettings.executeMode);
const editShowExecutionTargetPicker = ref(settingsStore.editorSettings.showExecutionTargetPicker);
+const editAutoAliasTables = ref(settingsStore.editorSettings.autoAliasTables);
const editWordWrap = ref(settingsStore.editorSettings.wordWrap);
const editConfirmDangerousSqlExecution = ref(settingsStore.editorSettings.confirmDangerousSqlExecution);
const editAppLayout = ref(settingsStore.editorSettings.appLayout);
@@ -365,6 +366,7 @@ watch(
editActiveCustomThemeId.value = settingsStore.editorSettings.activeCustomThemeId;
editExecuteMode.value = settingsStore.editorSettings.executeMode;
editShowExecutionTargetPicker.value = settingsStore.editorSettings.showExecutionTargetPicker;
+ editAutoAliasTables.value = settingsStore.editorSettings.autoAliasTables;
editWordWrap.value = settingsStore.editorSettings.wordWrap;
editConfirmDangerousSqlExecution.value = settingsStore.editorSettings.confirmDangerousSqlExecution;
editAppLayout.value = settingsStore.editorSettings.appLayout;
@@ -423,6 +425,7 @@ function hasChanges(): boolean {
editActiveCustomThemeId.value !== settingsStore.editorSettings.activeCustomThemeId ||
editExecuteMode.value !== settingsStore.editorSettings.executeMode ||
editShowExecutionTargetPicker.value !== settingsStore.editorSettings.showExecutionTargetPicker ||
+ editAutoAliasTables.value !== settingsStore.editorSettings.autoAliasTables ||
editWordWrap.value !== settingsStore.editorSettings.wordWrap ||
editConfirmDangerousSqlExecution.value !== settingsStore.editorSettings.confirmDangerousSqlExecution ||
editAppLayout.value !== settingsStore.editorSettings.appLayout ||
@@ -466,6 +469,7 @@ async function persistSettings() {
activeCustomThemeId: editActiveCustomThemeId.value,
executeMode: editExecuteMode.value,
showExecutionTargetPicker: editShowExecutionTargetPicker.value,
+ autoAliasTables: editAutoAliasTables.value,
wordWrap: editWordWrap.value,
confirmDangerousSqlExecution: editConfirmDangerousSqlExecution.value,
appLayout: editAppLayout.value,
@@ -519,6 +523,7 @@ function resetDefaultsForTab(tab: SettingsCategory) {
editFontSize.value = DEFAULT_EDITOR_SETTINGS.fontSize;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
+ editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
} else if (tab === "formatter") {
@@ -572,6 +577,7 @@ function resetAllDefaults() {
editActiveCustomThemeId.value = DEFAULT_EDITOR_SETTINGS.activeCustomThemeId;
editExecuteMode.value = DEFAULT_EDITOR_SETTINGS.executeMode;
editShowExecutionTargetPicker.value = DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker;
+ editAutoAliasTables.value = DEFAULT_EDITOR_SETTINGS.autoAliasTables;
editWordWrap.value = DEFAULT_EDITOR_SETTINGS.wordWrap;
editConfirmDangerousSqlExecution.value = DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution;
editAppLayout.value = DEFAULT_EDITOR_SETTINGS.appLayout;
@@ -1697,6 +1703,14 @@ watch(
+
+
+
+
+
{{ t("settings.autoAliasTablesDescription") }}
+
+
+
diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue
index 2f1852d1e2..eee5bbe5cc 100644
--- a/apps/desktop/src/components/editor/QueryEditor.vue
+++ b/apps/desktop/src/components/editor/QueryEditor.vue
@@ -1250,6 +1250,7 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
dialect: props.dialect,
databaseType: props.databaseType,
keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase,
+ autoAliasTables: settingsStore.editorSettings.autoAliasTables,
});
return buildCompletionResult(items, position - completionContext.prefix.length, getSqlCompletionResultValidFor(fullDoc, position));
}
@@ -1268,6 +1269,7 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E
dialect: props.dialect,
databaseType: props.databaseType,
keywordCase: settingsStore.editorSettings.sqlFormatter.keywordCase,
+ autoAliasTables: settingsStore.editorSettings.autoAliasTables,
});
return buildCompletionResult(items, position - completionContext.prefix.length, getSqlCompletionResultValidFor(fullDoc, position));
}
@@ -1412,6 +1414,7 @@ function buildLocalSqlCompletionResult(completionContext: ReturnType {
});
});
+describe("sqlCompletion table aliases", () => {
+ it("applies generated aliases to table completions when enabled", () => {
+ const sql = "SELECT * FROM ord";
+ const items = buildSqlCompletionItems(sql, sql.length, {
+ tables: [{ name: "order_items", type: "table" }],
+ columnsByTable: new Map(),
+ autoAliasTables: true,
+ });
+
+ const table = items.find((item) => item.label === "order_items" && item.type === "table");
+ expect(table?.apply).toBe("order_items AS oi");
+ });
+
+ it("keeps plain table completions when generated aliases are disabled", () => {
+ const sql = "SELECT * FROM ord";
+ const items = buildSqlCompletionItems(sql, sql.length, {
+ tables: [{ name: "order_items", type: "table" }],
+ columnsByTable: new Map(),
+ autoAliasTables: false,
+ });
+
+ const table = items.find((item) => item.label === "order_items" && item.type === "table");
+ expect(table?.apply).toBe("order_items");
+ });
+
+ it("uses a numbered alias when the generated table alias already exists", () => {
+ const sql = "SELECT * FROM order_items oi JOIN ord";
+ const items = buildSqlCompletionItems(sql, sql.length, {
+ tables: [{ name: "order_items", type: "table" }],
+ columnsByTable: new Map(),
+ autoAliasTables: true,
+ });
+
+ const table = items.find((item) => item.label === "order_items" && item.type === "table");
+ expect(table?.apply).toBe("order_items AS oi2");
+ });
+
+ it("applies generated aliases in comma-separated FROM table lists", () => {
+ const sql = "SELECT * FROM users u, ord";
+ const items = buildSqlCompletionItems(sql, sql.length, {
+ tables: [{ name: "order_items", type: "table" }],
+ columnsByTable: new Map(),
+ autoAliasTables: true,
+ });
+
+ const table = items.find((item) => item.label === "order_items" && item.type === "table");
+ expect(table?.apply).toBe("order_items AS oi");
+ });
+
+ it("does not apply generated aliases to non-query table completions", () => {
+ const sql = "INSERT INTO ord";
+ const items = buildSqlCompletionItems(sql, sql.length, {
+ tables: [{ name: "order_items", type: "table" }],
+ columnsByTable: new Map(),
+ autoAliasTables: true,
+ });
+
+ const table = items.find((item) => item.label === "order_items" && item.type === "table");
+ expect(table?.apply).toBe("order_items");
+ });
+});
+
describe("sqlCompletion scoped context classification", () => {
it("classifies JOIN table contexts", () => {
const sql = "SELECT * FROM users u JOIN ";
diff --git a/apps/desktop/src/lib/sqlCompletion.ts b/apps/desktop/src/lib/sqlCompletion.ts
index e49378f563..73cb8d91a1 100644
--- a/apps/desktop/src/lib/sqlCompletion.ts
+++ b/apps/desktop/src/lib/sqlCompletion.ts
@@ -1092,6 +1092,7 @@ export interface SqlCompletionContext {
updateTarget?: { table: string; schema?: string };
deleteTarget?: { table: string; schema?: string };
oracleTableFunctionContext?: boolean;
+ autoAliasTableCompletions: boolean;
contextKind: SqlCompletionContextKind;
}
@@ -1124,6 +1125,7 @@ export interface SqlCompletionProviderInput {
dialect?: "mysql" | "postgres" | "sqlserver";
databaseType?: DatabaseType;
keywordCase?: SqlKeywordCase;
+ autoAliasTables?: boolean;
}
export function buildSqlCompletionItems(
@@ -1139,6 +1141,7 @@ export function buildSqlCompletionItems(
dialect?: "mysql" | "postgres" | "sqlserver";
databaseType?: DatabaseType;
keywordCase?: SqlKeywordCase;
+ autoAliasTables?: boolean;
},
): SqlCompletionItem[] {
const context = getSqlCompletionContext(sql, cursor);
@@ -1217,7 +1220,7 @@ class SqlCompletionProvider {
if (!context.exclusiveColumnSuggestions && context.suggestTables) {
this.items.push(...buildForeignKeyRelatedTableItems(context, this.input.tables, this.input.foreignKeysByTable, this.dialect));
- this.items.push(...buildTableItems(context.prefix, this.input.tables, this.dialect));
+ this.items.push(...buildTableItems(context.prefix, this.input.tables, this.dialect, !!this.input.autoAliasTables && context.autoAliasTableCompletions, context.referencedTables));
if (isOracleLikeDatabase(this.databaseType)) {
this.items.push(...buildOracleTableFunctionItems(context.prefix));
}
@@ -1542,6 +1545,7 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
const afterTableTrigger = TABLE_TRIGGER_KEYWORDS.has(lastWord) || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken);
const exclusiveTableSuggestions = EXCLUSIVE_TABLE_TRIGGER_KEYWORDS.has(lastWord) || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken);
+ const autoAliasTableCompletions = lastWord === "from" || lastWord === "join" || (JOIN_MODIFIERS.has(lastWord) && isFollowedByJoin(beforeToken)) || isInTableListContext(beforeToken);
const exclusiveColumnSuggestions = !!qualifier && !exclusiveTableSuggestions && !insertInfo;
// Check if we're in a context where columns are expected
@@ -1597,6 +1601,7 @@ export function getSqlCompletionContext(sql: string, cursor: number): SqlComplet
updateTarget: updateInfo?.target,
deleteTarget: deleteInfo?.target,
oracleTableFunctionContext,
+ autoAliasTableCompletions,
contextKind,
};
}
@@ -2358,16 +2363,21 @@ function requiresPostgresIdentifierQuote(identifier: string): boolean {
const POSTGRES_IDENTIFIER_KEYWORDS = new Set(SQL_KEYWORDS.map((keyword) => keyword.toLowerCase()).concat(["current_user", "session_user", "user"]));
-function buildTableItems(prefix: string, tables: SqlCompletionTable[], dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] {
+function buildTableItems(prefix: string, tables: SqlCompletionTable[], dialect?: "mysql" | "postgres" | "sqlserver", autoAliasTables = false, referencedTables: SqlCompletionReferencedTable[] = []): SqlCompletionItem[] {
+ const existingAliases = new Set(referencedTables.map((ref) => ref.alias?.toLowerCase()).filter((alias): alias is string => !!alias));
return tables
.filter((table) => matchesPrefix(table.name, prefix))
- .map((table) => ({
- label: table.name,
- type: "table" as const,
- detail: table.schema ? `${table.schema}.${table.name}` : table.type,
- apply: quoteSqlIdentifier(table.name, dialect),
- boost: computeBoost(table.name, prefix) + 1000,
- }))
+ .map((table) => {
+ const applyName = quoteSqlIdentifier(table.name, dialect);
+ const alias = autoAliasTables ? generateTableCompletionAlias(table.name, existingAliases) : "";
+ return {
+ label: table.name,
+ type: "table" as const,
+ detail: table.schema ? `${table.schema}.${table.name}` : table.type,
+ apply: alias ? `${applyName} AS ${alias}` : applyName,
+ boost: computeBoost(table.name, prefix) + 1000,
+ };
+ })
.sort(compareCompletionItems)
.slice(0, MAX_TABLE_COMPLETION_ITEMS);
}
@@ -2666,6 +2676,36 @@ function buildAliasItems(context: SqlCompletionContext): SqlCompletionItem[] {
}
function generateAlias(tableName: string, existing = new Set()): string {
+ const candidates = buildAliasCandidates(tableName);
+
+ for (const candidate of candidates.filter(Boolean)) {
+ if (!aliasConflicts(candidate, existing)) return candidate;
+ }
+
+ const fallback = candidates.find(Boolean) ?? "tb";
+ for (let index = 2; index < 100; index++) {
+ const candidate = `${fallback}${index}`;
+ if (!aliasConflicts(candidate, existing)) return candidate;
+ }
+ return fallback;
+}
+
+function generateTableCompletionAlias(tableName: string, existing = new Set()): string {
+ const candidates = buildAliasCandidates(tableName);
+
+ for (const candidate of candidates.filter(Boolean)) {
+ if (SQL_ALIAS_RESERVED_WORDS.has(candidate.toLowerCase())) continue;
+ if (!existing.has(candidate.toLowerCase())) return candidate;
+ for (let index = 2; index < 100; index++) {
+ const numbered = `${candidate}${index}`;
+ if (!aliasConflicts(numbered, existing)) return numbered;
+ }
+ }
+
+ return generateAlias(tableName, existing);
+}
+
+function buildAliasCandidates(tableName: string): string[] {
const parts = identifierWords(tableName);
const candidates: string[] = [];
@@ -2685,16 +2725,7 @@ function generateAlias(tableName: string, existing = new Set()): string
if (chars.length >= 3) candidates.push(chars.slice(0, 3).join(""));
}
- for (const candidate of candidates.filter(Boolean)) {
- if (!aliasConflicts(candidate, existing)) return candidate;
- }
-
- const fallback = candidates.find(Boolean) ?? "tb";
- for (let index = 2; index < 100; index++) {
- const candidate = `${fallback}${index}`;
- if (!aliasConflicts(candidate, existing)) return candidate;
- }
- return fallback;
+ return candidates;
}
function aliasConflicts(candidate: string, existing: Set): boolean {
diff --git a/apps/desktop/src/stores/__tests__/settingsStore.spec.ts b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
new file mode 100644
index 0000000000..b725c7ec5a
--- /dev/null
+++ b/apps/desktop/src/stores/__tests__/settingsStore.spec.ts
@@ -0,0 +1,12 @@
+import { describe, expect, it } from "vitest";
+import { normalizeEditorSettings } from "@/stores/settingsStore";
+
+describe("normalizeEditorSettings", () => {
+ it("enables automatic table aliases by default", () => {
+ expect(normalizeEditorSettings({}).autoAliasTables).toBe(true);
+ });
+
+ it("preserves disabled automatic table aliases", () => {
+ expect(normalizeEditorSettings({ autoAliasTables: false }).autoAliasTables).toBe(false);
+ });
+});
diff --git a/apps/desktop/src/stores/settingsStore.ts b/apps/desktop/src/stores/settingsStore.ts
index 6c427efcc7..612bd33b27 100644
--- a/apps/desktop/src/stores/settingsStore.ts
+++ b/apps/desktop/src/stores/settingsStore.ts
@@ -308,6 +308,7 @@ export interface EditorSettings {
activeCustomThemeId: string;
executeMode: "all" | "current";
showExecutionTargetPicker: boolean;
+ autoAliasTables: boolean;
wordWrap: boolean;
confirmDangerousSqlExecution: boolean;
compactTabTitle: boolean;
@@ -407,6 +408,7 @@ export const DEFAULT_EDITOR_SETTINGS: EditorSettings = {
activeCustomThemeId: "default",
executeMode: "all",
showExecutionTargetPicker: false,
+ autoAliasTables: true,
wordWrap: false,
confirmDangerousSqlExecution: true,
compactTabTitle: false,
@@ -571,6 +573,7 @@ export function normalizeEditorSettings(settings: Partial, exist
activeCustomThemeId: settings.activeCustomThemeId ?? "default",
executeMode: settings.executeMode ?? DEFAULT_EDITOR_SETTINGS.executeMode,
showExecutionTargetPicker: settings.showExecutionTargetPicker ?? DEFAULT_EDITOR_SETTINGS.showExecutionTargetPicker,
+ autoAliasTables: settings.autoAliasTables ?? DEFAULT_EDITOR_SETTINGS.autoAliasTables,
wordWrap: settings.wordWrap ?? DEFAULT_EDITOR_SETTINGS.wordWrap,
confirmDangerousSqlExecution: settings.confirmDangerousSqlExecution ?? DEFAULT_EDITOR_SETTINGS.confirmDangerousSqlExecution,
compactTabTitle: settings.compactTabTitle ?? DEFAULT_EDITOR_SETTINGS.compactTabTitle,
@@ -730,6 +733,7 @@ export const useSettingsStore = defineStore("settings", () => {
}
if (partial.executeMode !== undefined) editorSettings.value.executeMode = partial.executeMode;
if (partial.showExecutionTargetPicker !== undefined) editorSettings.value.showExecutionTargetPicker = partial.showExecutionTargetPicker;
+ if (partial.autoAliasTables !== undefined) editorSettings.value.autoAliasTables = partial.autoAliasTables;
if (partial.wordWrap !== undefined) editorSettings.value.wordWrap = partial.wordWrap;
if (partial.confirmDangerousSqlExecution !== undefined) editorSettings.value.confirmDangerousSqlExecution = partial.confirmDangerousSqlExecution;
if (partial.compactTabTitle !== undefined) editorSettings.value.compactTabTitle = partial.compactTabTitle;
diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts
index fe39d1f5b6..a06b389b6e 100644
--- a/packages/app-tests/sqlCompletion.test.ts
+++ b/packages/app-tests/sqlCompletion.test.ts
@@ -1300,6 +1300,19 @@ test("table alias suggestions avoid reserved words", () => {
assert.equal(aliasItem!.apply, "AS ord ");
});
+test("automatic table aliases avoid reserved words", () => {
+ const items = buildSqlCompletionItems("select * from ord", "select * from ord".length, {
+ tables,
+ columnsByTable,
+ autoAliasTables: true,
+ });
+
+ const tableItem = items.find((item) => item.type === "table" && item.label === "orders");
+ assert.ok(tableItem);
+ assert.notEqual(tableItem!.apply, "orders AS or");
+ assert.equal(tableItem!.apply, "orders AS ord");
+});
+
test("table alias suggestions avoid existing aliases", () => {
const items = buildSqlCompletionItems("select * from customer_orders co join customer_orders ", "select * from customer_orders co join customer_orders ".length, {
tables: [...tables, { name: "customer_orders", schema: "public", type: "table" }],