diff --git a/apps/desktop/src/components/structure/TableStructureEditor.vue b/apps/desktop/src/components/structure/TableStructureEditor.vue
index 7ac344cb3d..0d16aabede 100644
--- a/apps/desktop/src/components/structure/TableStructureEditor.vue
+++ b/apps/desktop/src/components/structure/TableStructureEditor.vue
@@ -109,6 +109,8 @@ import {
structureColumnNamesForCopy,
tableStructureIdentifierComparisonKey,
toColumnNames,
+ copySourceColumnDetails,
+ matchesCopySourceColumnSearch,
} from "@/lib/table/tableStructureEditorState";
import { CREATE_DATABASE_CHARSET_OPTIONS, createDatabaseCollationOptionsForCharset, fallbackCreateDatabaseCharsetMetadata, normalizeCreateDatabaseCharsetKey, parseCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
import type { CreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
@@ -2501,14 +2503,14 @@ const copyableSourceColumns = computed(() => {
const existingNames = new Set(columns.value.filter((column) => !column.markedForDrop).map((column) => tableStructureIdentifierComparisonKey(column.name, databaseType.value, databaseInfo)));
return copySourceColumns.value.map((column) => ({
column,
+ details: copySourceColumnDetails(column, databaseType.value),
alreadyExists: existingNames.has(tableStructureIdentifierComparisonKey(column.name, databaseType.value, databaseInfo)),
}));
});
const filteredCopyableSourceColumns = computed(() => {
- const search = normalizedColumnSearch(copySourceColumnSearch.value);
- if (!search) return copyableSourceColumns.value;
- return copyableSourceColumns.value.filter(({ column }) => [column.name, column.data_type, column.comment ?? ""].some((value) => normalizedColumnSearch(value).includes(search)));
+ if (!normalizedColumnSearch(copySourceColumnSearch.value)) return copyableSourceColumns.value;
+ return copyableSourceColumns.value.filter(({ column }) => matchesCopySourceColumnSearch(column, copySourceColumnSearch.value, databaseType.value));
});
const copyableSourceColumnNames = computed(() => copyableSourceColumns.value.filter(({ alreadyExists }) => !alreadyExists).map(({ column }) => column.name));
@@ -5441,10 +5443,18 @@ watch(
{{ t("structureEditor.copyColumnsNoMatchingFields") }}
-
diff --git a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts
index 6beaa3ef1e..d13eb728b4 100644
--- a/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts
+++ b/apps/desktop/src/lib/__tests__/table/tableStructureEditorState.spec.ts
@@ -4,6 +4,7 @@ import {
cloneColumnDraftAsNew,
combineDataTypeForDatabase,
combineDataTypeForDatabaseWithLengthUnit,
+ copySourceColumnDetails,
createCopiedColumnDrafts,
createColumnDrafts,
createTriggerDrafts,
@@ -20,6 +21,7 @@ import {
isMysqlCharacterDataType,
isMysqlEnumDataType,
isSqlServerIdentityCompatibleDataType,
+ matchesCopySourceColumnSearch,
mysqlEnumDataType,
parseExtraToColumnExtra,
rehydrateColumnDraftsFromMetadata,
@@ -706,6 +708,54 @@ describe("tableStructureEditorState", () => {
});
});
+describe("copySourceColumnDetails", () => {
+ it("keeps the comment and default shown in the copy-fields dialog", () => {
+ expect(copySourceColumnDetails({ data_type: "varchar(20)", column_default: "'unknown'", comment: "名称" })).toEqual({ defaultValue: "'unknown'", comment: "名称" });
+ });
+
+ it("keeps falsy-looking defaults such as 0 and empty strings", () => {
+ expect(copySourceColumnDetails({ data_type: "int", column_default: "0", comment: null })).toEqual({ defaultValue: "0", comment: null });
+ expect(copySourceColumnDetails({ data_type: "varchar(20)", column_default: "''", comment: null })).toEqual({ defaultValue: "''", comment: null });
+ });
+
+ it("drops blank metadata instead of rendering an empty label", () => {
+ expect(copySourceColumnDetails({ data_type: "int", column_default: null, comment: null })).toEqual({ defaultValue: null, comment: null });
+ expect(copySourceColumnDetails({ data_type: "int", column_default: undefined, comment: undefined })).toEqual({ defaultValue: null, comment: null });
+ expect(copySourceColumnDetails({ data_type: "int", column_default: " ", comment: " " })).toEqual({ defaultValue: null, comment: null });
+ });
+
+ it("trims padded metadata for display", () => {
+ expect(copySourceColumnDetails({ data_type: "int", column_default: " 1 ", comment: " 备注 " })).toEqual({ defaultValue: "1", comment: "备注" });
+ });
+
+ it("normalizes defaults per database like the editor grid", () => {
+ expect(copySourceColumnDetails({ data_type: "character varying", column_default: "'unknown'::character varying", comment: null }, "postgres")).toEqual({ defaultValue: "'unknown'", comment: null });
+ expect(copySourceColumnDetails({ data_type: "int", column_default: "((0))", comment: null }, "sqlserver")).toEqual({ defaultValue: "0", comment: null });
+ expect(copySourceColumnDetails({ data_type: "varchar(50)", column_default: "", comment: null }, "mysql")).toEqual({ defaultValue: "''", comment: null });
+ });
+});
+
+describe("matchesCopySourceColumnSearch", () => {
+ const column = { name: "status", data_type: "tinyint", column_default: "1", comment: "状态:1启用 0停用" };
+
+ it("matches everything for a blank query", () => {
+ expect(matchesCopySourceColumnSearch(column, "")).toBe(true);
+ expect(matchesCopySourceColumnSearch(column, " ")).toBe(true);
+ });
+
+ it("matches name and type case-insensitively", () => {
+ expect(matchesCopySourceColumnSearch(column, "STAT")).toBe(true);
+ expect(matchesCopySourceColumnSearch(column, "TINY")).toBe(true);
+ expect(matchesCopySourceColumnSearch(column, "missing")).toBe(false);
+ });
+
+ it("matches comments and default values too", () => {
+ expect(matchesCopySourceColumnSearch(column, "启用")).toBe(true);
+ expect(matchesCopySourceColumnSearch(column, "1")).toBe(true);
+ expect(matchesCopySourceColumnSearch({ ...column, column_default: null, comment: null }, "1")).toBe(false);
+ });
+});
+
describe("structureColumnNamesForCopy", () => {
const column = (name: string, markedForDrop = false) => ({ name, markedForDrop });
diff --git a/apps/desktop/src/lib/table/tableStructureEditorState.ts b/apps/desktop/src/lib/table/tableStructureEditorState.ts
index 2039c732a3..5a75e2ede7 100644
--- a/apps/desktop/src/lib/table/tableStructureEditorState.ts
+++ b/apps/desktop/src/lib/table/tableStructureEditorState.ts
@@ -1,6 +1,37 @@
import type { ColumnInfo, DatabaseConnectionInfo, DatabaseType, ForeignKeyInfo, IndexInfo, TriggerInfo } from "@/types/database.ts";
import type { ColumnExtra, EditableStructureColumn, EditableStructureForeignKey, EditableStructureIndex, EditableStructureTrigger } from "@/lib/table/tableStructureEditorSql.ts";
+export interface CopySourceColumnDetails {
+ /** Column default as shown in the copy-fields dialog, or null when there is none. */
+ defaultValue: string | null;
+ /** Column comment as shown in the copy-fields dialog, or null when there is none. */
+ comment: string | null;
+}
+
+/**
+ * Read-only summary rendered under a column name in the "copy fields from another
+ * table" dialog. The source table is not open while copying, so the comment and
+ * default value are the only hint about what an unfamiliar field means.
+ */
+export function copySourceColumnDetails(column: Pick, databaseType?: DatabaseType): CopySourceColumnDetails {
+ // Match the main grid and the editor drafts so the dialog, the grid, and the
+ // copied result render the same normalized default for every database.
+ const defaultValue = column.column_default == null ? "" : columnDefaultForEditor(column, databaseType);
+ const rawComment = column.comment ?? "";
+ return {
+ defaultValue: defaultValue.trim() ? defaultValue.trim() : null,
+ comment: rawComment.trim() ? rawComment.trim() : null,
+ };
+}
+
+/** Copy-dialog search matches comments and default values on top of name and type. */
+export function matchesCopySourceColumnSearch(column: Pick, search: string, databaseType?: DatabaseType): boolean {
+ const query = search.trim().toLowerCase();
+ if (!query) return true;
+ const details = copySourceColumnDetails(column, databaseType);
+ return [column.name, column.data_type, details.defaultValue ?? "", details.comment ?? ""].some((value) => value.toLowerCase().includes(query));
+}
+
/**
* Column names offered by the structure editor's "copy all column names" action.
* Fields marked for drop disappear on save, so they are not offered.
@@ -847,7 +878,7 @@ function stripSqlServerDefaultOuterParens(defaultValue: string): string {
return value;
}
-function columnDefaultForEditor(column: ColumnInfo, databaseType?: DatabaseType): string {
+function columnDefaultForEditor(column: Pick, databaseType?: DatabaseType): string {
if (column.column_default === null) return "";
const defaultValue = column.column_default;
if (databaseType === "mysql" && defaultValue === "" && isMysqlCharacterDataType(column.data_type)) {
diff --git a/docs/screenshots/issue-9652-after.png b/docs/screenshots/issue-9652-after.png
new file mode 100644
index 0000000000..4619abe959
Binary files /dev/null and b/docs/screenshots/issue-9652-after.png differ
diff --git a/docs/screenshots/issue-9652-before.png b/docs/screenshots/issue-9652-before.png
new file mode 100644
index 0000000000..139fe1712a
Binary files /dev/null and b/docs/screenshots/issue-9652-before.png differ
diff --git a/docs/screenshots/issue-9652-search-by-comment.png b/docs/screenshots/issue-9652-search-by-comment.png
new file mode 100644
index 0000000000..af34804ec8
Binary files /dev/null and b/docs/screenshots/issue-9652-search-by-comment.png differ