From 5966aea43bdb71860fcd5b7835cdb1cebc8b632d Mon Sep 17 00:00:00 2001 From: zipg Date: Sun, 9 Aug 2026 23:24:36 +0800 Subject: [PATCH] feat(completion): expand INSERT all-columns with VALUES tab stops --- .../lib/__tests__/sql/semantic/completion.spec.ts | 12 +++++++++++- apps/desktop/src/lib/sql/sqlCompletion.ts | 12 ++++++++---- packages/app-tests/sqlCompletion.test.ts | 14 +++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts index d86f9c64b..44357620f 100644 --- a/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/semantic/completion.spec.ts @@ -417,7 +417,17 @@ WHERE a.id = b.fk_kpi_set_score_id`, const { context, items } = semanticCompletion("INSERT INTO users (|", { columnsByTable }); expect(context.insertTable).toBe("users"); - expect(items.find((item) => item.type === "snippet" && item.label === "users.*")?.apply).toBe("id, name, email"); + const allColumns = items.find((item) => item.type === "snippet" && item.label === "users.*"); + expect(allColumns?.apply).toBe("id, name, email) VALUES (${1:value}, ${2:value}, ${3:value})"); + expect(allColumns?.detail).toBe("3 columns: id, name, email) VALUES (value, value, value)"); + }); + + it("uses the configured keyword case for INSERT all-column snippets", () => { + const columnsByTable = new Map([["users", ["id", "name"].map((name) => ({ name, table: "users" }))]]); + + const { items } = semanticCompletion("insert into users (|", { columnsByTable, keywordCase: "lower" }); + + expect(items.find((item) => item.type === "snippet" && item.label === "users.*")?.apply).toBe("id, name) values (${1:value}, ${2:value})"); }); it("keeps partial INSERT INTO targets in table completion context", () => { diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index c3b830bb3..fb9385adb 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -1412,7 +1412,7 @@ class SqlCompletionProvider { if (!context.exclusiveTableSuggestions && context.suggestColumns) { this.items.push(...buildColumnItems(context, this.input.columnsByTable, this.dialect)); this.items.push(...buildSelectAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect)); - this.items.push(...buildInsertAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect)); + this.items.push(...buildInsertAllColumnItems(context, this.input.columnsByTable, this.t, this.dialect, this.input.keywordCase)); } const emptyTableNameCompletion = !context.prefix && (context.suggestTables || context.exclusiveTableSuggestions); @@ -3308,7 +3308,7 @@ function buildSelectAllColumnItems(context: SqlCompletionContext, columnsByTable return items; } -function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver"): SqlCompletionItem[] { +function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable: Map, t?: SqlCompletionTranslations, dialect?: "mysql" | "postgres" | "sqlserver", keywordCase?: SqlKeywordCase): SqlCompletionItem[] { if (!context.insertTable) return []; const columns = uniqueColumnsByName(columnsForInsertTarget(context, columnsByTable)); if (columns.length === 0) return []; @@ -3316,13 +3316,17 @@ function buildInsertAllColumnItems(context: SqlCompletionContext, columnsByTable const label = `${context.insertTable}.*`; if (!selectAllColumnItemMatchesPrefix(label, { name: context.insertTable, schema: context.insertSchema }, columns, context.prefix)) return []; - const expansion = columns.map((column) => quoteSqlIdentifier(column.name, dialect)).join(", "); + const columnList = columns.map((column) => quoteSqlIdentifier(column.name, dialect)).join(", "); + const valuesKeyword = applySqlKeywordCase("VALUES", keywordCase); + const valueList = columns.map((_, index) => `\${${index + 1}:value}`).join(", "); + const expansion = `${columnList}) ${valuesKeyword} (${valueList})`; + const preview = `${columnList}) ${valuesKeyword} (${columns.map(() => "value").join(", ")})`; const countText = (t?.starExpansionColumns ?? "{count} columns").replace("{count}", String(columns.length)); return [ { label, type: "snippet" as const, - detail: `${countText}: ${expansion.length > 60 ? expansion.slice(0, 57) + "..." : expansion}`, + detail: `${countText}: ${preview.length > 60 ? preview.slice(0, 57) + "..." : preview}`, apply: expansion, boost: 2450 + selectAllColumnItemPrefixBoost(label, { name: context.insertTable, schema: context.insertSchema }, columns, context.prefix), }, diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 4f9b3f856..17c2fb969 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -2291,7 +2291,7 @@ test("suggests INSERT columns for a SQL Server three-part target", () => { dialect: "sqlserver", }); - assert.equal(items.find((item) => item.type === "snippet" && item.label === "orders.*")?.apply, "target_marker"); + assert.equal(items.find((item) => item.type === "snippet" && item.label === "orders.*")?.apply, "target_marker) VALUES (${1:value})"); }); test("detects MySQL backtick-qualified INSERT INTO column list context", () => { @@ -2339,7 +2339,7 @@ test("suggests all target columns for INSERT INTO column list", () => { const allColumns = items.find((item) => item.type === "snippet" && item.label === "users.*"); assert.ok(allColumns); - assert.equal(allColumns.apply, "id, name, email"); + assert.equal(allColumns.apply, "id, name, email) VALUES (${1:value}, ${2:value}, ${3:value})"); }); test("keeps INSERT INTO all-column expansion available after a column prefix", () => { @@ -2350,7 +2350,7 @@ test("keeps INSERT INTO all-column expansion available after a column prefix", ( const allColumns = items.find((item) => item.type === "snippet" && item.label === "users.*"); assert.ok(allColumns); - assert.equal(allColumns.apply, "id, name, email"); + assert.equal(allColumns.apply, "id, name, email) VALUES (${1:value}, ${2:value}, ${3:value})"); }); test("quotes PostgreSQL identifiers in INSERT INTO all-column expansion", () => { @@ -2364,7 +2364,7 @@ test("quotes PostgreSQL identifiers in INSERT INTO all-column expansion", () => const allColumns = items.find((item) => item.type === "snippet" && item.label === "OrderLines.*"); assert.ok(allColumns); - assert.equal(allColumns.apply, 'article, "OrderId", "User", "has""quote"'); + assert.equal(allColumns.apply, 'article, "OrderId", "User", "has""quote") VALUES (${1:value}, ${2:value}, ${3:value}, ${4:value})'); }); test("suggests all target columns for schema-qualified INSERT INTO column lists", () => { @@ -2386,7 +2386,7 @@ test("suggests all target columns for schema-qualified INSERT INTO column lists" const allColumns = items.find((item) => item.type === "snippet" && item.label === "Users.*"); assert.ok(allColumns); - assert.equal(allColumns.apply, "Id, DisplayName"); + assert.equal(allColumns.apply, "Id, DisplayName) VALUES (${1:value}, ${2:value})"); }); test("scopes INSERT INTO all-column expansion to the database-qualified MySQL target", () => { @@ -2400,7 +2400,7 @@ test("scopes INSERT INTO all-column expansion to the database-qualified MySQL ta const allColumns = items.find((item) => item.type === "snippet" && item.label === "orders.*"); assert.ok(allColumns); - assert.equal(allColumns.apply, "id, number, status"); + assert.equal(allColumns.apply, "id, number, status) VALUES (${1:value}, ${2:value}, ${3:value})"); }); test("suggests all target columns for MySQL backtick-qualified INSERT INTO", () => { @@ -2414,7 +2414,7 @@ test("suggests all target columns for MySQL backtick-qualified INSERT INTO", () const allColumns = items.find((item) => item.type === "snippet" && item.label === "orders.*"); assert.ok(allColumns); - assert.equal(allColumns.apply, "id, number, status"); + assert.equal(allColumns.apply, "id, number, status) VALUES (${1:value}, ${2:value}, ${3:value})"); }); // --- Column data type in detail ---