190 lines
5.9 KiB
Go
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
|
|
}
|