fix(db): support routine rename editing
This commit is contained in:
parent
06b0c60a0e
commit
da00405e10
|
|
@ -34,7 +34,12 @@ import type { ConnectionConfig, ObjectInfo, ObjectSourceKind } from "@/types/dat
|
|||
import { isSchemaAware } from "@/lib/databaseCapabilities";
|
||||
import { buildTableSelectSql, qualifiedTableName } from "@/lib/tableSelectSql";
|
||||
import { useToast } from "@/composables/useToast";
|
||||
import { buildExecutableObjectSourceStatements, objectSourceSaveExecutionMode } from "@/lib/objectSourceEditor";
|
||||
import {
|
||||
buildExecutableObjectSourceStatements,
|
||||
buildRoutineRenameObjectSourceStatements,
|
||||
objectSourceSaveExecutionMode,
|
||||
supportsSourceBackedRoutineRename,
|
||||
} from "@/lib/objectSourceEditor";
|
||||
import { buildRenameObjectSql, supportsObjectRename } from "@/lib/objectRenameSql";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
|
|
@ -169,7 +174,10 @@ function canOpenSource(row: ObjectBrowserRow) {
|
|||
}
|
||||
|
||||
function canRename(row: ObjectBrowserRow) {
|
||||
return supportsObjectRename(props.connection.db_type, row.type);
|
||||
return (
|
||||
supportsObjectRename(props.connection.db_type, row.type) ||
|
||||
supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)
|
||||
);
|
||||
}
|
||||
|
||||
function sourceTitle(row: ObjectBrowserRow | null) {
|
||||
|
|
@ -250,6 +258,9 @@ function renamePreviewSql() {
|
|||
const row = renameTarget.value;
|
||||
const newName = renameInput.value.trim();
|
||||
if (!row || !newName || newName === row.name) return "";
|
||||
if (supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)) {
|
||||
return `-- Recreate ${row.type} from source, then drop the original object.`;
|
||||
}
|
||||
try {
|
||||
return buildRenameObjectSql({
|
||||
databaseType: props.connection.db_type,
|
||||
|
|
@ -270,14 +281,35 @@ async function confirmRename() {
|
|||
renameError.value = "";
|
||||
try {
|
||||
const schema = row.schema || selectedSchema.value || props.database;
|
||||
const sql = buildRenameObjectSql({
|
||||
databaseType: props.connection.db_type,
|
||||
objectType: row.type,
|
||||
schema,
|
||||
oldName: row.name,
|
||||
newName,
|
||||
});
|
||||
await api.executeQuery(props.connection.id, props.database, sql, schema);
|
||||
if (supportsSourceBackedRoutineRename(props.connection.db_type, row.type as ObjectSourceKind)) {
|
||||
const source = await api.getObjectSource(
|
||||
props.connection.id,
|
||||
props.database,
|
||||
schema,
|
||||
row.name,
|
||||
row.type as ObjectSourceKind,
|
||||
);
|
||||
const statements = buildRoutineRenameObjectSourceStatements({
|
||||
databaseType: props.connection.db_type,
|
||||
objectType: row.type as ObjectSourceKind,
|
||||
schema,
|
||||
name: row.name,
|
||||
newName,
|
||||
source: source.source,
|
||||
});
|
||||
for (const sql of statements) {
|
||||
await api.executeQuery(props.connection.id, props.database, sql, schema);
|
||||
}
|
||||
} else {
|
||||
const sql = buildRenameObjectSql({
|
||||
databaseType: props.connection.db_type,
|
||||
objectType: row.type,
|
||||
schema,
|
||||
oldName: row.name,
|
||||
newName,
|
||||
});
|
||||
await api.executeQuery(props.connection.id, props.database, sql, schema);
|
||||
}
|
||||
toast(t("contextMenu.renameObjectSuccess", { oldName: row.name, newName }));
|
||||
showRenameDialog.value = false;
|
||||
if (sourceRow.value?.id === row.id) closeSource();
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ import {
|
|||
uniqueDuckDbAttachedDatabaseName,
|
||||
} from "@/lib/createDatabaseSql";
|
||||
import { buildRenameObjectSql, supportsObjectRename, type RenameableObjectType } from "@/lib/objectRenameSql";
|
||||
import { buildRoutineRenameObjectSourceStatements, supportsSourceBackedRoutineRename } from "@/lib/objectSourceEditor";
|
||||
import { hexToRgba } from "@/lib/color";
|
||||
import { focusSidebarRenameInput, shouldPreventRenameCloseAutoFocus } from "@/lib/sidebarRenameFocus";
|
||||
import DangerConfirmDialog from "@/components/editor/DangerConfirmDialog.vue";
|
||||
|
|
@ -687,7 +688,11 @@ function nodeRenameObjectType(): RenameableObjectType | null {
|
|||
|
||||
const canRenameObject = computed(() => {
|
||||
const objectType = nodeRenameObjectType();
|
||||
return !!objectType && supportsObjectRename(currentDatabaseType(), objectType);
|
||||
return (
|
||||
!!objectType &&
|
||||
(supportsObjectRename(currentDatabaseType(), objectType) ||
|
||||
supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any))
|
||||
);
|
||||
});
|
||||
|
||||
function openRenameObjectDialog() {
|
||||
|
|
@ -700,6 +705,9 @@ function buildRenameObjectPreviewSql(): string {
|
|||
const objectType = nodeRenameObjectType();
|
||||
const newName = renameObjectName.value.trim();
|
||||
if (!objectType || !newName || newName === props.node.label) return "";
|
||||
if (supportsSourceBackedRoutineRename(currentDatabaseType(), objectType as any)) {
|
||||
return `-- Recreate ${objectType} from source, then drop the original object.`;
|
||||
}
|
||||
try {
|
||||
return buildRenameObjectSql({
|
||||
databaseType: currentDatabaseType(),
|
||||
|
|
@ -720,15 +728,32 @@ async function confirmRenameObject() {
|
|||
if (!objectType || !newName || newName === node.label || !node.connectionId || !node.database) return;
|
||||
renameObjectError.value = "";
|
||||
try {
|
||||
const sql = buildRenameObjectSql({
|
||||
databaseType: currentDatabaseType(),
|
||||
objectType,
|
||||
schema: node.schema,
|
||||
oldName: node.label,
|
||||
newName,
|
||||
});
|
||||
const dbType = currentDatabaseType();
|
||||
await connectionStore.ensureConnected(node.connectionId);
|
||||
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
|
||||
if (supportsSourceBackedRoutineRename(dbType, objectType as any)) {
|
||||
const schema = node.schema || node.database;
|
||||
const source = await api.getObjectSource(node.connectionId, node.database, schema, node.label, objectType as any);
|
||||
const statements = buildRoutineRenameObjectSourceStatements({
|
||||
databaseType: dbType!,
|
||||
objectType: objectType as any,
|
||||
schema,
|
||||
name: node.label,
|
||||
newName,
|
||||
source: source.source,
|
||||
});
|
||||
for (const sql of statements) {
|
||||
await api.executeQuery(node.connectionId, node.database, sql, schema);
|
||||
}
|
||||
} else {
|
||||
const sql = buildRenameObjectSql({
|
||||
databaseType: dbType,
|
||||
objectType,
|
||||
schema: node.schema,
|
||||
oldName: node.label,
|
||||
newName,
|
||||
});
|
||||
await api.executeQuery(node.connectionId, node.database, sql, node.schema);
|
||||
}
|
||||
toast(t("contextMenu.renameObjectSuccess", { oldName: node.label, newName }), 3000);
|
||||
showRenameObjectDialog.value = false;
|
||||
await refreshTableList(node);
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ export const TABLE_STRUCTURE_SUPPORTED_TYPES = new Set<DatabaseType>([
|
|||
"sqlite",
|
||||
"duckdb",
|
||||
"sqlserver",
|
||||
"oracle",
|
||||
"dameng",
|
||||
]);
|
||||
|
||||
export const CREATE_DATABASE_SUPPORTED_TYPES = new Set<DatabaseType>([
|
||||
|
|
|
|||
|
|
@ -49,7 +49,9 @@ export function supportsObjectRename(
|
|||
): boolean {
|
||||
if (!databaseType) return false;
|
||||
if (databaseType === "sqlserver") return true;
|
||||
if (objectType === "PROCEDURE" || objectType === "FUNCTION") return false;
|
||||
if (objectType === "PROCEDURE" || objectType === "FUNCTION") {
|
||||
return false;
|
||||
}
|
||||
if (databaseType === "sqlite") return objectType === "TABLE";
|
||||
if (databaseType === "mysql" || databaseType === "goldendb") return objectType === "TABLE" || objectType === "VIEW";
|
||||
if (postgresLikeRenameTypes.has(databaseType)) return objectType === "TABLE" || objectType === "VIEW";
|
||||
|
|
@ -79,7 +81,7 @@ export function buildRenameObjectSql(options: BuildRenameObjectSqlOptions): stri
|
|||
postgresLikeRenameTypes.has(databaseType as DatabaseType) ||
|
||||
oracleLikeRenameTypes.has(databaseType as DatabaseType)
|
||||
) {
|
||||
const keyword = objectType === "VIEW" ? "VIEW" : "TABLE";
|
||||
const keyword = objectType;
|
||||
return `ALTER ${keyword} ${qualifiedName(databaseType, schema, oldName)} RENAME TO ${quoteRenameIdentifier(databaseType, newName)};`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,10 @@ type BuildEditableObjectSourceSqlInput = {
|
|||
source: string;
|
||||
};
|
||||
|
||||
type BuildRoutineRenameObjectSourceInput = BuildEditableObjectSourceSqlInput & {
|
||||
newName: string;
|
||||
};
|
||||
|
||||
export type ObjectSourceSaveExecutionMode = "single" | "script";
|
||||
|
||||
const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
|
||||
|
|
@ -18,11 +22,17 @@ const postgresLikeRoutineRenameTypes = new Set<DatabaseType>([
|
|||
"highgo",
|
||||
"vastbase",
|
||||
]);
|
||||
const mysqlLikeRoutineRenameTypes = new Set<DatabaseType>(["mysql", "goldendb"]);
|
||||
const oracleLikeRoutineRenameTypes = new Set<DatabaseType>(["oracle", "dameng"]);
|
||||
|
||||
function quotePostgresIdentifier(value: string) {
|
||||
return `"${value.replaceAll('"', '""')}"`;
|
||||
}
|
||||
|
||||
function quoteMysqlIdentifier(value: string) {
|
||||
return `\`${value.replaceAll("`", "``")}\``;
|
||||
}
|
||||
|
||||
function ensureSemicolon(sql: string) {
|
||||
const trimmed = sql.trim();
|
||||
return trimmed.endsWith(";") ? trimmed : `${trimmed};`;
|
||||
|
|
@ -35,6 +45,13 @@ function postgresQualifiedName(schema: string | null | undefined, name: string)
|
|||
.join(".");
|
||||
}
|
||||
|
||||
function mysqlQualifiedName(schema: string | null | undefined, name: string) {
|
||||
return [schema, name]
|
||||
.filter(Boolean)
|
||||
.map((part) => quoteMysqlIdentifier(part as string))
|
||||
.join(".");
|
||||
}
|
||||
|
||||
function unquotePostgresIdentifier(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith('"') && trimmed.endsWith('"')) return trimmed.slice(1, -1).replaceAll('""', '"');
|
||||
|
|
@ -46,9 +63,20 @@ function splitQualifiedRoutineName(value: string) {
|
|||
return parts.map(unquotePostgresIdentifier);
|
||||
}
|
||||
|
||||
function unquoteMysqlIdentifier(value: string) {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed.startsWith("`") && trimmed.endsWith("`")) return trimmed.slice(1, -1).replaceAll("``", "`");
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function splitMysqlQualifiedRoutineName(value: string) {
|
||||
const parts = value.match(/`(?:``|[^`])+`|[A-Za-z_][\w$]*/g) ?? [];
|
||||
return parts.map(unquoteMysqlIdentifier);
|
||||
}
|
||||
|
||||
function routineDeclaration(source: string) {
|
||||
const match = source.match(
|
||||
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(FUNCTION|PROCEDURE)\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)\s*(\([^]*?\))?/i,
|
||||
/^\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(FUNCTION|PROCEDURE)\s+((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)\s*(\([^]*?\))?/i,
|
||||
);
|
||||
if (!match) return null;
|
||||
const nameParts = splitQualifiedRoutineName(match[2]);
|
||||
|
|
@ -61,14 +89,57 @@ function routineDeclaration(source: string) {
|
|||
};
|
||||
}
|
||||
|
||||
function replaceSqlRoutineDeclarationName(source: string, schema: string | null | undefined, newName: string) {
|
||||
const match = source.match(
|
||||
/^(\s*CREATE\s+(?:OR\s+REPLACE\s+)?(?:(?:NON)?EDITIONABLE\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:"(?:""|[^"])+"|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:"(?:""|[^"])+"|[A-Za-z_][\w$]*))?)/i,
|
||||
);
|
||||
if (!match) return null;
|
||||
const existingParts = splitQualifiedRoutineName(match[2]);
|
||||
const schemaName = schema || (existingParts.length > 1 ? existingParts[0] : null);
|
||||
const replacement = schemaName
|
||||
? `${quotePostgresIdentifier(schemaName)}.${quotePostgresIdentifier(newName)}`
|
||||
: quotePostgresIdentifier(newName);
|
||||
return `${source.slice(0, match.index)}${match[1]}${replacement}${source.slice((match.index ?? 0) + match[0].length)}`;
|
||||
}
|
||||
|
||||
function mysqlRoutineDeclaration(source: string) {
|
||||
const match = source.match(
|
||||
/^\s*CREATE\s+(?:DEFINER\s*=\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s+)?(FUNCTION|PROCEDURE)\s+((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)/i,
|
||||
);
|
||||
if (!match) return null;
|
||||
const nameParts = splitMysqlQualifiedRoutineName(match[2]);
|
||||
const name = nameParts[nameParts.length - 1];
|
||||
if (!name) return null;
|
||||
return {
|
||||
kind: match[1].toUpperCase() as "FUNCTION" | "PROCEDURE",
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
function replaceMysqlRoutineDeclarationName(source: string, newName: string) {
|
||||
const match = source.match(
|
||||
/^(\s*CREATE\s+(?:DEFINER\s*=\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s*@\s*(?:`(?:``|[^`])+`|'(?:''|[^'])+'|[^\s]+)\s+)?(?:FUNCTION|PROCEDURE)\s+)((?:`(?:``|[^`])+`|[A-Za-z_][\w$]*)(?:\s*\.\s*(?:`(?:``|[^`])+`|[A-Za-z_][\w$]*))?)/i,
|
||||
);
|
||||
if (!match) return null;
|
||||
return `${source.slice(0, match.index)}${match[1]}${quoteMysqlIdentifier(newName)}${source.slice((match.index ?? 0) + match[0].length)}`;
|
||||
}
|
||||
|
||||
function routineNameChanged(sourceName: string, savedName: string) {
|
||||
return sourceName.toLowerCase() !== savedName.toLowerCase();
|
||||
}
|
||||
|
||||
function buildRoutineRenameCleanup(input: BuildEditableObjectSourceSqlInput, source: string) {
|
||||
if (!postgresLikeRoutineRenameTypes.has(input.databaseType)) return null;
|
||||
if (input.objectType !== "FUNCTION" && input.objectType !== "PROCEDURE") return null;
|
||||
|
||||
if (mysqlLikeRoutineRenameTypes.has(input.databaseType)) {
|
||||
const declaration = mysqlRoutineDeclaration(source);
|
||||
if (!declaration || declaration.kind !== input.objectType) return null;
|
||||
if (!routineNameChanged(declaration.name, input.name)) return null;
|
||||
return `DROP ${input.objectType} IF EXISTS ${mysqlQualifiedName(input.schema, input.name)};`;
|
||||
}
|
||||
|
||||
if (!postgresLikeRoutineRenameTypes.has(input.databaseType)) return null;
|
||||
|
||||
const declaration = routineDeclaration(source);
|
||||
if (!declaration || declaration.kind !== input.objectType) return null;
|
||||
if (!routineNameChanged(declaration.name, input.name)) return null;
|
||||
|
|
@ -76,6 +147,55 @@ function buildRoutineRenameCleanup(input: BuildEditableObjectSourceSqlInput, sou
|
|||
return `DROP ${input.objectType} IF EXISTS ${postgresQualifiedName(input.schema, input.name)}${declaration.signature};`;
|
||||
}
|
||||
|
||||
export function supportsSourceBackedRoutineRename(
|
||||
databaseType: DatabaseType | undefined,
|
||||
objectType: ObjectSourceKind,
|
||||
): boolean {
|
||||
if (objectType !== "FUNCTION" && objectType !== "PROCEDURE") return false;
|
||||
if (!databaseType || databaseType === "sqlserver") return false;
|
||||
return (
|
||||
mysqlLikeRoutineRenameTypes.has(databaseType) ||
|
||||
postgresLikeRoutineRenameTypes.has(databaseType) ||
|
||||
oracleLikeRoutineRenameTypes.has(databaseType)
|
||||
);
|
||||
}
|
||||
|
||||
export function buildRoutineRenameObjectSourceStatements(input: BuildRoutineRenameObjectSourceInput) {
|
||||
if (!supportsSourceBackedRoutineRename(input.databaseType, input.objectType)) {
|
||||
throw new Error(`Renaming ${input.objectType} from source is not supported for ${input.databaseType}.`);
|
||||
}
|
||||
|
||||
const source = input.source.trim();
|
||||
const declaration = mysqlLikeRoutineRenameTypes.has(input.databaseType)
|
||||
? mysqlRoutineDeclaration(source)
|
||||
: routineDeclaration(source);
|
||||
if (!declaration || declaration.kind !== input.objectType) {
|
||||
throw new Error(`Cannot find a CREATE ${input.objectType} declaration in the object source.`);
|
||||
}
|
||||
|
||||
const renamedSource = mysqlLikeRoutineRenameTypes.has(input.databaseType)
|
||||
? replaceMysqlRoutineDeclarationName(source, input.newName)
|
||||
: replaceSqlRoutineDeclarationName(source, input.schema, input.newName);
|
||||
if (!renamedSource) {
|
||||
throw new Error(`Cannot rewrite the ${input.objectType} name in the object source.`);
|
||||
}
|
||||
|
||||
if (oracleLikeRoutineRenameTypes.has(input.databaseType)) {
|
||||
return [
|
||||
ensureSemicolon(renamedSource),
|
||||
`DROP ${input.objectType} ${postgresQualifiedName(input.schema, input.name)};`,
|
||||
];
|
||||
}
|
||||
|
||||
return buildExecutableObjectSourceStatements({
|
||||
databaseType: input.databaseType,
|
||||
objectType: input.objectType,
|
||||
schema: input.schema,
|
||||
name: input.name,
|
||||
source: renamedSource,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildExecutableObjectSourceStatements(input: BuildEditableObjectSourceSqlInput) {
|
||||
const source = input.source.trim();
|
||||
if (input.databaseType === "sqlserver") {
|
||||
|
|
|
|||
|
|
@ -45,8 +45,12 @@ function quoteIdent(databaseType: DatabaseType | undefined, name: string): strin
|
|||
return `"${name.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
function isOracleLike(databaseType: DatabaseType | undefined): databaseType is "oracle" | "dameng" {
|
||||
return databaseType === "oracle" || databaseType === "dameng";
|
||||
}
|
||||
|
||||
function qualifiedTable(databaseType: DatabaseType | undefined, schema: string | undefined, tableName: string): string {
|
||||
if ((databaseType === "postgres" || databaseType === "oracle" || databaseType === "sqlserver") && schema) {
|
||||
if ((databaseType === "postgres" || isOracleLike(databaseType) || databaseType === "sqlserver") && schema) {
|
||||
return `${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, tableName)}`;
|
||||
}
|
||||
return quoteIdent(databaseType, tableName);
|
||||
|
|
@ -67,7 +71,7 @@ function normalizeDefault(value: string | null | undefined): string {
|
|||
|
||||
function columnDefinition(databaseType: DatabaseType | undefined, column: EditableStructureColumn): string {
|
||||
const parts = [quoteIdent(databaseType, column.name), column.dataType.trim()];
|
||||
if (!column.isNullable) parts.push("NOT NULL");
|
||||
if (!column.isNullable && !isOracleLike(databaseType)) parts.push("NOT NULL");
|
||||
const defaultValue = normalizeDefault(column.defaultValue);
|
||||
if (defaultValue) parts.push(`DEFAULT ${defaultValue}`);
|
||||
if (databaseType === "mysql" && clean(column.comment)) {
|
||||
|
|
@ -102,8 +106,11 @@ function buildAddColumnSql(
|
|||
column: EditableStructureColumn,
|
||||
): string[] {
|
||||
const addKeyword = databaseType === "sqlserver" ? "ADD" : "ADD COLUMN";
|
||||
const statements = [`ALTER TABLE ${table} ${addKeyword} ${columnDefinition(databaseType, column)};`];
|
||||
if (databaseType === "postgres" && clean(column.comment)) {
|
||||
const definition = columnDefinition(databaseType, column);
|
||||
const statements = isOracleLike(databaseType)
|
||||
? [`ALTER TABLE ${table} ADD (${definition});`]
|
||||
: [`ALTER TABLE ${table} ${addKeyword} ${definition};`];
|
||||
if ((databaseType === "postgres" || isOracleLike(databaseType)) && clean(column.comment)) {
|
||||
statements.push(
|
||||
`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, column.name)} IS ${quoteString(clean(column.comment))};`,
|
||||
);
|
||||
|
|
@ -111,6 +118,42 @@ function buildAddColumnSql(
|
|||
return statements;
|
||||
}
|
||||
|
||||
function buildOracleLikeExistingColumnSql(
|
||||
databaseType: DatabaseType,
|
||||
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} RENAME COLUMN ${quoteIdent(databaseType, original.name)} TO ${quoteIdent(databaseType, column.name)};`,
|
||||
);
|
||||
currentName = column.name;
|
||||
}
|
||||
if (column.dataType.trim() !== original.data_type.trim()) {
|
||||
statements.push(
|
||||
`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} ${column.dataType.trim()});`,
|
||||
);
|
||||
}
|
||||
if (column.isNullable !== original.is_nullable) {
|
||||
const nullability = column.isNullable ? "NULL" : "NOT NULL";
|
||||
statements.push(`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} ${nullability});`);
|
||||
}
|
||||
if (normalizeDefault(column.defaultValue) !== originalDefault(column)) {
|
||||
const defaultValue = normalizeDefault(column.defaultValue) || "NULL";
|
||||
statements.push(`ALTER TABLE ${table} MODIFY (${quoteIdent(databaseType, currentName)} DEFAULT ${defaultValue});`);
|
||||
}
|
||||
if (clean(column.comment) !== originalComment(column)) {
|
||||
const commentValue = clean(column.comment) ? quoteString(clean(column.comment)) : "NULL";
|
||||
statements.push(`COMMENT ON COLUMN ${table}.${quoteIdent(databaseType, currentName)} IS ${commentValue};`);
|
||||
}
|
||||
return statements;
|
||||
}
|
||||
|
||||
function buildMysqlExistingColumnSql(table: string, column: EditableStructureColumn): string[] {
|
||||
const originalName = column.original?.name ?? column.name;
|
||||
const operation =
|
||||
|
|
@ -199,6 +242,8 @@ function buildColumnSql(options: BuildTableStructureChangeSqlOptions, warnings:
|
|||
statements.push(...buildMysqlExistingColumnSql(table, column));
|
||||
} else if (databaseType === "postgres") {
|
||||
statements.push(...buildPostgresExistingColumnSql(table, column));
|
||||
} else if (isOracleLike(databaseType)) {
|
||||
statements.push(...buildOracleLikeExistingColumnSql(databaseType, table, column));
|
||||
} else if (databaseType === "sqlite") {
|
||||
statements.push(...buildSqliteExistingColumnSql(table, column, warnings));
|
||||
} else {
|
||||
|
|
@ -217,7 +262,7 @@ function buildDropIndexSql(
|
|||
): string {
|
||||
if (databaseType === "mysql") return `DROP INDEX ${quoteIdent(databaseType, indexName)} ON ${table};`;
|
||||
if (databaseType === "sqlserver") return `DROP INDEX ${quoteIdent(databaseType, indexName)} ON ${table};`;
|
||||
if ((databaseType === "postgres" || databaseType === "oracle") && schema) {
|
||||
if ((databaseType === "postgres" || isOracleLike(databaseType)) && schema) {
|
||||
return `DROP INDEX ${quoteIdent(databaseType, schema)}.${quoteIdent(databaseType, indexName)};`;
|
||||
}
|
||||
return `DROP INDEX ${quoteIdent(databaseType, indexName)};`;
|
||||
|
|
@ -344,7 +389,7 @@ export function buildCreateTableSql(options: BuildTableStructureChangeSqlOptions
|
|||
|
||||
statements.push(`CREATE TABLE ${table} (\n ${colDefs.join(",\n ")}\n);`);
|
||||
|
||||
if (databaseType === "postgres") {
|
||||
if (databaseType === "postgres" || isOracleLike(databaseType)) {
|
||||
for (const col of activeColumns) {
|
||||
if (clean(col.comment)) {
|
||||
statements.push(
|
||||
|
|
|
|||
|
|
@ -113,7 +113,8 @@ test("describes feature support through capability helpers", () => {
|
|||
assert.equal(supportsTableImport("hive"), false);
|
||||
assert.equal(supportsTableStructureEditing("postgres"), true);
|
||||
assert.equal(supportsTableStructureEditing("duckdb"), true);
|
||||
assert.equal(supportsTableStructureEditing("oracle"), false);
|
||||
assert.equal(supportsTableStructureEditing("oracle"), true);
|
||||
assert.equal(supportsTableStructureEditing("dameng"), true);
|
||||
assert.equal(supportsDatabaseCreation("clickhouse"), true);
|
||||
assert.equal(supportsDatabaseCreation("sqlite"), false);
|
||||
assert.equal(supportsFieldLineage("gaussdb"), true);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,22 @@ test("builds Oracle-family table and view rename statements", () => {
|
|||
);
|
||||
});
|
||||
|
||||
test("does not build direct Oracle-family routine rename statements", () => {
|
||||
assert.equal(supportsObjectRename("oracle", "FUNCTION"), false);
|
||||
assert.equal(supportsObjectRename("dameng", "PROCEDURE"), false);
|
||||
assert.throws(
|
||||
() =>
|
||||
buildRenameObjectSql({
|
||||
databaseType: "dameng",
|
||||
objectType: "PROCEDURE",
|
||||
schema: "SYSDBA",
|
||||
oldName: "REFRESH_CACHE",
|
||||
newName: "REFRESH_CACHE_V2",
|
||||
}),
|
||||
/Renaming PROCEDURE is not supported/,
|
||||
);
|
||||
});
|
||||
|
||||
test("reports unsupported routine rename cases", () => {
|
||||
assert.equal(supportsObjectRename("mysql", "PROCEDURE"), false);
|
||||
assert.equal(supportsObjectRename("postgres", "FUNCTION"), false);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import test from "node:test";
|
|||
import {
|
||||
buildExecutableObjectSourceSql,
|
||||
buildExecutableObjectSourceStatements,
|
||||
buildRoutineRenameObjectSourceStatements,
|
||||
objectSourceSaveExecutionMode,
|
||||
supportsSourceBackedRoutineRename,
|
||||
} from "../../apps/desktop/src/lib/objectSourceEditor.ts";
|
||||
|
||||
test("SQL Server edited source saves as ALTER", () => {
|
||||
|
|
@ -104,3 +106,62 @@ test("object source SQL joins generated save statements for previews", () => {
|
|||
'CREATE OR REPLACE PROCEDURE "public"."refresh_cache_v2"(mode text)\nLANGUAGE SQL\nAS $$ SELECT 1 $$;\nDROP PROCEDURE IF EXISTS "public"."refresh_cache"(mode text);',
|
||||
);
|
||||
});
|
||||
|
||||
test("Oracle and Dameng object source saves as semicolon-terminated source", () => {
|
||||
assert.equal(
|
||||
buildExecutableObjectSourceSql({
|
||||
databaseType: "oracle",
|
||||
objectType: "VIEW",
|
||||
schema: "HR",
|
||||
name: "ACTIVE_USERS",
|
||||
source: "CREATE OR REPLACE VIEW HR.ACTIVE_USERS AS SELECT ID FROM USERS",
|
||||
}),
|
||||
"CREATE OR REPLACE VIEW HR.ACTIVE_USERS AS SELECT ID FROM USERS;",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
buildExecutableObjectSourceSql({
|
||||
databaseType: "dameng",
|
||||
objectType: "PROCEDURE",
|
||||
schema: "SYSDBA",
|
||||
name: "REFRESH_CACHE",
|
||||
source: "CREATE OR REPLACE PROCEDURE SYSDBA.REFRESH_CACHE AS BEGIN SELECT 1; END;",
|
||||
}),
|
||||
"CREATE OR REPLACE PROCEDURE SYSDBA.REFRESH_CACHE AS BEGIN SELECT 1; END;",
|
||||
);
|
||||
});
|
||||
|
||||
test("MySQL routine rename creates the renamed routine and drops the original routine", () => {
|
||||
const statements = buildExecutableObjectSourceStatements({
|
||||
databaseType: "mysql",
|
||||
objectType: "PROCEDURE",
|
||||
schema: "app",
|
||||
name: "refresh_cache",
|
||||
source: "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END",
|
||||
});
|
||||
|
||||
assert.deepEqual(statements, [
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END;",
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;",
|
||||
]);
|
||||
});
|
||||
|
||||
test("Oracle-family routine rename rewrites source and drops the original routine", () => {
|
||||
assert.equal(supportsSourceBackedRoutineRename("dameng", "PROCEDURE"), true);
|
||||
assert.equal(supportsSourceBackedRoutineRename("oracle", "FUNCTION"), true);
|
||||
|
||||
const statements = buildRoutineRenameObjectSourceStatements({
|
||||
databaseType: "dameng",
|
||||
objectType: "PROCEDURE",
|
||||
schema: "SYSDBA",
|
||||
name: "SP_TAB_BAKSET_REMOVE_BATCH",
|
||||
newName: "SP_TAB_BAKSET_REMOVE_BATCH_2",
|
||||
source:
|
||||
'CREATE OR REPLACE PROCEDURE "SYSDBA"."SP_TAB_BAKSET_REMOVE_BATCH" AS\nBEGIN\n SELECT 1;\nEND;',
|
||||
});
|
||||
|
||||
assert.deepEqual(statements, [
|
||||
'CREATE OR REPLACE PROCEDURE "SYSDBA"."SP_TAB_BAKSET_REMOVE_BATCH_2" AS\nBEGIN\n SELECT 1;\nEND;',
|
||||
'DROP PROCEDURE "SYSDBA"."SP_TAB_BAKSET_REMOVE_BATCH";',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -473,3 +473,122 @@ test("index with empty name and columns produces warnings and no statements", ()
|
|||
]);
|
||||
assert.deepEqual(result.statements, []);
|
||||
});
|
||||
|
||||
test("builds Oracle column, comment, and index change statements", () => {
|
||||
const result = buildTableStructureChangeSql({
|
||||
databaseType: "oracle",
|
||||
schema: "HR",
|
||||
tableName: "EMPLOYEES",
|
||||
columns: [
|
||||
column({
|
||||
id: "status",
|
||||
name: "EMP_STATUS",
|
||||
dataType: "VARCHAR2(20)",
|
||||
isNullable: false,
|
||||
defaultValue: "'ACTIVE'",
|
||||
comment: "Employment status",
|
||||
original: {
|
||||
name: "STATUS",
|
||||
data_type: "VARCHAR2(10)",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: "",
|
||||
},
|
||||
}),
|
||||
column({ id: "email", name: "EMAIL", dataType: "VARCHAR2(255)", isNullable: true, comment: "Work email" }),
|
||||
column({
|
||||
id: "legacy",
|
||||
name: "LEGACY_CODE",
|
||||
markedForDrop: true,
|
||||
original: {
|
||||
name: "LEGACY_CODE",
|
||||
data_type: "VARCHAR2(20)",
|
||||
is_nullable: true,
|
||||
column_default: null,
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
},
|
||||
}),
|
||||
],
|
||||
indexes: [
|
||||
index({
|
||||
id: "old",
|
||||
name: "IDX_EMP_OLD",
|
||||
markedForDrop: true,
|
||||
original: { name: "IDX_EMP_OLD", columns: ["STATUS"], is_unique: false, is_primary: false },
|
||||
}),
|
||||
index({ id: "new", name: "IDX_EMP_STATUS", columns: ["EMP_STATUS"], isUnique: true }),
|
||||
],
|
||||
});
|
||||
|
||||
assert.deepEqual(result.warnings, []);
|
||||
assert.deepEqual(result.statements, [
|
||||
'ALTER TABLE "HR"."EMPLOYEES" RENAME COLUMN "STATUS" TO "EMP_STATUS";',
|
||||
'ALTER TABLE "HR"."EMPLOYEES" MODIFY ("EMP_STATUS" VARCHAR2(20));',
|
||||
'ALTER TABLE "HR"."EMPLOYEES" MODIFY ("EMP_STATUS" NOT NULL);',
|
||||
'ALTER TABLE "HR"."EMPLOYEES" MODIFY ("EMP_STATUS" DEFAULT \'ACTIVE\');',
|
||||
'COMMENT ON COLUMN "HR"."EMPLOYEES"."EMP_STATUS" IS \'Employment status\';',
|
||||
'ALTER TABLE "HR"."EMPLOYEES" ADD ("EMAIL" VARCHAR2(255));',
|
||||
'COMMENT ON COLUMN "HR"."EMPLOYEES"."EMAIL" IS \'Work email\';',
|
||||
'ALTER TABLE "HR"."EMPLOYEES" DROP COLUMN "LEGACY_CODE";',
|
||||
'DROP INDEX "HR"."IDX_EMP_OLD";',
|
||||
'CREATE UNIQUE INDEX "IDX_EMP_STATUS" ON "HR"."EMPLOYEES" ("EMP_STATUS");',
|
||||
]);
|
||||
});
|
||||
|
||||
test("builds Dameng existing column and create table statements", () => {
|
||||
const change = buildTableStructureChangeSql({
|
||||
databaseType: "dameng",
|
||||
schema: "SYSDBA",
|
||||
tableName: "USERS",
|
||||
columns: [
|
||||
column({
|
||||
id: "name",
|
||||
name: "DISPLAY_NAME",
|
||||
dataType: "VARCHAR(120)",
|
||||
isNullable: true,
|
||||
defaultValue: "",
|
||||
comment: "",
|
||||
original: {
|
||||
name: "NAME",
|
||||
data_type: "VARCHAR(80)",
|
||||
is_nullable: false,
|
||||
column_default: "'guest'",
|
||||
is_primary_key: false,
|
||||
extra: null,
|
||||
comment: "Old name",
|
||||
},
|
||||
}),
|
||||
],
|
||||
indexes: [],
|
||||
});
|
||||
|
||||
assert.deepEqual(change.warnings, []);
|
||||
assert.deepEqual(change.statements, [
|
||||
'ALTER TABLE "SYSDBA"."USERS" RENAME COLUMN "NAME" TO "DISPLAY_NAME";',
|
||||
'ALTER TABLE "SYSDBA"."USERS" MODIFY ("DISPLAY_NAME" VARCHAR(120));',
|
||||
'ALTER TABLE "SYSDBA"."USERS" MODIFY ("DISPLAY_NAME" NULL);',
|
||||
'ALTER TABLE "SYSDBA"."USERS" MODIFY ("DISPLAY_NAME" DEFAULT NULL);',
|
||||
'COMMENT ON COLUMN "SYSDBA"."USERS"."DISPLAY_NAME" IS NULL;',
|
||||
]);
|
||||
|
||||
const create = buildCreateTableSql({
|
||||
databaseType: "dameng",
|
||||
schema: "SYSDBA",
|
||||
tableName: "USERS",
|
||||
columns: [
|
||||
column({ id: "id", name: "ID", dataType: "NUMBER", isNullable: false, isPrimaryKey: true }),
|
||||
column({ id: "name", name: "NAME", dataType: "VARCHAR(120)", isNullable: false, comment: "Display name" }),
|
||||
],
|
||||
indexes: [index({ id: "idx", name: "IDX_USERS_NAME", columns: ["NAME"] })],
|
||||
});
|
||||
|
||||
assert.deepEqual(create.warnings, []);
|
||||
assert.deepEqual(create.statements, [
|
||||
'CREATE TABLE "SYSDBA"."USERS" (\n "ID" NUMBER,\n "NAME" VARCHAR(120) NOT NULL,\n PRIMARY KEY ("ID")\n);',
|
||||
'COMMENT ON COLUMN "SYSDBA"."USERS"."NAME" IS \'Display name\';',
|
||||
'CREATE INDEX "IDX_USERS_NAME" ON "SYSDBA"."USERS" ("NAME");',
|
||||
]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue