mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-20 00:17:44 +00:00
feat(editor): include column comments in batch column insert
This commit is contained in:
+39
-3
@@ -28,7 +28,7 @@ afterEach(() => {
|
||||
toast.mockReset();
|
||||
});
|
||||
|
||||
function createHarness(doc = "SELECT FROM users", mode: "select" | "insert" = "select") {
|
||||
function createHarness(doc = "SELECT FROM users", mode: "select" | "insert" = "select", customItems?: SqlCompletionItem[], customFrom?: number) {
|
||||
const completionComp = new Compartment();
|
||||
const parent = document.createElement("div");
|
||||
document.body.append(parent);
|
||||
@@ -46,8 +46,8 @@ function createHarness(doc = "SELECT FROM users", mode: "select" | "insert" = "
|
||||
};
|
||||
const accepted = vi.fn();
|
||||
const batch = useQueryEditorBatchSelection({ view, runtime, settingsStore: { editorSettings: { sqlFormatter: { keywordCase: "upper" } } } as Options["settingsStore"], markCompletionAccepted: accepted });
|
||||
const items: SqlCompletionItem[] = ["id", "name", "email"].map((label) => ({ label, apply: label, type: "column", batchSelectionMode: mode }));
|
||||
const from = mode === "select" ? 7 : doc.length;
|
||||
const items: SqlCompletionItem[] = customItems ?? ["id", "name", "email"].map((label) => ({ label, apply: label, type: "column", batchSelectionMode: mode }));
|
||||
const from = customFrom ?? (mode === "select" ? 7 : doc.length);
|
||||
const session = batch.prepareBatchColumnSelectionSession(items, doc, from, from)!;
|
||||
batch.attach(currentView, parent);
|
||||
cleanups.push(() => {
|
||||
@@ -136,6 +136,42 @@ describe("QueryEditor batch selection ownership", () => {
|
||||
expect(currentView.state.doc.toString()).toBe("SELECT id, email FROM users");
|
||||
});
|
||||
|
||||
it("inserts fields with column comments formatted with indentation and comments (#11354)", () => {
|
||||
const doc = "SELECT\n t.\nFROM users";
|
||||
const from = "SELECT\n t.".length;
|
||||
const items: SqlCompletionItem[] = [
|
||||
{ label: "id", apply: "id", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "编号" },
|
||||
{ label: "name", apply: "name", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "名称" },
|
||||
];
|
||||
const { batch, session, currentView } = createHarness(doc, "select", items, from);
|
||||
batch.toggleAllBatchColumnSelection(currentView, session.key);
|
||||
expect(batch.applySelectedBatchColumnSelection(currentView)).toBe(true);
|
||||
expect(currentView.state.doc.toString()).toBe("SELECT\n t.id, -- 编号\n t.name -- 名称\nFROM users");
|
||||
});
|
||||
|
||||
it("pushes trailing SQL to a new line when inserting commented columns on a single line (#11354)", () => {
|
||||
const doc = "SELECT t. FROM users";
|
||||
const from = "SELECT t.".length;
|
||||
const items: SqlCompletionItem[] = [
|
||||
{ label: "id", apply: "id", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "编号" },
|
||||
{ label: "name", apply: "name", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "名称" },
|
||||
];
|
||||
const { batch, session, currentView } = createHarness(doc, "select", items, from);
|
||||
batch.toggleAllBatchColumnSelection(currentView, session.key);
|
||||
expect(batch.applySelectedBatchColumnSelection(currentView)).toBe(true);
|
||||
expect(currentView.state.doc.toString()).toBe("SELECT t.id, -- 编号\n t.name -- 名称\nFROM users");
|
||||
});
|
||||
|
||||
it("moves existing trailing comma before comment when inserting commented columns (#11354)", () => {
|
||||
const doc = "SELECT\n t.,\n other\nFROM users";
|
||||
const from = "SELECT\n t.".length;
|
||||
const items: SqlCompletionItem[] = [{ label: "id", apply: "id", type: "column", batchSelectionMode: "select", batchSelectionQualifier: "t", comment: "编号" }];
|
||||
const { batch, session, currentView } = createHarness(doc, "select", items, from);
|
||||
batch.toggleAllBatchColumnSelection(currentView, session.key);
|
||||
expect(batch.applySelectedBatchColumnSelection(currentView)).toBe(true);
|
||||
expect(currentView.state.doc.toString()).toBe("SELECT\n t.id, -- 编号\n other\nFROM users");
|
||||
});
|
||||
|
||||
it("rejects stale documents and pending completion without recording acceptance", () => {
|
||||
const { batch, session, runtime, currentView, accepted } = createHarness();
|
||||
batch.toggleAllBatchColumnSelection(currentView, session.key);
|
||||
|
||||
@@ -57,6 +57,7 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
|
||||
interface BatchColumnSelectionCandidate {
|
||||
key: string;
|
||||
apply: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
interface BatchColumnSelectionSession {
|
||||
@@ -269,7 +270,7 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
|
||||
}
|
||||
|
||||
const mode = selectableItems[0]!.batchSelectionMode!;
|
||||
const candidates = selectableItems.filter((item) => item.batchSelectionMode === mode).map((item) => ({ key: batchColumnSelectionCandidateKey(item), apply: item.batchSelectionApply ?? item.apply! }));
|
||||
const candidates = selectableItems.filter((item) => item.batchSelectionMode === mode).map((item) => ({ key: batchColumnSelectionCandidateKey(item), apply: item.batchSelectionApply ?? item.apply!, comment: item.comment }));
|
||||
const key = `${mode}\u0000${from}\u0000${to}\u0000${document}`;
|
||||
if (!batchColumnSelectionSession || batchColumnSelectionSession.key !== key) {
|
||||
batchColumnSelectionSession = {
|
||||
@@ -538,11 +539,10 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
|
||||
return;
|
||||
}
|
||||
|
||||
const columns = batchColumnSelectionColumnList(
|
||||
selected.map((candidate) => candidate.apply),
|
||||
session.mode,
|
||||
session.qualifier,
|
||||
);
|
||||
const line = view.state.doc.lineAt(from);
|
||||
const lineIndent = line.text.match(/^\s*/)?.[0] ?? "";
|
||||
const indent = lineIndent || " ";
|
||||
|
||||
let replaceTo = batchColumnSelectionReplaceTo({
|
||||
from,
|
||||
to,
|
||||
@@ -551,7 +551,34 @@ export function useQueryEditorBatchSelection(options: QueryEditorBatchSelectionO
|
||||
replaceClosingQuote: session.replaceClosingQuote,
|
||||
replaceSelectWildcard: session.replaceSelectWildcard,
|
||||
});
|
||||
|
||||
const hasComments = session.mode === "select" && selected.some((candidate) => !!candidate.comment?.trim());
|
||||
let trailingComma = false;
|
||||
let pushRestToNewLine = false;
|
||||
|
||||
if (hasComments) {
|
||||
const lineAfterTo = view.state.doc.lineAt(replaceTo);
|
||||
const restOfLine = lineAfterTo.text.slice(replaceTo - lineAfterTo.from);
|
||||
const commaMatch = restOfLine.match(/^\s*,/);
|
||||
if (commaMatch) {
|
||||
trailingComma = true;
|
||||
replaceTo += commaMatch[0].length;
|
||||
}
|
||||
const remainingAfterComma = commaMatch ? restOfLine.slice(commaMatch[0].length) : restOfLine;
|
||||
if (remainingAfterComma.trim().length > 0) {
|
||||
pushRestToNewLine = true;
|
||||
const leadingSpaceMatch = remainingAfterComma.match(/^\s+/);
|
||||
if (leadingSpaceMatch) {
|
||||
replaceTo += leadingSpaceMatch[0].length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columns = batchColumnSelectionColumnList(selected, session.mode, session.qualifier, indent, { trailingComma });
|
||||
let insert = columns;
|
||||
if (pushRestToNewLine) {
|
||||
insert = `${insert}\n${lineIndent}`;
|
||||
}
|
||||
if (session.mode === "insert") {
|
||||
const replacement = batchColumnSelectionInsertReplacement({
|
||||
document: view.state.doc.toString(),
|
||||
|
||||
@@ -22,6 +22,32 @@ describe("batchColumnSelectionColumnList", () => {
|
||||
it("does not add a qualifier to INSERT target columns", () => {
|
||||
expect(batchColumnSelectionColumnList(["id", "name"], "insert", "users")).toBe("id, name");
|
||||
});
|
||||
|
||||
it("formats candidates with column comments in SELECT mode (#11354)", () => {
|
||||
const candidates = [{ apply: "order_start_time", comment: "医嘱开始时间" }, { apply: "order_end_time", comment: "医嘱结束时间" }, { apply: "patient_id" }];
|
||||
expect(batchColumnSelectionColumnList(candidates, "select", "t", " ")).toBe("order_start_time, -- 医嘱开始时间\n t.order_end_time, -- 医嘱结束时间\n t.patient_id");
|
||||
});
|
||||
|
||||
it("normalizes newlines in column comments (#11354)", () => {
|
||||
const candidates = [
|
||||
{ apply: "status", comment: "状态:\n0-停用\r\n1-启用" },
|
||||
{ apply: "created_at", comment: "创建时间" },
|
||||
];
|
||||
expect(batchColumnSelectionColumnList(candidates, "select", undefined, " ")).toBe("status, -- 状态: 0-停用 1-启用\n created_at -- 创建时间");
|
||||
});
|
||||
|
||||
it("adds trailing comma when trailingComma option is enabled (#11354)", () => {
|
||||
const candidates = [{ apply: "id", comment: "主键ID" }];
|
||||
expect(batchColumnSelectionColumnList(candidates, "select", undefined, " ", { trailingComma: true })).toBe("id, -- 主键ID");
|
||||
});
|
||||
|
||||
it("ignores candidate comments in INSERT mode (#11354)", () => {
|
||||
const candidates = [
|
||||
{ apply: "id", comment: "主键ID" },
|
||||
{ apply: "name", comment: "名称" },
|
||||
];
|
||||
expect(batchColumnSelectionColumnList(candidates, "insert", "users")).toBe("id, name");
|
||||
});
|
||||
});
|
||||
|
||||
describe("batchColumnSelectionReplaceTo", () => {
|
||||
|
||||
@@ -68,6 +68,7 @@ describe("semantic SQL completion candidates", () => {
|
||||
|
||||
const nickname = items.find((item) => item.type === "column" && item.label === "nickname");
|
||||
expect(nickname?.detail).toContain("-- 用户昵称");
|
||||
expect(nickname?.comment).toBe("用户昵称");
|
||||
});
|
||||
|
||||
it("ignores line-comment semicolons after a real statement boundary", () => {
|
||||
|
||||
@@ -1,7 +1,31 @@
|
||||
export type BatchColumnSelectionMode = "select" | "insert";
|
||||
|
||||
export function batchColumnSelectionColumnList(candidates: string[], mode: BatchColumnSelectionMode, qualifier?: string): string {
|
||||
return candidates.map((candidate, index) => (mode === "select" && qualifier && index > 0 ? `${qualifier}.${candidate}` : candidate)).join(", ");
|
||||
export interface BatchColumnCandidate {
|
||||
apply: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export function batchColumnSelectionColumnList(candidates: Array<string | BatchColumnCandidate>, mode: BatchColumnSelectionMode, qualifier?: string, indent = " ", options?: { trailingComma?: boolean }): string {
|
||||
const normalized = candidates.map((candidate) => (typeof candidate === "string" ? { apply: candidate } : candidate));
|
||||
const hasComments = mode === "select" && normalized.some((candidate) => !!candidate.comment?.trim());
|
||||
|
||||
if (!hasComments) {
|
||||
return normalized.map((candidate, index) => (mode === "select" && qualifier && index > 0 ? `${qualifier}.${candidate.apply}` : candidate.apply)).join(", ");
|
||||
}
|
||||
|
||||
return normalized
|
||||
.map((candidate, index) => {
|
||||
const col = qualifier && index > 0 ? `${qualifier}.${candidate.apply}` : candidate.apply;
|
||||
const isLast = index === normalized.length - 1;
|
||||
const comment = candidate.comment?.replace(/[\r\n]+/g, " ").trim();
|
||||
const linePrefix = index > 0 ? indent : "";
|
||||
const shouldHaveComma = !isLast || options?.trailingComma;
|
||||
if (comment) {
|
||||
return shouldHaveComma ? `${linePrefix}${col}, -- ${comment}` : `${linePrefix}${col} -- ${comment}`;
|
||||
}
|
||||
return shouldHaveComma ? `${linePrefix}${col},` : `${linePrefix}${col}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
export function shouldResolveSqlColumnCompletion(options: { suggestColumns: boolean; hasReferencedTables: boolean; prefix: string; typedActivation: boolean; selectListColumnContext: boolean }): boolean {
|
||||
|
||||
@@ -1505,6 +1505,8 @@ export interface SqlCompletionItem {
|
||||
batchSelectionApply?: string;
|
||||
/** Qualifier to prepend to every batch-selected column after the first one. */
|
||||
batchSelectionQualifier?: string;
|
||||
/** Optional column comment for batch insertion documentation. */
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export function shouldChainSqlCompletionAfterAccept(item: { type?: string; apply?: string }): boolean {
|
||||
@@ -5367,6 +5369,7 @@ function buildColumnItems(context: SqlCompletionContext, columnsByTable: Map<str
|
||||
// document. Subsequent columns must use that same user-typed qualifier,
|
||||
// not a referenced-table alias which may be different from it.
|
||||
batchSelectionQualifier: batchSelectionMode === "select" && context.qualifier ? (context.qualifierParts ?? context.qualifier.split(".").filter(Boolean)).map((part) => quoteCompletionApplyIdentifier(part, dialect)).join(".") : undefined,
|
||||
comment: column.comment?.trim() || undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user