Skip to content

Commit 34c19af

Browse files
authored
Add new command: create to add new migrations (#815)
This PR adds a new command named `create`. It lets users create new migration files. It has three flags: `--empty` if the user wants to create an empty file and `--name` if the user wants to pass a name instead of being prompted. Similarly to `pull` command it also has a flag called `--json` to output the migration file in json format. I am adding documentation in a follow-up PR.
1 parent 02b1da7 commit 34c19af

17 files changed

+524
-12
lines changed

cli-definition.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,34 @@
3737
"migration-file"
3838
]
3939
},
40+
{
41+
"name": "create",
42+
"short": "Create a new migration interactively",
43+
"use": "create",
44+
"example": "",
45+
"flags": [
46+
{
47+
"name": "empty",
48+
"shorthand": "e",
49+
"description": "Create empty migration file",
50+
"default": "false"
51+
},
52+
{
53+
"name": "json",
54+
"shorthand": "j",
55+
"description": "Output migration file in JSON format instead of YAML",
56+
"default": "false"
57+
},
58+
{
59+
"name": "name",
60+
"shorthand": "n",
61+
"description": "Migration name",
62+
"default": ""
63+
}
64+
],
65+
"subcommands": [],
66+
"args": []
67+
},
4068
{
4169
"name": "init",
4270
"short": "Initialize pgroll in the target database",

cmd/create.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
//nolint:goconst
4+
package cmd
5+
6+
import (
7+
"encoding/json"
8+
"fmt"
9+
"os"
10+
11+
"github.com/pterm/pterm"
12+
"github.com/spf13/cobra"
13+
"github.com/xataio/pgroll/pkg/migrations"
14+
"sigs.k8s.io/yaml"
15+
)
16+
17+
func createCmd() *cobra.Command {
18+
var isEmpty bool
19+
var useJSON bool
20+
var name string
21+
22+
createCmd := &cobra.Command{
23+
Use: "create",
24+
Short: "Create a new migration interactively",
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
if name == "" {
27+
name, _ = pterm.DefaultInteractiveTextInput.
28+
WithDefaultText("Set the name of your migration").
29+
Show()
30+
}
31+
32+
mig := &migrations.Migration{}
33+
addMoreOperations := !isEmpty
34+
35+
for addMoreOperations {
36+
selectedOption, _ := pterm.DefaultInteractiveSelect.
37+
WithDefaultText("Select operation").
38+
WithOptions(migrations.AllNonDeprecatedOperations).
39+
Show()
40+
41+
op, _ := migrations.OperationFromName(migrations.OpName(selectedOption))
42+
mig.Operations = append(mig.Operations, op)
43+
if operation, ok := op.(migrations.Createable); ok {
44+
operation.Create()
45+
}
46+
addMoreOperations, _ = pterm.DefaultInteractiveConfirm.
47+
WithDefaultText("Add more operations").
48+
Show()
49+
}
50+
51+
outputFormat := "yaml"
52+
if useJSON {
53+
outputFormat = "json"
54+
}
55+
migrationFileName := fmt.Sprintf("%s.%s", name, outputFormat)
56+
file, err := os.Create(migrationFileName)
57+
if err != nil {
58+
return fmt.Errorf("failed to create migration file: %w", err)
59+
}
60+
defer file.Close()
61+
62+
switch outputFormat {
63+
case "json":
64+
enc := json.NewEncoder(file)
65+
enc.SetIndent("", " ")
66+
if err := enc.Encode(mig); err != nil {
67+
return fmt.Errorf("failed to encode migration: %w", err)
68+
}
69+
case "yaml":
70+
out, err := yaml.Marshal(mig)
71+
if err != nil {
72+
return fmt.Errorf("failed to encode migration: %w", err)
73+
}
74+
_, err = file.Write(out)
75+
if err != nil {
76+
return fmt.Errorf("failed to write migration: %w", err)
77+
}
78+
default:
79+
return fmt.Errorf("invalid output format: %q", outputFormat)
80+
}
81+
82+
pterm.Success.Println("Migration written to " + migrationFileName)
83+
84+
return nil
85+
},
86+
}
87+
createCmd.Flags().BoolVarP(&isEmpty, "empty", "e", false, "Create empty migration file")
88+
createCmd.Flags().BoolVarP(&useJSON, "json", "j", false, "Output migration file in JSON format instead of YAML")
89+
createCmd.Flags().StringVarP(&name, "name", "n", "", "Migration name")
90+
91+
return createCmd
92+
}

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ func Prepare() *cobra.Command {
101101
rootCmd.AddCommand(analyzeCmd)
102102
rootCmd.AddCommand(initCmd)
103103
rootCmd.AddCommand(statusCmd)
104+
rootCmd.AddCommand(createCmd())
104105
rootCmd.AddCommand(migrateCmd())
105106
rootCmd.AddCommand(pullCmd())
106107
rootCmd.AddCommand(latestCmd())

pkg/migrations/migrations.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,22 @@ type Operation interface {
3434
Validate(ctx context.Context, s *schema.Schema) error
3535
}
3636

37+
// Createable interface must be implemented for all operations
38+
// that can be created using the CLI create command.
39+
//
40+
// The function must prompt users to configure all attributes of an operation.
41+
//
42+
// Example implementation for OpMyOperation that has 3 attributes: table, column and down:
43+
//
44+
// func (o *OpMyOperation) Create() {
45+
// o.Table, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("table").Show()
46+
// o.Column, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("column").Show()
47+
// o.Down, _ = pterm.DefaultInteractiveTextInput.WithDefaultText("down").Show()
48+
// }
49+
type Createable interface {
50+
Create()
51+
}
52+
3753
// IsolatedOperation is an operation that cannot be executed with other operations
3854
// in the same migration.
3955
type IsolatedOperation interface {

pkg/migrations/migrations_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,14 @@ func TestCollectFilesFromDir(t *testing.T) {
103103
})
104104
}
105105
}
106+
107+
func TestAllNonDeprecatedOperationsAreCreateable(t *testing.T) {
108+
for _, opName := range migrations.AllNonDeprecatedOperations {
109+
t.Run(opName, func(t *testing.T) {
110+
op, err := migrations.OperationFromName(migrations.OpName(opName))
111+
assert.NoError(t, err)
112+
_, ok := op.(migrations.Createable)
113+
assert.True(t, ok, "operation %q must have a Create function", opName)
114+
})
115+
}
116+
}

