From bec25607a6bf353076e5a22a4bdd795b613b0454 Mon Sep 17 00:00:00 2001 From: DeprecatedLuar Date: Wed, 22 Jul 2026 13:46:19 -0300 Subject: [PATCH 1/2] feat: add native Postgres backup export via `squix export --backup` Add internal/backup/ (Dumper interface, ResolveFormat/ResolvePath helpers, factory, postgresDumper wrapping pg_dump) and wire `squix export [path] --backup [--format ]` into cmd/squix/export.go, app.go, help.go, and completion.go. Postgres-only this phase; other engines error clearly. Verified end-to-end against a live Postgres container (default/subdir/plain exports, pg_restore --list, format-conflict and missing-engine/missing-binary error paths) plus unit tests for ResolveFormat/ResolvePath. --- cmd/squix/app.go | 2 + cmd/squix/completion.go | 1 + cmd/squix/export.go | 65 ++++++++++++++++++++++++ cmd/squix/help.go | 44 +++++++++++++++++ internal/backup/backup.go | 90 ++++++++++++++++++++++++++++++++++ internal/backup/backup_test.go | 88 +++++++++++++++++++++++++++++++++ internal/backup/factory.go | 13 +++++ internal/backup/postgres.go | 64 ++++++++++++++++++++++++ 8 files changed, 367 insertions(+) create mode 100644 cmd/squix/export.go create mode 100644 internal/backup/backup.go create mode 100644 internal/backup/backup_test.go create mode 100644 internal/backup/factory.go create mode 100644 internal/backup/postgres.go diff --git a/cmd/squix/app.go b/cmd/squix/app.go index d6ba36c..c2fb3b2 100644 --- a/cmd/squix/app.go +++ b/cmd/squix/app.go @@ -72,6 +72,8 @@ func (a *App) Run() { a.handleExplain() case "example": a.handleExample() + case "export": + a.handleExport() case "help": a.handleHelp() case "__complete": diff --git a/cmd/squix/completion.go b/cmd/squix/completion.go index e0fb0b9..bbc8580 100644 --- a/cmd/squix/completion.go +++ b/cmd/squix/completion.go @@ -136,6 +136,7 @@ func getSubcommands() []string { "config", "explain", "example", + "export", "help", } } diff --git a/cmd/squix/export.go b/cmd/squix/export.go new file mode 100644 index 0000000..35fd6c9 --- /dev/null +++ b/cmd/squix/export.go @@ -0,0 +1,65 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/eduardofuncao/squix/internal/backup" + "github.com/eduardofuncao/squix/internal/styles" +) + +func (a *App) handleExport() { + var path string + var doBackup bool + var format string + + for i := 2; i < len(os.Args); i++ { + arg := os.Args[i] + switch arg { + case "--backup": + doBackup = true + case "--format": + if i+1 < len(os.Args) { + i++ + format = os.Args[i] + } + default: + if path == "" && arg[0] != '-' { + path = arg + } + } + } + + if !doBackup { + printError("only --backup is supported currently") + } + + if a.config.CurrentConnection == "" { + printError("No active connection. Use 'squix switch ' 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) + 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) + + if err := dumper.Dump(connString, formatName, outPath); err != nil { + printError("%v", err) + } + + fmt.Println(styles.Success.Render(fmt.Sprintf("✓ Backup written to %s", outPath))) +} diff --git a/cmd/squix/help.go b/cmd/squix/help.go index f054e3a..f2a25a4 100644 --- a/cmd/squix/help.go +++ b/cmd/squix/help.go @@ -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", @@ -635,6 +640,45 @@ 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 using the engine's own dump tool.", + ), + ) + fmt.Println() + section("Usage") + fmt.Println(" squix export [path] --backup [--format ]") + fmt.Println() + section("Description") + fmt.Println( + " - Currently supports Postgres only (via pg_dump); other engines error.", + ) + 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 '_.' inside it.", + ) + fmt.Println( + " - If path is omitted, writes to the current directory.", + ) + fmt.Println() + section("Postgres formats") + fmt.Println(" custom (default) " + styles.Faint.Render(".dump, -Fc")) + fmt.Println(" plain " + styles.Faint.Render(".sql, -Fp")) + fmt.Println(" tar " + styles.Faint.Render(".tar, -Ft")) + fmt.Println() + section("Examples") + fmt.Println(" squix export --backup") + fmt.Println(" squix export ./backups --backup") + fmt.Println(" squix export dump.sql --backup") + fmt.Println(" squix export dump --backup --format tar") + case "info": section("Command: info") fmt.Println( diff --git a/internal/backup/backup.go b/internal/backup/backup.go new file mode 100644 index 0000000..4525f50 --- /dev/null +++ b/internal/backup/backup.go @@ -0,0 +1,90 @@ +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 +} + +// Dumper performs a native, engine-specific database dump. +type Dumper interface { + DefaultFormat() string + Formats() map[string]FormatSpec + Dump(connString, format, outPath string) 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 +} diff --git a/internal/backup/backup_test.go b/internal/backup/backup_test.go new file mode 100644 index 0000000..11b0083 --- /dev/null +++ b/internal/backup/backup_test.go @@ -0,0 +1,88 @@ +package backup + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveFormat(t *testing.T) { + d := newPostgresDumper() + + tests := []struct { + name string + fileExt string + formatFlag string + want string + wantErr bool + }{ + {name: "both set errors", fileExt: ".sql", formatFlag: "custom", wantErr: true}, + {name: "extension only", fileExt: ".sql", want: "plain"}, + {name: "extension only dump", fileExt: ".dump", want: "custom"}, + {name: "unknown extension errors", fileExt: ".txt", wantErr: true}, + {name: "flag only", formatFlag: "tar", want: "tar"}, + {name: "unknown flag errors", formatFlag: "bogus", wantErr: true}, + {name: "neither set uses default", want: "custom"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ResolveFormat(tt.fileExt, tt.formatFlag, d) + if tt.wantErr { + if err == nil { + t.Fatalf("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != tt.want { + t.Fatalf("got %q, want %q", got, tt.want) + } + }) + } +} + +func TestResolvePath(t *testing.T) { + dir := t.TempDir() + + t.Run("existing directory", func(t *testing.T) { + got := ResolvePath(dir, "mydb", "dump") + gotDir := filepath.Dir(got) + if gotDir != dir { + t.Fatalf("got dir %q, want %q", gotDir, dir) + } + if filepath.Ext(got) != ".dump" { + t.Fatalf("expected .dump extension, got %q", got) + } + }) + + t.Run("file with extension used as-is", func(t *testing.T) { + path := filepath.Join(dir, "out.sql") + got := ResolvePath(path, "mydb", "sql") + if got != path { + t.Fatalf("got %q, want %q", got, path) + } + }) + + t.Run("file without extension gets extension appended", func(t *testing.T) { + path := filepath.Join(dir, "out") + got := ResolvePath(path, "mydb", "dump") + want := path + ".dump" + if got != want { + t.Fatalf("got %q, want %q", got, want) + } + }) + + t.Run("empty path uses cwd", func(t *testing.T) { + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + got := ResolvePath("", "mydb", "dump") + if filepath.Dir(got) != cwd { + t.Fatalf("got dir %q, want cwd %q", filepath.Dir(got), cwd) + } + }) +} diff --git a/internal/backup/factory.go b/internal/backup/factory.go new file mode 100644 index 0000000..b5b29df --- /dev/null +++ b/internal/backup/factory.go @@ -0,0 +1,13 @@ +package backup + +import "fmt" + +// CreateDumper returns the Dumper implementation for the given database type. +func CreateDumper(dbType string) (Dumper, error) { + switch dbType { + case "postgres", "postgresql": + return newPostgresDumper(), nil + default: + return nil, fmt.Errorf("native backup not implemented for %s", dbType) + } +} diff --git a/internal/backup/postgres.go b/internal/backup/postgres.go new file mode 100644 index 0000000..7b2c5a6 --- /dev/null +++ b/internal/backup/postgres.go @@ -0,0 +1,64 @@ +package backup + +import ( + "bytes" + "fmt" + "net/url" + "os" + "os/exec" + "strings" +) + +const pgDumpBin = "pg_dump" + +var postgresFormats = map[string]FormatSpec{ + "custom": {Flag: "-Fc", Ext: "dump"}, + "plain": {Flag: "-Fp", Ext: "sql"}, + "tar": {Flag: "-Ft", Ext: "tar"}, +} + +const postgresDefaultFormat = "custom" + +type postgresDumper struct{} + +func newPostgresDumper() *postgresDumper { + return &postgresDumper{} +} + +func (d *postgresDumper) DefaultFormat() string { + return postgresDefaultFormat +} + +func (d *postgresDumper) Formats() map[string]FormatSpec { + return postgresFormats +} + +func (d *postgresDumper) Dump(connString, format, outPath string) error { + if _, err := exec.LookPath(pgDumpBin); err != nil { + return fmt.Errorf("%s not found on PATH: %w", pgDumpBin, err) + } + + spec, ok := postgresFormats[format] + if !ok { + return fmt.Errorf("unknown postgres backup format %q", format) + } + + u, err := url.Parse(connString) + if err != nil { + return fmt.Errorf("invalid connection string: %w", err) + } + + password, _ := u.User.Password() + + cmd := exec.Command(pgDumpBin, spec.Flag, "-f", outPath, connString) + cmd.Env = append(os.Environ(), "PGPASSWORD="+password) + + var stderr bytes.Buffer + cmd.Stderr = &stderr + + if err := cmd.Run(); err != nil { + return fmt.Errorf("pg_dump failed: %w: %s", err, strings.TrimSpace(stderr.String())) + } + + return nil +} From ae89f69ba72b5d935041b921feb35b9ce6021a27 Mon Sep 17 00:00:00 2001 From: DeprecatedLuar Date: Thu, 23 Jul 2026 07:43:00 -0300 Subject: [PATCH 2/2] feat: add `squix export schema` subcommand with --table filtering Restructures `squix export` into `backup` and `schema` subcommands (replacing the old `--backup` flag), and adds pg_dump --schema-only support with optional -t/--table narrowing for schema-only dumps. --- cmd/squix/completion.go | 14 +++++++++ cmd/squix/export.go | 51 ++++++++++++++++++++++++++------- cmd/squix/help.go | 33 +++++++++++++++------ internal/backup/backup.go | 10 ++++++- internal/backup/backup_test.go | 47 ++++++++++++++++++++++++++++++ internal/backup/postgres.go | 25 +++++++++++++--- test/integration/export_test.go | 45 +++++++++++++++++++++++++++++ 7 files changed, 200 insertions(+), 25 deletions(-) create mode 100644 test/integration/export_test.go diff --git a/cmd/squix/completion.go b/cmd/squix/completion.go index bbc8580..034e76d 100644 --- a/cmd/squix/completion.go +++ b/cmd/squix/completion.go @@ -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": diff --git a/cmd/squix/export.go b/cmd/squix/export.go index 35fd6c9..c2cca66 100644 --- a/cmd/squix/export.go +++ b/cmd/squix/export.go @@ -10,19 +10,37 @@ import ( ) 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 doBackup bool var format string + var tables []string - for i := 2; i < len(os.Args); i++ { - arg := os.Args[i] + for i := 0; i < len(args); i++ { + arg := args[i] switch arg { - case "--backup": - doBackup = true case "--format": - if i+1 < len(os.Args) { + if i+1 < len(args) { i++ - format = os.Args[i] + format = args[i] + } + case "--table": + if i+1 < len(args) { + i++ + tables = append(tables, args[i]) } default: if path == "" && arg[0] != '-' { @@ -31,8 +49,8 @@ func (a *App) handleExport() { } } - if !doBackup { - printError("only --backup is supported currently") + if !schemaOnly && len(tables) > 0 { + printError("--table is only valid for export schema") } if a.config.CurrentConnection == "" { @@ -49,6 +67,10 @@ func (a *App) handleExport() { } fileExt := filepath.Ext(path) + if schemaOnly && fileExt == "" && format == "" { + format = "plain" + } + formatName, err := backup.ResolveFormat(fileExt, format, dumper) if err != nil { printError("%v", err) @@ -57,9 +79,16 @@ func (a *App) handleExport() { spec := dumper.Formats()[formatName] outPath := backup.ResolvePath(path, a.config.CurrentConnection, spec.Ext) - if err := dumper.Dump(connString, formatName, outPath); err != nil { + 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("✓ Backup written to %s", outPath))) + fmt.Println(styles.Success.Render(fmt.Sprintf("✓ %s written to %s", label, outPath))) } diff --git a/cmd/squix/help.go b/cmd/squix/help.go index f2a25a4..051a729 100644 --- a/cmd/squix/help.go +++ b/cmd/squix/help.go @@ -644,17 +644,30 @@ func (a *App) PrintCommandHelp() { section("Command: export") fmt.Println( styles.Faint.Render( - "Export a native database backup using the engine's own dump tool.", + "Export a native database backup or schema-only dump using the engine's own dump tool.", ), ) fmt.Println() section("Usage") - fmt.Println(" squix export [path] --backup [--format ]") + fmt.Println(" squix export backup [path] [--format ]") + fmt.Println(" squix export schema [path] [--table ...] [--format ]") 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", ) @@ -669,15 +682,17 @@ func (a *App) PrintCommandHelp() { ) fmt.Println() section("Postgres formats") - fmt.Println(" custom (default) " + styles.Faint.Render(".dump, -Fc")) - fmt.Println(" plain " + styles.Faint.Render(".sql, -Fp")) - fmt.Println(" tar " + styles.Faint.Render(".tar, -Ft")) + 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 ./backups --backup") - fmt.Println(" squix export dump.sql --backup") - fmt.Println(" squix export dump --backup --format tar") + 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") diff --git a/internal/backup/backup.go b/internal/backup/backup.go index 4525f50..08217d3 100644 --- a/internal/backup/backup.go +++ b/internal/backup/backup.go @@ -13,11 +13,19 @@ type FormatSpec struct { 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, format, outPath string) error + Dump(connString string, opts DumpOptions) error } // ResolveFormat determines the dump format from a file extension and/or an diff --git a/internal/backup/backup_test.go b/internal/backup/backup_test.go index 11b0083..1dccf6c 100644 --- a/internal/backup/backup_test.go +++ b/internal/backup/backup_test.go @@ -3,6 +3,7 @@ package backup import ( "os" "path/filepath" + "reflect" "testing" ) @@ -23,6 +24,7 @@ func TestResolveFormat(t *testing.T) { {name: "flag only", formatFlag: "tar", want: "tar"}, {name: "unknown flag errors", formatFlag: "bogus", wantErr: true}, {name: "neither set uses default", want: "custom"}, + {name: "schema default (plain) resolves like any other flag", formatFlag: "plain", want: "plain"}, } for _, tt := range tests { @@ -44,6 +46,51 @@ func TestResolveFormat(t *testing.T) { } } +func TestPgDumpArgs(t *testing.T) { + plain := postgresFormats["plain"] + + tests := []struct { + name string + opts DumpOptions + want []string + }{ + { + name: "plain, no schema-only, no tables", + opts: DumpOptions{Format: "plain", OutPath: "out.sql"}, + want: []string{"-Fp", "-f", "out.sql"}, + }, + { + name: "schema-only, no tables", + opts: DumpOptions{Format: "plain", OutPath: "out.sql", SchemaOnly: true}, + want: []string{"-Fp", "-f", "out.sql", "--schema-only"}, + }, + { + name: "schema-only, one table", + opts: DumpOptions{Format: "plain", OutPath: "out.sql", SchemaOnly: true, Tables: []string{"users"}}, + want: []string{"-Fp", "-f", "out.sql", "--schema-only", "-t", "users"}, + }, + { + name: "schema-only, many tables", + opts: DumpOptions{Format: "plain", OutPath: "out.sql", SchemaOnly: true, Tables: []string{"users", "orders"}}, + want: []string{"-Fp", "-f", "out.sql", "--schema-only", "-t", "users", "-t", "orders"}, + }, + { + name: "tables without schema-only still narrows", + opts: DumpOptions{Format: "plain", OutPath: "out.sql", Tables: []string{"users"}}, + want: []string{"-Fp", "-f", "out.sql", "-t", "users"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := pgDumpArgs(plain, tt.opts) + if !reflect.DeepEqual(got, tt.want) { + t.Fatalf("got %v, want %v", got, tt.want) + } + }) + } +} + func TestResolvePath(t *testing.T) { dir := t.TempDir() diff --git a/internal/backup/postgres.go b/internal/backup/postgres.go index 7b2c5a6..5038b24 100644 --- a/internal/backup/postgres.go +++ b/internal/backup/postgres.go @@ -33,14 +33,30 @@ func (d *postgresDumper) Formats() map[string]FormatSpec { return postgresFormats } -func (d *postgresDumper) Dump(connString, format, outPath string) error { +// pgDumpArgs builds the pg_dump argv (excluding the connection string) for +// the given format spec and dump options. +func pgDumpArgs(spec FormatSpec, opts DumpOptions) []string { + args := []string{spec.Flag, "-f", opts.OutPath} + + if opts.SchemaOnly { + args = append(args, "--schema-only") + } + + for _, table := range opts.Tables { + args = append(args, "-t", table) + } + + return args +} + +func (d *postgresDumper) Dump(connString string, opts DumpOptions) error { if _, err := exec.LookPath(pgDumpBin); err != nil { return fmt.Errorf("%s not found on PATH: %w", pgDumpBin, err) } - spec, ok := postgresFormats[format] + spec, ok := postgresFormats[opts.Format] if !ok { - return fmt.Errorf("unknown postgres backup format %q", format) + return fmt.Errorf("unknown postgres backup format %q", opts.Format) } u, err := url.Parse(connString) @@ -50,7 +66,8 @@ func (d *postgresDumper) Dump(connString, format, outPath string) error { password, _ := u.User.Password() - cmd := exec.Command(pgDumpBin, spec.Flag, "-f", outPath, connString) + args := append(pgDumpArgs(spec, opts), connString) + cmd := exec.Command(pgDumpBin, args...) cmd.Env = append(os.Environ(), "PGPASSWORD="+password) var stderr bytes.Buffer diff --git a/test/integration/export_test.go b/test/integration/export_test.go new file mode 100644 index 0000000..45b76bc --- /dev/null +++ b/test/integration/export_test.go @@ -0,0 +1,45 @@ +package integration + +import ( + "strings" + "testing" +) + +func TestExportUsageWithoutSubcommand(t *testing.T) { + env := Setup(t) + env.SeedDefaults() + + _, stderr, exitCode := env.RunSquix("export") + if exitCode == 0 { + t.Fatalf("expected non-zero exit") + } + if !strings.Contains(stderr, "usage: squix export [backup|schema]") { + t.Errorf("expected usage message:\n%s", stderr) + } +} + +func TestExportUnsupportedEngine(t *testing.T) { + env := Setup(t) + env.SeedDefaults() + + _, stderr, exitCode := env.RunSquix("export", "schema") + if exitCode == 0 { + t.Fatalf("expected non-zero exit") + } + if !strings.Contains(stderr, "native backup not implemented for sqlite") { + t.Errorf("expected unsupported engine error:\n%s", stderr) + } +} + +func TestExportTableFlagUnderBackup(t *testing.T) { + env := Setup(t) + env.SeedDefaults() + + _, stderr, exitCode := env.RunSquix("export", "backup", "--table", "users") + if exitCode == 0 { + t.Fatalf("expected non-zero exit") + } + if !strings.Contains(stderr, "--table is only valid for export schema") { + t.Errorf("expected --table error:\n%s", stderr) + } +}