Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/squix/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ func (a *App) Run() {
a.handleExplain()
case "example":
a.handleExample()
case "export":
a.handleExport()
case "help":
a.handleHelp()
case "__complete":
Expand Down
15 changes: 15 additions & 0 deletions cmd/squix/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,20 @@ func getCompletions(args []string, cfg *config.Config) []string {
return []string{"table", "view"}
}
return []string{"table", "view"}
case "export":
for i, arg := range args {
if arg == "--format" && i == len(args)-1 {
return []string{"custom", "plain", "tar"}
}
}
if len(args) == 1 {
return []string{"backup", "schema"}
}
result := []string{"--format"}
if args[1] == "schema" {
result = append(result, "--table")
}
return result
case "edit", "delete", "rm", "remove":
return getCurrentConnectionQueries(cfg)
case "--connection", "-c":
Expand Down Expand Up @@ -136,6 +150,7 @@ func getSubcommands() []string {
"config",
"explain",
"example",
"export",
"help",
}
}
Expand Down
94 changes: 94 additions & 0 deletions cmd/squix/export.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package main

import (
"fmt"
"os"
"path/filepath"

"github.com/eduardofuncao/squix/internal/backup"
"github.com/eduardofuncao/squix/internal/styles"
)

func (a *App) handleExport() {
if len(os.Args) < 3 {
printError("usage: squix export [backup|schema]")
}

switch os.Args[2] {
case "backup":
a.runExport(os.Args[3:], false, "Backup")
case "schema":
a.runExport(os.Args[3:], true, "Schema")
default:
printError("usage: squix export [backup|schema]")
}
}

