🐛 fix(schema): 修复 PostgreSQL 模式备份恢复与重命名

- 模式备份脚本写入安全转义的 CREATE SCHEMA IF NOT EXISTS
- 支持仅大小写变化的 PostgreSQL 模式重命名
- 补充备份头顺序、非 PostgreSQL 隔离和模式 DDL 回归测试

Fixes #480
This commit is contained in:
Syngnat
2026-07-18 15:51:48 +08:00
parent 15fba1d89f
commit 5f5b28ced3
5 changed files with 91 additions and 6 deletions

View File

@@ -322,7 +322,7 @@ func buildRenameSchemaSQLWithText(dbType string, oldSchemaName string, newSchema
if oldSchemaName == "" || newSchemaName == "" {
return "", fmt.Errorf("%s", text("db.backend.error.schema_name_required", nil))
}
if strings.EqualFold(oldSchemaName, newSchemaName) {
if oldSchemaName == newSchemaName {
return "", fmt.Errorf("%s", text("db.backend.error.schema_same_name", nil))
}
if !isPostgresSchemaDDLDBType(dbType) {

View File

@@ -406,7 +406,7 @@ func TestMethodsDBSchemaDDLUsesEnglishMessages(t *testing.T) {
t.Fatalf("expected localized schema name message, got %q", missingSchemaName.Message)
}
sameSchemaName := app.RenameSchema(connection.ConnectionConfig{Type: "postgres", Database: "tenant"}, "", "sales", "SALES")
sameSchemaName := app.RenameSchema(connection.ConnectionConfig{Type: "postgres", Database: "tenant"}, "", "sales", "sales")
if sameSchemaName.Success {
t.Fatalf("RenameSchema with same names returned success: %+v", sameSchemaName)
}

View File

@@ -62,6 +62,17 @@ func TestBuildRenameSchemaSQL_PostgresQuotesIdentifiers(t *testing.T) {
}
}
func TestBuildRenameSchemaSQL_PostgresAllowsCaseOnlyRename(t *testing.T) {
got, err := buildRenameSchemaSQL("postgresql", "sales", "SALES")
if err != nil {
t.Fatalf("expected postgres case-only schema rename SQL, got error: %v", err)
}
const want = `ALTER SCHEMA "sales" RENAME TO "SALES"`
if got != want {
t.Fatalf("unexpected case-only rename schema SQL, want %q got %q", want, got)
}
}
func TestBuildDropSchemaSQL_PostgresUsesCascade(t *testing.T) {
got, err := buildDropSchemaSQL("postgresql", `sales"ops`)
if err != nil {

View File

@@ -3065,10 +3065,7 @@ func (a *App) ExportSchemaSQL(config connection.ConnectionConfig, dbName string,
w := bufio.NewWriterSize(f, 1024*1024)
defer w.Flush()
if err := writeSQLHeader(w, runConfig, dbName); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
if _, err := w.WriteString(fmt.Sprintf("-- Schema: %s\n\n", safeSchemaName)); err != nil {
if err := writeSQLSchemaExportHeader(w, runConfig, dbName, safeSchemaName); err != nil {
return connection.QueryResult{Success: false, Message: err.Error()}
}
for _, objectName := range objects {
@@ -3322,6 +3319,33 @@ func writeSQLHeader(w *bufio.Writer, config connection.ConnectionConfig, dbName
return writeSQLHeaderWithDatabaseBootstrap(w, config, dbName, false)
}
func writeSQLSchemaExportHeader(
w *bufio.Writer,
config connection.ConnectionConfig,
dbName string,
schemaName string,
) error {
safeSchemaName := strings.TrimSpace(schemaName)
if safeSchemaName == "" {
return errors.New("schema name is required")
}
if err := writeSQLHeader(w, config, dbName); err != nil {
return err
}
if _, err := w.WriteString(fmt.Sprintf("-- Schema: %s\n\n", safeSchemaName)); err != nil {
return err
}
dbType := resolveDDLDBType(config)
if isPostgresSchemaDDLDBType(dbType) {
_, err := w.WriteString(fmt.Sprintf(
"CREATE SCHEMA IF NOT EXISTS %s;\n\n",
quoteIdentByType(dbType, safeSchemaName),
))
return err
}
return nil
}
func writeSQLDatabaseBackupHeader(w *bufio.Writer, config connection.ConnectionConfig, dbName string) error {
return writeSQLHeaderWithDatabaseBootstrap(w, config, dbName, true)
}

View File

@@ -1717,3 +1717,53 @@ func TestFilterExportViewLookupBySchema_PostgresQualifiedViewsOnly(t *testing.T)
t.Fatalf("expected public.v_users to be filtered out, got=%v", got)
}
}
func TestWriteSQLSchemaExportHeaderPostgresCreatesQuotedSchema(t *testing.T) {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
if err := writeSQLSchemaExportHeader(
writer,
connection.ConnectionConfig{Type: "postgres"},
"app_db",
`Sales"Ops`,
); err != nil {
t.Fatalf("write postgres schema export header: %v", err)
}
if err := writer.Flush(); err != nil {
t.Fatalf("flush postgres schema export header: %v", err)
}
content := output.String()
if !strings.Contains(content, `-- Schema: Sales"Ops`) {
t.Fatalf("schema export header must describe the selected schema, content=%q", content)
}
if !strings.Contains(content, `CREATE SCHEMA IF NOT EXISTS "Sales""Ops";`) {
t.Fatalf("schema export header must bootstrap the quoted schema, content=%q", content)
}
databaseIndex := strings.Index(content, "-- Database: app_db")
schemaIndex := strings.Index(content, `-- Schema: Sales"Ops`)
createIndex := strings.Index(content, `CREATE SCHEMA IF NOT EXISTS "Sales""Ops";`)
if databaseIndex < 0 || schemaIndex < databaseIndex || createIndex < schemaIndex {
t.Fatalf("schema bootstrap must follow the database and schema metadata, content=%q", content)
}
}
func TestWriteSQLSchemaExportHeaderDoesNotBootstrapNonPostgresSchema(t *testing.T) {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
if err := writeSQLSchemaExportHeader(
writer,
connection.ConnectionConfig{Type: "mysql"},
"app_db",
"sales",
); err != nil {
t.Fatalf("write non-postgres schema export header: %v", err)
}
if err := writer.Flush(); err != nil {
t.Fatalf("flush non-postgres schema export header: %v", err)
}
if strings.Contains(output.String(), "CREATE SCHEMA") {
t.Fatalf("non-postgres schema export must not inject postgres bootstrap SQL, content=%q", output.String())
}
}