diff --git a/internal/app/methods_db.go b/internal/app/methods_db.go index f585ecf4..cecb37d6 100644 --- a/internal/app/methods_db.go +++ b/internal/app/methods_db.go @@ -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) { diff --git a/internal/app/methods_db_i18n_test.go b/internal/app/methods_db_i18n_test.go index e951074e..63f35ba4 100644 --- a/internal/app/methods_db_i18n_test.go +++ b/internal/app/methods_db_i18n_test.go @@ -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) } diff --git a/internal/app/methods_db_schema_test.go b/internal/app/methods_db_schema_test.go index 0ede71cb..aa422efd 100644 --- a/internal/app/methods_db_schema_test.go +++ b/internal/app/methods_db_schema_test.go @@ -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 { diff --git a/internal/app/methods_file.go b/internal/app/methods_file.go index 332597ac..3a2dce13 100644 --- a/internal/app/methods_file.go +++ b/internal/app/methods_file.go @@ -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) } diff --git a/internal/app/methods_file_export_test.go b/internal/app/methods_file_export_test.go index 342f83bf..aaff7a2f 100644 --- a/internal/app/methods_file_export_test.go +++ b/internal/app/methods_file_export_test.go @@ -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()) + } +}