feat(structure): support column reordering
This commit is contained in:
1 parent
2cade12b54
commit
b0a4061484
11 files changed
+332
-88
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
AlertTriangle,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Database,
|
||||
KeyRound,
|
||||
Loader2,
|
||||
@@ -224,6 +225,21 @@ function removeNewColumn(column: EditableStructureColumn) {
|
||||
columns.value = columns.value.filter((item) => item.id !== column.id);
|
||||
}
|
||||
|
||||
function canMoveColumn(index: number, direction: -1 | 1): boolean {
|
||||
const targetIndex = index + direction;
|
||||
if (targetIndex < 0 || targetIndex >= columns.value.length) return false;
|
||||
if (columns.value[index]?.markedForDrop || columns.value[targetIndex]?.markedForDrop) return false;
|
||||
return isCreateMode.value || structureCapabilities.value.reorderColumn;
|
||||
}
|
||||
|
||||
function moveColumn(index: number, direction: -1 | 1) {
|
||||
if (!canMoveColumn(index, direction)) return;
|
||||
const targetIndex = index + direction;
|
||||
const [column] = columns.value.splice(index, 1);
|
||||
if (!column) return;
|
||||
columns.value.splice(targetIndex, 0, column);
|
||||
}
|
||||
|
||||
function toggleDropColumn(column: EditableStructureColumn) {
|
||||
if (!canDropColumn(column)) return;
|
||||
column.markedForDrop = !column.markedForDrop;
|
||||
@@ -455,7 +471,7 @@ watch(
|
||||
<th class="min-w-36 border-b border-r px-2 py-2 text-left">
|
||||
{{ t("structureEditor.comment") }}
|
||||
</th>
|
||||
<th class="w-24 border-b px-2 py-2 text-left">
|
||||
<th class="w-40 border-b px-2 py-2 text-left">
|
||||
{{ t("structureEditor.actions") }}
|
||||
</th>
|
||||
</tr>
|
||||
@@ -544,21 +560,45 @@ watch(
|
||||
</div>
|
||||
</td>
|
||||
<td class="border-b px-2 py-1.5">
|
||||
<Button
|
||||
v-if="column.original"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="!canDropColumn(column)"
|
||||
@click="toggleDropColumn(column)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
{{ column.markedForDrop ? t("structureEditor.restore") : t("structureEditor.drop") }}
|
||||
</Button>
|
||||
<Button v-else variant="ghost" size="sm" class="h-7 gap-1" @click="removeNewColumn(column)">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.remove") }}
|
||||
</Button>
|
||||
<div class="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="!canMoveColumn(index, -1)"
|
||||
:title="t('structureEditor.moveColumnUp')"
|
||||
:aria-label="t('structureEditor.moveColumnUp')"
|
||||
@click="moveColumn(index, -1)"
|
||||
>
|
||||
<ChevronUp class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
:disabled="!canMoveColumn(index, 1)"
|
||||
:title="t('structureEditor.moveColumnDown')"
|
||||
:aria-label="t('structureEditor.moveColumnDown')"
|
||||
@click="moveColumn(index, 1)"
|
||||
>
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="column.original"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="!canDropColumn(column)"
|
||||
@click="toggleDropColumn(column)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
{{ column.markedForDrop ? t("structureEditor.restore") : t("structureEditor.drop") }}
|
||||
</Button>
|
||||
<Button v-else variant="ghost" size="sm" class="h-7 gap-1" @click="removeNewColumn(column)">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.remove") }}
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -846,6 +846,8 @@ export default {
|
||||
drop: "Drop",
|
||||
restore: "Restore",
|
||||
remove: "Remove",
|
||||
moveColumnUp: "Move column up",
|
||||
moveColumnDown: "Move column down",
|
||||
yes: "Yes",
|
||||
no: "No",
|
||||
emptyReadonly: "No records",
|
||||
|
||||
@@ -743,6 +743,8 @@ export default {
|
||||
drop: "Eliminar",
|
||||
restore: "Restaurar",
|
||||
remove: "Quitar",
|
||||
moveColumnUp: "Mover columna arriba",
|
||||
moveColumnDown: "Mover columna abajo",
|
||||
yes: "Sí",
|
||||
no: "No",
|
||||
emptyReadonly: "Sin registros",
|
||||
|
||||
@@ -827,6 +827,8 @@ export default {
|
||||
drop: "删除",
|
||||
restore: "恢复",
|
||||
remove: "移除",
|
||||
moveColumnUp: "上移字段",
|
||||
moveColumnDown: "下移字段",
|
||||
yes: "是",
|
||||
no: "否",
|
||||
emptyReadonly: "暂无记录",
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface TableStructureCapabilities {
|
||||
alterType: boolean;
|
||||
alterNullability: boolean;
|
||||
alterDefault: boolean;
|
||||
reorderColumn: boolean;
|
||||
comment: boolean;
|
||||
createIndex: boolean;
|
||||
dropIndex: boolean;
|
||||
@@ -41,6 +42,7 @@ const unsupportedCapabilities: TableStructureCapabilities = {
|
||||
alterType: false,
|
||||
alterNullability: false,
|
||||
alterDefault: false,
|
||||
reorderColumn: false,
|
||||
comment: false,
|
||||
createIndex: false,
|
||||
dropIndex: false,
|
||||
@@ -65,6 +67,7 @@ const mysqlCapabilities = capabilities({
|
||||
alterType: true,
|
||||
alterNullability: true,
|
||||
alterDefault: true,
|
||||
reorderColumn: true,
|
||||
comment: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
@@ -182,6 +185,7 @@ const clickhouseCapabilities = capabilities({
|
||||
alterType: true,
|
||||
alterNullability: true,
|
||||
alterDefault: true,
|
||||
reorderColumn: true,
|
||||
comment: true,
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export interface EditableStructureColumn {
|
||||
comment: string;
|
||||
isPrimaryKey: boolean;
|
||||
original?: ColumnInfo;
|
||||
originalPosition?: number;
|
||||
markedForDrop: boolean;
|
||||
}
|
||||
|
||||
@@ -130,12 +131,17 @@ function hasExistingColumnAttributeChange(column: EditableStructureColumn): bool
|
||||
);
|
||||
}
|
||||
|
||||
function buildAddColumnSql(databaseType: StructureSqlFlavor, table: string, column: EditableStructureColumn): string[] {
|
||||
function buildAddColumnSql(
|
||||
databaseType: StructureSqlFlavor,
|
||||
table: string,
|
||||
column: EditableStructureColumn,
|
||||
positionClause = "",
|
||||
): string[] {
|
||||
const addKeyword = databaseType === "sqlserver" ? "ADD" : "ADD COLUMN";
|
||||
const definition = columnDefinition(databaseType, column);
|
||||
const statements = isOracleLike(databaseType)
|
||||
? [`ALTER TABLE ${table} ADD (${definition});`]
|
||||
: [`ALTER TABLE ${table} ${addKeyword} ${definition};`];
|
||||
: [`ALTER TABLE ${table} ${addKeyword} ${definition}${positionClause};`];
|
||||
if ((databaseType === "postgres" || isOracleLike(databaseType)) && clean(column.comment)) {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, column.name)} IS ${quoteString(clean(column.comment))};`,
|
||||
@@ -185,13 +191,13 @@ function buildOracleLikeExistingColumnSql(
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildMysqlExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
|
||||
function buildMysqlExistingColumnSql(table: string, column: EditableStructureColumn, positionClause = ""): string[] {
|
||||
const originalName = column.original?.name ?? column.name;
|
||||
const operation =
|
||||
column.name === originalName
|
||||
? `MODIFY COLUMN ${columnDefinition("mysql", column)}`
|
||||
: `CHANGE COLUMN ${quoteIdent("mysql", originalName)} ${columnDefinition("mysql", column)}`;
|
||||
return [`ALTER TABLE ${table} ${operation};`];
|
||||
return [`ALTER TABLE ${table} ${operation}${positionClause};`];
|
||||
}
|
||||
|
||||
function buildPostgresExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
|
||||
@@ -259,7 +265,11 @@ function buildH2ExistingColumnSql(table: string, column: EditableStructureColumn
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildClickHouseExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
|
||||
function buildClickHouseExistingColumnSql(
|
||||
table: string,
|
||||
column: EditableStructureColumn,
|
||||
positionClause = "",
|
||||
): string[] {
|
||||
const original = column.original;
|
||||
if (!original) return [];
|
||||
|
||||
@@ -278,20 +288,28 @@ function buildClickHouseExistingColumnSql(table: string, column: EditableStructu
|
||||
const defaultValue = normalizeDefault(column.defaultValue);
|
||||
if (defaultValue) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)} DEFAULT ${defaultValue};`,
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)} DEFAULT ${defaultValue}${positionClause};`,
|
||||
);
|
||||
} else if (originalDefault(column)) {
|
||||
statements.push(`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} REMOVE DEFAULT;`);
|
||||
if (clickHouseColumnType(column) !== original.data_type.trim()) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)};`,
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
|
||||
);
|
||||
} else if (positionClause) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)};`,
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
|
||||
);
|
||||
}
|
||||
} else if (positionClause) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} MODIFY COLUMN ${quoteIdent("clickhouse", currentName)} ${clickHouseColumnType(column)}${positionClause};`,
|
||||
);
|
||||
}
|
||||
if (clean(column.comment) !== originalComment(column)) {
|
||||
statements.push(
|
||||
@@ -322,6 +340,41 @@ function buildSqliteExistingColumnSql(table: string, column: EditableStructureCo
|
||||
return statements;
|
||||
}
|
||||
|
||||
function columnPositionClause(
|
||||
dialect: TableStructureDialect,
|
||||
columns: EditableStructureColumn[],
|
||||
index: number,
|
||||
): string {
|
||||
if (dialect !== "mysql" && dialect !== "clickhouse") return "";
|
||||
if (index <= 0) return " FIRST";
|
||||
return ` AFTER ${quoteIdent(dialect, columns[index - 1]?.name ?? "")}`;
|
||||
}
|
||||
|
||||
function originalPreviousColumnName(
|
||||
columns: EditableStructureColumn[],
|
||||
column: EditableStructureColumn,
|
||||
): string | null {
|
||||
if (!column.original || typeof column.originalPosition !== "number") return null;
|
||||
const originalColumns = columns
|
||||
.filter((item) => item.original && typeof item.originalPosition === "number")
|
||||
.sort((a, b) => (a.originalPosition ?? 0) - (b.originalPosition ?? 0));
|
||||
const index = originalColumns.findIndex((item) => item.id === column.id);
|
||||
if (index < 0) return null;
|
||||
return index === 0 ? null : (originalColumns[index - 1]?.original?.name ?? null);
|
||||
}
|
||||
|
||||
function currentPreviousOriginalColumnName(columns: EditableStructureColumn[], index: number): string | null {
|
||||
if (index <= 0) return null;
|
||||
const previous = columns[index - 1];
|
||||
return previous?.original?.name ?? previous?.name ?? null;
|
||||
}
|
||||
|
||||
function mysqlColumnPositionChanged(columns: EditableStructureColumn[], index: number): boolean {
|
||||
const column = columns[index];
|
||||
if (!column?.original || typeof column.originalPosition !== "number") return false;
|
||||
return currentPreviousOriginalColumnName(columns, index) !== originalPreviousColumnName(columns, column);
|
||||
}
|
||||
|
||||
function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings: string[]): string[] {
|
||||
const databaseType = options.databaseType;
|
||||
const capabilities = getTableStructureCapabilities(databaseType);
|
||||
@@ -329,6 +382,8 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
||||
const table = qualifiedTable(dialect, options.schema, options.tableName);
|
||||
const statements: string[] = [];
|
||||
const databaseLabel = databaseType ?? "this database";
|
||||
const activeColumns = options.columns.filter((column) => !column.markedForDrop);
|
||||
const hasOriginalColumnPositions = activeColumns.some((column) => typeof column.originalPosition === "number");
|
||||
|
||||
for (const column of options.columns) {
|
||||
if (column.markedForDrop) {
|
||||
@@ -345,16 +400,24 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
||||
continue;
|
||||
}
|
||||
|
||||
const activeIndex = activeColumns.indexOf(column);
|
||||
const positionClause = hasOriginalColumnPositions ? columnPositionClause(dialect, activeColumns, activeIndex) : "";
|
||||
const hasPositionChange =
|
||||
hasOriginalColumnPositions &&
|
||||
(dialect === "mysql" || dialect === "clickhouse") &&
|
||||
!!column.original &&
|
||||
mysqlColumnPositionChanged(activeColumns, activeIndex);
|
||||
|
||||
if (!column.original) {
|
||||
if (!capabilities.addColumn) {
|
||||
warnings.push(`Adding columns is not supported for ${databaseLabel} from this editor.`);
|
||||
continue;
|
||||
}
|
||||
statements.push(...buildAddColumnSql(dialect, table, column));
|
||||
statements.push(...buildAddColumnSql(dialect, table, column, positionClause));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasExistingColumnAttributeChange(column)) continue;
|
||||
if (!hasExistingColumnAttributeChange(column) && !hasPositionChange) continue;
|
||||
const original = column.original;
|
||||
const hasRename = column.name !== original.name;
|
||||
const hasAttributeChange =
|
||||
@@ -362,6 +425,9 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
||||
column.isNullable !== original.is_nullable ||
|
||||
normalizeDefault(column.defaultValue) !== originalDefault(column) ||
|
||||
clean(column.comment) !== originalComment(column);
|
||||
if (hasPositionChange && !capabilities.reorderColumn) {
|
||||
warnings.push(`Reordering columns is not supported for ${databaseLabel} from this editor.`);
|
||||
}
|
||||
if (hasRename && !capabilities.renameColumn) {
|
||||
warnings.push(`Renaming columns is not supported for ${databaseLabel} from this editor.`);
|
||||
}
|
||||
@@ -369,13 +435,14 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
||||
warnings.push(`Editing existing columns is not supported for ${databaseLabel} yet.`);
|
||||
}
|
||||
if (
|
||||
(hasPositionChange && !capabilities.reorderColumn) ||
|
||||
(hasRename && !capabilities.renameColumn) ||
|
||||
(hasAttributeChange && !capabilities.alterExistingColumn && dialect !== "sqlite")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (dialect === "mysql") {
|
||||
statements.push(...buildMysqlExistingColumnSql(table, column));
|
||||
statements.push(...buildMysqlExistingColumnSql(table, column, hasPositionChange ? positionClause : ""));
|
||||
} else if (dialect === "postgres") {
|
||||
statements.push(...buildPostgresExistingColumnSql(table, column));
|
||||
} else if (dialect === "oracle") {
|
||||
@@ -383,7 +450,7 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
||||
} else if (dialect === "h2") {
|
||||
statements.push(...buildH2ExistingColumnSql(table, column));
|
||||
} else if (dialect === "clickhouse") {
|
||||
statements.push(...buildClickHouseExistingColumnSql(table, column));
|
||||
statements.push(...buildClickHouseExistingColumnSql(table, column, hasPositionChange ? positionClause : ""));
|
||||
} else if (dialect === "sqlite") {
|
||||
statements.push(...buildSqliteExistingColumnSql(table, column, warnings));
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ColumnInfo, IndexInfo } from "../types/database.ts";
|
||||
import type { EditableStructureColumn, EditableStructureIndex } from "./tableStructureEditorSql.ts";
|
||||
|
||||
export function createColumnDrafts(columns: ColumnInfo[]): EditableStructureColumn[] {
|
||||
return columns.map((column) => ({
|
||||
return columns.map((column, index) => ({
|
||||
id: `existing:${column.name}`,
|
||||
name: column.name,
|
||||
dataType: column.data_type,
|
||||
@@ -11,6 +11,7 @@ export function createColumnDrafts(columns: ColumnInfo[]): EditableStructureColu
|
||||
comment: column.comment ?? "",
|
||||
isPrimaryKey: column.is_primary_key,
|
||||
original: column,
|
||||
originalPosition: index,
|
||||
markedForDrop: false,
|
||||
}));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user