pkg/migrations/op_add_column.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import (
1616
"github.com/xataio/pgroll/pkg/schema"
1717
)
1818

19-
var _ Operation = (*OpAddColumn)(nil)
19+
var (
20+
_ Operation = (*OpAddColumn)(nil)
21+
_ Createable = (*OpAddColumn)(nil)
22+
)
2023

2124
func (o *OpAddColumn) Start(ctx context.Context, l Logger, conn db.DB, latestSchema string, s *schema.Schema) (*schema.Table, error) {
2225
l.LogOperationStart(o)

pkg/migrations/op_alter_column.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
"github.com/xataio/pgroll/pkg/schema"
1414
)
1515

16-
var _ Operation = (*OpAlterColumn)(nil)
16+
var (
17+
_ Operation = (*OpAlterColumn)(nil)
18+
_ Createable = (*OpAlterColumn)(nil)
19+
)
1720

1821
func (o *OpAlterColumn) Start(ctx context.Context, l Logger, conn db.DB, latestSchema string, s *schema.Schema) (*schema.Table, error) {
1922
l.LogOperationStart(o)

pkg/migrations/op_common.go

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,25 @@ const (
3636
OpCreateConstraintName OpName = "create_constraint"
3737
)
3838

39+
// AllNonDeprecatedOperations contains the list of operations
40+
// that are not deprecated. These operations must implement
41+
// migrations.Createable interface.
42+
var AllNonDeprecatedOperations = []string{
43+
string(OpNameCreateTable),
44+
string(OpNameRenameTable),
45+
string(OpNameRenameColumn),
46+
string(OpNameDropTable),
47+
string(OpNameAddColumn),
48+
string(OpNameDropColumn),
49+
string(OpNameAlterColumn),
50+
string(OpNameCreateIndex),
51+
string(OpNameDropIndex),
52+
string(OpNameRenameConstraint),
53+
string(OpNameDropMultiColumnConstraint),
54+
string(OpRawSQLName),
55+
string(OpCreateConstraintName),
56+
}
57+
3958
const (
4059
temporaryPrefix = "_pgroll_new_"
4160
deletedPrefix = "_pgroll_del_"
@@ -155,7 +174,7 @@ func (v *Operations) UnmarshalJSON(data []byte) error {
155174
logBody = v
156175
}
157176

158-
item, err := operationFromName(opName)
177+
item, err := OperationFromName(opName)
159178
if err != nil {
160179
return err
161180
}
@@ -253,7 +272,7 @@ func OperationName(op Operation) OpName {
253272
panic(fmt.Errorf("unknown operation for %T", op))
254273
}
255274

256-
func operationFromName(name OpName) (Operation, error) {
275+
func OperationFromName(name OpName) (Operation, error) {
257276
switch name {
258277
case OpNameCreateTable:
259278
return &OpCreateTable{}, nil

pkg/migrations/op_create_constraint.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
"github.com/xataio/pgroll/pkg/schema"
1414
)
1515

16-
var _ Operation = (*OpCreateConstraint)(nil)
16+
var (
17+
_ Operation = (*OpCreateConstraint)(nil)
18+
_ Createable = (*OpCreateConstraint)(nil)
19+
)
1720

1821
func (o *OpCreateConstraint) Start(ctx context.Context, l Logger, conn db.DB, latestSchema string, s *schema.Schema) (*schema.Table, error) {
1922
l.LogOperationStart(o)

pkg/migrations/op_create_index.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ import (
1313
"github.com/xataio/pgroll/pkg/schema"
1414
)
1515

16-
var _ Operation = (*OpCreateIndex)(nil)
16+
var (
17+
_ Operation = (*OpCreateIndex)(nil)
18+
_ Createable = (*OpCreateIndex)(nil)
19+
)
1720

1821
func (o *OpCreateIndex) Start(ctx context.Context, l Logger, conn db.DB, latestSchema string, s *schema.Schema) (*schema.Table, error) {
1922
l.LogOperationStart(o)

0 commit comments

Comments
 (0)