sq/drivers/postgres/tools.go
Neil O'Toole 99454852f0
db tools preliminary work; --src.schema changes (#392)
- Preliminary work on the (currently hidden) `db` cmds.
- Improvements to `--src.schema`
2024-02-09 09:08:39 -07:00

333 lines
10 KiB
Go

package postgres
import (
"github.com/neilotoole/sq/libsq/core/errz"
"github.com/neilotoole/sq/libsq/core/execz"
"github.com/neilotoole/sq/libsq/source"
)
// REVISIT: DumpCatalogCmd and DumpClusterCmd could be methods on driver.SQLDriver.
// TODO: Unify DumpCatalogCmd and DumpClusterCmd, as they're almost identical, probably
// in the form:
// DumpCatalogCmd(src *source.Source, all bool) (cmd []string, err error).
// ToolParams are parameters for postgres tools such as pg_dump and pg_restore.
//
// - https://www.postgresql.org/docs/9.6/app-pgdump.html
// - https://www.postgresql.org/docs/9.6/app-pgrestore.html.
// - https://www.postgresql.org/docs/9.6/app-pg-dumpall.html
// - https://cloud.google.com/sql/docs/postgres/import-export/import-export-dmp
//
// Not every flag is applicable to all tools.
type ToolParams struct {
// File is the path to the file.
File string
// Verbose indicates verbose output (progress).
Verbose bool
// NoOwner won't output commands to set ownership of objects; the source's
// connection user will own all objects. This also sets the --no-acl flag.
// Maybe NoOwner should be named "no security" or similar?
NoOwner bool
// LongFlags indicates whether to use long flags, e.g. --no-owner instead
// of -O.
LongFlags bool
}
func (p *ToolParams) flag(name string) string {
if p.LongFlags {
return flagsLong[name]
}
return flagsShort[name]
}
// DumpCatalogCmd returns the shell command to execute pg_dump for src.
// Example output:
//
// pg_dump -Fc -d postgres://alice:vNgR6R@db.acme.com:5432/sales sales.dump
//
// Reference:
//
// - https://www.postgresql.org/docs/9.6/app-pgdump.html
// - https://www.postgresql.org/docs/9.6/app-pgrestore.html
//
// See also: [RestoreCatalogCmd].
func DumpCatalogCmd(src *source.Source, p *ToolParams) (*execz.Cmd, error) {
// - https://www.postgresql.org/docs/9.6/app-pgdump.html
// - https://cloud.google.com/sql/docs/postgres/import-export/import-export-dmp
// - https://gist.github.com/vielhuber/96eefdb3aff327bdf8230d753aaee1e1
cfg, err := getPoolConfig(src, true)
if err != nil {
return nil, err
}
cmd := &execz.Cmd{Name: "pg_dump"}
if p.Verbose {
cmd.ProgressFromStderr = true
cmd.Args = append(cmd.Args, p.flag(flagVerbose))
}
cmd.Args = append(cmd.Args, p.flag(flagClean), p.flag(flagIfExists)) // TODO: should be optional
cmd.Args = append(cmd.Args, p.flag(flagFormatCustomArchive))
if p.NoOwner {
// You might expect we'd add --no-owner, but if we're outputting a custom
// archive (-Fc), then --no-owner is the default. From the pg_dump docs:
//
// This option is ignored when emitting an archive (non-text) output file.
// For the archive formats, you can specify the option when you call pg_restore.
//
// If we ultimately allow non-archive formats, then we'll need to add
// special handling for --no-owner.
cmd.Args = append(cmd.Args, p.flag(flagNoACL))
}
cmd.Args = append(cmd.Args, p.flag(flagDBName), cfg.ConnString())
if p.File != "" {
cmd.UsesOutputFile = p.File
cmd.Args = append(cmd.Args, p.flag(flagFile), p.File)
}
return cmd, nil
}
// RestoreCatalogCmd returns the shell command to restore a pg catalog (db) from
// a dump produced by pg_dump ([DumpClusterCmd]). Example command:
//
// pg_restore -d postgres://alice:vNgR6R@db.acme.com:5432/sales sales.dump
//
// Reference:
//
// - https://www.postgresql.org/docs/9.6/app-pgrestore.html
// - https://www.postgresql.org/docs/9.6/app-pgdump.html
//
// See also: [DumpCatalogCmd].
func RestoreCatalogCmd(src *source.Source, p *ToolParams) (*execz.Cmd, error) {
// - https://cloud.google.com/sql/docs/postgres/import-export/import-export-dmp
// - https://gist.github.com/vielhuber/96eefdb3aff327bdf8230d753aaee1e1
cfg, err := getPoolConfig(src, true)
if err != nil {
return nil, err
}
cmd := &execz.Cmd{Name: "pg_restore", CmdDirPath: true}
if p.Verbose {
cmd.ProgressFromStderr = true
cmd.Args = append(cmd.Args, p.flag(flagVerbose))
}
if p.NoOwner {
// NoOwner sets both --no-owner and --no-acl. Maybe these should
// be separate options.
cmd.Args = append(cmd.Args, p.flag(flagNoACL), p.flag(flagNoOwner)) // -O is --no-owner
}
cmd.Args = append(cmd.Args,
p.flag(flagClean),
p.flag(flagIfExists),
p.flag(flagCreate),
p.flag(flagDBName),
cfg.ConnString(),
)
if p.File != "" {
cmd.UsesOutputFile = p.File
cmd.Args = append(cmd.Args, p.File)
}
return cmd, nil
}
// DumpClusterCmd returns the shell command to execute pg_dumpall for src.
// Example output (components concatenated with space):
//
// PGPASSWORD=vNgR6R pg_dumpall -w -l sales -d postgres://alice:vNgR6R@db.acme.com:5432/sales -f cluster.dump
//
// Note that the dump produced by pg_dumpall is executed by psql, not pg_restore.
//
// - https://www.postgresql.org/docs/9.6/app-pg-dumpall.html
// - https://www.postgresql.org/docs/9.6/app-psql.html
// - https://www.postgresql.org/docs/9.6/app-pgdump.html
// - https://www.postgresql.org/docs/9.6/app-pgrestore.html
// - https://cloud.google.com/sql/docs/postgres/import-export/import-export-dmp
//
// See also: [RestoreClusterCmd].
func DumpClusterCmd(src *source.Source, p *ToolParams) (*execz.Cmd, error) {
// - https://www.postgresql.org/docs/9.6/app-pg-dumpall.html
// - https://cloud.google.com/sql/docs/postgres/import-export/import-export-dmp
cfg, err := getPoolConfig(src, true)
if err != nil {
return nil, err
}
cmd := &execz.Cmd{
Name: "pg_dumpall",
CmdDirPath: true,
Env: []string{"PGPASSWORD=" + cfg.ConnConfig.Password},
}
// FIXME: need mechanism to indicate that env contains password
if p.Verbose {
cmd.ProgressFromStderr = true
cmd.Args = append(cmd.Args, p.flag(flagVerbose))
}
cmd.Args = append(cmd.Args, p.flag(flagClean), p.flag(flagIfExists)) // TODO: should be optional
if p.NoOwner {
// NoOwner sets both --no-owner and --no-acl. Maybe these should
// be separate options.
cmd.Args = append(cmd.Args, p.flag(flagNoACL), p.flag(flagNoOwner))
}
cmd.Args = append(cmd.Args,
p.flag(flagNoPassword),
p.flag(flagDatabase), cfg.ConnConfig.Database,
p.flag(flagDBName), cfg.ConnString(),
)
if p.File != "" {
cmd.Args = append(cmd.Args, p.flag(flagFile), p.File)
}
return cmd, nil
}
// RestoreClusterCmd returns the shell command to restore a pg cluster from a
// dump produced by pg_dumpall (DumpClusterCmd). Note that the dump produced
// by pg_dumpall is executed by psql, not pg_restore. Example command:
//
// psql -d postgres://alice:vNgR6R@db.acme.com:5432/sales -f sales.dump
//
// Reference:
//
// - https://www.postgresql.org/docs/9.6/app-pg-dumpall.html
// - https://www.postgresql.org/docs/9.6/app-psql.html
// - https://www.postgresql.org/docs/9.6/app-pgdump.html
// - https://www.postgresql.org/docs/9.6/app-pgrestore.html
// - https://cloud.google.com/sql/docs/postgres/import-export/import-export-dmp
//
// See also: [DumpClusterCmd].
func RestoreClusterCmd(src *source.Source, p *ToolParams) (*execz.Cmd, error) {
// - https://gist.github.com/vielhuber/96eefdb3aff327bdf8230d753aaee1e1
cfg, err := getPoolConfig(src, true)
if err != nil {
return nil, err
}
cmd := &execz.Cmd{Name: "psql", CmdDirPath: true}
if p.Verbose {
cmd.ProgressFromStderr = true
cmd.Args = append(cmd.Args, p.flag(flagVerbose))
}
cmd.Args = append(cmd.Args, p.flag(flagDBName), cfg.ConnString())
if p.File != "" {
cmd.Args = append(cmd.Args, p.flag(flagFile), p.File)
}
return cmd, nil
}
type ExecToolParams struct {
// ScriptFile is the path to the script file.
// Only one of ScriptFile or CmdString will be set.
ScriptFile string
// CmdString is the literal SQL command string.
CmdString string
// Verbose indicates verbose output (progress).
// Only one of ScriptFile or CmdString will be set.
Verbose bool
// LongFlags indicates whether to use long flags, e.g. --file instead of -f.
LongFlags bool
}
func (p *ExecToolParams) flag(name string) string {
if p.LongFlags {
return flagsLong[name]
}
return flagsShort[name]
}
// ExecCmd returns the shell command to execute psql with a script file
// or command string. Example command:
//
// psql -d postgres://alice:vNgR6R@db.acme.com:5432/sales -f query.sql
//
// See: https://www.postgresql.org/docs/9.6/app-psql.html.
func ExecCmd(src *source.Source, p *ExecToolParams) (*execz.Cmd, error) {
cfg, err := getPoolConfig(src, true)
if err != nil {
return nil, err
}
cmd := &execz.Cmd{Name: "psql"}
if !p.Verbose {
cmd.Args = append(cmd.Args, p.flag(flagQuiet))
}
cmd.Args = append(cmd.Args, p.flag(flagDBName), cfg.ConnString())
if p.ScriptFile != "" {
cmd.Args = append(cmd.Args, p.flag(flagFile), p.ScriptFile)
}
if p.CmdString != "" {
if p.ScriptFile != "" {
return nil, errz.Errorf("only one of --file or --command may be set")
}
cmd.Args = append(cmd.Args, p.flag(flagCommand), p.CmdString)
}
return cmd, nil
}
// flags for pg_dump and pg_restore programs.
const (
flagNoOwner = "--no-owner"
flagVerbose = "--verbose"
flagQuiet = "--quiet"
flagNoACL = "--no-acl"
flagCreate = "--create"
flagDBName = "--dbname"
flagDatabase = "--database"
flagFormatCustomArchive = "--format=custom"
flagIfExists = "--if-exists"
flagClean = "--clean"
flagNoPassword = "--no-password"
flagFile = "--file"
flagCommand = "--command"
)
var flagsLong = map[string]string{
flagNoOwner: flagNoOwner,
flagVerbose: flagVerbose,
flagQuiet: flagQuiet,
flagNoACL: flagNoACL,
flagCreate: flagCreate,
flagDBName: flagDBName,
flagIfExists: flagIfExists,
flagFormatCustomArchive: flagFormatCustomArchive,
flagClean: flagClean,
flagNoPassword: flagNoPassword,
flagDatabase: flagDatabase,
flagFile: flagFile,
flagCommand: flagCommand,
}
var flagsShort = map[string]string{
flagNoOwner: "-O",
flagVerbose: "-v",
flagQuiet: "-q",
flagNoACL: "-x",
flagCreate: "-C",
flagClean: "-c",
flagDBName: "-d",
flagFormatCustomArchive: "-Fc",
flagIfExists: flagIfExists,
flagNoPassword: "-w",
flagDatabase: "-l",
flagFile: "-f",
flagCommand: "-c",
}