Track database flavor (vendor and version) in .skeema files

This commit adds a new "flavor" option, which in Skeema v1.0.x is just
informational (no functional impact). `skeema init`, `skeema pull`, and
`skeema add-environment` now automatically populate this option in host-level
.skeema files, to persist the database's vendor (mysql/percona/mariadb) and
major.minor versions. This information may be used in new features beginning in
Skeema v1.1.
This commit is contained in:
Evan Elias 2018-08-01 17:05:56 -04:00
Родитель d5db1f4db0
Коммит 6f660d90c6
24 изменённых файлов: 78 добавлений и 13 удалений

Просмотреть файл

@ -43,7 +43,7 @@ func AddEnvHandler(cfg *mybase.Config) error {
}
hostOptionFile, err := dir.OptionFile()
if err != nil || hostOptionFile == nil {
if err != nil {
return NewExitValue(CodeBadInput, "Unable to read .skeema file for %s: %s", dir, err)
}
@ -80,6 +80,9 @@ func AddEnvHandler(cfg *mybase.Config) error {
} else {
hostOptionFile.SetOptionValue(environment, "port", strconv.Itoa(inst.Port))
}
if flavor := inst.Flavor(); flavor != tengo.FlavorUnknown {
hostOptionFile.SetOptionValue(environment, "flavor", flavor.String())
}
if cfg.OnCLI("user") {
hostOptionFile.SetOptionValue(environment, "user", cfg.Get("user"))
}

Просмотреть файл

@ -123,6 +123,9 @@ func InitHandler(cfg *mybase.Config) error {
if cfg.OnCLI("user") {
hostOptionFile.SetOptionValue(environment, "user", cfg.Get("user"))
}
if flavor := inst.Flavor(); flavor != tengo.FlavorUnknown {
hostOptionFile.SetOptionValue(environment, "flavor", flavor.String())
}
if cfg.OnCLI("ignore-schema") {
hostOptionFile.SetOptionValue(environment, "ignore-schema", cfg.Get("ignore-schema"))
}

Просмотреть файл

@ -69,11 +69,8 @@ func PullHandler(cfg *mybase.Config) error {
if err != nil {
return err
}
optionFile, err := t.Dir.OptionFile()
if err != nil {
if optionFile, err := t.Dir.OptionFile(); err != nil {
log.Warnf("Unable to update character set and/or collation for %s/.skeema: %s", t.Dir, err)
} else if optionFile == nil {
log.Warnf("Unable to update character set and/or collation for %s/.skeema: cannot read file", t.Dir)
} else {
if instCharSet != t.SchemaFromInstance.CharSet {
optionFile.SetOptionValue("", "default-character-set", t.SchemaFromInstance.CharSet)
@ -246,6 +243,25 @@ func findNewSchemas(dir *Dir) error {
return err
}
// Update the instance dir's .skeema option file if the instance's current
// flavor does not match what's in the file
if instFlavor := instance.Flavor(); instFlavor.String() != dir.Config.Get("flavor") {
if optionFile, err := dir.OptionFile(); err != nil {
log.Warnf("Unable to update flavor in %s/.skeema: %s", dir, err)
} else {
if instFlavor == tengo.FlavorUnknown {
optionFile.UnsetOptionValue(dir.section, "flavor")
} else {
optionFile.SetOptionValue(dir.section, "flavor", instFlavor.String())
}
if err := optionFile.Write(true); err != nil {
log.Warnf("Unable to update flavor in %s: %s", optionFile.Path(), err)
} else {
log.Infof("Wrote %s -- updated flavor to %s", optionFile.Path(), instFlavor.String())
}
}
}
subdirHasSchema := make(map[string]bool)
for _, subdir := range subdirs {
// We only want to evaluate subdirs that explicitly define the schema option

Просмотреть файл

@ -30,6 +30,7 @@ func AddGlobalOptions(cmd *mybase.Command) {
cmd.AddOption(mybase.StringOption("ignore-table", 0, "", "Ignore tables that match regex").Hidden())
cmd.AddOption(mybase.StringOption("default-character-set", 0, "", "Schema-level default character set").Hidden())
cmd.AddOption(mybase.StringOption("default-collation", 0, "", "Schema-level default collation").Hidden())
cmd.AddOption(mybase.StringOption("flavor", 0, "", "Database server expressed in format vendor:major.minor, for use in vendor/version specific syntax").Hidden())
// Visible global options
cmd.AddOption(mybase.StringOption("user", 'u', "root", "Username to connect to database host"))

6
dir.go
Просмотреть файл

@ -120,7 +120,7 @@ func (dir *Dir) HasOptionFile() bool {
// .skeema option file in the currently-selected environment section.
func (dir *Dir) HasHost() bool {
optionFile, err := dir.OptionFile()
if err != nil || optionFile == nil {
if err != nil {
return false
}
_, ok := optionFile.OptionValue("host")
@ -131,7 +131,7 @@ func (dir *Dir) HasHost() bool {
// .skeema option file in the currently-selected environment section.
func (dir *Dir) HasSchema() bool {
optionFile, err := dir.OptionFile()
if err != nil || optionFile == nil {
if err != nil {
return false
}
_, ok := optionFile.OptionValue("schema")
@ -633,6 +633,8 @@ func (dir *Dir) TargetTemplate(instance *tengo.Instance) Target {
// the dir's .skeema file, if one exists. The file will be read and parsed; any
// errors in either process will be returned. The section specified by
// dir.section will automatically be selected for use in the file if it exists.
// If there is no option file in this dir, it is considered an error (return is
// nil File and non-nil error).
func (dir *Dir) OptionFile() (*mybase.File, error) {
f := mybase.NewFile(dir.Path, ".skeema")
if err := f.Read(); err != nil {

Просмотреть файл

@ -18,6 +18,7 @@
* [dry-run](#dry-run)
* [exact-match](#exact-match)
* [first-only](#first-only)
* [flavor](#flavor)
* [foreign-key-checks](#foreign-key-checks)
* [host](#host)
* [host-wrapper](#host-wrapper)
@ -330,6 +331,20 @@ Ordinarily, for individual directories that map to multiple instances and/or mul
In a sharded environment, this option can be useful to examine or execute a change only on one shard, before pushing it out on all shards. Alternatively, for more complex control, a similar effect can be achieved by using environment names. For example, you could create an environment called "production-canary" with [host](#host) configured to map to a subset of the instances in the "production" environment.
### flavor
Commands | *all*
--- | :---
**Default** | *empty string*
**Type** | string
**Restrictions** | Should only appear in a .skeema option file that also contains [host](#host)
This option indicates the database server vendor and version corresponding to the first [host](#host) defined in this directory. The value is formatted as "vendor:major.minor", for example "mysql:5.6", "percona:5.7", or "mariadb:10.1".
This option is automatically populated in host-level .skeema files by `skeema init`, `skeema pull`, and `skeema add-environment` beginning in Skeema v1.0.3.
Currently, this option is just informational and has no functional effect whatsoever in Skeema v1.0.x. In future releases, it may be used for purposes such as optionally offloading the [temporary schema operations](faq.md#temporary-schema-usage) to a local Docker container; the [flavor](#flavor) value will then be used to ensure the correct Docker image is used.
### foreign-key-checks
Commands | push

Просмотреть файл

@ -14,7 +14,7 @@ schema to the filesystem, and apply online schema changes by modifying files.`
// Globals overridden by GoReleaser's ldflags
var (
version = "1.0.2-dev"
version = "1.0.3-dev"
commit = "unknown"
date = "unknown"
)

Просмотреть файл

@ -123,18 +123,20 @@ func (s *SkeemaIntegrationSuite) TestAddEnvHandler(t *testing.T) {
origFile := getOptionFile(t, "mydb", cfg)
// valid dir should succeed and add the section to the .skeema file
cfg = s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --host my.staging.db.com --dir mydb staging")
// Intentionally using a low connection timeout here to avoid delaying the
// test with the invalid hostname
cfg = s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --host my.staging.invalid --dir mydb staging --connect-options='timeout=10ms'")
file := getOptionFile(t, "mydb", cfg)
origFile.SetOptionValue("staging", "host", "my.staging.db.com")
origFile.SetOptionValue("staging", "host", "my.staging.invalid")
origFile.SetOptionValue("staging", "port", "3306")
if !origFile.SameContents(file) {
t.Fatalf("File contents of %s do not match expectation", file.Path())
}
// Nonstandard port should work properly; ditto for user option persisting
cfg = s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --host my.ci.db.com -P 3307 -ufoobar --dir mydb ci")
cfg = s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --host my.ci.invalid -P 3307 -ufoobar --dir mydb ci --connect-options='timeout=10ms'")
file = getOptionFile(t, "mydb", cfg)
origFile.SetOptionValue("ci", "host", "my.ci.db.com")
origFile.SetOptionValue("ci", "host", "my.ci.invalid")
origFile.SetOptionValue("ci", "port", "3307")
origFile.SetOptionValue("ci", "user", "foobar")
if !origFile.SameContents(file) {
@ -163,8 +165,10 @@ func (s *SkeemaIntegrationSuite) TestPullHandler(t *testing.T) {
// Revert db back to previous state, and pull again to test the opposite
// behaviors: delete dir for new schema, remove charset/collation from .skeema,
// etc
// etc. Also edit the host .skeema file to remove flavor, to test logic that
// adds/updates flavor on pull.
s.cleanData(t, "setup.sql")
writeFile(t, "mydb/.skeema", strings.Replace(readFile(t, "mydb/.skeema"), "flavor", "#flavor", 1))
cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull")
s.verifyFiles(t, cfg, "../golden/init")

Просмотреть файл

@ -213,9 +213,15 @@ func (s *SkeemaIntegrationSuite) verifyFiles(t *testing.T, cfg *mybase.Config, d
aOptionFile.SetOptionValue(section, "port", forcedValue)
}
}
// Force flavor of a to match the DockerizedInstance's flavor
for _, section := range aOptionFile.SectionsWithOption("flavor") {
aOptionFile.SetOptionValue(section, "flavor", s.d.Flavor().String())
}
if !aOptionFile.SameContents(bOptionFile) {
t.Errorf("File contents do not match between %s and %s", aOptionFile.Path(), bOptionFile.Path())
fmt.Println("Expected:\n", readFile(t, aOptionFile.Path()))
fmt.Println("Actual:\n", readFile(t, bOptionFile.Path()))
}
}

Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

Просмотреть файл

@ -1,5 +1,6 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}
ignore-schema=^archives$
ignore-table=^_

Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

1
testdata/golden-mysql55/ignore/mydb/.skeema поставляемый
Просмотреть файл

@ -1,5 +1,6 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}
ignore-schema=^archives$
ignore-table=^_

1
testdata/golden-mysql55/init/mydb/.skeema поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

1
testdata/golden-mysql55/pull1/mydb/.skeema поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

1
testdata/golden/autoinc/mydb/.skeema поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

1
testdata/golden/ignore/mydb/.skeema поставляемый
Просмотреть файл

@ -1,5 +1,6 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}
ignore-schema=^archives$
ignore-table=^_

1
testdata/golden/init/mydb/.skeema поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

1
testdata/golden/pull1/mydb/.skeema поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}

1
testdata/golden/unsupported/mydb/.skeema поставляемый
Просмотреть файл

@ -1,3 +1,4 @@
[production]
host=127.0.0.1
port={DYNAMICALLY INSERTED BY TEST}
flavor={DYNAMICALLY INSERTED BY TEST}