fix: guard index column splits, restrict INCLUDE/WHERE by db, unify index type dropdowns

- Filter empty strings from STRING_AGG/LISTAGG splits in SQL Server, Oracle, MySQL
- Emit INCLUDE clause only for PostgreSQL and SQL Server (not MySQL/SQLite)
- Emit WHERE clause only for PostgreSQL, SQL Server, SQLite (not MySQL)
- Replace index type free-text input with dropdown for all 5 database types
- Fix hardcoded column count in resize handle (use indexColLabels.length - 1)
- Fix pg_get_indexdef type mismatch: cast k.n from bigint to integer
This commit is contained in:
yavon007
2026-05-03 20:33:09 +08:00
parent 25f2f46718
commit a59494e4c4
8 changed files with 50 additions and 21 deletions
@@ -16,6 +16,9 @@ import {
import {
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { useConnectionStore } from "@/stores/connectionStore";
import { useToast } from "@/composables/useToast";
import { buildTableStructureChangeSql, type EditableStructureColumn, type EditableStructureIndex } from "@/lib/tableStructureEditorSql";
@@ -72,6 +75,26 @@ const connection = computed(() =>
props.prefillConnectionId ? store.getConfig(props.prefillConnectionId) : undefined
);
const databaseType = computed(() => connection.value?.db_type);
const indexTypesByDb: Record<string, string[]> = {
postgres: ["BTREE", "HASH", "GIST", "SPGIST", "GIN", "BRIN"],
mysql: ["BTREE", "HASH", "FULLTEXT", "SPATIAL", "RTREE"],
sqlserver: ["CLUSTERED", "NONCLUSTERED", "COLUMNSTORE", "NONCLUSTERED COLUMNSTORE", "XML", "SPATIAL"],
oracle: ["NORMAL", "BITMAP", "FUNCTION-BASED NORMAL", "FUNCTION-BASED DOMAIN", "DOMAIN", "CLUSTER"],
sqlite: ["BTREE"],
};
const indexTypeOptions = computed(() => indexTypesByDb[databaseType.value ?? ""] ?? []);
const indexColLabels = computed(() => [
t('structureEditor.indexName'),
t('structureEditor.indexColumns'),
t('structureEditor.unique'),
t('structureEditor.indexType'),
t('structureEditor.includedColumns'),
t('structureEditor.filter'),
t('structureEditor.comment'),
t('structureEditor.actions'),
]);
const targetSchema = computed(() => props.prefillSchema || props.prefillDatabase || "");
const targetLabel = computed(() => [
connection.value?.name,
@@ -341,23 +364,14 @@ watch(open, (value) => {
<thead class="sticky top-0 z-10 bg-background">
<tr>
<th
v-for="(label, i) in [
t('structureEditor.indexName'),
t('structureEditor.indexColumns'),
t('structureEditor.unique'),
t('structureEditor.indexType'),
t('structureEditor.includedColumns'),
t('structureEditor.filter'),
t('structureEditor.comment'),
t('structureEditor.actions'),
]"
v-for="(label, i) in indexColLabels"
:key="i"
class="relative border-b border-r px-2 py-2 text-left"
:style="{ width: indexColWidths[i] + 'px', minWidth: indexColWidths[i] + 'px' }"
>
{{ label }}
<div
v-if="i < 7"
v-if="i < indexColLabels.length - 1"
class="absolute right-0 top-0 z-20 h-full w-1 cursor-col-resize hover:bg-primary/30"
:class="resizing?.col === i ? 'bg-primary/30' : ''"
@mousedown="onIndexColResize($event, i)"
@@ -404,6 +418,19 @@ watch(open, (value) => {
</td>
<td class="border-b border-r px-2 py-1.5">
<span v-if="index.original" class="text-muted-foreground">{{ index.indexType || 'BTREE' }}</span>
<Select
v-else-if="indexTypeOptions.length > 0"
:model-value="index.indexType || 'BTREE'"
:disabled="index.markedForDrop"
@update:model-value="(v: string) => index.indexType = v"
>
<SelectTrigger class="h-7 font-mono text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="opt in indexTypeOptions" :key="opt" :value="opt">{{ opt }}</SelectItem>
</SelectContent>
</Select>
<Input
v-else
v-model="index.indexType"
+3 -2
View File
@@ -184,8 +184,9 @@ export function generateSyncSql(
const usingClause = idxType && dbType === "postgres" ? ` USING ${idxType}` : "";
const typePrefix = idxType && dbType === "sqlserver" ? `${idxType} ` : "";
const incCols = idx.source.included_columns ?? [];
const includeClause = incCols.length > 0 ? ` INCLUDE (${incCols.map((c) => quoteId(c, dbType)).join(", ")})` : "";
const filter = idx.source.filter ? ` WHERE ${idx.source.filter}` : "";
const includeClause = incCols.length > 0 && (dbType === "postgres" || dbType === "sqlserver") ? ` INCLUDE (${incCols.map((c) => quoteId(c, dbType)).join(", ")})` : "";
const supportsWhere = dbType === "postgres" || dbType === "sqlserver" || dbType === "sqlite";
const filter = idx.source.filter && supportsWhere ? ` WHERE ${idx.source.filter}` : "";
lines.push(`CREATE ${unique}${typePrefix}INDEX ${quoteId(idx.name, dbType)} ON ${qt}${usingClause} (${cols})${includeClause}${filter};`);
} else if (idx.type === "removed") {
if (isMySQL) {
+3 -2
View File
@@ -253,9 +253,10 @@ function buildIndexSql(options: BuildTableStructureChangeSqlOptions, warnings: s
const usingClause = idxType && databaseType === "postgres" ? ` USING ${idxType}` : "";
const typePrefix = idxType && databaseType === "sqlserver" ? `${idxType} ` : "";
const incCols = index.includedColumns.map(clean).filter(Boolean);
const includeClause = incCols.length > 0 ? ` INCLUDE (${incCols.map((c) => quoteIdent(databaseType, c)).join(", ")})` : "";
const includeClause = incCols.length > 0 && (databaseType === "postgres" || databaseType === "sqlserver") ? ` INCLUDE (${incCols.map((c) => quoteIdent(databaseType, c)).join(", ")})` : "";
const filter = clean(index.filter);
const whereClause = filter ? ` WHERE ${filter}` : "";
const supportsWhere = databaseType === "postgres" || databaseType === "sqlserver" || databaseType === "sqlite";
const whereClause = filter && supportsWhere ? ` WHERE ${filter}` : "";
statements.push(`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)} ON ${table}${usingClause} (${cols})${includeClause}${whereClause};`);
const comment = clean(index.comment);
if (comment && databaseType === "postgres") {