feat(structure): add DDL capability matrix
This commit is contained in:
parent
f89b3a2bee
commit
cf46aec158
|
|
@ -38,6 +38,7 @@ import {
|
|||
type EditableStructureColumn,
|
||||
type EditableStructureIndex,
|
||||
} from "@/lib/tableStructureEditorSql";
|
||||
import { getTableStructureCapabilities } from "@/lib/tableStructureCapabilities";
|
||||
import { createColumnDrafts, createIndexDrafts, toColumnNames } from "@/lib/tableStructureEditorState";
|
||||
import type { ForeignKeyInfo, TriggerInfo } from "@/types/database";
|
||||
import * as api from "@/lib/api";
|
||||
|
|
@ -89,6 +90,7 @@ function onIndexColResize(e: MouseEvent, col: number) {
|
|||
|
||||
const connection = computed(() => (props.prefillConnectionId ? store.getConfig(props.prefillConnectionId) : undefined));
|
||||
const databaseType = computed(() => connection.value?.db_type);
|
||||
const structureCapabilities = computed(() => getTableStructureCapabilities(databaseType.value));
|
||||
|
||||
const indexTypesByDb: Record<string, string[]> = {
|
||||
postgres: ["BTREE", "HASH", "GIST", "SPGIST", "GIN", "BRIN"],
|
||||
|
|
@ -97,7 +99,9 @@ const indexTypesByDb: Record<string, string[]> = {
|
|||
oracle: ["NORMAL", "BITMAP", "FUNCTION-BASED NORMAL", "FUNCTION-BASED DOMAIN", "DOMAIN", "CLUSTER"],
|
||||
sqlite: ["BTREE"],
|
||||
};
|
||||
const indexTypeOptions = computed(() => indexTypesByDb[databaseType.value ?? ""] ?? []);
|
||||
const indexTypeOptions = computed(() =>
|
||||
structureCapabilities.value.indexType ? (indexTypesByDb[databaseType.value ?? ""] ?? []) : [],
|
||||
);
|
||||
|
||||
const indexColLabels = computed(() => [
|
||||
t("structureEditor.indexName"),
|
||||
|
|
@ -195,6 +199,7 @@ async function loadStructure() {
|
|||
}
|
||||
|
||||
function addColumn() {
|
||||
if (!structureCapabilities.value.addColumn) return;
|
||||
columns.value.push({
|
||||
id: `new:${uuid()}`,
|
||||
name: "",
|
||||
|
|
@ -212,11 +217,38 @@ function removeNewColumn(column: EditableStructureColumn) {
|
|||
}
|
||||
|
||||
function toggleDropColumn(column: EditableStructureColumn) {
|
||||
if (!column.original || column.isPrimaryKey) return;
|
||||
if (!canDropColumn(column)) return;
|
||||
column.markedForDrop = !column.markedForDrop;
|
||||
}
|
||||
|
||||
function isColumnNameDisabled(column: EditableStructureColumn): boolean {
|
||||
return column.markedForDrop || (!!column.original && !structureCapabilities.value.renameColumn);
|
||||
}
|
||||
|
||||
function isColumnTypeDisabled(column: EditableStructureColumn): boolean {
|
||||
return column.markedForDrop || (!!column.original && !structureCapabilities.value.alterType);
|
||||
}
|
||||
|
||||
function isColumnNullableDisabled(column: EditableStructureColumn): boolean {
|
||||
return (
|
||||
column.markedForDrop || column.isPrimaryKey || (!!column.original && !structureCapabilities.value.alterNullability)
|
||||
);
|
||||
}
|
||||
|
||||
function isColumnDefaultDisabled(column: EditableStructureColumn): boolean {
|
||||
return column.markedForDrop || (!!column.original && !structureCapabilities.value.alterDefault);
|
||||
}
|
||||
|
||||
function isColumnCommentDisabled(column: EditableStructureColumn): boolean {
|
||||
return column.markedForDrop || !structureCapabilities.value.comment;
|
||||
}
|
||||
|
||||
function canDropColumn(column: EditableStructureColumn): boolean {
|
||||
return !!column.original && !column.isPrimaryKey && structureCapabilities.value.dropColumn;
|
||||
}
|
||||
|
||||
function addIndex() {
|
||||
if (!structureCapabilities.value.createIndex) return;
|
||||
indexes.value.push({
|
||||
id: `new:${uuid()}`,
|
||||
name: "",
|
||||
|
|
@ -252,6 +284,7 @@ function toggleIndexColumn(index: EditableStructureIndex, col: string) {
|
|||
}
|
||||
|
||||
function toggleIncludedColumn(index: EditableStructureIndex, col: string) {
|
||||
if (!structureCapabilities.value.indexInclude) return;
|
||||
const i = index.includedColumns.indexOf(col);
|
||||
if (i >= 0) index.includedColumns.splice(i, 1);
|
||||
else index.includedColumns.push(col);
|
||||
|
|
@ -262,10 +295,22 @@ function removeNewIndex(index: EditableStructureIndex) {
|
|||
}
|
||||
|
||||
function toggleDropIndex(index: EditableStructureIndex) {
|
||||
if (!index.original || index.isPrimary) return;
|
||||
if (!canDropIndex(index)) return;
|
||||
index.markedForDrop = !index.markedForDrop;
|
||||
}
|
||||
|
||||
function canEditIndexDraft(index: EditableStructureIndex): boolean {
|
||||
return !index.original && !index.markedForDrop && structureCapabilities.value.createIndex;
|
||||
}
|
||||
|
||||
function canEditIndexFilter(index: EditableStructureIndex): boolean {
|
||||
return canEditIndexDraft(index) && structureCapabilities.value.indexFilter;
|
||||
}
|
||||
|
||||
function canDropIndex(index: EditableStructureIndex): boolean {
|
||||
return !!index.original && !index.isPrimary && structureCapabilities.value.dropIndex;
|
||||
}
|
||||
|
||||
async function applyChanges() {
|
||||
if (!canApply.value || !props.prefillConnectionId || !props.prefillDatabase) return;
|
||||
saving.value = true;
|
||||
|
|
@ -346,11 +391,23 @@ watch(open, (value) => {
|
|||
<TabsTrigger value="foreignKeys">{{ t("structureEditor.foreignKeys") }}</TabsTrigger>
|
||||
<TabsTrigger value="triggers">{{ t("structureEditor.triggers") }}</TabsTrigger>
|
||||
</TabsList>
|
||||
<Button v-if="activeTab === 'columns'" size="sm" class="h-7 gap-1" @click="addColumn">
|
||||
<Button
|
||||
v-if="activeTab === 'columns'"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="!structureCapabilities.addColumn"
|
||||
@click="addColumn"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.addColumn") }}
|
||||
</Button>
|
||||
<Button v-if="activeTab === 'indexes'" size="sm" class="h-7 gap-1" @click="addIndex">
|
||||
<Button
|
||||
v-if="activeTab === 'indexes'"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="!structureCapabilities.createIndex"
|
||||
@click="addIndex"
|
||||
>
|
||||
<Plus class="h-3.5 w-3.5" />
|
||||
{{ t("structureEditor.addIndex") }}
|
||||
</Button>
|
||||
|
|
@ -394,13 +451,17 @@ watch(open, (value) => {
|
|||
</div>
|
||||
</td>
|
||||
<td class="border-b border-r px-2 py-1.5">
|
||||
<Input v-model="column.name" class="h-7 min-w-32 text-xs" :disabled="column.markedForDrop" />
|
||||
<Input
|
||||
v-model="column.name"
|
||||
class="h-7 min-w-32 text-xs"
|
||||
:disabled="isColumnNameDisabled(column)"
|
||||
/>
|
||||
</td>
|
||||
<td class="border-b border-r px-2 py-1.5">
|
||||
<Input
|
||||
v-model="column.dataType"
|
||||
class="h-7 min-w-36 font-mono text-xs"
|
||||
:disabled="column.markedForDrop"
|
||||
:disabled="isColumnTypeDisabled(column)"
|
||||
/>
|
||||
</td>
|
||||
<td class="border-b border-r px-2 py-1.5">
|
||||
|
|
@ -409,7 +470,7 @@ watch(open, (value) => {
|
|||
v-model="column.isNullable"
|
||||
type="checkbox"
|
||||
class="h-3.5 w-3.5"
|
||||
:disabled="column.markedForDrop || column.isPrimaryKey"
|
||||
:disabled="isColumnNullableDisabled(column)"
|
||||
/>
|
||||
<span>{{ column.isNullable ? t("structureEditor.yes") : t("structureEditor.no") }}</span>
|
||||
</label>
|
||||
|
|
@ -418,7 +479,7 @@ watch(open, (value) => {
|
|||
<Input
|
||||
v-model="column.defaultValue"
|
||||
class="h-7 min-w-28 font-mono text-xs"
|
||||
:disabled="column.markedForDrop"
|
||||
:disabled="isColumnDefaultDisabled(column)"
|
||||
/>
|
||||
</td>
|
||||
<td class="border-b border-r px-2 py-1.5">
|
||||
|
|
@ -426,7 +487,7 @@ watch(open, (value) => {
|
|||
<Input
|
||||
v-model="column.comment"
|
||||
class="h-7 min-w-0 flex-1 text-xs"
|
||||
:disabled="column.markedForDrop"
|
||||
:disabled="isColumnCommentDisabled(column)"
|
||||
/>
|
||||
<Popover>
|
||||
<PopoverTrigger as-child>
|
||||
|
|
@ -434,7 +495,7 @@ watch(open, (value) => {
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7 shrink-0"
|
||||
:disabled="column.markedForDrop"
|
||||
:disabled="isColumnCommentDisabled(column)"
|
||||
:aria-label="t('structureEditor.editComment')"
|
||||
:title="t('structureEditor.editComment')"
|
||||
>
|
||||
|
|
@ -454,7 +515,7 @@ watch(open, (value) => {
|
|||
v-model="column.comment"
|
||||
class="min-h-36 w-full resize-y rounded-md border bg-background px-2.5 py-2 text-xs leading-5 outline-none focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
:placeholder="t('structureEditor.commentPlaceholder')"
|
||||
:disabled="column.markedForDrop"
|
||||
:disabled="isColumnCommentDisabled(column)"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
|
@ -466,7 +527,7 @@ watch(open, (value) => {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="column.isPrimaryKey"
|
||||
:disabled="!canDropColumn(column)"
|
||||
@click="toggleDropColumn(column)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
|
|
@ -512,14 +573,10 @@ watch(open, (value) => {
|
|||
:class="index.markedForDrop ? 'bg-destructive/5 opacity-60' : ''"
|
||||
>
|
||||
<td class="border-b border-r px-2 py-1.5">
|
||||
<Input
|
||||
v-model="index.name"
|
||||
class="h-7 text-xs"
|
||||
:disabled="!!index.original || index.markedForDrop"
|
||||
/>
|
||||
<Input v-model="index.name" class="h-7 text-xs" :disabled="!canEditIndexDraft(index)" />
|
||||
</td>
|
||||
<td class="overflow-hidden border-b border-r px-2 py-1.5">
|
||||
<DropdownMenu v-if="!index.original && !index.markedForDrop">
|
||||
<DropdownMenu v-if="canEditIndexDraft(index)">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" class="h-7 w-full justify-between font-mono text-xs">
|
||||
<span class="truncate">{{
|
||||
|
|
@ -565,7 +622,7 @@ watch(open, (value) => {
|
|||
v-model="index.isUnique"
|
||||
type="checkbox"
|
||||
class="h-3.5 w-3.5"
|
||||
:disabled="!!index.original || index.markedForDrop"
|
||||
:disabled="!canEditIndexDraft(index)"
|
||||
/>
|
||||
<span>{{ index.isUnique ? t("structureEditor.yes") : t("structureEditor.no") }}</span>
|
||||
</label>
|
||||
|
|
@ -577,7 +634,7 @@ watch(open, (value) => {
|
|||
<Select
|
||||
v-else-if="indexTypeOptions.length > 0"
|
||||
:model-value="index.indexType || 'BTREE'"
|
||||
:disabled="index.markedForDrop"
|
||||
:disabled="!canEditIndexDraft(index)"
|
||||
@update:model-value="(v: any) => (index.indexType = String(v ?? ''))"
|
||||
>
|
||||
<SelectTrigger class="h-7 font-mono text-xs">
|
||||
|
|
@ -592,11 +649,11 @@ watch(open, (value) => {
|
|||
v-model="index.indexType"
|
||||
class="h-7 font-mono text-xs"
|
||||
placeholder="BTREE"
|
||||
:disabled="index.markedForDrop"
|
||||
:disabled="!canEditIndexDraft(index)"
|
||||
/>
|
||||
</td>
|
||||
<td class="overflow-hidden border-b border-r px-2 py-1.5">
|
||||
<DropdownMenu v-if="!index.original && !index.markedForDrop">
|
||||
<DropdownMenu v-if="canEditIndexDraft(index) && structureCapabilities.indexInclude">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="outline" class="h-7 w-full justify-between font-mono text-xs">
|
||||
<span class="truncate">{{
|
||||
|
|
@ -639,12 +696,17 @@ watch(open, (value) => {
|
|||
v-model="index.filter"
|
||||
class="h-7 font-mono text-xs"
|
||||
:placeholder="index.original?.filter || ''"
|
||||
:disabled="!!index.original || index.markedForDrop"
|
||||
:disabled="!canEditIndexFilter(index)"
|
||||
/>
|
||||
</td>
|
||||
<td class="border-b border-r px-2 py-1.5">
|
||||
<span v-if="index.original" class="text-muted-foreground text-xs">{{ index.comment }}</span>
|
||||
<Input v-else v-model="index.comment" class="h-7 text-xs" :disabled="index.markedForDrop" />
|
||||
<Input
|
||||
v-else
|
||||
v-model="index.comment"
|
||||
class="h-7 text-xs"
|
||||
:disabled="!canEditIndexDraft(index) || !structureCapabilities.indexComment"
|
||||
/>
|
||||
</td>
|
||||
<td class="border-b px-2 py-1.5">
|
||||
<Badge v-if="index.isPrimary" variant="outline">{{ t("structureEditor.primary") }}</Badge>
|
||||
|
|
@ -653,6 +715,7 @@ watch(open, (value) => {
|
|||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 gap-1"
|
||||
:disabled="!canDropIndex(index)"
|
||||
@click="toggleDropIndex(index)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
|
|
|
|||
|
|
@ -123,9 +123,22 @@ export const TABLE_STRUCTURE_SUPPORTED_TYPES = new Set<DatabaseType>([
|
|||
"postgres",
|
||||
"sqlite",
|
||||
"duckdb",
|
||||
"clickhouse",
|
||||
"sqlserver",
|
||||
"oracle",
|
||||
"doris",
|
||||
"starrocks",
|
||||
"redshift",
|
||||
"dameng",
|
||||
"gaussdb",
|
||||
"kingbase",
|
||||
"highgo",
|
||||
"vastbase",
|
||||
"goldendb",
|
||||
"opengauss",
|
||||
"oceanbase-oracle",
|
||||
"h2",
|
||||
"sundb",
|
||||
]);
|
||||
|
||||
export const CREATE_DATABASE_SUPPORTED_TYPES = new Set<DatabaseType>([
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { DatabaseType, TreeNodeType } from "@/types/database";
|
||||
import { canEditTableStructure } from "./tableStructureCapabilities";
|
||||
import {
|
||||
AGENT_DRIVER_TYPES,
|
||||
CREATE_DATABASE_SUPPORTED_TYPES,
|
||||
|
|
@ -11,7 +12,6 @@ import {
|
|||
SINGLE_DATABASE_TYPES,
|
||||
SQL_FILE_UNSUPPORTED_TYPES,
|
||||
TABLE_IMPORT_SUPPORTED_TYPES,
|
||||
TABLE_STRUCTURE_SUPPORTED_TYPES,
|
||||
TRANSFER_SQL_TYPES,
|
||||
TREE_SCHEMA_TYPES,
|
||||
} from "./databaseCapabilitySets";
|
||||
|
|
@ -49,7 +49,7 @@ export function supportsTableImport(dbType?: DatabaseType): boolean {
|
|||
}
|
||||
|
||||
export function supportsTableStructureEditing(dbType?: DatabaseType): boolean {
|
||||
return !!dbType && TABLE_STRUCTURE_SUPPORTED_TYPES.has(dbType);
|
||||
return canEditTableStructure(dbType);
|
||||
}
|
||||
|
||||
export function supportsDatabaseCreation(dbType?: DatabaseType): boolean {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
export type TableStructureDialect =
|
||||
| "mysql"
|
||||
| "postgres"
|
||||
| "sqlite"
|
||||
| "duckdb"
|
||||
| "sqlserver"
|
||||
| "oracle"
|
||||
| "h2"
|
||||
| "clickhouse"
|
||||
| "unsupported";
|
||||
|
||||
export interface TableStructureCapabilities {
|
||||
dialect: TableStructureDialect;
|
||||
createTable: boolean;
|
||||
addColumn: boolean;
|
||||
dropColumn: boolean;
|
||||
renameColumn: boolean;
|
||||
alterExistingColumn: boolean;
|
||||
alterType: boolean;
|
||||
alterNullability: boolean;
|
||||
alterDefault: boolean;
|
||||
comment: boolean;
|
||||
createIndex: boolean;
|
||||
dropIndex: boolean;
|
||||
indexType: boolean;
|
||||
indexInclude: boolean;
|
||||
indexFilter: boolean;
|
||||
indexComment: boolean;
|
||||
}
|
||||
|
||||
const unsupportedCapabilities: TableStructureCapabilities = {
|
||||
dialect: "unsupported",
|
||||
createTable: false,
|
||||
addColumn: false,
|
||||
dropColumn: false,
|
||||
renameColumn: false,
|
||||
alterExistingColumn: false,
|
||||
alterType: false,
|
||||
alterNullability: false,
|
||||
alterDefault: false,
|
||||
comment: false,
|
||||
createIndex: false,
|
||||
dropIndex: false,
|
||||
indexType: false,
|
||||
indexInclude: false,
|
||||
indexFilter: false,
|
||||
indexComment: false,
|
||||
};
|
||||
|
||||
function capabilities(overrides: Partial<TableStructureCapabilities>): TableStructureCapabilities {
|
||||
return { ...unsupportedCapabilities, ...overrides };
|
||||
}
|
||||
|
||||
const mysqlCapabilities = capabilities({
|
||||
dialect: "mysql",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
renameColumn: true,
|
||||
alterExistingColumn: true,
|
||||
alterType: true,
|
||||
alterNullability: true,
|
||||
alterDefault: true,
|
||||
comment: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
indexType: true,
|
||||
});
|
||||
|
||||
const postgresCapabilities = capabilities({
|
||||
dialect: "postgres",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
renameColumn: true,
|
||||
alterExistingColumn: true,
|
||||
alterType: true,
|
||||
alterNullability: true,
|
||||
alterDefault: true,
|
||||
comment: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
indexType: true,
|
||||
indexInclude: true,
|
||||
indexFilter: true,
|
||||
indexComment: true,
|
||||
});
|
||||
|
||||
const redshiftCapabilities = capabilities({
|
||||
...postgresCapabilities,
|
||||
createIndex: false,
|
||||
dropIndex: false,
|
||||
indexType: false,
|
||||
indexInclude: false,
|
||||
indexFilter: false,
|
||||
indexComment: false,
|
||||
});
|
||||
|
||||
const sqliteCapabilities = capabilities({
|
||||
dialect: "sqlite",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
renameColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
indexFilter: true,
|
||||
});
|
||||
|
||||
const duckdbCapabilities = capabilities({
|
||||
dialect: "duckdb",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
renameColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
});
|
||||
|
||||
const sqlserverCapabilities = capabilities({
|
||||
dialect: "sqlserver",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
indexType: true,
|
||||
indexInclude: true,
|
||||
indexFilter: true,
|
||||
});
|
||||
|
||||
const oracleCapabilities = capabilities({
|
||||
dialect: "oracle",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
renameColumn: true,
|
||||
alterExistingColumn: true,
|
||||
alterType: true,
|
||||
alterNullability: true,
|
||||
alterDefault: true,
|
||||
comment: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
indexType: true,
|
||||
});
|
||||
|
||||
const h2Capabilities = capabilities({
|
||||
dialect: "h2",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
renameColumn: true,
|
||||
alterExistingColumn: true,
|
||||
alterType: true,
|
||||
alterNullability: true,
|
||||
alterDefault: true,
|
||||
comment: true,
|
||||
createIndex: true,
|
||||
dropIndex: true,
|
||||
});
|
||||
|
||||
const clickhouseCapabilities = capabilities({
|
||||
dialect: "clickhouse",
|
||||
createTable: true,
|
||||
addColumn: true,
|
||||
dropColumn: true,
|
||||
});
|
||||
|
||||
const capabilityByType: Partial<Record<DatabaseType, TableStructureCapabilities>> = {
|
||||
mysql: mysqlCapabilities,
|
||||
doris: mysqlCapabilities,
|
||||
starrocks: mysqlCapabilities,
|
||||
goldendb: mysqlCapabilities,
|
||||
sundb: mysqlCapabilities,
|
||||
postgres: postgresCapabilities,
|
||||
gaussdb: postgresCapabilities,
|
||||
opengauss: postgresCapabilities,
|
||||
redshift: redshiftCapabilities,
|
||||
highgo: postgresCapabilities,
|
||||
vastbase: postgresCapabilities,
|
||||
kingbase: postgresCapabilities,
|
||||
sqlite: sqliteCapabilities,
|
||||
duckdb: duckdbCapabilities,
|
||||
sqlserver: sqlserverCapabilities,
|
||||
oracle: oracleCapabilities,
|
||||
dameng: oracleCapabilities,
|
||||
"oceanbase-oracle": oracleCapabilities,
|
||||
h2: h2Capabilities,
|
||||
clickhouse: clickhouseCapabilities,
|
||||
};
|
||||
|
||||
export function getTableStructureCapabilities(dbType?: DatabaseType): TableStructureCapabilities {
|
||||
return dbType ? (capabilityByType[dbType] ?? unsupportedCapabilities) : unsupportedCapabilities;
|
||||
}
|
||||
|
||||
export function canEditTableStructure(dbType?: DatabaseType): boolean {
|
||||
const caps = getTableStructureCapabilities(dbType);
|
||||
return caps.createTable || caps.addColumn || caps.alterExistingColumn || caps.createIndex || caps.dropIndex;
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { ColumnInfo, DatabaseType, IndexInfo } from "../types/database.ts";
|
||||
import { getTableStructureCapabilities, type TableStructureDialect } from "./tableStructureCapabilities.ts";
|
||||
|
||||
export interface EditableStructureColumn {
|
||||
id: string;
|
||||
|
|
@ -39,18 +40,33 @@ export interface TableStructureChangeSql {
|
|||
warnings: string[];
|
||||
}
|
||||
|
||||
function quoteIdent(databaseType: DatabaseType | undefined, name: string): string {
|
||||
if (databaseType === "mysql") return `\`${name.replace(/`/g, "``")}\``;
|
||||
type StructureSqlFlavor = DatabaseType | TableStructureDialect | undefined;
|
||||
|
||||
function quoteIdent(databaseType: StructureSqlFlavor, name: string): string {
|
||||
if (
|
||||
databaseType === "mysql" ||
|
||||
databaseType === "doris" ||
|
||||
databaseType === "starrocks" ||
|
||||
databaseType === "goldendb" ||
|
||||
databaseType === "sundb"
|
||||
)
|
||||
return `\`${name.replace(/`/g, "``")}\``;
|
||||
if (databaseType === "sqlserver") return `[${name.replace(/\]/g, "]]")}]`;
|
||||
return `"${name.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function isOracleLike(databaseType: DatabaseType | undefined): databaseType is "oracle" | "dameng" {
|
||||
return databaseType === "oracle" || databaseType === "dameng";
|
||||
function isOracleLike(databaseType: StructureSqlFlavor): boolean {
|
||||
return databaseType === "oracle" || databaseType === "dameng" || databaseType === "oceanbase-oracle";
|
||||
}
|
||||
|
||||
function qualifiedTable(databaseType: DatabaseType | undefined, schema: string | undefined, tableName: string): string {
|
||||
if ((databaseType === "postgres" || isOracleLike(databaseType) || databaseType === "sqlserver") && schema) {
|
||||
function qualifiedTable(databaseType: StructureSqlFlavor, schema: string | undefined, tableName: string): string {
|
||||
if (
|
||||
(databaseType === "postgres" ||
|
||||
isOracleLike(databaseType) ||
|
||||
databaseType === "sqlserver" ||
|
||||
databaseType === "h2") &&
|
||||
schema
|
||||
) {
|
||||
return `${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, tableName)}`;
|
||||
}
|
||||
return quoteIdent(databaseType, tableName);
|
||||
|
|
@ -69,7 +85,7 @@ function normalizeDefault(value: string | null | undefined): string {
|
|||
return trimmed.toLowerCase() === "null" ? "" : trimmed;
|
||||
}
|
||||
|
||||
function columnDefinition(databaseType: DatabaseType | undefined, column: EditableStructureColumn): string {
|
||||
function columnDefinition(databaseType: StructureSqlFlavor, column: EditableStructureColumn): string {
|
||||
const parts = [quoteIdent(databaseType, column.name), column.dataType.trim()];
|
||||
if (!column.isNullable && !isOracleLike(databaseType)) parts.push("NOT NULL");
|
||||
const defaultValue = normalizeDefault(column.defaultValue);
|
||||
|
|
@ -100,11 +116,7 @@ function hasExistingColumnAttributeChange(column: EditableStructureColumn): bool
|
|||
);
|
||||
}
|
||||
|
||||
function buildAddColumnSql(
|
||||
databaseType: DatabaseType | undefined,
|
||||
table: string,
|
||||
column: EditableStructureColumn,
|
||||
): string[] {
|
||||
function buildAddColumnSql(databaseType: StructureSqlFlavor, table: string, column: EditableStructureColumn): string[] {
|
||||
const addKeyword = databaseType === "sqlserver" ? "ADD" : "ADD COLUMN";
|
||||
const definition = columnDefinition(databaseType, column);
|
||||
const statements = isOracleLike(databaseType)
|
||||
|
|
@ -119,7 +131,7 @@ function buildAddColumnSql(
|
|||
}
|
||||
|
||||
function buildOracleLikeExistingColumnSql(
|
||||
databaseType: DatabaseType,
|
||||
databaseType: StructureSqlFlavor,
|
||||
table: string,
|
||||
column: EditableStructureColumn,
|
||||
): string[] {
|
||||
|
|
@ -195,6 +207,39 @@ function buildPostgresExistingColumnSql(table: string, column: EditableStructure
|
|||
return statements;
|
||||
}
|
||||
|
||||
function buildH2ExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
|
||||
const original = column.original;
|
||||
if (!original) return [];
|
||||
|
||||
const statements: string[] = [];
|
||||
let currentName = original.name;
|
||||
if (column.name !== original.name) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", original.name)} RENAME TO ${quoteIdent("h2", column.name)};`,
|
||||
);
|
||||
currentName = column.name;
|
||||
}
|
||||
if (column.dataType.trim() !== original.data_type.trim()) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", currentName)} SET DATA TYPE ${column.dataType.trim()};`,
|
||||
);
|
||||
}
|
||||
if (column.isNullable !== original.is_nullable) {
|
||||
const action = column.isNullable ? "DROP NOT NULL" : "SET NOT NULL";
|
||||
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", currentName)} ${action};`);
|
||||
}
|
||||
if (normalizeDefault(column.defaultValue) !== originalDefault(column)) {
|
||||
const defaultValue = normalizeDefault(column.defaultValue);
|
||||
const action = defaultValue ? `SET DEFAULT ${defaultValue}` : "DROP DEFAULT";
|
||||
statements.push(`ALTER TABLE ${table} ALTER COLUMN ${quoteIdent("h2", currentName)} ${action};`);
|
||||
}
|
||||
if (clean(column.comment) !== originalComment(column)) {
|
||||
const commentValue = clean(column.comment) ? quoteString(clean(column.comment)) : "NULL";
|
||||
statements.push(`COMMENT ON COLUMN ${table}.${quoteIdent("h2", currentName)} IS ${commentValue};`);
|
||||
}
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildSqliteExistingColumnSql(table: string, column: EditableStructureColumn, warnings: string[]): string[] {
|
||||
const original = column.original;
|
||||
if (!original) return [];
|
||||
|
|
@ -218,36 +263,68 @@ function buildSqliteExistingColumnSql(table: string, column: EditableStructureCo
|
|||
|
||||
function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings: string[]): string[] {
|
||||
const databaseType = options.databaseType;
|
||||
const table = qualifiedTable(databaseType, options.schema, options.tableName);
|
||||
const capabilities = getTableStructureCapabilities(databaseType);
|
||||
const dialect = capabilities.dialect;
|
||||
const table = qualifiedTable(dialect, options.schema, options.tableName);
|
||||
const statements: string[] = [];
|
||||
const databaseLabel = databaseType ?? "this database";
|
||||
|
||||
for (const column of options.columns) {
|
||||
if (column.markedForDrop) {
|
||||
if (!column.original) continue;
|
||||
if (!capabilities.dropColumn) {
|
||||
warnings.push(`Dropping columns is not supported for ${databaseLabel} from this editor.`);
|
||||
continue;
|
||||
}
|
||||
if (column.original.is_primary_key) {
|
||||
warnings.push(`Primary key column "${column.original.name}" cannot be dropped from this editor.`);
|
||||
continue;
|
||||
}
|
||||
statements.push(`ALTER TABLE ${table} DROP COLUMN ${quoteIdent(databaseType, column.original.name)};`);
|
||||
statements.push(`ALTER TABLE ${table} DROP COLUMN ${quoteIdent(dialect, column.original.name)};`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!column.original) {
|
||||
statements.push(...buildAddColumnSql(databaseType, table, column));
|
||||
if (!capabilities.addColumn) {
|
||||
warnings.push(`Adding columns is not supported for ${databaseLabel} from this editor.`);
|
||||
continue;
|
||||
}
|
||||
statements.push(...buildAddColumnSql(dialect, table, column));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!hasExistingColumnAttributeChange(column)) continue;
|
||||
if (databaseType === "mysql") {
|
||||
const original = column.original;
|
||||
const hasRename = column.name !== original.name;
|
||||
const hasAttributeChange =
|
||||
column.dataType.trim() !== original.data_type.trim() ||
|
||||
column.isNullable !== original.is_nullable ||
|
||||
normalizeDefault(column.defaultValue) !== originalDefault(column) ||
|
||||
clean(column.comment) !== originalComment(column);
|
||||
if (hasRename && !capabilities.renameColumn) {
|
||||
warnings.push(`Renaming columns is not supported for ${databaseLabel} from this editor.`);
|
||||
}
|
||||
if (hasAttributeChange && !capabilities.alterExistingColumn && dialect !== "sqlite") {
|
||||
warnings.push(`Editing existing columns is not supported for ${databaseLabel} yet.`);
|
||||
}
|
||||
if (
|
||||
(hasRename && !capabilities.renameColumn) ||
|
||||
(hasAttributeChange && !capabilities.alterExistingColumn && dialect !== "sqlite")
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (dialect === "mysql") {
|
||||
statements.push(...buildMysqlExistingColumnSql(table, column));
|
||||
} else if (databaseType === "postgres") {
|
||||
} else if (dialect === "postgres") {
|
||||
statements.push(...buildPostgresExistingColumnSql(table, column));
|
||||
} else if (isOracleLike(databaseType)) {
|
||||
statements.push(...buildOracleLikeExistingColumnSql(databaseType, table, column));
|
||||
} else if (databaseType === "sqlite") {
|
||||
} else if (dialect === "oracle") {
|
||||
statements.push(...buildOracleLikeExistingColumnSql(dialect, table, column));
|
||||
} else if (dialect === "h2") {
|
||||
statements.push(...buildH2ExistingColumnSql(table, column));
|
||||
} else if (dialect === "sqlite") {
|
||||
statements.push(...buildSqliteExistingColumnSql(table, column, warnings));
|
||||
} else {
|
||||
warnings.push(`Editing existing columns is not supported for ${databaseType ?? "this database"} yet.`);
|
||||
warnings.push(`Editing existing columns is not supported for ${databaseLabel} yet.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -255,7 +332,7 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
|||
}
|
||||
|
||||
function buildDropIndexSql(
|
||||
databaseType: DatabaseType | undefined,
|
||||
databaseType: StructureSqlFlavor,
|
||||
table: string,
|
||||
schema: string | undefined,
|
||||
indexName: string,
|
||||
|
|
@ -270,17 +347,24 @@ function buildDropIndexSql(
|
|||
|
||||
function buildIndexSql(options: BuildTableStructureChangeSqlOptions, warnings: string[]): string[] {
|
||||
const databaseType = options.databaseType;
|
||||
const table = qualifiedTable(databaseType, options.schema, options.tableName);
|
||||
const capabilities = getTableStructureCapabilities(databaseType);
|
||||
const dialect = capabilities.dialect;
|
||||
const table = qualifiedTable(dialect, options.schema, options.tableName);
|
||||
const statements: string[] = [];
|
||||
const databaseLabel = databaseType ?? "this database";
|
||||
|
||||
for (const index of options.indexes) {
|
||||
if (index.markedForDrop) {
|
||||
if (!index.original) continue;
|
||||
if (!capabilities.dropIndex) {
|
||||
warnings.push(`Dropping indexes is not supported for ${databaseLabel} from this editor.`);
|
||||
continue;
|
||||
}
|
||||
if (index.original.is_primary) {
|
||||
warnings.push(`Primary index "${index.original.name}" cannot be dropped from this editor.`);
|
||||
continue;
|
||||
}
|
||||
statements.push(buildDropIndexSql(databaseType, table, options.schema, index.original.name));
|
||||
statements.push(buildDropIndexSql(dialect, table, options.schema, index.original.name));
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
@ -288,25 +372,30 @@ function buildIndexSql(options: BuildTableStructureChangeSqlOptions, warnings: s
|
|||
const name = clean(index.name);
|
||||
const columns = index.columns.map(clean).filter(Boolean);
|
||||
if (!name || columns.length === 0) continue;
|
||||
if (!capabilities.createIndex) {
|
||||
warnings.push(`Creating indexes is not supported for ${databaseLabel} from this editor.`);
|
||||
continue;
|
||||
}
|
||||
const unique = index.isUnique ? "UNIQUE " : "";
|
||||
const cols = columns.map((column) => quoteIdent(databaseType, column)).join(", ");
|
||||
const cols = columns.map((column) => quoteIdent(dialect, column)).join(", ");
|
||||
const idxType = clean(index.indexType);
|
||||
const usingClause = idxType && databaseType === "postgres" ? ` USING ${idxType}` : "";
|
||||
const typePrefix = idxType && databaseType === "sqlserver" ? `${idxType} ` : "";
|
||||
const usingClause = idxType && capabilities.indexType && dialect === "postgres" ? ` USING ${idxType}` : "";
|
||||
const typePrefix = idxType && capabilities.indexType && dialect === "sqlserver" ? `${idxType} ` : "";
|
||||
const incCols = index.includedColumns.map(clean).filter(Boolean);
|
||||
const includeClause =
|
||||
incCols.length > 0 && (databaseType === "postgres" || databaseType === "sqlserver")
|
||||
? ` INCLUDE (${incCols.map((c) => quoteIdent(databaseType, c)).join(", ")})`
|
||||
incCols.length > 0 && capabilities.indexInclude && (dialect === "postgres" || dialect === "sqlserver")
|
||||
? ` INCLUDE (${incCols.map((c) => quoteIdent(dialect, c)).join(", ")})`
|
||||
: "";
|
||||
const filter = clean(index.filter);
|
||||
const supportsWhere = databaseType === "postgres" || databaseType === "sqlserver" || databaseType === "sqlite";
|
||||
const supportsWhere =
|
||||
capabilities.indexFilter && (dialect === "postgres" || dialect === "sqlserver" || dialect === "sqlite");
|
||||
const whereClause = filter && supportsWhere ? ` WHERE ${filter}` : "";
|
||||
statements.push(
|
||||
`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)} ON ${table}${usingClause} (${cols})${includeClause}${whereClause};`,
|
||||
`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(dialect, name)} ON ${table}${usingClause} (${cols})${includeClause}${whereClause};`,
|
||||
);
|
||||
const comment = clean(index.comment);
|
||||
if (comment && databaseType === "postgres") {
|
||||
statements.push(`COMMENT ON INDEX ${quoteIdent(databaseType, name)} IS ${quoteString(comment)};`);
|
||||
if (comment && capabilities.indexComment && dialect === "postgres") {
|
||||
statements.push(`COMMENT ON INDEX ${quoteIdent(dialect, name)} IS ${quoteString(comment)};`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -367,33 +456,35 @@ export function buildCreateTableSql(options: BuildTableStructureChangeSqlOptions
|
|||
if (warnings.length > 0) return { statements: [], warnings };
|
||||
|
||||
const databaseType = options.databaseType;
|
||||
const table = qualifiedTable(databaseType, options.schema, options.tableName);
|
||||
const capabilities = getTableStructureCapabilities(databaseType);
|
||||
const dialect = capabilities.dialect;
|
||||
const table = qualifiedTable(dialect, options.schema, options.tableName);
|
||||
const statements: string[] = [];
|
||||
|
||||
const pkColumns = activeColumns.filter((c) => c.isPrimaryKey);
|
||||
const colDefs = activeColumns.map((col) => {
|
||||
const parts = [quoteIdent(databaseType, col.name), col.dataType.trim()];
|
||||
const parts = [quoteIdent(dialect, col.name), col.dataType.trim()];
|
||||
if (!col.isNullable && !col.isPrimaryKey) parts.push("NOT NULL");
|
||||
const defaultValue = normalizeDefault(col.defaultValue);
|
||||
if (defaultValue) parts.push(`DEFAULT ${defaultValue}`);
|
||||
if (databaseType === "mysql" && clean(col.comment)) {
|
||||
if (dialect === "mysql" && capabilities.comment && clean(col.comment)) {
|
||||
parts.push(`COMMENT ${quoteString(clean(col.comment))}`);
|
||||
}
|
||||
return parts.join(" ");
|
||||
});
|
||||
|
||||
if (pkColumns.length > 0) {
|
||||
const pkList = pkColumns.map((c) => quoteIdent(databaseType, c.name)).join(", ");
|
||||
const pkList = pkColumns.map((c) => quoteIdent(dialect, c.name)).join(", ");
|
||||
colDefs.push(`PRIMARY KEY (${pkList})`);
|
||||
}
|
||||
|
||||
statements.push(`CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n);`);
|
||||
|
||||
if (databaseType === "postgres" || isOracleLike(databaseType)) {
|
||||
if (capabilities.comment && (dialect === "postgres" || dialect === "oracle" || dialect === "h2")) {
|
||||
for (const col of activeColumns) {
|
||||
if (clean(col.comment)) {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, col.name)} IS ${quoteString(clean(col.comment))};`,
|
||||
`COMMENT ON COLUMN ${table}.${quoteIdent(dialect, col.name)} IS ${quoteString(clean(col.comment))};`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -403,13 +494,17 @@ export function buildCreateTableSql(options: BuildTableStructureChangeSqlOptions
|
|||
const name = clean(index.name);
|
||||
const columns = index.columns.map(clean).filter(Boolean);
|
||||
if (!name || columns.length === 0) continue;
|
||||
if (!capabilities.createIndex) {
|
||||
warnings.push(`Creating indexes is not supported for ${databaseType ?? "this database"} from this editor.`);
|
||||
continue;
|
||||
}
|
||||
const unique = index.isUnique ? "UNIQUE " : "";
|
||||
const cols = columns.map((c) => quoteIdent(databaseType, c)).join(", ");
|
||||
const cols = columns.map((c) => quoteIdent(dialect, c)).join(", ");
|
||||
const idxType = clean(index.indexType);
|
||||
const usingClause = idxType && databaseType === "postgres" ? ` USING ${idxType}` : "";
|
||||
const typePrefix = idxType && databaseType === "sqlserver" ? `${idxType} ` : "";
|
||||
const usingClause = idxType && capabilities.indexType && dialect === "postgres" ? ` USING ${idxType}` : "";
|
||||
const typePrefix = idxType && capabilities.indexType && dialect === "sqlserver" ? `${idxType} ` : "";
|
||||
statements.push(
|
||||
`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(databaseType, name)} ON ${table}${usingClause} (${cols});`,
|
||||
`CREATE ${unique}${typePrefix}INDEX ${quoteIdent(dialect, name)} ON ${table}${usingClause} (${cols});`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -137,6 +137,11 @@ test("describes feature support through capability helpers", () => {
|
|||
assert.equal(supportsTableStructureEditing("duckdb"), true);
|
||||
assert.equal(supportsTableStructureEditing("oracle"), true);
|
||||
assert.equal(supportsTableStructureEditing("dameng"), true);
|
||||
assert.equal(supportsTableStructureEditing("gaussdb"), true);
|
||||
assert.equal(supportsTableStructureEditing("opengauss"), true);
|
||||
assert.equal(supportsTableStructureEditing("redshift"), true);
|
||||
assert.equal(supportsTableStructureEditing("clickhouse"), true);
|
||||
assert.equal(supportsTableStructureEditing("mongodb"), false);
|
||||
assert.equal(supportsDatabaseCreation("clickhouse"), true);
|
||||
assert.equal(supportsDatabaseCreation("sqlite"), false);
|
||||
assert.equal(supportsFieldLineage("gaussdb"), true);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
canEditTableStructure,
|
||||
getTableStructureCapabilities,
|
||||
} from "../../apps/desktop/src/lib/tableStructureCapabilities.ts";
|
||||
|
||||
test("postgres-like databases expose safe structure editing capabilities", () => {
|
||||
for (const dbType of ["postgres", "gaussdb", "opengauss", "highgo", "vastbase", "kingbase"] as const) {
|
||||
const caps = getTableStructureCapabilities(dbType);
|
||||
assert.equal(caps.dialect, "postgres", `${dbType} should reuse postgres DDL`);
|
||||
assert.equal(caps.createTable, true, `${dbType} should create tables`);
|
||||
assert.equal(caps.addColumn, true, `${dbType} should add columns`);
|
||||
assert.equal(caps.dropColumn, true, `${dbType} should drop columns`);
|
||||
assert.equal(caps.renameColumn, true, `${dbType} should rename columns`);
|
||||
assert.equal(caps.alterExistingColumn, true, `${dbType} should edit existing columns`);
|
||||
assert.equal(caps.comment, true, `${dbType} should support comments`);
|
||||
assert.equal(caps.createIndex, true, `${dbType} should create indexes`);
|
||||
assert.equal(caps.dropIndex, true, `${dbType} should drop indexes`);
|
||||
assert.equal(caps.indexFilter, true, `${dbType} should support filtered indexes`);
|
||||
assert.equal(canEditTableStructure(dbType), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("redshift reuses postgres column DDL but keeps indexes disabled", () => {
|
||||
const caps = getTableStructureCapabilities("redshift");
|
||||
assert.equal(caps.dialect, "postgres");
|
||||
assert.equal(caps.createTable, true);
|
||||
assert.equal(caps.addColumn, true);
|
||||
assert.equal(caps.dropColumn, true);
|
||||
assert.equal(caps.renameColumn, true);
|
||||
assert.equal(caps.alterExistingColumn, true);
|
||||
assert.equal(caps.comment, true);
|
||||
assert.equal(caps.createIndex, false);
|
||||
assert.equal(caps.dropIndex, false);
|
||||
assert.equal(caps.indexFilter, false);
|
||||
assert.equal(canEditTableStructure("redshift"), true);
|
||||
});
|
||||
|
||||
test("oracle-like databases expose oracle-compatible structure editing capabilities", () => {
|
||||
for (const dbType of ["oracle", "dameng", "oceanbase-oracle"] as const) {
|
||||
const caps = getTableStructureCapabilities(dbType);
|
||||
assert.equal(caps.dialect, "oracle", `${dbType} should reuse oracle DDL`);
|
||||
assert.equal(caps.createTable, true);
|
||||
assert.equal(caps.addColumn, true);
|
||||
assert.equal(caps.dropColumn, true);
|
||||
assert.equal(caps.renameColumn, true);
|
||||
assert.equal(caps.alterExistingColumn, true);
|
||||
assert.equal(caps.comment, true);
|
||||
assert.equal(caps.createIndex, true);
|
||||
assert.equal(caps.dropIndex, true);
|
||||
assert.equal(canEditTableStructure(dbType), true);
|
||||
}
|
||||
});
|
||||
|
||||
test("limited analytic engines can open the editor for supported operations only", () => {
|
||||
const clickhouse = getTableStructureCapabilities("clickhouse");
|
||||
assert.equal(clickhouse.dialect, "clickhouse");
|
||||
assert.equal(clickhouse.createTable, true);
|
||||
assert.equal(clickhouse.addColumn, true);
|
||||
assert.equal(clickhouse.dropColumn, true);
|
||||
assert.equal(clickhouse.renameColumn, false);
|
||||
assert.equal(clickhouse.alterExistingColumn, false);
|
||||
assert.equal(clickhouse.comment, false);
|
||||
assert.equal(clickhouse.createIndex, false);
|
||||
assert.equal(canEditTableStructure("clickhouse"), true);
|
||||
});
|
||||
|
||||
test("unsupported non-relational databases do not open the structure editor", () => {
|
||||
for (const dbType of ["redis", "mongodb", "elasticsearch", "neo4j", undefined] as const) {
|
||||
const caps = getTableStructureCapabilities(dbType);
|
||||
assert.equal(caps.dialect, "unsupported");
|
||||
assert.equal(canEditTableStructure(dbType), false);
|
||||
}
|
||||
});
|
||||
|
|
@ -17,3 +17,18 @@ test("structure editor keeps columns when optional metadata fails", () => {
|
|||
assert.match(source, /api\s*\n\s*\.listForeignKeys[\s\S]*\.catch\(\(\) => \[\]\)/);
|
||||
assert.match(source, /api\s*\n\s*\.listTriggers[\s\S]*\.catch\(\(\) => \[\]\)/);
|
||||
});
|
||||
|
||||
test("structure editor gates controls through table structure capabilities", () => {
|
||||
assert.match(source, /getTableStructureCapabilities/);
|
||||
assert.match(source, /const structureCapabilities = computed/);
|
||||
assert.match(source, /function isColumnNameDisabled/);
|
||||
assert.match(source, /function isColumnTypeDisabled/);
|
||||
assert.match(source, /function isColumnDefaultDisabled/);
|
||||
assert.match(source, /function isColumnCommentDisabled/);
|
||||
assert.match(source, /function canDropColumn/);
|
||||
assert.match(source, /function canEditIndexDraft/);
|
||||
assert.match(source, /structureCapabilities\.value\.createIndex/);
|
||||
assert.match(source, /structureCapabilities\.value\.dropIndex/);
|
||||
assert.match(source, /structureCapabilities\.value\.indexInclude/);
|
||||
assert.match(source, /structureCapabilities\.value\.indexFilter/);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -592,3 +592,151 @@ test("builds Dameng existing column and create table statements", () => {
|
|||
'CREATE INDEX "IDX_USERS_NAME" ON "SYSDBA"."USERS" ("NAME");',
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds GaussDB statements with PostgreSQL-compatible DDL", () => {
|
||||
const result = buildTableStructureChangeSql({
|
||||
databaseType: "gaussdb",
|
||||
schema: "public",
|
||||
tableName: "accounts",
|
||||
columns: [
|
||||
column({
|
||||
id: "status",
|
||||
name: "account_status",
|
||||
dataType: "text",
|
||||
isNullable: false,
|
||||
defaultValue: "'active'",
|
||||
comment: "Current status",
|
||||
original: {
|
||||
name: "status",
|
||||
data_type: "varchar",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: "",
|
||||
},
|
||||
}),
|
||||
],
|
||||
indexes: [index({ id: "idx", name: "idx_accounts_status", columns: ["account_status"] })],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.warnings, []);
|
||||
assert.deepEqual(result.statements, [
|
||||
'ALTER TABLE "public"."accounts" RENAME COLUMN "status" TO "account_status";',
|
||||
'ALTER TABLE "public"."accounts" ALTER COLUMN "account_status" TYPE text;',
|
||||
'ALTER TABLE "public"."accounts" ALTER COLUMN "account_status" SET NOT NULL;',
|
||||
'ALTER TABLE "public"."accounts" ALTER COLUMN "account_status" SET DEFAULT \'active\';',
|
||||
'COMMENT ON COLUMN "public"."accounts"."account_status" IS \'Current status\';',
|
||||
'CREATE INDEX "idx_accounts_status" ON "public"."accounts" ("account_status");',
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds openGauss statements with PostgreSQL-compatible DDL", () => {
|
||||
const result = buildTableStructureChangeSql({
|
||||
databaseType: "opengauss",
|
||||
schema: "public",
|
||||
tableName: "accounts",
|
||||
columns: [column({ id: "email", name: "email", dataType: "text", isNullable: true })],
|
||||
indexes: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.warnings, []);
|
||||
assert.deepEqual(result.statements, ['ALTER TABLE "public"."accounts" ADD COLUMN "email" text;']);
|
||||
});
|
||||
|
||||
test("Redshift skips unsupported index operations while keeping column DDL", () => {
|
||||
const result = buildTableStructureChangeSql({
|
||||
databaseType: "redshift",
|
||||
schema: "public",
|
||||
tableName: "events",
|
||||
columns: [column({ id: "email", name: "email", dataType: "varchar(255)", isNullable: true })],
|
||||
indexes: [index({ id: "idx", name: "idx_events_email", columns: ["email"], filter: "email IS NOT NULL" })],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.statements, ['ALTER TABLE "public"."events" ADD COLUMN "email" varchar(255);']);
|
||||
assert.deepEqual(result.warnings, ['Creating indexes is not supported for redshift from this editor.']);
|
||||
});
|
||||
|
||||
test("builds ClickHouse limited column DDL and skips indexes", () => {
|
||||
const result = buildTableStructureChangeSql({
|
||||
databaseType: "clickhouse",
|
||||
tableName: "events",
|
||||
columns: [
|
||||
column({ id: "new", name: "name", dataType: "String", isNullable: true }),
|
||||
column({
|
||||
id: "legacy",
|
||||
name: "legacy",
|
||||
markedForDrop: true,
|
||||
original: {
|
||||
name: "legacy",
|
||||
data_type: "String",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
},
|
||||
}),
|
||||
column({
|
||||
id: "kind",
|
||||
name: "event_kind",
|
||||
dataType: "String",
|
||||
original: {
|
||||
name: "kind",
|
||||
data_type: "String",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
},
|
||||
}),
|
||||
],
|
||||
indexes: [index({ id: "idx", name: "idx_events_name", columns: ["name"] })],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.statements, [
|
||||
'ALTER TABLE "events" ADD COLUMN "name" String;',
|
||||
'ALTER TABLE "events" DROP COLUMN "legacy";',
|
||||
]);
|
||||
assert.deepEqual(result.warnings, [
|
||||
'Renaming columns is not supported for clickhouse from this editor.',
|
||||
'Creating indexes is not supported for clickhouse from this editor.',
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds H2 schema-qualified existing column statements", () => {
|
||||
const result = buildTableStructureChangeSql({
|
||||
databaseType: "h2",
|
||||
schema: "PUBLIC",
|
||||
tableName: "USERS",
|
||||
columns: [
|
||||
column({
|
||||
id: "name",
|
||||
name: "DISPLAY_NAME",
|
||||
dataType: "VARCHAR(120)",
|
||||
isNullable: false,
|
||||
defaultValue: "'guest'",
|
||||
comment: "Display name",
|
||||
original: {
|
||||
name: "NAME",
|
||||
data_type: "VARCHAR(80)",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: "",
|
||||
},
|
||||
}),
|
||||
],
|
||||
indexes: [index({ id: "idx", name: "IDX_USERS_DISPLAY_NAME", columns: ["DISPLAY_NAME"] })],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.warnings, []);
|
||||
assert.deepEqual(result.statements, [
|
||||
'ALTER TABLE "PUBLIC"."USERS" ALTER COLUMN "NAME" RENAME TO "DISPLAY_NAME";',
|
||||
'ALTER TABLE "PUBLIC"."USERS" ALTER COLUMN "DISPLAY_NAME" SET DATA TYPE VARCHAR(120);',
|
||||
'ALTER TABLE "PUBLIC"."USERS" ALTER COLUMN "DISPLAY_NAME" SET NOT NULL;',
|
||||
'ALTER TABLE "PUBLIC"."USERS" ALTER COLUMN "DISPLAY_NAME" SET DEFAULT \'guest\';',
|
||||
'COMMENT ON COLUMN "PUBLIC"."USERS"."DISPLAY_NAME" IS \'Display name\';',
|
||||
'CREATE INDEX "IDX_USERS_DISPLAY_NAME" ON "PUBLIC"."USERS" ("DISPLAY_NAME");',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue