mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 06:00:39 +00:00
feat(structure): auto name new indexes
This commit is contained in:
@@ -25,7 +25,7 @@ import { type EditableStructureColumn, type EditableStructureIndex } from "@/lib
|
||||
import { getTableMetadataCapabilities } from "@/lib/tableMetadataCapabilities";
|
||||
import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
|
||||
import { connectionObjectTreeQuerySchema, effectiveDatabaseTypeForConnection } from "@/lib/jdbcDialect";
|
||||
import { buildStructureTargetLabel, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, getDataTypeOptions, getDefaultLengthForType, splitDataType, toColumnNames } from "@/lib/tableStructureEditorState";
|
||||
import { buildStructureTargetLabel, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, generateIndexName, generateUniqueIndexName, getDataTypeOptions, getDefaultLengthForType, splitDataType, toColumnNames } from "@/lib/tableStructureEditorState";
|
||||
import type { ForeignKeyInfo, TriggerInfo } from "@/types/database";
|
||||
import * as api from "@/lib/api";
|
||||
|
||||
@@ -482,6 +482,7 @@ function addIndex() {
|
||||
id: `new:${uuid()}`,
|
||||
name: "",
|
||||
columns: [],
|
||||
nameEdited: false,
|
||||
isUnique: false,
|
||||
isPrimary: false,
|
||||
filter: "",
|
||||
@@ -500,6 +501,32 @@ function addIndex() {
|
||||
});
|
||||
}
|
||||
|
||||
function structureIndexTableName(): string {
|
||||
return (isCreateMode.value ? newTableName.value : props.tableName).trim();
|
||||
}
|
||||
|
||||
function existingIndexNamesForDraft(index: EditableStructureIndex): string[] {
|
||||
return indexes.value.filter((item) => item.id !== index.id && !item.markedForDrop).map((item) => item.name);
|
||||
}
|
||||
|
||||
function generatedIndexNameForDraft(index: EditableStructureIndex, columnsForName = index.columns): string {
|
||||
return generateUniqueIndexName(structureIndexTableName(), columnsForName, existingIndexNamesForDraft(index));
|
||||
}
|
||||
|
||||
function refreshAutoIndexName(index: EditableStructureIndex, previousColumns = index.columns) {
|
||||
if (index.original || index.nameEdited) return;
|
||||
const previousName = generateIndexName(structureIndexTableName(), previousColumns);
|
||||
const previousUniqueName = generateUniqueIndexName(structureIndexTableName(), previousColumns, existingIndexNamesForDraft(index));
|
||||
const currentName = index.name.trim();
|
||||
if (currentName && currentName !== previousName && currentName !== previousUniqueName) return;
|
||||
index.name = generatedIndexNameForDraft(index);
|
||||
}
|
||||
|
||||
function onIndexNameInput(index: EditableStructureIndex, value: string | number) {
|
||||
index.name = String(value ?? "");
|
||||
index.nameEdited = true;
|
||||
}
|
||||
|
||||
const availableColumnNames = computed(() =>
|
||||
columns.value
|
||||
.filter((c) => !c.markedForDrop)
|
||||
@@ -515,9 +542,11 @@ const filteredColumnNames = computed(() => {
|
||||
});
|
||||
|
||||
function toggleIndexColumn(index: EditableStructureIndex, col: string) {
|
||||
const previousColumns = [...index.columns];
|
||||
const i = index.columns.indexOf(col);
|
||||
if (i >= 0) index.columns.splice(i, 1);
|
||||
else index.columns.push(col);
|
||||
refreshAutoIndexName(index, previousColumns);
|
||||
}
|
||||
|
||||
function toggleIncludedColumn(index: EditableStructureIndex, col: string) {
|
||||
@@ -699,6 +728,12 @@ watch(
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
watch([() => props.tableName, newTableName], () => {
|
||||
for (const index of indexes.value) {
|
||||
refreshAutoIndexName(index);
|
||||
}
|
||||
});
|
||||
|
||||
watch(refreshVersion, (version, previous) => {
|
||||
if (version === previous || !version || isCreateMode.value) return;
|
||||
void loadStructure(true);
|
||||
@@ -1019,7 +1054,7 @@ watch(activeTab, (tab) => {
|
||||
<tbody>
|
||||
<tr v-for="index in indexes" :key="index.id" :class="index.markedForDrop ? 'bg-destructive/5 opacity-60' : ''" :data-new-index-row="!index.original ? 'true' : undefined">
|
||||
<td :class="structureCellClass">
|
||||
<Input v-model="index.name" :class="structureControlClass" :disabled="!canEditIndexDraft(index)" data-index-name-input />
|
||||
<Input :model-value="index.name" :class="structureControlClass" :disabled="!canEditIndexDraft(index)" data-index-name-input @update:model-value="(value: string | number) => onIndexNameInput(index, value)" />
|
||||
</td>
|
||||
<td :class="[structureCellClass, 'overflow-hidden']">
|
||||
<DropdownMenu v-if="canEditIndexDraft(index)">
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface EditableStructureIndex {
|
||||
id: string;
|
||||
name: string;
|
||||
columns: string[];
|
||||
nameEdited?: boolean;
|
||||
isUnique: boolean;
|
||||
isPrimary: boolean;
|
||||
filter: string;
|
||||
|
||||
@@ -358,6 +358,7 @@ export function createIndexDrafts(indexes: IndexInfo[]): EditableStructureIndex[
|
||||
id: `existing:${index.name}`,
|
||||
name: index.name,
|
||||
columns: [...index.columns],
|
||||
nameEdited: true,
|
||||
isUnique: index.is_unique,
|
||||
isPrimary: index.is_primary,
|
||||
filter: index.filter ?? "",
|
||||
@@ -373,6 +374,48 @@ export function toColumnNames(columns: string[]): string {
|
||||
return columns.join(", ");
|
||||
}
|
||||
|
||||
const AUTO_INDEX_NAME_MAX_LENGTH = 63;
|
||||
|
||||
function normalizeIndexNamePart(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
const unquoted = (trimmed.startsWith("[") && trimmed.endsWith("]")) || (trimmed.startsWith("`") && trimmed.endsWith("`")) || (trimmed.startsWith('"') && trimmed.endsWith('"')) ? trimmed.slice(1, -1) : trimmed;
|
||||
return unquoted
|
||||
.trim()
|
||||
.replace(/[^a-zA-Z0-9]+/g, "_")
|
||||
.replace(/_+/g, "_")
|
||||
.replace(/^_+|_+$/g, "")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
function truncateIndexName(value: string, maxLength = AUTO_INDEX_NAME_MAX_LENGTH): string {
|
||||
if (value.length <= maxLength) return value;
|
||||
const suffix = "_IDX";
|
||||
if (!value.endsWith(suffix)) return value.slice(0, maxLength).replace(/_+$/g, "");
|
||||
return `${value.slice(0, maxLength - suffix.length).replace(/_+$/g, "")}${suffix}`;
|
||||
}
|
||||
|
||||
export function generateIndexName(tableName: string, columns: string[], maxLength = AUTO_INDEX_NAME_MAX_LENGTH): string {
|
||||
const parts = [tableName, ...columns].map(normalizeIndexNamePart).filter(Boolean);
|
||||
if (parts.length === 0) return "";
|
||||
return truncateIndexName(`${parts.join("_")}_IDX`, maxLength);
|
||||
}
|
||||
|
||||
export function generateUniqueIndexName(tableName: string, columns: string[], existingNames: Iterable<string>, maxLength = AUTO_INDEX_NAME_MAX_LENGTH): string {
|
||||
const base = generateIndexName(tableName, columns, maxLength);
|
||||
if (!base) return "";
|
||||
|
||||
const taken = new Set([...existingNames].map((name) => name.trim().toLowerCase()).filter(Boolean));
|
||||
if (!taken.has(base.toLowerCase())) return base;
|
||||
|
||||
for (let counter = 2; counter < 10_000; counter++) {
|
||||
const suffix = `_${counter}`;
|
||||
const stem = base.length + suffix.length <= maxLength ? base : base.slice(0, maxLength - suffix.length).replace(/_+$/g, "");
|
||||
const candidate = `${stem}${suffix}`;
|
||||
if (!taken.has(candidate.toLowerCase())) return candidate;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function splitDataType(raw: string): { baseType: string; params: string } {
|
||||
const trimmed = raw.trim();
|
||||
const parenIdx = trimmed.indexOf("(");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
import { buildStructureTargetLabel, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, getDataTypeOptions, normalizeDataTypeParams, parseExtraToColumnExtra, toColumnNames } from "../../apps/desktop/src/lib/tableStructureEditorState.ts";
|
||||
import { buildStructureTargetLabel, combineDataTypeForDatabase, createColumnDrafts, createIndexDrafts, generateIndexName, generateUniqueIndexName, getDataTypeOptions, normalizeDataTypeParams, parseExtraToColumnExtra, toColumnNames } from "../../apps/desktop/src/lib/tableStructureEditorState.ts";
|
||||
import type { ColumnInfo, IndexInfo } from "../../apps/desktop/src/types/database.ts";
|
||||
|
||||
const columns: ColumnInfo[] = [
|
||||
@@ -153,6 +153,17 @@ test("creates editable index drafts and splits pasted column lists", () => {
|
||||
assert.equal(toColumnNames(["id", "name"]), "id, name");
|
||||
});
|
||||
|
||||
test("generates conventional index names from table and columns", () => {
|
||||
assert.equal(generateIndexName("A", ["B"]), "A_B_IDX");
|
||||
assert.equal(generateIndexName("order item", ["customer-id", "created_at"]), "ORDER_ITEM_CUSTOMER_ID_CREATED_AT_IDX");
|
||||
assert.equal(generateIndexName("users", ["email"], 12), "USERS_EM_IDX");
|
||||
});
|
||||
|
||||
test("generates unique index names when automatic name already exists", () => {
|
||||
assert.equal(generateUniqueIndexName("users", ["email"], ["USERS_EMAIL_IDX"]), "USERS_EMAIL_IDX_2");
|
||||
assert.equal(generateUniqueIndexName("users", ["email"], ["users_email_idx", "USERS_EMAIL_IDX_2"]), "USERS_EMAIL_IDX_3");
|
||||
});
|
||||
|
||||
test("structure editor target label omits duplicate database and schema", () => {
|
||||
assert.equal(buildStructureTargetLabel("online-clickhouse", "testdb", "testdb", "users"), "online-clickhouse / testdb / users");
|
||||
assert.equal(buildStructureTargetLabel("online-postgres", "app", "public", "users"), "online-postgres / app / public / users");
|
||||
|
||||
Reference in New Issue
Block a user