mirror of
https://github.com/neilotoole/sq.git
synced 2024-12-18 13:41:49 +03:00
99454852f0
- Preliminary work on the (currently hidden) `db` cmds. - Improvements to `--src.schema`
333 lines
10 KiB
Go
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",
|
|
}
|