diff --git a/krab/action_custom.go b/krab/action_custom.go new file mode 100644 index 0000000..b080402 --- /dev/null +++ b/krab/action_custom.go @@ -0,0 +1,43 @@ +package krab + +import ( + "context" + "fmt" + + "github.com/ohkrab/krab/cli" + "github.com/ohkrab/krab/krabdb" + "github.com/ohkrab/krab/tpls" +) + +// ActionCustom keeps data needed to perform this action. +type ActionCustom struct { + Ui cli.UI + Action *Action + Arguments Arguments + Connection krabdb.Connection +} + +func (a *ActionCustom) Help() string { + return fmt.Sprint( + `Usage: krab action namespace name`, + "\n\n", + a.Arguments.Help(), + ` +Performs custom action. +`, + ) +} + +func (a *ActionCustom) Synopsis() string { + return fmt.Sprintf("Action") +} + +// Run in CLI. +func (a *ActionCustom) Run(args []string) int { + return 0 +} + +// Do performs the action. +func (a *ActionCustom) Do(ctx context.Context, db krabdb.DB, tpl *tpls.Templates) error { + return nil +} diff --git a/krab/addr.go b/krab/addr.go index f247c1c..862d960 100644 --- a/krab/addr.go +++ b/krab/addr.go @@ -14,12 +14,17 @@ type Addr struct { } // String returns full reference name including the keyword. -func (a *Addr) String() string { +func (a Addr) String() string { return fmt.Sprintf("%s.%s", a.Keyword, a.OnlyRefNames()) } +// Absolute returns keyword and labels as a single slice. +func (a Addr) Absolute() []string { + return append([]string{a.Keyword}, a.Labels...) +} + // OnlyRefNames returns reference name without the keyword. -func (a *Addr) OnlyRefNames() string { +func (a Addr) OnlyRefNames() string { return strings.Join(a.Labels, ".") } diff --git a/krab/config.go b/krab/config.go index 41649da..7c132d6 100644 --- a/krab/config.go +++ b/krab/config.go @@ -11,14 +11,16 @@ import ( type Config struct { MigrationSets map[string]*MigrationSet Migrations map[string]*Migration + Actions map[string]*Action } // NewConfig returns new configuration that was read from Parser. // Transient attributes are updated with parsed data. func NewConfig(files []*File) (*Config, error) { c := &Config{ - MigrationSets: make(map[string]*MigrationSet), - Migrations: make(map[string]*Migration), + MigrationSets: map[string]*MigrationSet{}, + Migrations: map[string]*Migration{}, + Actions: map[string]*Action{}, } // append files @@ -30,7 +32,7 @@ func NewConfig(files []*File) (*Config, error) { // parse refs for _, set := range c.MigrationSets { - set.Migrations = make([]*Migration, 0) + set.Migrations = []*Migration{} traversals := set.MigrationsExpr.Variables() for _, t := range traversals { @@ -88,6 +90,14 @@ func (c *Config) appendFile(file *File) error { c.MigrationSets[s.RefName] = s } + for _, a := range file.Actions { + if _, found := c.Actions[a.Addr().OnlyRefNames()]; found { + return fmt.Errorf("Action with the name '%s' already exists", a.Addr().OnlyRefNames()) + } + + c.Actions[a.Addr().OnlyRefNames()] = a + } + return nil } diff --git a/krab/file.go b/krab/file.go index b398dcf..bff0a9d 100644 --- a/krab/file.go +++ b/krab/file.go @@ -6,6 +6,7 @@ import "github.com/hashicorp/hcl/v2" type File struct { Migrations []*Migration `hcl:"migration,block"` MigrationSets []*MigrationSet `hcl:"migration_set,block"` + Actions []*Action `hcl:"action,block"` Raw *RawFile } diff --git a/krab/type_action.go b/krab/type_action.go new file mode 100644 index 0000000..0c8431c --- /dev/null +++ b/krab/type_action.go @@ -0,0 +1,29 @@ +package krab + +import ( + "io" +) + +// Action represents custom action to execute. +// +type Action struct { + Namespace string `hcl:"namespace,label"` + RefName string `hcl:"ref_name,label"` + + SQL string `hcl:"sql"` +} + +func (a *Action) Addr() Addr { + return Addr{Keyword: "action", Labels: []string{a.Namespace, a.RefName}} +} + +func (a *Action) Validate() error { + return ErrorCoalesce( + ValidateRefName(a.Namespace), + ValidateRefName(a.RefName), + ) +} + +func (m *Action) ToSQL(w io.StringWriter) { + w.WriteString(m.SQL) +} diff --git a/krab/type_migration.go b/krab/type_migration.go index c6408ef..941e5f6 100644 --- a/krab/type_migration.go +++ b/krab/type_migration.go @@ -1,7 +1,6 @@ package krab import ( - "fmt" "io" "github.com/hashicorp/hcl/v2" @@ -54,7 +53,6 @@ type RawMigrationUpOrDown struct { func (ms *Migration) Validate() error { return ErrorCoalesce( ValidateRefName(ms.RefName), - ValidateStringNonEmpty(fmt.Sprint("`version` attribute in `", ms.RefName, "` migration"), ms.Version), ms.Up.Validate(), ms.Down.Validate(), ) diff --git a/krabcli/app.go b/krabcli/app.go index 4d80002..c55b1d8 100644 --- a/krabcli/app.go +++ b/krabcli/app.go @@ -2,6 +2,7 @@ package krabcli import ( "fmt" + "strings" mcli "github.com/mitchellh/cli" "github.com/ohkrab/krab/cli" @@ -44,6 +45,13 @@ func (a *App) RegisterAll() { return &krab.ActionVersion{Ui: a.Ui} }) + for _, action := range a.Config.Actions { + localAction := action + a.RegisterCmd(strings.Join(action.Addr().Absolute(), " "), func() Command { + return &krab.ActionCustom{Ui: a.Ui, Action: localAction, Connection: a.connection} + }) + } + for _, set := range a.Config.MigrationSets { localSet := set diff --git a/spec/action_custom_test.go b/spec/action_custom_test.go new file mode 100644 index 0000000..55da539 --- /dev/null +++ b/spec/action_custom_test.go @@ -0,0 +1,56 @@ +package spec + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestActionCustom(t *testing.T) { + c := mockCli(mockConfig(` +migration "create_animals" { + version = "v1" + + up { sql = "CREATE TABLE animals(name VARCHAR)" } + down { sql = "DROP TABLE animals" } +} + +migration "create_animals_view" { + version = "v2" + + up { sql = "CREATE MATERIALIZED VIEW anims AS SELECT name FROM animals" } + down { sql = "DROP VIEW anims" } +} + +migration "seed_animals" { + version = "v3" + + up { sql = "INSERT INTO animals(name) VALUES('Elephant'),('Turtle'),('Cat')" } + down { sql = "TRUNACTE animals" } +} + +migration_set "animals" { + migrations = [ + migration.create_animals, + migration.create_animals_view, + migration.seed_animals, + ] +} + +action "view" "refresh" { + sql = "REFRESH VIEW anims" +} +`)) + defer c.Teardown() + + c.AssertSuccessfulRun(t, []string{"migrate", "up", "animals"}) + c.AssertSchemaMigrationTable(t, "public", "v1", "v2", "v3") + + _, vals := c.Query(t, "SELECT * FROM anims") + if assert.Len(t, vals, 0, "No values should be returned") { + c.AssertSuccessfulRun(t, []string{"action", "view", "refresh"}) + _, vals := c.Query(t, "SELECT * FROM anims") + assert.Len(t, vals, 3, "There should be 3 animals after refresh") + } + +}