Files
RenewCMS/internal/infrastructure/persistence/database/postgresBackup.go
T

190 lines
5.9 KiB
Go

package database
import (
"fmt"
"strings"
"time"
"gorm.io/gorm"
)
const (
postgresSchema = "public"
postgresTablesQuery = "SELECT table_name FROM information_schema.tables WHERE table_schema = ? AND table_type = 'BASE TABLE' ORDER BY table_name"
postgresColumnsQuery = `SELECT a.attname AS name, format_type(a.atttypid, a.atttypmod) AS data_type, a.attnotnull AS not_null, COALESCE(pg_get_expr(d.adbin, d.adrelid), '') AS default_value, a.attidentity::text AS identity
FROM pg_attribute a
JOIN pg_class c ON c.oid = a.attrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
LEFT JOIN pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
WHERE n.nspname = ? AND c.relname = ? AND a.attnum > 0 AND NOT a.attisdropped
ORDER BY a.attnum`
postgresConstraintsQuery = `SELECT con.conname AS name, pg_get_constraintdef(con.oid) AS definition
FROM pg_constraint con
JOIN pg_class c ON c.oid = con.conrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = ? AND c.relname = ? AND con.contype IN ('p', 'u', 'c')
ORDER BY CASE con.contype WHEN 'p' THEN 0 ELSE 1 END, con.conname`
postgresIndexesQuery = `SELECT i.indexdef
FROM pg_indexes i
WHERE i.schemaname = ? AND i.tablename = ?
AND i.indexname NOT IN (
SELECT con.conname FROM pg_constraint con
JOIN pg_class c ON c.oid = con.conrelid
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE n.nspname = ? AND c.relname = ?
)
ORDER BY i.indexname`
postgresIdentityColumn = "id"
postgresTimeLayout = "2006-01-02 15:04:05.999999999Z07:00"
postgresByteaType = "BYTEA"
)
var postgresNumericTypes = map[string]bool{
"INT2": true, "INT4": true, "INT8": true, "FLOAT4": true, "FLOAT8": true, "NUMERIC": true, "OID": true,
}
var postgresSerialTypes = map[string]string{
"smallint": "smallserial",
"integer": "serial",
"bigint": "bigserial",
}
var postgresDumpDialect = dumpDialect{
preamble: []string{
"SET client_encoding = 'UTF8';",
"SET standard_conforming_strings = on;",
"SET TimeZone = 'UTC';",
},
quote: quotePostgresIdentifier,
literal: postgresLiteral,
tableTrailer: postgresSequenceReset,
}
func NewPostgresBackup(db *gorm.DB, directory string, now func() time.Time) *DumpBackup {
backup := NewDumpBackup(func() (DumpSource, error) { return openPostgresDumpSource(db) }, directory, now)
backup.dialect = postgresDumpDialect
return backup
}
type postgresDumpSource struct {
*txDumpSource
}
var _ DumpSource = &postgresDumpSource{}
func openPostgresDumpSource(db *gorm.DB) (*postgresDumpSource, error) {
source, err := openTxDumpSource(db, quotePostgresIdentifier, postgresDumpBytes)
if err != nil {
return nil, err
}
return &postgresDumpSource{source}, nil
}
func (s *postgresDumpSource) Tables() ([]string, error) {
var tables []string
err := s.tx.Raw(postgresTablesQuery, postgresSchema).Scan(&tables).Error
return tables, err
}
type postgresColumn struct {
Name string
DataType string
NotNull bool
DefaultValue string
Identity string
}
type postgresConstraint struct {
Name string
Definition string
}
func (s *postgresDumpSource) CreateStatement(table string) (string, error) {
var columns []postgresColumn
if err := s.tx.Raw(postgresColumnsQuery, postgresSchema, table).Scan(&columns).Error; err != nil {
return "", err
}
var constraints []postgresConstraint
if err := s.tx.Raw(postgresConstraintsQuery, postgresSchema, table).Scan(&constraints).Error; err != nil {
return "", err
}
var indexes []string
if err := s.tx.Raw(postgresIndexesQuery, postgresSchema, table, postgresSchema, table).Scan(&indexes).Error; err != nil {
return "", err
}
var definitions []string
for _, column := range columns {
definitions = append(definitions, " "+postgresColumnDefinition(column))
}
for _, constraint := range constraints {
definitions = append(definitions, fmt.Sprintf(" CONSTRAINT %s %s", quotePostgresIdentifier(constraint.Name), constraint.Definition))
}
statements := []string{fmt.Sprintf("CREATE TABLE %s (\n%s\n)", quotePostgresIdentifier(table), strings.Join(definitions, ",\n"))}
statements = append(statements, indexes...)
return strings.Join(statements, ";\n"), nil
}
func postgresColumnDefinition(column postgresColumn) string {
quoted := quotePostgresIdentifier(column.Name)
serial, isSerial := postgresSerialTypes[column.DataType]
if isSerial && strings.HasPrefix(column.DefaultValue, "nextval(") {
return quoted + " " + serial + " NOT NULL"
}
if column.Identity != "" {
return quoted + " " + column.DataType + " GENERATED BY DEFAULT AS IDENTITY NOT NULL"
}
definition := quoted + " " + column.DataType
if column.DefaultValue != "" {
definition += " DEFAULT " + column.DefaultValue
}
if column.NotNull {
definition += " NOT NULL"
}
return definition
}
func postgresDumpBytes(value any) []byte {
if moment, isTime := value.(time.Time); isTime {
return []byte(moment.UTC().Format(postgresTimeLayout))
}
return dumpBytes(value)
}
func quotePostgresIdentifier(name string) string {
return `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
}
func postgresLiteral(databaseType string, value []byte) string {
if value == nil {
return "NULL"
}
upper := strings.ToUpper(databaseType)
if postgresNumericTypes[upper] && plainNumber.Match(value) {
return string(value)
}
if upper == postgresByteaType {
return fmt.Sprintf(`'\x%x'`, value)
}
return "'" + strings.ReplaceAll(string(value), "'", "''") + "'"
}
func postgresSequenceReset(table string, columns []DumpColumn) []string {
for _, column := range columns {
if column.Name != postgresIdentityColumn {
continue
}
quotedTable, quotedColumn := quotePostgresIdentifier(table), quotePostgresIdentifier(column.Name)
sequenceTable := strings.ReplaceAll(quotedTable, "'", "''")
return []string{fmt.Sprintf(
"SELECT setval(pg_get_serial_sequence('%s', '%s'), COALESCE((SELECT MAX(%s) FROM %s), 1), (SELECT COUNT(*) > 0 FROM %s));",
sequenceTable, column.Name, quotedColumn, quotedTable, quotedTable,
)}
}
return nil
}