Skip to content
Merged
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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,12 @@ echo "CREATE TABLE bar (id varchar(255), message TEXT NOT NULL);" > schema/bar.s
Apply the schema to a fresh database. [The connection string spec can be found here](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING).
Setting the `PGPASSWORD` env var will override any password set in the connection string and is recommended.
```bash
pg-schema-diff apply --from-dsn "postgres://postgres:postgres@localhost:5432/postgres" --to-dir schema
pg-schema-diff apply --from-dsn "postgres://postgres:postgres@localhost:5432/postgres" --to-dir schema
```

Alternatively, if you have an existing database, you can dump its schema to use as a starting point:
```bash
mkdir -p schema && pg-schema-diff dump --dsn "postgres://postgres:postgres@localhost:5432/postgres" > schema/schema.sql
```

## 2. Updating schema
Expand Down
105 changes: 105 additions & 0 deletions cmd/pg-schema-diff/dump_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package main

import (
"context"
"database/sql"
"fmt"
"strings"

"github.com/jackc/pgx/v4"
"github.com/spf13/cobra"
"github.com/stripe/pg-schema-diff/pkg/diff"
"github.com/stripe/pg-schema-diff/pkg/log"
"github.com/stripe/pg-schema-diff/pkg/tempdb"
)

func buildDumpCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "dump",
Short: "Dump the schema of a database as SQL DDL statements (effectively pg_dump)",
}

connFlags := createConnectionFlags(cmd, "", "The database to dump")

var includeSchemas []string
var excludeSchemas []string
cmd.Flags().StringArrayVar(&includeSchemas, "include-schema", nil, "Include the specified schema in the dump")
cmd.Flags().StringArrayVar(&excludeSchemas, "exclude-schema", nil, "Exclude the specified schema from the dump")

cmd.RunE = func(cmd *cobra.Command, args []string) error {
connConfig, err := parseConnectionFlags(connFlags)
if err != nil {
return err
}

cmd.SilenceUsage = true

plan, err := generateDump(cmd.Context(), generateDumpParams{
connConfig: connConfig,
includeSchemas: includeSchemas,
excludeSchemas: excludeSchemas,
})
if err != nil {
return err
}

cmdPrintln(cmd, dumpToSql(plan))
return nil
}

return cmd
}

type generateDumpParams struct {
connConfig *pgx.ConnConfig
includeSchemas []string
excludeSchemas []string
}

func generateDump(ctx context.Context, params generateDumpParams) (diff.Plan, error) {
connPool, err := openDbWithPgxConfig(params.connConfig)
if err != nil {
return diff.Plan{}, fmt.Errorf("opening database connection: %w", err)
}
defer connPool.Close()
connPool.SetMaxOpenConns(defaultMaxConnections)

tempDbFactory, err := tempdb.NewOnInstanceFactory(ctx, func(ctx context.Context, dbName string) (*sql.DB, error) {
cfg := params.connConfig.Copy()
cfg.Database = dbName
return openDbWithPgxConfig(cfg)
}, tempdb.WithRootDatabase(params.connConfig.Database))
if err != nil {
return diff.Plan{}, fmt.Errorf("creating temp db factory: %w", err)
}
defer func() {
if err := tempDbFactory.Close(); err != nil {
log.SimpleLogger().Errorf("error shutting down temp db factory: %v", err)
}
}()

plan, err := diff.Generate(ctx, diff.DDLSchemaSource([]string{}), diff.DBSchemaSource(connPool),
diff.WithTempDbFactory(tempDbFactory),
diff.WithIncludeSchemas(params.includeSchemas...),
diff.WithExcludeSchemas(params.excludeSchemas...),
diff.WithDoNotValidatePlan(),
diff.WithNoConcurrentIndexOps(),
)
if err != nil {
return diff.Plan{}, fmt.Errorf("generating plan: %w", err)
}
return plan, nil
}

// dumpToSql converts the plan to clean SQL without metadata
func dumpToSql(plan diff.Plan) string {
sb := strings.Builder{}
for i, stmt := range plan.Statements {
sb.WriteString(stmt.DDL)
sb.WriteString(";")
if i < len(plan.Statements)-1 {
sb.WriteString("\n\n")
}
}
return sb.String()
}
41 changes: 41 additions & 0 deletions cmd/pg-schema-diff/dump_cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package main

func (suite *cmdTestSuite) TestDumpCmd() {
type testCase struct {
name string
args []string
dynamicArgs []dArgGenerator

// outputContains is a list of substrings that are expected to be contained in the stdout output of the command.
outputContains []string
// expectErrContains is a list of substrings that are expected to be contained in the error returned by
// cmd.RunE. This is DISTINCT from stdErr.
expectErrContains []string
}

for _, tc := range []testCase{
{
name: "dump database with table",
dynamicArgs: []dArgGenerator{
tempDsnDArg(suite.pgEngine, "dsn", []string{
"CREATE TABLE foobar(id INT PRIMARY KEY, name TEXT NOT NULL)",
}),
},
outputContains: []string{
"CREATE TABLE",
"foobar",
"id",
"name",
},
},
} {
suite.Run(tc.name, func() {
suite.runCmdWithAssertions(runCmdWithAssertionsParams{
args: append([]string{"dump"}, tc.args...),
dynamicArgs: tc.dynamicArgs,
outputContains: tc.outputContains,
expectErrContains: tc.expectErrContains,
})
})
}
}
1 change: 1 addition & 0 deletions cmd/pg-schema-diff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func buildRootCmd() *cobra.Command {
}
rootCmd.AddCommand(buildPlanCmd())
rootCmd.AddCommand(buildApplyCmd())
rootCmd.AddCommand(buildDumpCmd())
rootCmd.AddCommand(buildVersionCmd())
return rootCmd
}
Expand Down