func (a *App) runExport(args []string, schemaOnly bool, label string) {
var path string
var format string
var tables []string

for i := 0; i < len(args); i++ {
arg := args[i]
switch arg {
case "--format":
if i+1 < len(args) {
i++
format = args[i]
}
case "--table":
if i+1 < len(args) {
i++
tables = append(tables, args[i])
}
default:
if path == "" && arg[0] != '-' {
path = arg
}
}
}

if !schemaOnly && len(tables) > 0 {
printError("--table is only valid for export schema")
}

if a.config.CurrentConnection == "" {
printError("No active connection. Use 'squix switch <connection>' or 'squix init' first")
}

conn := a.config.Connections[a.config.CurrentConnection]
dbType := conn.DBType
connString := os.ExpandEnv(conn.ConnString)

dumper, err := backup.CreateDumper(dbType)
if err != nil {
printError("%v", err)
}

fileExt := filepath.Ext(path)
if schemaOnly && fileExt == "" && format == "" {
format = "plain"
}

formatName, err := backup.ResolveFormat(fileExt, format, dumper)
if err != nil {
printError("%v", err)
}

spec := dumper.Formats()[formatName]
outPath := backup.ResolvePath(path, a.config.CurrentConnection, spec.Ext)

opts := backup.DumpOptions{
Format: formatName,
OutPath: outPath,
SchemaOnly: schemaOnly,
Tables: tables,
}

if err := dumper.Dump(connString, opts); err != nil {
printError("%v", err)
}

fmt.Println(styles.Success.Render(fmt.Sprintf("✓ %s written to %s", label, outPath)))
}
59 changes: 59 additions & 0 deletions cmd/squix/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ func (a *App) PrintGeneralHelp() {
"Create a sample SQLite database to try squix",
),
)
fmt.Println(
" export " + styles.Faint.Render(
"Export a native database backup",
),
)
fmt.Println(
" help " + styles.Faint.Render(
"Show help for squix or a specific command",
Expand Down Expand Up @@ -635,6 +640,60 @@ func (a *App) PrintCommandHelp() {
fmt.Println(" squix example my.db # create ./my.db")
fmt.Println(" squix example --force # recreate ./example.db")

case "export":
section("Command: export")
fmt.Println(
styles.Faint.Render(
"Export a native database backup or schema-only dump using the engine's own dump tool.",
),
)
fmt.Println()
section("Usage")
fmt.Println(" squix export backup [path] [--format <fmt>]")
fmt.Println(" squix export schema [path] [--table <name> ...] [--format <fmt>]")
fmt.Println()
section("Description")
fmt.Println(
" - Currently supports Postgres only (via pg_dump); other engines error.",
)
fmt.Println(
" - export backup dumps full data + schema; default format custom.",
)
fmt.Println(
" - export schema dumps schema only (DDL, no data); default format plain.",
)
fmt.Println(
" - --table narrows a schema export to specific tables (repeatable);",
)
fmt.Println(
" it is only valid with export schema.",
)
fmt.Println(
" - Format is inferred from the file extension, or set explicitly with",
)
fmt.Println(
" --format. Setting both is an error.",
)
fmt.Println(
" - If path is a directory, writes '<dbName>_<timestamp>.<ext>' inside it.",
)
fmt.Println(
" - If path is omitted, writes to the current directory.",
)
fmt.Println()
section("Postgres formats")
fmt.Println(" custom " + styles.Faint.Render(".dump, -Fc (backup default)"))
fmt.Println(" plain " + styles.Faint.Render(".sql, -Fp (schema default)"))
fmt.Println(" tar " + styles.Faint.Render(".tar, -Ft"))
fmt.Println()
section("Examples")
fmt.Println(" squix export backup")
fmt.Println(" squix export backup ./backups")
fmt.Println(" squix export backup dump --format tar")
fmt.Println(" squix export schema")
fmt.Println(" squix export schema --table users --table orders")
fmt.Println(" squix export schema out.dump --format custom")

case "info":
section("Command: info")
fmt.Println(
Expand Down
98 changes: 98 additions & 0 deletions internal/backup/backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package backup

import (
"fmt"
"os"
"path/filepath"
"time"
)

// FormatSpec describes how a dump format maps to a dumper flag and file extension.
type FormatSpec struct {
Flag string
Ext string
}

// DumpOptions configures a native database dump.
type DumpOptions struct {
Format string
OutPath string
SchemaOnly bool
Tables []string
}

// Dumper performs a native, engine-specific database dump.
type Dumper interface {
DefaultFormat() string
Formats() map[string]FormatSpec
Dump(connString string, opts DumpOptions) error
}

// ResolveFormat determines the dump format from a file extension and/or an
// explicit --format flag. Exactly one of fileExt/formatFlag may be set;
// setting both is an error.
func ResolveFormat(fileExt, formatFlag string, d Dumper) (string, error) {
extName := ""
if fileExt != "" {
ext := fileExt
if ext[0] == '.' {
ext = ext[1:]
}
for name, spec := range d.Formats() {
if spec.Ext == ext {
extName = name
break
}
}
if extName == "" {
return "", fmt.Errorf("unknown file extension %q for backup format", fileExt)
}
}

if extName != "" && formatFlag != "" {
return "", fmt.Errorf("format declared twice; use the filename extension or --format, not both")
}

if extName != "" {
return extName, nil
}

if formatFlag != "" {
if _, ok := d.Formats()[formatFlag]; !ok {
names := make([]string, 0, len(d.Formats()))
for name := range d.Formats() {
names = append(names, name)
}
return "", fmt.Errorf("unknown format %q; valid formats: %v", formatFlag, names)
}
return formatFlag, nil
}

return d.DefaultFormat(), nil
}

// ResolvePath determines the output file path for a dump given a user-supplied
// path (which may be empty, a directory, or a file with/without extension),
// the database name, and the target file extension.
func ResolvePath(path, dbName, ext string) string {
timestamp := time.Now().Format("20060102-150405")
filename := fmt.Sprintf("%s_%s.%s", dbName, timestamp, ext)

if path == "" {
cwd, err := os.Getwd()
if err != nil {
cwd = "."
}
return filepath.Join(cwd, filename)
}

if info, err := os.Stat(path); err == nil && info.IsDir() {
return filepath.Join(path, filename)
}

if filepath.Ext(path) == "" {
return path + "." + ext
}

return path
}
Loading
Loading