Skip to content

Commit

Permalink
Add squash support (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
silas authored Oct 31, 2024
1 parent aaed292 commit fd745be
Show file tree
Hide file tree
Showing 19 changed files with 244 additions and 42 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.jimmy.yaml
/.pre-commit-config.yaml
/dist
/migrations
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
# Jimmy

Jimmy is a Google Spanner schema migrations tool.

## Usage

```
Usage:
jimmy [command]
Available Commands:
init Initialize migrations
bootstrap Create initial migration
create Create a new migration
add Add a statement to an existing migration
upgrade Run all schema upgrades
show Show options
help Help about any command
Flags:
-c, --config string configuration file (default ".jimmy.yaml")
-d, --database string set Spanner database ID
--emulator set whether to enable emulator mode (default automatically detected)
-h, --help help for jimmy
-i, --instance string set Spanner instance ID
-p, --project string set Google project ID
```
7 changes: 4 additions & 3 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const (
)

func New() *cobra.Command {
cobra.EnableCommandSorting = false

cmd := &cobra.Command{
Use: constants.AppName,
SilenceUsage: false,
Expand All @@ -24,7 +26,7 @@ func New() *cobra.Command {

cmd.PersistentFlags().StringP(flagConfig, "c", constants.ConfigFile, "configuration file")
cmd.PersistentFlags().BoolP(flagEmulator, "", false, "set whether to enable emulator mode (default automatically detected)")
cmd.PersistentFlags().StringP(flagProject, "p", "", "set Google project")
cmd.PersistentFlags().StringP(flagProject, "p", "", "set Google project ID")
cmd.PersistentFlags().StringP(flagInstance, "i", "", "set Spanner instance ID")
cmd.PersistentFlags().StringP(flagDatabase, "d", "", "set Spanner database ID")

Expand All @@ -33,8 +35,7 @@ func New() *cobra.Command {
cmd.AddCommand(newCreate())
cmd.AddCommand(newAdd())
cmd.AddCommand(newUpgrade())
cmd.AddCommand(newEnvironments())
cmd.AddCommand(newTemplates())
cmd.AddCommand(newShow())

return cmd
}
9 changes: 9 additions & 0 deletions internal/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const (
flagSQL = "sql"
flagTemplate = "template"
flagType = "type"
flagSquash = "squash"
)

func newCreate() *cobra.Command {
Expand All @@ -30,12 +31,18 @@ func newCreate() *cobra.Command {
return err
}

squashID, err := cmd.Flags().GetInt(flagSquash)
if err != nil {
return err
}

m, err := ms.Create(cmd.Context(), migrations.CreateInput{
Name: args[0],
SQL: flags.SQL,
Env: flags.Env,
Template: flags.Template,
Type: flags.Type,
SquashID: squashID,
})
if err != nil {
return err
Expand All @@ -49,5 +56,7 @@ func newCreate() *cobra.Command {

setupMigrationFlags(cmd)

cmd.Flags().IntP(flagSquash, "", 0, "squash ID")

return cmd
}
2 changes: 1 addition & 1 deletion internal/cmd/environments.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func newEnvironments() *cobra.Command {
cmd := &cobra.Command{
Use: "environments",
Short: "Show environments",
Short: "Show environment options",
Aliases: []string{"envs"},
Args: args(),
RunE: func(cmd *cobra.Command, args []string) error {
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
func newInit() *cobra.Command {
cmd := &cobra.Command{
Use: "init",
Short: "Initialize configuration files",
Short: "Initialize migrations",
Args: args(),
RunE: func(cmd *cobra.Command, args []string) error {
ms, err := newMigrations(cmd, false)
Expand Down
19 changes: 19 additions & 0 deletions internal/cmd/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package cmd

import (
"github.com/spf13/cobra"
)

func newShow() *cobra.Command {
cmd := &cobra.Command{
Use: "show",
Short: "Show options",
Args: args(),
}
cmd.CompletionOptions.DisableDefaultCmd = true

cmd.AddCommand(newEnvironments())
cmd.AddCommand(newTemplates())

return cmd
}
2 changes: 1 addition & 1 deletion internal/cmd/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
func newTemplates() *cobra.Command {
cmd := &cobra.Command{
Use: "templates",
Short: "Show templates",
Short: "Show template options",
Args: args(),
RunE: func(cmd *cobra.Command, args []string) error {
outputEnums(
Expand Down
9 changes: 4 additions & 5 deletions internal/migrations/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ func (ms *Migrations) Bootstrap(ctx context.Context) (*Migration, error) {
return nil, err
}

if ms.latestID != 0 {
return nil, errors.New("bootstrap requires no existing migrations")
}

var upgrade []*jimmyv1.Statement

dbAdmin, err := ms.DatabaseAdmin(ctx)
Expand Down Expand Up @@ -51,7 +47,10 @@ func (ms *Migrations) Bootstrap(ctx context.Context) (*Migration, error) {
upgrade = append(upgrade, statement)
}

squashID := int32(ms.latestID)

return ms.create("init", &jimmyv1.Migration{
Upgrade: upgrade,
Upgrade: upgrade,
SquashId: &squashID,
})
}
25 changes: 20 additions & 5 deletions internal/migrations/bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package migrations_test

import (
"slices"
"testing"

"cloud.google.com/go/spanner/admin/database/apiv1/databasepb"
"github.com/stretchr/testify/require"

"github.com/silas/jimmy/internal/constants"
"github.com/silas/jimmy/internal/migrations"
jimmyv1 "github.com/silas/jimmy/internal/pb/jimmy/v1"
)

Expand Down Expand Up @@ -35,9 +37,22 @@ func TestMigrations_Bootstrap(t *testing.T) {
require.Equal(t, 1, m.ID())
require.Equal(t, m.ID(), h.Migrations.LatestID())

data := m.Data()
require.NotNil(t, data)
require.Len(t, data.Upgrade, 1)
require.Contains(t, data.Upgrade[0].Sql, "CREATE TABLE test")
require.Equal(t, jimmyv1.Type_DDL, data.Upgrade[0].Type)
statements := slices.Collect(m.Upgrade())
require.Len(t, statements, 1)
require.Contains(t, statements[0].Sql, "CREATE TABLE test")
require.Equal(t, jimmyv1.Type_DDL, statements[0].Type)

_, err = h.Migrations.Create(h.Ctx, migrations.CreateInput{
Name: "insert",
SQL: `INSERT INTO test (id, update_time) VALUES ("one", CURRENT_TIMESTAMP)`,
})
require.NoError(t, err)

err = h.Migrations.Upgrade(h.Ctx)
require.NoError(t, err)

records, err := h.records()
require.NoError(t, err)
require.Len(t, records, 1)
require.Equal(t, 2, records[0].ID)
}
42 changes: 36 additions & 6 deletions internal/migrations/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type CreateInput struct {
Env jimmyv1.Environment
Template jimmyv1.Template
Type jimmyv1.Type
SquashID int
}

func (ms *Migrations) Create(ctx context.Context, input CreateInput) (*Migration, error) {
Expand All @@ -37,22 +38,41 @@ func (ms *Migrations) Create(ctx context.Context, input CreateInput) (*Migration
}
}

return ms.create(slug, &jimmyv1.Migration{
m := &jimmyv1.Migration{
Upgrade: []*jimmyv1.Statement{statement},
})
}
if input.SquashID > 0 {
sm, err := ms.Get(input.SquashID)
if err != nil {
return nil, err
}

if squashID, found := sm.SquashID(); found {
return nil, fmt.Errorf(
"new squash migration can't reference existing squash migration %d",
squashID,
)
}

squashID := int32(sm.ID())

m.SquashId = &squashID
}

return ms.create(slug, m)
}

func (ms *Migrations) create(slug string, data *jimmyv1.Migration) (*Migration, error) {
ms.latestID++
id := ms.latestID + 1

m := newMigration(
ms,
ms.latestID,
fmt.Sprintf("%05d_%s%s", ms.latestID, slug, constants.FileExt),
id,
fmt.Sprintf("%05d_%s%s", id, slug, constants.FileExt),
data,
)

ms.migrations[ms.latestID] = m
ms.setMigration(m)

err := Marshal(m.Path(), data)
if err != nil {
Expand All @@ -61,3 +81,13 @@ func (ms *Migrations) create(slug string, data *jimmyv1.Migration) (*Migration,

return m, err
}

func (ms *Migrations) setMigration(m *Migration) {
ms.migrations[m.id] = m
ms.latestID = max(ms.latestID, m.id)

squashID, found := m.SquashID()
if found {
ms.squash[squashID] = max(m.id, ms.squash[squashID])
}
}
4 changes: 1 addition & 3 deletions internal/migrations/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,7 @@ func (ms *Migrations) Load(_ context.Context) error {
return err
}

ms.migrations[id] = m

ms.latestID = max(ms.latestID, id)
ms.setMigration(m)
}

return nil
Expand Down
19 changes: 16 additions & 3 deletions internal/migrations/migration.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package migrations

import (
"iter"
"path/filepath"
"strings"

Expand Down Expand Up @@ -69,9 +70,21 @@ func (m *Migration) Summary() string {
return strings.ReplaceAll(m.Slug(), "_", " ")
}

func (m *Migration) Data() *jimmyv1.Migration {
func (m *Migration) SquashID() (int, bool) {
if m != nil && m.data != nil {
return proto.Clone(m.data).(*jimmyv1.Migration)
return int(m.data.GetSquashId()), m.data.SquashId != nil
}
return 0, false
}

func (m *Migration) Upgrade() iter.Seq[*jimmyv1.Statement] {
return func(yield func(*jimmyv1.Statement) bool) {
if m != nil {
for _, s := range m.data.GetUpgrade() {
if !yield(proto.Clone(s).(*jimmyv1.Statement)) {
return
}
}
}
}
return nil
}
2 changes: 2 additions & 0 deletions internal/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type Migrations struct {

emulator bool
migrations map[int]*Migration
squash map[int]int
latestID int

instanceAdmin *instance.InstanceAdminClient
Expand All @@ -35,6 +36,7 @@ func New(path string) *Migrations {

emulator: os.Getenv(constants.EnvEmulatorHost) != "",
migrations: map[int]*Migration{},
squash: map[int]int{},
}
}

Expand Down
Loading

0 comments on commit fd745be

Please sign in to comment.