github.com/github/skeema@v1.2.6/skeema_cmd_test.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/skeema/skeema/fs"
    10  	"github.com/skeema/tengo"
    11  )
    12  
    13  func (s SkeemaIntegrationSuite) TestInitHandler(t *testing.T) {
    14  	s.handleCommand(t, CodeBadConfig, ".", "skeema init") // no host
    15  
    16  	// Invalid environment name
    17  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir mydb -h %s -P %d '[nope]'", s.d.Instance.Host, s.d.Instance.Port)
    18  
    19  	// Specifying a single schema that doesn't exist on the instance
    20  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir mydb -h %s -P %d --schema doesntexist", s.d.Instance.Host, s.d.Instance.Port)
    21  
    22  	// Specifying a single schema that is a system schema
    23  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir mydb -h %s -P %d --schema mysql", s.d.Instance.Host, s.d.Instance.Port)
    24  
    25  	// Successful standard execution. Also confirm user is not persisted to .skeema
    26  	// since not specified on CLI.
    27  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
    28  	s.verifyFiles(t, cfg, "../golden/init")
    29  	if _, setsOption := getOptionFile(t, "mydb", cfg).OptionValue("user"); setsOption {
    30  		t.Error("Did not expect user to be persisted to .skeema, but it was")
    31  	}
    32  
    33  	// Specifying an unreachable host should fail with fatal error
    34  	s.handleCommand(t, CodeFatalError, ".", "skeema init --dir baddb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port-100)
    35  
    36  	// host-wrapper with no output should fail
    37  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir baddb -h xyz --host-wrapper='echo'")
    38  
    39  	// Test successful init with --user specified on CLI, persisting to .skeema
    40  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema init --dir withuser -h %s -P %d --user root", s.d.Instance.Host, s.d.Instance.Port)
    41  	if _, setsOption := getOptionFile(t, "withuser", cfg).OptionValue("user"); !setsOption {
    42  		t.Error("Expected user to be persisted to .skeema, but it was not")
    43  	}
    44  
    45  	// Can't init into a dir with existing option file
    46  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
    47  
    48  	// Can't init off of base dir that already specifies a schema
    49  	s.handleCommand(t, CodeBadConfig, "mydb/product", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
    50  
    51  	// Test successful init for a single schema. Source a SQL file first that,
    52  	// among other things, changes the default charset and collation for the
    53  	// schema in question.
    54  	s.sourceSQL(t, "push1.sql")
    55  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema init --dir combined -h %s -P %d --schema product ", s.d.Instance.Host, s.d.Instance.Port)
    56  	dir, err := fs.ParseDir("combined", cfg)
    57  	if err != nil {
    58  		t.Fatalf("Unexpected error from ParseDir: %s", err)
    59  	}
    60  	for _, option := range []string{"host", "schema", "default-character-set", "default-collation"} {
    61  		if _, setsOption := dir.OptionFile.OptionValue(option); !setsOption {
    62  			t.Errorf("Expected .skeema to contain %s, but it does not", option)
    63  		}
    64  	}
    65  	if subdirs, badSubdirCount, err := dir.Subdirs(); err != nil || badSubdirCount > 0 {
    66  		t.Fatalf("Unexpected error listing subdirs of %s: %s (bad subdir count %d)", dir, err, badSubdirCount)
    67  	} else if len(subdirs) > 0 {
    68  		t.Errorf("Expected %s to have no subdirs, but it has %d", dir, len(subdirs))
    69  	}
    70  	if len(dir.SQLFiles) < 1 {
    71  		t.Errorf("Expected %s to have *.sql files, but it does not", dir)
    72  	}
    73  
    74  	// Test successful init without a --dir. Also test persistence of --connect-options.
    75  	expectDir := fmt.Sprintf("%s:%d", s.d.Instance.Host, s.d.Instance.Port)
    76  	if _, err = os.Stat(expectDir); err == nil {
    77  		t.Fatalf("Expected dir %s to not exist yet, but it does", expectDir)
    78  	}
    79  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema init -h %s -P %d --connect-options='wait_timeout=3'", s.d.Instance.Host, s.d.Instance.Port)
    80  	if dir, err = fs.ParseDir(expectDir, cfg); err != nil {
    81  		t.Fatalf("Unexpected error from ParseDir: %s", err)
    82  	}
    83  	for _, option := range []string{"host", "port", "connect-options"} {
    84  		if _, setsOption := dir.OptionFile.OptionValue(option); !setsOption {
    85  			t.Errorf("Expected host-level .skeema to contain %s, but it does not", option)
    86  		}
    87  	}
    88  	for _, option := range []string{"schema", "default-character-set", "default-collation"} {
    89  		if _, setsOption := dir.OptionFile.OptionValue(option); setsOption {
    90  			t.Errorf("Expected host-level .skeema to NOT contain %s, but it does", option)
    91  		}
    92  	}
    93  
    94  	// Test successful init on a schema that isn't strict-mode compliant
    95  	s.dbExecWithParams(t, "product", "sql_mode=%27NO_ENGINE_SUBSTITUTION%27", "ALTER TABLE posts MODIFY COLUMN created_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00'")
    96  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema init -h %s -P %d --dir nonstrict", s.d.Instance.Host, s.d.Instance.Port)
    97  	if dir, err = fs.ParseDir("nonstrict", cfg); err != nil {
    98  		t.Fatalf("Unexpected error from ParseDir: %s", err)
    99  	}
   100  	value, _ := dir.OptionFile.OptionValue("connect-options")
   101  	if !strings.Contains(value, "innodb_strict_mode=0") {
   102  		t.Errorf("Expected non-strict-compliant schema to use relaxed connect-options; instead found connect-options=%s", value)
   103  	}
   104  
   105  	// init should fail if a parent dir has an invalid .skeema file
   106  	fs.MakeTestDirectory(t, "hasbadoptions")
   107  	fs.WriteTestFile(t, "hasbadoptions/.skeema", "invalid file will not parse")
   108  	s.handleCommand(t, CodeBadConfig, "hasbadoptions", "skeema init -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   109  
   110  	// init should fail if the --dir specifies an existing non-directory file; or
   111  	// if the --dir already contains a subdir matching a schema name; or if the
   112  	// --dir already contains a .sql file and --schema was used to only do 1 level
   113  	fs.WriteTestFile(t, "nondir", "foo bar")
   114  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir nondir -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   115  	fs.WriteTestFile(t, "alreadyexists/product/.skeema", "schema=product\n")
   116  	s.handleCommand(t, CodeFatalError, ".", "skeema init --dir alreadyexists -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   117  	fs.MakeTestDirectory(t, "hassql")
   118  	fs.WriteTestFile(t, "hassql/foo.sql", "foo")
   119  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir hassql --schema product -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   120  }
   121  
   122  func (s SkeemaIntegrationSuite) TestAddEnvHandler(t *testing.T) {
   123  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   124  
   125  	// add-environment should fail on a dir that does not exist
   126  	s.handleCommand(t, CodeBadConfig, ".", "skeema add-environment --host my.staging.db.com --dir does/not/exist staging")
   127  
   128  	// add-environment should fail on a dir that does not already contain a .skeema file
   129  	s.handleCommand(t, CodeBadConfig, ".", "skeema add-environment --host my.staging.db.com staging")
   130  
   131  	// bad environment name should fail
   132  	s.handleCommand(t, CodeBadConfig, ".", "skeema add-environment --host my.staging.db.com --dir mydb '[staging]'")
   133  
   134  	// preexisting environment name should fail
   135  	s.handleCommand(t, CodeBadConfig, ".", "skeema add-environment --host my.staging.db.com --dir mydb production")
   136  
   137  	// non-host-level directory should fail
   138  	s.handleCommand(t, CodeBadConfig, ".", "skeema add-environment --host my.staging.db.com --dir mydb/product staging")
   139  
   140  	// lack of host on CLI should fail
   141  	s.handleCommand(t, CodeBadConfig, ".", "skeema add-environment --dir mydb staging")
   142  
   143  	// None of the above failed commands should have modified any files
   144  	s.verifyFiles(t, cfg, "../golden/init")
   145  	origFile := getOptionFile(t, "mydb", cfg)
   146  
   147  	// valid dir should succeed and add the section to the .skeema file
   148  	// Intentionally using a low connection timeout here to avoid delaying the
   149  	// test with the invalid hostname
   150  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --host my.staging.invalid --dir mydb staging --connect-options='timeout=10ms'")
   151  	file := getOptionFile(t, "mydb", cfg)
   152  	origFile.SetOptionValue("staging", "host", "my.staging.invalid")
   153  	origFile.SetOptionValue("staging", "port", "3306")
   154  	origFile.SetOptionValue("staging", "connect-options", "timeout=10ms")
   155  	if !origFile.SameContents(file) {
   156  		t.Fatalf("File contents of %s do not match expectation", file.Path())
   157  	}
   158  
   159  	// Nonstandard port should work properly; ditto for user option persisting
   160  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --host my.ci.invalid -P 3307 -ufoobar --dir mydb ci  --connect-options='timeout=10ms'")
   161  	file = getOptionFile(t, "mydb", cfg)
   162  	origFile.SetOptionValue("ci", "host", "my.ci.invalid")
   163  	origFile.SetOptionValue("ci", "port", "3307")
   164  	origFile.SetOptionValue("ci", "user", "foobar")
   165  	origFile.SetOptionValue("ci", "connect-options", "timeout=10ms")
   166  	if !origFile.SameContents(file) {
   167  		t.Fatalf("File contents of %s do not match expectation", file.Path())
   168  	}
   169  
   170  	// localhost and socket should work properly
   171  	s.handleCommand(t, CodeSuccess, ".", "skeema add-environment -h localhost -S /var/lib/mysql/mysql.sock --dir mydb development")
   172  	file = getOptionFile(t, "mydb", cfg)
   173  	origFile.SetOptionValue("development", "host", "localhost")
   174  	origFile.SetOptionValue("development", "socket", "/var/lib/mysql/mysql.sock")
   175  	if !origFile.SameContents(file) {
   176  		t.Fatalf("File contents of %s do not match expectation", file.Path())
   177  	}
   178  
   179  	// valid instance should work properly and even populate flavor. Also confirm
   180  	// persistence of ignore-schema and ignore-table.
   181  	cfg = s.handleCommand(t, CodeSuccess, "mydb", "skeema add-environment --ignore-schema='^test' --ignore-table='^_' --host %s:%d cloud", s.d.Instance.Host, s.d.Instance.Port)
   182  	file = getOptionFile(t, "mydb", cfg)
   183  	origFile.SetOptionValue("cloud", "host", s.d.Instance.Host)
   184  	origFile.SetOptionValue("cloud", "port", fmt.Sprintf("%d", s.d.Instance.Port))
   185  	origFile.SetOptionValue("cloud", "ignore-schema", "^test")
   186  	origFile.SetOptionValue("cloud", "ignore-table", "^_")
   187  	origFile.SetOptionValue("cloud", "flavor", s.d.Flavor().String())
   188  	if !origFile.SameContents(file) {
   189  		t.Fatalf("File contents of %s do not match expectation", file.Path())
   190  	}
   191  }
   192  
   193  func (s SkeemaIntegrationSuite) TestPullHandler(t *testing.T) {
   194  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   195  
   196  	// In product db, alter one table and drop one table;
   197  	// In analytics db, add one table and alter the schema's charset and collation;
   198  	// Create a new db and put one table in it
   199  	s.sourceSQL(t, "pull1.sql")
   200  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   201  	s.verifyFiles(t, cfg, "../golden/pull1")
   202  
   203  	// Revert db back to previous state, and pull again to test the opposite
   204  	// behaviors: delete dir for new schema, restore charset/collation in .skeema,
   205  	// etc. Also edit the host .skeema file to remove flavor, to test logic that
   206  	// adds/updates flavor on pull.
   207  	s.cleanData(t, "setup.sql")
   208  	fs.WriteTestFile(t, "mydb/.skeema", strings.Replace(fs.ReadTestFile(t, "mydb/.skeema"), "flavor", "#flavor", 1))
   209  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   210  	s.verifyFiles(t, cfg, "../golden/init")
   211  
   212  	// Files with invalid SQL should still be corrected upon pull. Files with
   213  	// nonstandard formatting of their CREATE TABLE should be normalized, even if
   214  	// there was an ignored auto-increment change. Files with extraneous text
   215  	// before/after the CREATE TABLE should remain as-is, regardless of whether
   216  	// there were other changes triggering a file rewrite. Files containing
   217  	// commands plus a table that doesn't exist should be deleted, instead of
   218  	// leaving a file with lingering commands.
   219  	contents := fs.ReadTestFile(t, "mydb/analytics/activity.sql")
   220  	fs.WriteTestFile(t, "mydb/analytics/activity.sql", strings.Replace(contents, "DEFAULT", "DEFALUT", 1))
   221  	s.dbExec(t, "product", "INSERT INTO comments (post_id, user_id) VALUES (555, 777)")
   222  	contents = fs.ReadTestFile(t, "mydb/product/comments.sql")
   223  	fs.WriteTestFile(t, "mydb/product/comments.sql", strings.Replace(contents, "`", "", -1))
   224  	contents = fs.ReadTestFile(t, "mydb/product/posts.sql")
   225  	fs.WriteTestFile(t, "mydb/product/posts.sql", fmt.Sprintf("# random comment\n%s", contents))
   226  	fs.WriteTestFile(t, "mydb/product/noexist.sql", "DELIMITER //\nCREATE TABLE noexist (id int)//\nDELIMITER ;\n")
   227  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull --debug")
   228  	s.verifyFiles(t, cfg, "../golden/init")
   229  	contents = fs.ReadTestFile(t, "mydb/product/posts.sql")
   230  	if !strings.Contains(contents, "# random comment") {
   231  		t.Error("Expected mydb/product/posts.sql to retain its extraneous comment, but it was removed")
   232  	}
   233  	fs.WriteTestFile(t, "mydb/product/posts.sql", strings.Replace(contents, "`", "", -1))
   234  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull --debug")
   235  	s.verifyFiles(t, cfg, "../golden/init")
   236  	if !strings.Contains(contents, "# random comment") {
   237  		t.Error("Expected mydb/product/posts.sql to retain its extraneous comment, but it was removed")
   238  	}
   239  
   240  	// Test behavior with --skip-new-schemas: new schema should not have a dir in
   241  	// fs, but changes to existing schemas should still be made
   242  	s.sourceSQL(t, "pull1.sql")
   243  	s.handleCommand(t, CodeSuccess, ".", "skeema pull --skip-new-schemas")
   244  	if _, err := os.Stat("mydb/archives"); !os.IsNotExist(err) {
   245  		t.Errorf("Expected os.Stat to return IsNotExist error for mydb/archives; instead err=%v", err)
   246  	}
   247  	if _, err := os.Stat("mydb/analytics/widget_counts.sql"); err != nil {
   248  		t.Errorf("Expected os.Stat to return nil error for mydb/analytics/widget_counts.sql; instead err=%v", err)
   249  	}
   250  
   251  	// If a dir has a bad option file, new schema detection should also be skipped,
   252  	// since we don't know what schemas the bad subdir maps to
   253  	fs.WriteTestFile(t, "mydb/analytics/.skeema", "this won't parse anymore")
   254  	s.handleCommand(t, CodePartialError, ".", "skeema pull")
   255  	if _, err := os.Stat("mydb/archives"); !os.IsNotExist(err) {
   256  		t.Errorf("Expected os.Stat to return IsNotExist error for mydb/archives; instead err=%v", err)
   257  	}
   258  }
   259  
   260  func (s SkeemaIntegrationSuite) TestLintHandler(t *testing.T) {
   261  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   262  
   263  	// Initial lint should be a no-op that returns exit code 0
   264  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema lint")
   265  	s.verifyFiles(t, cfg, "../golden/init")
   266  
   267  	// Invalid options should error with CodeBadConfig
   268  	s.handleCommand(t, CodeBadConfig, ".", "skeema lint --workspace=doesnt-exist")
   269  	s.handleCommand(t, CodeBadConfig, "mydb/product", "skeema lint --password=wrong")
   270  
   271  	// Alter a few files in a way that is still valid SQL, but doesn't match
   272  	// the database's native format. Lint should rewrite these files and then
   273  	// return exit code CodeDifferencesFound.
   274  	productDir, err := fs.ParseDir("mydb/product", cfg)
   275  	if err != nil {
   276  		t.Fatalf("Unable to obtain dir for mydb/product: %s", err)
   277  	}
   278  	if len(productDir.SQLFiles) < 4 {
   279  		t.Fatalf("Unable to obtain *.sql files from %s", productDir)
   280  	}
   281  	rewriteFiles := func(includeSyntaxError bool) {
   282  		for n, sf := range productDir.SQLFiles {
   283  			contents := fs.ReadTestFile(t, sf.Path())
   284  			switch n {
   285  			case 0:
   286  				if includeSyntaxError {
   287  					contents = strings.Replace(contents, "DEFAULT", "DEFALUT", 1)
   288  				}
   289  			case 1:
   290  				contents = strings.ToLower(contents)
   291  			case 2:
   292  				contents = strings.Replace(contents, "`", "", -1)
   293  			case 3:
   294  				contents = strings.Replace(contents, " ", "  ", -1)
   295  			}
   296  			fs.WriteTestFile(t, sf.Path(), contents)
   297  		}
   298  	}
   299  	rewriteFiles(false)
   300  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema lint")
   301  	s.verifyFiles(t, cfg, "../golden/init")
   302  
   303  	// Add a new file with invalid SQL, and also make the previous valid rewrites.
   304  	// Lint should rewrite the valid files but return exit code CodeFatalError due
   305  	// to there being at least 1 file with invalid SQL.
   306  	rewriteFiles(true)
   307  	s.handleCommand(t, CodeFatalError, ".", "skeema lint")
   308  
   309  	// Manually restore the file with invalid SQL; the files should now verify,
   310  	// confirming that the fatal error did not prevent the other files from being
   311  	// reformatted; re-linting should yield no changes.
   312  	contents := fs.ReadTestFile(t, productDir.SQLFiles[0].Path())
   313  	fs.WriteTestFile(t, productDir.SQLFiles[0].Path(), strings.Replace(contents, "DEFALUT", "DEFAULT", 1))
   314  	s.verifyFiles(t, cfg, "../golden/init")
   315  	s.handleCommand(t, CodeSuccess, ".", "skeema lint")
   316  
   317  	// Files with SQL statements unsupported by this package should yield a
   318  	// warning, resulting in CodeDifferencesFound
   319  	fs.WriteTestFile(t, productDir.SQLFiles[0].Path(), "INSERT INTO foo (col1, col2) VALUES (123, 456)")
   320  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema lint")
   321  
   322  	// Directories that have invalid options should yield CodeFatalError
   323  	fs.WriteTestFile(t, "mydb/uhoh/.skeema", "this is not a valid .skeema file")
   324  	s.handleCommand(t, CodeFatalError, ".", "skeema lint")
   325  }
   326  
   327  func (s SkeemaIntegrationSuite) TestDiffHandler(t *testing.T) {
   328  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   329  
   330  	// no-op diff should yield no differences
   331  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   332  
   333  	// --host and --schema should error if supplied on CLI
   334  	s.handleCommand(t, CodeBadConfig, ".", "skeema diff --host=1.2.3.4 --schema=whatever")
   335  
   336  	// It isn't possible to disable --dry-run with diff
   337  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema diff --skip-dry-run")
   338  	if !cfg.GetBool("dry-run") {
   339  		t.Error("Expected --skip-dry-run to have no effect on `skeema diff`, but it disabled dry-run")
   340  	}
   341  
   342  	s.dbExec(t, "analytics", "ALTER TABLE pageviews DROP COLUMN domain")
   343  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff")
   344  
   345  	// Confirm --brief works as expected
   346  	oldStdout := os.Stdout
   347  	if outFile, err := os.Create("diff-brief.out"); err != nil {
   348  		t.Fatalf("Unable to redirect stdout to a file: %s", err)
   349  	} else {
   350  		os.Stdout = outFile
   351  		s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --brief")
   352  		outFile.Close()
   353  		os.Stdout = oldStdout
   354  		expectOut := fmt.Sprintf("%s\n", s.d.Instance)
   355  		actualOut := fs.ReadTestFile(t, "diff-brief.out")
   356  		if actualOut != expectOut {
   357  			t.Errorf("Unexpected output from `skeema diff --brief`\nExpected:\n%sActual:\n%s", expectOut, actualOut)
   358  		}
   359  		if err := os.Remove("diff-brief.out"); err != nil {
   360  			t.Fatalf("Unable to delete diff-brief.out: %s", err)
   361  		}
   362  	}
   363  }
   364  
   365  func (s SkeemaIntegrationSuite) TestPushHandler(t *testing.T) {
   366  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   367  
   368  	// Verify clean-slate operation: wipe the DB; push; wipe the files; re-init
   369  	// the files; verify the files match. The push inherently verifies creation of
   370  	// schemas and tables.
   371  	s.cleanData(t)
   372  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   373  	s.reinitAndVerifyFiles(t, "", "")
   374  
   375  	// Test bad option values
   376  	s.handleCommand(t, CodeBadConfig, ".", "skeema push --concurrent-instances=0")
   377  	s.handleCommand(t, CodeBadConfig, ".", "skeema push --alter-algorithm=invalid")
   378  	s.handleCommand(t, CodeBadConfig, ".", "skeema push --alter-lock=invalid")
   379  	s.handleCommand(t, CodeBadConfig, ".", "skeema push --ignore-table='+'")
   380  
   381  	// Make some changes on the db side, mix of safe and unsafe changes to
   382  	// multiple schemas. Remember, subsequent pushes will effectively be UN-DOING
   383  	// what push1.sql did, since we updated the db but not the filesystem.
   384  	s.sourceSQL(t, "push1.sql")
   385  
   386  	// push from base dir, without any args, should succeed for schemas with safe
   387  	// changes (analytics) but not for schemas with 1 or more unsafe changes
   388  	// (product). It shouldn't not affect the `bonus` schema (which exists on db
   389  	// but not on filesystem, but push should never drop schemas)
   390  	s.handleCommand(t, CodeFatalError, "", "skeema push")            // CodeFatalError due to unsafe changes not being allowed
   391  	s.handleCommand(t, CodeSuccess, "mydb/analytics", "skeema diff") // analytics dir was pushed fine tho
   392  	s.assertTableExists(t, "analytics", "pageviews", "")             // re-created by push
   393  	s.assertTableMissing(t, "product", "users", "credits")           // product DDL skipped due to unsafe stmt
   394  	s.assertTableExists(t, "product", "posts", "featured")           // product DDL skipped due to unsafe stmt
   395  	s.assertTableExists(t, "bonus", "placeholder", "")               // not affected by push (never drops schemas)
   396  
   397  	// The "skip whole schema upon unsafe stmt" rule also affects schema-level DDL
   398  	if product, err := s.d.Schema("product"); err != nil || product == nil {
   399  		t.Fatalf("Unexpected error obtaining schema: %s", err)
   400  	} else {
   401  		if product.CharSet != "utf8" || product.Collation != "utf8_swedish_ci" {
   402  			t.Errorf("Expected schema should have charset/collation=utf8/utf8_swedish_ci from push1.sql, instead found %s/%s", product.CharSet, product.Collation)
   403  		}
   404  	}
   405  
   406  	// Delete *.sql file for analytics.rollups. Push from analytics dir with
   407  	// --safe-below-size=1 should fail since it has a row. Delete that row and
   408  	// try again, should succeed that time.
   409  	if err := os.Remove("mydb/analytics/rollups.sql"); err != nil {
   410  		t.Fatalf("Unexpected error removing a file: %s", err)
   411  	}
   412  	s.handleCommand(t, CodeFatalError, "mydb/analytics", "skeema push --safe-below-size=1")
   413  	s.assertTableExists(t, "analytics", "rollups", "")
   414  	s.dbExec(t, "analytics", "DELETE FROM rollups")
   415  	s.handleCommand(t, CodeSuccess, "mydb/analytics", "skeema push --safe-below-size=1")
   416  	s.assertTableMissing(t, "analytics", "rollups", "")
   417  
   418  	// push from base dir, with --allow-unsafe, will permit the changes to product
   419  	// schema to proceed
   420  	s.handleCommand(t, CodeSuccess, ".", "skeema push --allow-unsafe")
   421  	s.assertTableMissing(t, "product", "posts", "featured")
   422  	s.assertTableExists(t, "product", "users", "credits")
   423  	s.assertTableExists(t, "bonus", "placeholder", "")
   424  	if product, err := s.d.Schema("product"); err != nil || product == nil {
   425  		t.Fatalf("Unexpected error obtaining schema: %s", err)
   426  	} else {
   427  		serverCharSet, serverCollation, err := s.d.DefaultCharSetAndCollation()
   428  		if err != nil {
   429  			t.Fatalf("Unable to obtain server default charset and collation: %s", err)
   430  		}
   431  		if serverCharSet != product.CharSet || serverCollation != product.Collation {
   432  			t.Errorf("Expected schema should have charset/collation=%s/%s, instead found %s/%s", serverCharSet, serverCollation, product.CharSet, product.Collation)
   433  		}
   434  	}
   435  
   436  	// invalid SQL prevents push from working in an entire dir, but not in a
   437  	// dir for a different schema
   438  	contents := fs.ReadTestFile(t, "mydb/product/comments.sql")
   439  	fs.WriteTestFile(t, "mydb/product/comments.sql", strings.Replace(contents, "PRIMARY KEY", "foo int,\nPRIMARY KEY", 1))
   440  	contents = fs.ReadTestFile(t, "mydb/product/users.sql")
   441  	fs.WriteTestFile(t, "mydb/product/users.sql", strings.Replace(contents, "PRIMARY KEY", "foo int INVALID SQL HERE,\nPRIMARY KEY", 1))
   442  	fs.WriteTestFile(t, "mydb/bonus/.skeema", "schema=bonus\n")
   443  	fs.WriteTestFile(t, "mydb/bonus/placeholder.sql", "CREATE TABLE placeholder (id int unsigned NOT NULL, PRIMARY KEY (id)) ENGINE=InnoDB")
   444  	fs.WriteTestFile(t, "mydb/bonus/table2.sql", "CREATE TABLE table2 (name varchar(20) NOT NULL, PRIMARY KEY (name))")
   445  	s.handleCommand(t, CodeFatalError, ".", "skeema push")
   446  	s.assertTableMissing(t, "product", "comments", "foo")
   447  	s.assertTableMissing(t, "product", "users", "foo")
   448  	s.assertTableExists(t, "bonus", "table2", "")
   449  }
   450  
   451  func (s SkeemaIntegrationSuite) TestHelpHandler(t *testing.T) {
   452  	// Simple tests just to confirm the commands don't error
   453  	fs.WriteTestFile(t, "fake-etc/skeema", "# hello world")
   454  	s.handleCommand(t, CodeSuccess, ".", "skeema")
   455  	s.handleCommand(t, CodeSuccess, ".", "skeema help")
   456  	s.handleCommand(t, CodeSuccess, ".", "skeema --help")
   457  	s.handleCommand(t, CodeSuccess, ".", "skeema --help=add-environment")
   458  	s.handleCommand(t, CodeSuccess, ".", "skeema help add-environment")
   459  	s.handleCommand(t, CodeSuccess, ".", "skeema add-environment --help")
   460  	s.handleCommand(t, CodeFatalError, ".", "skeema help doesntexist")
   461  	s.handleCommand(t, CodeFatalError, ".", "skeema --help=doesntexist")
   462  }
   463  
   464  func (s SkeemaIntegrationSuite) TestIndexOrdering(t *testing.T) {
   465  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   466  
   467  	// Add 6 new redundant indexes to posts.sql. Place them before the existing
   468  	// secondary index.
   469  	contentsOrig := fs.ReadTestFile(t, "mydb/product/posts.sql")
   470  	lines := make([]string, 6)
   471  	for n := range lines {
   472  		lines[n] = fmt.Sprintf("KEY `idxnew_%d` (`created_at`)", n)
   473  	}
   474  	joinedLines := strings.Join(lines, ",\n  ")
   475  	contentsIndexesFirst := strings.Replace(contentsOrig, "PRIMARY KEY (`id`),\n", fmt.Sprintf("PRIMARY KEY (`id`),\n  %s,\n", joinedLines), 1)
   476  	fs.WriteTestFile(t, "mydb/product/posts.sql", contentsIndexesFirst)
   477  
   478  	// push should add the indexes, and afterwards diff should report no
   479  	// differences, even though the index order in the file differs from what is
   480  	// in mysql
   481  	s.handleCommand(t, CodeSuccess, "", "skeema push")
   482  	s.handleCommand(t, CodeSuccess, "", "skeema diff")
   483  
   484  	// however, diff --exact-match can see the differences
   485  	s.handleCommand(t, CodeDifferencesFound, "", "skeema diff --exact-match")
   486  
   487  	// pull should re-write the file such that the indexes are now last, just like
   488  	// what's actually in mysql
   489  	s.handleCommand(t, CodeSuccess, "", "skeema pull")
   490  	contentsIndexesLast := strings.Replace(contentsOrig, ")\n", fmt.Sprintf("),\n  %s\n", joinedLines), 1)
   491  	if fileContents := fs.ReadTestFile(t, "mydb/product/posts.sql"); fileContents == contentsIndexesFirst {
   492  		t.Error("Expected skeema pull to rewrite mydb/product/posts.sql to put indexes last, but file remained unchanged")
   493  	} else if fileContents != contentsIndexesLast {
   494  		t.Errorf("Expected skeema pull to rewrite mydb/product/posts.sql to put indexes last, but it did something else entirely. Contents:\n%s\nExpected:\n%s\n", fileContents, contentsIndexesLast)
   495  	}
   496  
   497  	// Edit posts.sql to put the new indexes first again, and ensure
   498  	// push --exact-match actually reorders them.
   499  	fs.WriteTestFile(t, "mydb/product/posts.sql", contentsIndexesFirst)
   500  	if major, minor, _ := s.d.Version(); major == 5 && minor == 5 {
   501  		s.handleCommand(t, CodeSuccess, "", "skeema push --exact-match")
   502  	} else {
   503  		s.handleCommand(t, CodeSuccess, "", "skeema push --exact-match --alter-algorithm=COPY")
   504  	}
   505  	s.handleCommand(t, CodeSuccess, "", "skeema diff")
   506  	s.handleCommand(t, CodeSuccess, "", "skeema diff --exact-match")
   507  	s.handleCommand(t, CodeSuccess, "", "skeema pull")
   508  	if fileContents := fs.ReadTestFile(t, "mydb/product/posts.sql"); fileContents != contentsIndexesFirst {
   509  		t.Errorf("Expected skeema pull to have no effect at this point, but instead file now looks like this:\n%s", fileContents)
   510  	}
   511  }
   512  
   513  func (s SkeemaIntegrationSuite) TestForeignKeys(t *testing.T) {
   514  	s.sourceSQL(t, "foreignkey.sql")
   515  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   516  
   517  	// Renaming an FK should not be considered a difference by default
   518  	oldContents := fs.ReadTestFile(t, "mydb/product/posts.sql")
   519  	contents1 := strings.Replace(oldContents, "user_fk", "usridfk", 1)
   520  	if oldContents == contents1 {
   521  		t.Fatal("Expected mydb/product/posts.sql to contain foreign key definition, but it did not")
   522  	}
   523  	fs.WriteTestFile(t, "mydb/product/posts.sql", contents1)
   524  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   525  
   526  	// pull won't update the file unless normalizing
   527  	s.handleCommand(t, CodeSuccess, ".", "skeema pull --skip-normalize")
   528  	if fs.ReadTestFile(t, "mydb/product/posts.sql") != contents1 {
   529  		t.Error("Expected skeema pull --skip-normalize to leave file untouched, but it rewrote it")
   530  	}
   531  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   532  	if fs.ReadTestFile(t, "mydb/product/posts.sql") != oldContents {
   533  		t.Error("Expected skeema pull to rewrite file, but it did not")
   534  	}
   535  
   536  	// Renaming an FK should be considered a difference with --exact-match
   537  	fs.WriteTestFile(t, "mydb/product/posts.sql", contents1)
   538  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --exact-match")
   539  	s.handleCommand(t, CodeSuccess, ".", "skeema push --exact-match")
   540  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   541  
   542  	// Changing an FK definition should not break push or pull, even though this
   543  	// will be two non-noop ALTERs to the same table
   544  	contents2 := strings.Replace(contents1,
   545  		"FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)",
   546  		"FOREIGN KEY (`user_id`, `byline`) REFERENCES `users` (`id`, `name`)",
   547  		1)
   548  	if contents2 == contents1 {
   549  		t.Fatal("Failed to update contents as expected")
   550  	}
   551  	fs.WriteTestFile(t, "mydb/product/posts.sql", contents2)
   552  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff")
   553  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   554  	fs.WriteTestFile(t, "mydb/product/posts.sql", contents1)
   555  	s.handleCommand(t, CodeSuccess, ".", "skeema pull --skip-normalize")
   556  	if fs.ReadTestFile(t, "mydb/product/posts.sql") != contents2 {
   557  		t.Error("Expected skeema pull to rewrite file, but it did not")
   558  	}
   559  
   560  	// Confirm that adding foreign keys occurs after other changes: construct
   561  	// a scenario where we're adding an FK that needs a new index on the "parent"
   562  	// (referenced) table, where the parent table name is alphabetically after
   563  	// the child table
   564  	s.dbExec(t, "product", "ALTER TABLE posts DROP FOREIGN KEY usridfk")
   565  	s.dbExec(t, "product", "ALTER TABLE users DROP KEY idname")
   566  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   567  
   568  	// Test handling of unsafe operations combined with FK operations:
   569  	// Drop FK + add different FK + drop another col
   570  	// Confirm that if an unsafe operation is blocked, but there's also a 2nd
   571  	// ALTER for same table (due to splitting of drop FK + add FK into separate
   572  	// ALTERs) that both ALTERs are skipped.
   573  	contents3 := strings.Replace(oldContents, "`body` text,\n", "", 1)
   574  	contents3 = strings.Replace(contents3, "`body` text DEFAULT NULL,\n", "", 1) // MariaDB 10.2+
   575  	if strings.Contains(contents3, "`body`") || !strings.Contains(contents3, "`user_fk`") {
   576  		t.Fatal("Failed to update contents as expected")
   577  	}
   578  	fs.WriteTestFile(t, "mydb/product/posts.sql", contents3)
   579  	s.handleCommand(t, CodeFatalError, ".", "skeema push")
   580  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   581  	checkContents := fs.ReadTestFile(t, "mydb/product/posts.sql")
   582  	if !strings.Contains(checkContents, "`body`") || strings.Contains(checkContents, "`user_fk`") {
   583  		t.Error("Unsafe status did not properly affect both ALTERs on the table")
   584  	}
   585  
   586  	// Test adding an FK where the existing data does not meet the constraint:
   587  	// should fail if foreign_key_checks=1, succeed if foreign_key_checks=0
   588  	s.dbExec(t, "product", "ALTER TABLE posts DROP FOREIGN KEY usridfk")
   589  	s.dbExec(t, "product", "INSERT INTO posts (user_id, byline) VALUES (1234, 'someone')")
   590  	fs.WriteTestFile(t, "mydb/product/posts.sql", contents1)
   591  	s.handleCommand(t, CodeFatalError, ".", "skeema push --foreign-key-checks")
   592  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   593  }
   594  
   595  func (s SkeemaIntegrationSuite) TestAutoInc(t *testing.T) {
   596  	// Insert 2 rows into product.users, so that next auto-inc value is now 3
   597  	s.dbExec(t, "product", "INSERT INTO users (name) VALUES (?), (?)", "foo", "bar")
   598  
   599  	// Normal init omits auto-inc values. diff views this as no differences.
   600  	s.reinitAndVerifyFiles(t, "", "")
   601  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   602  
   603  	// pull and lint should make no changes
   604  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   605  	s.verifyFiles(t, cfg, "../golden/init")
   606  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema lint")
   607  	s.verifyFiles(t, cfg, "../golden/init")
   608  
   609  	// pull with --include-auto-inc should include auto-inc values greater than 1
   610  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull --include-auto-inc")
   611  	s.verifyFiles(t, cfg, "../golden/autoinc")
   612  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   613  
   614  	// Inserting another row should still be ignored by diffs
   615  	s.dbExec(t, "product", "INSERT INTO users (name) VALUES (?)", "something")
   616  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   617  
   618  	// However, if table's next auto-inc is LOWER than sqlfile's, this is a
   619  	// difference.
   620  	s.dbExec(t, "product", "DELETE FROM users WHERE id > 1")
   621  	s.dbExec(t, "product", "ALTER TABLE users AUTO_INCREMENT=2")
   622  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff")
   623  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   624  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   625  
   626  	// init with --include-auto-inc should include auto-inc values greater than 1
   627  	s.reinitAndVerifyFiles(t, "--include-auto-inc", "../golden/autoinc")
   628  
   629  	// now that the file has a next auto-inc value, subsequent pull operations
   630  	// should update the value, even without --include-auto-inc
   631  	s.dbExec(t, "product", "INSERT INTO users (name) VALUES (?)", "something")
   632  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   633  	if !strings.Contains(fs.ReadTestFile(t, "mydb/product/users.sql"), "AUTO_INCREMENT=4") {
   634  		t.Error("Expected mydb/product/users.sql to contain AUTO_INCREMENT=4 after pull, but it did not")
   635  	}
   636  
   637  }
   638  
   639  func (s SkeemaIntegrationSuite) TestUnsupportedAlter(t *testing.T) {
   640  	s.sourceSQL(t, "unsupported1.sql")
   641  
   642  	// init should work fine with an unsupported table
   643  	s.reinitAndVerifyFiles(t, "", "../golden/unsupported")
   644  
   645  	// Back to clean slate for db and files
   646  	s.cleanData(t, "setup.sql")
   647  	s.reinitAndVerifyFiles(t, "", "../golden/init")
   648  
   649  	// apply change to db directly, and confirm pull still works
   650  	s.sourceSQL(t, "unsupported1.sql")
   651  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema pull --debug")
   652  	s.verifyFiles(t, cfg, "../golden/unsupported")
   653  
   654  	// back to clean slate for db only
   655  	s.cleanData(t, "setup.sql")
   656  
   657  	// lint should be able to fix formatting problems in unsupported table files
   658  	contents := fs.ReadTestFile(t, "mydb/product/subscriptions.sql")
   659  	fs.WriteTestFile(t, "mydb/product/subscriptions.sql", strings.Replace(contents, "`", "", -1))
   660  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema lint")
   661  	s.verifyFiles(t, cfg, "../golden/unsupported")
   662  
   663  	// diff should return CodeDifferencesFound, vs push should return
   664  	// CodePartialError
   665  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --debug")
   666  	s.handleCommand(t, CodePartialError, ".", "skeema push")
   667  
   668  	// diff/push still ok if *creating* or *dropping* unsupported table
   669  	s.dbExec(t, "product", "DROP TABLE subscriptions")
   670  	s.assertTableMissing(t, "product", "subscriptions", "")
   671  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff")
   672  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   673  	s.assertTableExists(t, "product", "subscriptions", "")
   674  	if err := os.Remove("mydb/product/subscriptions.sql"); err != nil {
   675  		t.Fatalf("Unexpected error removing a file: %s", err)
   676  	}
   677  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --allow-unsafe")
   678  	s.handleCommand(t, CodeSuccess, ".", "skeema push --allow-unsafe")
   679  	s.assertTableMissing(t, "product", "subscriptions", "")
   680  }
   681  
   682  func (s SkeemaIntegrationSuite) TestIgnoreOptions(t *testing.T) {
   683  	s.sourceSQL(t, "ignore1.sql")
   684  
   685  	// init: valid regexes should work properly and persist to option files
   686  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d --ignore-schema='^archives$' --ignore-table='^_'", s.d.Instance.Host, s.d.Instance.Port)
   687  	s.verifyFiles(t, cfg, "../golden/ignore")
   688  
   689  	// pull: nothing should be updated due to ignore options. Ditto even if we add
   690  	// a dir with schema name corresponding to ignored schema.
   691  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   692  	s.verifyFiles(t, cfg, "../golden/ignore")
   693  	fs.WriteTestFile(t, "mydb/archives/.skeema", "schema=archives")
   694  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   695  	if _, err := os.Stat("mydb/archives/foo.sql"); err == nil {
   696  		t.Error("ignore-options not affecting `skeema pull` as expected")
   697  	}
   698  
   699  	// diff/push: no differences. This should still be the case even if we add a
   700  	// file corresponding to an ignored table, with a different definition than
   701  	// the db has.
   702  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   703  	fs.WriteTestFile(t, "mydb/product/_widgets.sql", "CREATE TABLE _widgets (id int) ENGINE=InnoDB;\n")
   704  	fs.WriteTestFile(t, "mydb/analytics/_newtable.sql", "CREATE TABLE _newtable (id int) ENGINE=InnoDB;\n")
   705  	fs.WriteTestFile(t, "mydb/archives/bar.sql", "CREATE TABLE bar (id int) ENGINE=InnoDB;\n")
   706  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   707  
   708  	// pull should also ignore that file corresponding to an ignored table
   709  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   710  	if fs.ReadTestFile(t, "mydb/product/_widgets.sql") != "CREATE TABLE _widgets (id int) ENGINE=InnoDB;\n" {
   711  		t.Error("Expected pull to ignore mydb/product/_widgets.sql entirely, but it did not")
   712  	}
   713  
   714  	// lint: ignored tables should be ignored
   715  	// To set up this test, we do a pull that overrides the previous ignore options
   716  	// and then edit those files so that they contain formatting mistakes or even
   717  	// invalid SQL.
   718  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull --ignore-table=''")
   719  	contents := fs.ReadTestFile(t, "mydb/analytics/_trending.sql")
   720  	newContents := strings.Replace(contents, "`", "", -1)
   721  	fs.WriteTestFile(t, "mydb/analytics/_trending.sql", newContents)
   722  	fs.WriteTestFile(t, "mydb/analytics/_hmm.sql", "CREATE TABLE _hmm uhoh this is not valid;\n")
   723  	fs.WriteTestFile(t, "mydb/archives/bar.sql", "CREATE TABLE bar uhoh this is not valid;\n")
   724  	s.handleCommand(t, CodeSuccess, ".", "skeema lint")
   725  	if fs.ReadTestFile(t, "mydb/analytics/_trending.sql") != newContents {
   726  		t.Error("Expected `skeema lint` to ignore mydb/analytics/_trending.sql, but it did not")
   727  	}
   728  
   729  	// push, pull, lint, init: invalid regexes should error. Error is CodeBadConfig
   730  	// except for cases of invalid ignore-schema being hit in fs.Dir.SchemaNames().
   731  	s.handleCommand(t, CodeBadConfig, ".", "skeema lint --ignore-table='+'")
   732  	s.handleCommand(t, CodeBadConfig, ".", "skeema lint --ignore-schema='+'")
   733  	s.handleCommand(t, CodeBadConfig, ".", "skeema pull --ignore-table='+'")
   734  	s.handleCommand(t, CodeFatalError, ".", "skeema pull --ignore-schema='+'")
   735  	s.handleCommand(t, CodeBadConfig, ".", "skeema push --ignore-table='+'")
   736  	s.handleCommand(t, CodeFatalError, ".", "skeema push --ignore-schema='+'")
   737  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir badre1 -h %s -P %d --ignore-schema='+'", s.d.Instance.Host, s.d.Instance.Port)
   738  	s.handleCommand(t, CodeBadConfig, ".", "skeema init --dir badre2 -h %s -P %d --ignore-table='+'", s.d.Instance.Host, s.d.Instance.Port)
   739  }
   740  
   741  func (s SkeemaIntegrationSuite) TestDirEdgeCases(t *testing.T) {
   742  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   743  
   744  	// Invalid option file should break all commands
   745  	oldContents := fs.ReadTestFile(t, "mydb/.skeema")
   746  	fs.WriteTestFile(t, "mydb/.skeema", "invalid contents\n")
   747  	s.handleCommand(t, CodeFatalError, "mydb", "skeema pull")
   748  	s.handleCommand(t, CodeFatalError, "mydb", "skeema diff")
   749  	s.handleCommand(t, CodeFatalError, "mydb", "skeema lint")
   750  	s.handleCommand(t, CodeFatalError, ".", "skeema add-environment --host my.staging.db.com --dir mydb staging")
   751  	fs.WriteTestFile(t, "mydb/.skeema", oldContents)
   752  
   753  	// Hidden directories are ignored, even if they contain a .skeema file, whether
   754  	// valid or invalid.
   755  	fs.WriteTestFile(t, ".hidden/.skeema", "invalid contents\n")
   756  	fs.WriteTestFile(t, ".hidden/whatever.sql", "CREATE TABLE whatever (this is not valid SQL oh well)")
   757  	fs.WriteTestFile(t, "mydb/.hidden/.skeema", "schema=whatever\n")
   758  	fs.WriteTestFile(t, "mydb/.hidden/whatever.sql", "CREATE TABLE whatever (this is not valid SQL oh well)")
   759  	fs.WriteTestFile(t, "mydb/product/.hidden/.skeema", "schema=whatever\n")
   760  	fs.WriteTestFile(t, "mydb/product/.hidden/whatever.sql", "CREATE TABLE whatever (this is not valid SQL oh well)")
   761  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   762  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   763  	s.handleCommand(t, CodeSuccess, ".", "skeema lint")
   764  
   765  	// Extra subdirs with .skeema files and *.sql files don't inherit "schema"
   766  	// option value from parent dir, and are ignored by diff/push/pull as long
   767  	// as they don't specify a schema value directly. lint still works since its
   768  	// execution model does not require a schema to be defined.
   769  	fs.WriteTestFile(t, "mydb/product/subdir/.skeema", "# nothing relevant here\n")
   770  	fs.WriteTestFile(t, "mydb/product/subdir/hello.sql", "CREATE TABLE hello (id int);\n")
   771  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   772  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   773  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema lint") // should rewrite hello.sql
   774  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   775  
   776  	// Dirs with no *.sql files, but have a schema defined in .skeema, should
   777  	// be interpreted as a logical schema without any objects
   778  	s.dbExec(t, "", "CREATE DATABASE otherdb")
   779  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   780  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   781  	if contents := fs.ReadTestFile(t, "mydb/otherdb/.skeema"); contents == "" {
   782  		t.Error("Unexpectedly found no contents in mydb/otherdb/.skeema")
   783  	}
   784  	s.dbExec(t, "otherdb", "CREATE TABLE othertable (id int)")
   785  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   786  	if contents := fs.ReadTestFile(t, "mydb/otherdb/othertable.sql"); contents == "" {
   787  		t.Error("Unexpectedly found no contents in mydb/otherdb/othertable.sql")
   788  	}
   789  	fs.RemoveTestFile(t, "mydb/otherdb/othertable.sql")
   790  	s.handleCommand(t, CodeFatalError, ".", "skeema diff")
   791  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --allow-unsafe")
   792  	s.handleCommand(t, CodeSuccess, ".", "skeema push --allow-unsafe")
   793  }
   794  
   795  // This test covers usage of clauses that have no effect in InnoDB, but are still
   796  // shown by MySQL in SHOW CREATE TABLE, despite not being reflected anywhere in
   797  // information_schema. Skeema ignores/strips these clauses so that they do not
   798  // trip up its "unsupported table" validation logic.
   799  func (s SkeemaIntegrationSuite) TestNonInnoClauses(t *testing.T) {
   800  	// MariaDB does not consider STORAGE or COLUMN_FORMAT clauses as valid SQL.
   801  	// Ditto for MySQL 5.5.
   802  	if s.d.Flavor().Vendor == tengo.VendorMariaDB {
   803  		t.Skip("Test not relevant for MariaDB-based image", s.d.Image)
   804  	} else if major, minor, _ := s.d.Version(); major == 5 && minor == 5 {
   805  		t.Skip("Test not relevant for 5.5-based image", s.d.Image)
   806  	}
   807  
   808  	// By default, Skeema uses innodb_strict_mode=1 in its connections. MySQL 5.6
   809  	// interprets that option to prevent use of KEY_BLOCK_SIZE in nonsensical ways,
   810  	// so we must disable it for this test.
   811  	var connectOpts string
   812  	if major, minor, _ := s.d.Version(); major == 5 && minor == 6 {
   813  		connectOpts = " --connect-options=\"innodb_strict_mode=0\""
   814  	}
   815  
   816  	withClauses := "CREATE TABLE `problems` (\n" +
   817  		"  `name` varchar(30) /*!50606 STORAGE MEMORY */ /*!50606 COLUMN_FORMAT DYNAMIC */ DEFAULT NULL,\n" +
   818  		"  `num` int(10) unsigned NOT NULL /*!50606 STORAGE DISK */ /*!50606 COLUMN_FORMAT FIXED */,\n" +
   819  		"  KEY `idx1` (`name`) USING HASH KEY_BLOCK_SIZE=4 COMMENT 'lol',\n" +
   820  		"  KEY `idx2` (`num`) USING BTREE\n" +
   821  		") ENGINE=InnoDB DEFAULT CHARSET=latin1 KEY_BLOCK_SIZE=8;\n"
   822  	withoutClauses := "CREATE TABLE `problems` (\n" +
   823  		"  `name` varchar(30) DEFAULT NULL,\n" +
   824  		"  `num` int(10) unsigned NOT NULL,\n" +
   825  		"  KEY `idx1` (`name`) COMMENT 'lol',\n" +
   826  		"  KEY `idx2` (`num`)\n" +
   827  		") ENGINE=InnoDB DEFAULT CHARSET=latin1 KEY_BLOCK_SIZE=8;\n"
   828  	assertFileNormalized := func() {
   829  		t.Helper()
   830  		if contents := fs.ReadTestFile(t, "mydb/product/problems.sql"); contents != withoutClauses {
   831  			t.Errorf("File mydb/product/problems.sql not normalized. Expected:\n%s\nFound:\n%s", withoutClauses, contents)
   832  		}
   833  	}
   834  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   835  
   836  	// pull strips the clauses from new table
   837  	s.dbExec(t, "product", withClauses)
   838  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   839  	assertFileNormalized()
   840  
   841  	// pull normalizes files to remove the clauses from an unchanged table
   842  	fs.WriteTestFile(t, "mydb/product/problems.sql", withClauses)
   843  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   844  	assertFileNormalized()
   845  
   846  	// lint normalizes files to remove the clauses
   847  	fs.WriteTestFile(t, "mydb/product/problems.sql", withClauses)
   848  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema lint%s --errors=''", connectOpts)
   849  	assertFileNormalized()
   850  
   851  	// diff views the clauses as no-ops if present in file but not db, or vice versa
   852  	s.dbExec(t, "product", "DROP TABLE `problems`")
   853  	s.dbExec(t, "product", withoutClauses)
   854  	fs.WriteTestFile(t, "mydb/product/problems.sql", withClauses)
   855  	s.handleCommand(t, CodeSuccess, ".", "skeema diff%s", connectOpts)
   856  	s.dbExec(t, "product", "DROP TABLE `problems`")
   857  	s.dbExec(t, "product", withClauses)
   858  	fs.WriteTestFile(t, "mydb/product/problems.sql", withoutClauses)
   859  	s.handleCommand(t, CodeSuccess, ".", "skeema diff%s", connectOpts)
   860  
   861  	// init strips the clauses when it writes files
   862  	// (current db state: file still has extra clauses from previous)
   863  	if err := os.RemoveAll("mydb"); err != nil {
   864  		t.Fatalf("Unable to clean directory: %s", err)
   865  	}
   866  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   867  	assertFileNormalized()
   868  
   869  	// push with other changes to same table ignores clauses / does not break
   870  	// validation, in either direction
   871  	newFileContents := strings.Replace(withoutClauses, "  KEY `idx1`", "  newcol int COLUMN_FORMAT FIXED,\n  KEY `idx1`", 1)
   872  	fs.WriteTestFile(t, "mydb/product/problems.sql", newFileContents)
   873  	s.handleCommand(t, CodeSuccess, ".", "skeema push%s", connectOpts)
   874  	s.dbExec(t, "product", "DROP TABLE `problems`")
   875  	s.dbExec(t, "product", withoutClauses)
   876  	s.dbExec(t, "product", "ALTER TABLE `problems` DROP KEY `idx2`")
   877  	fs.WriteTestFile(t, "mydb/product/problems.sql", withClauses)
   878  	s.handleCommand(t, CodeSuccess, ".", "skeema push%s", connectOpts)
   879  }
   880  
   881  func (s SkeemaIntegrationSuite) TestReuseTempSchema(t *testing.T) {
   882  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   883  
   884  	// Ensure that re-using temp schema works as expected, and does not confuse
   885  	// subsequent commands
   886  	for n := 0; n < 2; n++ {
   887  		// Need --skip-normalize in order for pull to use temp schema
   888  		cfg := s.handleCommand(t, CodeSuccess, ".", "skeema pull --skip-normalize --reuse-temp-schema --temp-schema=verytemp")
   889  		s.assertTableExists(t, "verytemp", "", "")
   890  		s.verifyFiles(t, cfg, "../golden/init")
   891  	}
   892  
   893  	// Invalid workspace option should error
   894  	s.handleCommand(t, CodeBadConfig, ".", "skeema pull --workspace=doesnt-exist --skip-normalize --reuse-temp-schema --temp-schema=verytemp")
   895  }
   896  
   897  func (s SkeemaIntegrationSuite) TestShardedSchemas(t *testing.T) {
   898  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
   899  
   900  	// Make product dir now map to 3 schemas: product, product2, product3
   901  	contents := fs.ReadTestFile(t, "mydb/product/.skeema")
   902  	contents = strings.Replace(contents, "schema=product", "schema=product,product2,product3,product4", 1)
   903  	fs.WriteTestFile(t, "mydb/product/.skeema", contents)
   904  
   905  	// push that ignores 4$ should now create product2 and product3
   906  	s.handleCommand(t, CodeSuccess, ".", "skeema push --ignore-schema=4$")
   907  	s.assertTableExists(t, "product2", "", "")
   908  	s.assertTableExists(t, "product3", "posts", "")
   909  
   910  	// diff should be clear after
   911  	s.handleCommand(t, CodeSuccess, ".", "skeema diff --ignore-schema=4$")
   912  
   913  	// pull should not create separate dirs for the new schemas or mess with
   914  	// the .skeema file
   915  	assertDirMissing := func(dirPath string) {
   916  		t.Helper()
   917  		if _, err := os.Stat(dirPath); !os.IsNotExist(err) {
   918  			t.Errorf("Expected dir %s to not exist, but it does (or other err=%v)", dirPath, err)
   919  		}
   920  	}
   921  	assertDirMissing("mydb/product1")
   922  	assertDirMissing("mydb/product2")
   923  	if fs.ReadTestFile(t, "mydb/product/.skeema") != contents {
   924  		t.Error("Unexpected change to mydb/product/.skeema contents")
   925  	}
   926  
   927  	// pull should still reflect changes properly, if made to the first sharded
   928  	// product schema or to the unsharded analytics schema
   929  	s.dbExec(t, "product", "ALTER TABLE comments ADD COLUMN `approved` tinyint(1) unsigned NOT NULL")
   930  	s.dbExec(t, "analytics", "ALTER TABLE activity ADD COLUMN `rolled_up` tinyint(1) unsigned NOT NULL")
   931  	s.handleCommand(t, CodeSuccess, ".", "skeema pull --ignore-schema=4$")
   932  	sfContents := fs.ReadTestFile(t, "mydb/product/comments.sql")
   933  	if !strings.Contains(sfContents, "`approved` tinyint(1) unsigned") {
   934  		t.Error("Pull did not update mydb/product/comments.sql as expected")
   935  	}
   936  	sfContents = fs.ReadTestFile(t, "mydb/analytics/activity.sql")
   937  	if !strings.Contains(sfContents, "`rolled_up` tinyint(1) unsigned") {
   938  		t.Error("Pull did not update mydb/analytics/activity.sql as expected")
   939  	}
   940  
   941  	// push should re-apply the changes to the other 2 product shards; diff
   942  	// should be clean after
   943  	s.handleCommand(t, CodeSuccess, ".", "skeema push --ignore-schema=4$")
   944  	s.assertTableExists(t, "product2", "comments", "approved")
   945  	s.assertTableExists(t, "product3", "comments", "approved")
   946  	s.handleCommand(t, CodeSuccess, ".", "skeema diff --ignore-schema=4$")
   947  
   948  	// schema shellouts should also work properly. First get rid of product schema
   949  	// manually (since push won't ever drop a db) and then push should create
   950  	// product1 as a new schema.
   951  	contents = strings.Replace(contents, "schema=product,product2,product3,product4", "schema=`/usr/bin/printf 'product1 product2 product3 product4'`", 1)
   952  	fs.WriteTestFile(t, "mydb/product/.skeema", contents)
   953  	s.dbExec(t, "", "DROP DATABASE product")
   954  	s.handleCommand(t, CodeSuccess, ".", "skeema push --ignore-schema=4$")
   955  	s.assertTableExists(t, "product1", "posts", "")
   956  	s.handleCommand(t, CodeSuccess, ".", "skeema diff --ignore-schema=4$")
   957  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   958  	assertDirMissing("mydb/product1") // dir is still called mydb/product
   959  	assertDirMissing("mydb/product2")
   960  	if fs.ReadTestFile(t, "mydb/product/.skeema") != contents {
   961  		t.Error("Unexpected change to mydb/product/.skeema contents")
   962  	}
   963  
   964  	// Test schema=* behavior, which should map to all the schemas, meaning that
   965  	// a push will replace the previous tables of the analytics schema with the
   966  	// tables of the product schemas
   967  	if err := os.RemoveAll("mydb/analytics"); err != nil {
   968  		t.Fatalf("Unable to delete mydb/analytics/: %s", err)
   969  	}
   970  	contents = strings.Replace(contents, "schema=`/usr/bin/printf 'product1 product2 product3 product4'`", "schema=*", 1)
   971  	fs.WriteTestFile(t, "mydb/product/.skeema", contents)
   972  	s.handleCommand(t, CodeSuccess, ".", "skeema push --allow-unsafe")
   973  	s.assertTableExists(t, "product1", "posts", "")
   974  	s.assertTableExists(t, "analytics", "posts", "")
   975  	s.assertTableMissing(t, "analytics", "pageviews", "")
   976  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   977  
   978  	// Since analytics is the first alphabetically, it is now the prototype
   979  	// as far as pull is concerned
   980  	s.dbExec(t, "analytics", "CREATE TABLE `foo` (id int)")
   981  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
   982  	fs.ReadTestFile(t, "mydb/product/foo.sql")                   // just confirming it exists
   983  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff") // since 3 schemas missing foo
   984  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
   985  	s.assertTableExists(t, "product1", "foo", "")
   986  	s.assertTableExists(t, "product2", "foo", "")
   987  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
   988  
   989  	// Test combination of ignore-schema and schema=*
   990  	fs.WriteTestFile(t, "mydb/product/foo2.sql", "CREATE TABLE `foo2` (id int);\n")
   991  	s.handleCommand(t, CodeSuccess, ".", "skeema push --ignore-schema=2$")
   992  	s.assertTableExists(t, "product1", "foo2", "")
   993  	s.assertTableMissing(t, "product2", "foo2", "")
   994  	s.assertTableExists(t, "product3", "foo2", "")
   995  }
   996  
   997  func (s SkeemaIntegrationSuite) TestFlavorConfig(t *testing.T) {
   998  	// Set up dir mydb to have flavor set, and then remove the flavor from
   999  	// the cached Instance, so that we can test the ability of the flavor option
  1000  	// to be a fallback.
  1001  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydb -h %s -P %d", s.d.Instance.Host, s.d.Instance.Port)
  1002  	dir, err := fs.ParseDir("mydb", cfg)
  1003  	if err != nil {
  1004  		t.Fatalf("Unexpected error from ParseDir: %s", err)
  1005  	}
  1006  	inst, err := dir.FirstInstance()
  1007  	if inst == nil || err != nil {
  1008  		t.Fatalf("No instances returned for %s: %s", dir, err)
  1009  	}
  1010  
  1011  	realFlavor := inst.Flavor()
  1012  	badFlavor := tengo.Flavor{Vendor: tengo.VendorUnknown, Major: 10, Minor: 3}
  1013  
  1014  	// diff should return no differences
  1015  	inst.ForceFlavor(badFlavor)
  1016  	s.handleCommand(t, CodeSuccess, "mydb", "skeema diff --debug")
  1017  
  1018  	// pull should keep the flavor override in place
  1019  	// (note that we need to keep re-forcing the bad flavor each time, since some
  1020  	// commands will forcibly override it using the dir config one!)
  1021  	inst.ForceFlavor(badFlavor)
  1022  	cfg = s.handleCommand(t, CodeSuccess, "mydb", "skeema pull --debug")
  1023  	s.verifyFiles(t, cfg, "../golden/init")
  1024  
  1025  	// Doing init again to new dir mydbnf, confirm no flavor in mydbnf/.skeema
  1026  	inst.ForceFlavor(badFlavor)
  1027  	var extraFlag string
  1028  	if realFlavor.HasDataDictionary() {
  1029  		// Very counter-intuitive, but we need to ensure init is reusing the
  1030  		// cached Instance that has param information_schema_stats_expiry=0 in the
  1031  		// DSN, since that's what "inst" points to! Supplying the flavor on the CLI
  1032  		// won't otherwise affect anything in init except for this one piece of logic
  1033  		// in fs.Dir.InstanceDefaultParams().
  1034  		// Note that supplying flavor on the CLI is undocumented. Behavior changes
  1035  		// may break this test in the future; find a less hacky solution here if so!
  1036  		extraFlag = " --flavor mysql:8.0"
  1037  	}
  1038  	s.handleCommand(t, CodeSuccess, ".", "skeema init --dir mydbnf -h %s -P %d%s", s.d.Instance.Host, s.d.Instance.Port, extraFlag)
  1039  	contents := fs.ReadTestFile(t, "mydbnf/.skeema")
  1040  	if strings.Contains(contents, "flavor") {
  1041  		t.Error("Expected init to skip flavor, but it was found")
  1042  	}
  1043  	fs.RemoveTestDirectory(t, "mydbnf")
  1044  
  1045  	// Restore the instance's correct flavor, and set a different flavor back in
  1046  	// mydb/.skeema. Confirm diff behavior unaffected, meaning the instance flavor
  1047  	// takes precedence over the dir one if both are known.
  1048  	inst.ForceFlavor(realFlavor)
  1049  	newFlavor := tengo.FlavorMariaDB103
  1050  	if realFlavor.Vendor == tengo.VendorMariaDB {
  1051  		newFlavor = tengo.FlavorMySQL57
  1052  	}
  1053  	contents = fs.ReadTestFile(t, "mydb/.skeema")
  1054  	if !strings.Contains(contents, realFlavor.String()) {
  1055  		t.Fatal("Could not find flavor line in mydb/.skeema")
  1056  	}
  1057  	contents = strings.Replace(contents, realFlavor.String(), newFlavor.String(), 1)
  1058  	fs.WriteTestFile(t, "mydb/.skeema", contents)
  1059  	s.handleCommand(t, CodeSuccess, "mydb", "skeema diff --debug")
  1060  
  1061  	// pull should fix flavor line
  1062  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull --debug")
  1063  	s.verifyFiles(t, cfg, "../golden/init")
  1064  }
  1065  
  1066  func (s SkeemaIntegrationSuite) TestRoutines(t *testing.T) {
  1067  	origCreate := `CREATE definer=root@localhost FUNCTION routine1(a int, b int)
  1068  RETURNS int
  1069  DETERMINISTIC
  1070  BEGIN
  1071  	return a * b;
  1072  END`
  1073  	create := origCreate
  1074  	s.dbExec(t, "product", create)
  1075  
  1076  	// Confirm init works properly with one function present
  1077  	s.reinitAndVerifyFiles(t, "", "../golden/routines")
  1078  
  1079  	// diff, pull, lint should all be no-ops at this point
  1080  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
  1081  	cfg := s.handleCommand(t, CodeSuccess, ".", "skeema pull")
  1082  	s.verifyFiles(t, cfg, "../golden/routines")
  1083  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema lint")
  1084  	s.verifyFiles(t, cfg, "../golden/routines")
  1085  
  1086  	// Change routine1.sql to use Windows-style CRLF line-end in one spot. No
  1087  	// diff should be present. Pull should restore UNIX-style LFs.
  1088  	routine1 := fs.ReadTestFile(t, "mydb/product/routine1.sql")
  1089  	fs.WriteTestFile(t, "mydb/product/routine1.sql", strings.Replace(routine1, "BEGIN\n", "BEGIN\r\n", 1))
  1090  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
  1091  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull")
  1092  	s.verifyFiles(t, cfg, "../golden/routines")
  1093  
  1094  	// Modify the db representation of the routine; diff/push should work, but only
  1095  	// with --allow-unsafe (and not with --safe-below-size)
  1096  	s.dbExec(t, "product", "DROP FUNCTION routine1")
  1097  	create = strings.Replace(create, "a * b", "b * a", 1)
  1098  	if create == origCreate {
  1099  		t.Fatal("Test setup incorrect")
  1100  	}
  1101  	s.dbExec(t, "product", create)
  1102  	s.handleCommand(t, CodeFatalError, ".", "skeema diff")
  1103  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --allow-unsafe")
  1104  	s.handleCommand(t, CodeFatalError, ".", "skeema push --safe-below-size=10000")
  1105  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema push --allow-unsafe")
  1106  	s.verifyFiles(t, cfg, "../golden/routines")
  1107  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
  1108  
  1109  	// Delete that file and do a pull; file should be back, even with
  1110  	// --skip-normalize
  1111  	fs.RemoveTestFile(t, "mydb/product/routine1.sql")
  1112  	cfg = s.handleCommand(t, CodeSuccess, ".", "skeema pull --skip-normalize")
  1113  	s.verifyFiles(t, cfg, "../golden/routines")
  1114  
  1115  	// Confirm changing the db's collation counts as a diff for routines if (and
  1116  	// only if) --compare-metadata is used
  1117  	s.dbExec(t, "", "ALTER DATABASE product DEFAULT COLLATE = latin1_general_ci")
  1118  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
  1119  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
  1120  	s.handleCommand(t, CodeFatalError, ".", "skeema diff --compare-metadata")
  1121  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema diff --compare-metadata --allow-unsafe")
  1122  	s.handleCommand(t, CodeSuccess, ".", "skeema push --compare-metadata --allow-unsafe")
  1123  	s.handleCommand(t, CodeSuccess, ".", "skeema diff --compare-metadata")
  1124  	s.d.CloseAll() // avoid mysql bug where ALTER DATABASE doesn't affect existing sessions
  1125  
  1126  	// Add a file creating another routine. Push it and confirm the routine is
  1127  	// using the sql_mode of the server.
  1128  	origContents := "CREATE FUNCTION routine2() returns varchar(30) DETERMINISTIC return 'abc''def';\n"
  1129  	fs.WriteTestFile(t, "mydb/product/routine2.sql", origContents)
  1130  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
  1131  	schema, err := s.d.Schema("product")
  1132  	if err != nil || schema == nil {
  1133  		t.Fatal("Unexpected error obtaining product schema")
  1134  	}
  1135  	funcs := schema.FunctionsByName()
  1136  	if r2, ok := funcs["routine2"]; !ok {
  1137  		t.Fatal("Unable to locate routine2")
  1138  	} else {
  1139  		var serverSQLMode string
  1140  		db, _ := s.d.Connect("", "")
  1141  		if err := db.QueryRow("SELECT @@global.sql_mode").Scan(&serverSQLMode); err != nil {
  1142  			t.Fatalf("Unexpected error querying sql_mode: %s", err)
  1143  		}
  1144  		if r2.SQLMode != serverSQLMode {
  1145  			t.Errorf("Expected routine2 to have sql_mode %s, instead found %s", serverSQLMode, r2.SQLMode)
  1146  		}
  1147  	}
  1148  
  1149  	// Lint that new file; confirm new formatting matches expectation.
  1150  	s.handleCommand(t, CodeDifferencesFound, ".", "skeema lint")
  1151  	normalizedContents := `CREATE DEFINER=~root~@~%~ FUNCTION ~routine2~() RETURNS varchar(30) CHARSET latin1 COLLATE latin1_general_ci
  1152      DETERMINISTIC
  1153  return 'abc''def';
  1154  `
  1155  	normalizedContents = strings.Replace(normalizedContents, "~", "`", -1)
  1156  	if contents := fs.ReadTestFile(t, "mydb/product/routine2.sql"); contents != normalizedContents {
  1157  		t.Errorf("Unexpected contents after linting; found:\n%s", contents)
  1158  	}
  1159  
  1160  	// Restore old formatting and test pull, with and without --normalize
  1161  	fs.WriteTestFile(t, "mydb/product/routine2.sql", origContents)
  1162  	s.handleCommand(t, CodeSuccess, ".", "skeema pull --skip-normalize")
  1163  	if contents := fs.ReadTestFile(t, "mydb/product/routine2.sql"); contents != origContents {
  1164  		t.Errorf("Expected contents unchanged from pull with --skip-normalize; instead found:\n%s", contents)
  1165  	}
  1166  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
  1167  	if contents := fs.ReadTestFile(t, "mydb/product/routine2.sql"); contents != normalizedContents {
  1168  		t.Errorf("Expected contents to be normalized from pull with --normalize; instead found:\n%s", contents)
  1169  	}
  1170  
  1171  	// Add a *procedure* called routine2. pull should place this in same file as
  1172  	// the function with same name.
  1173  	r2dupe := `CREATE PROCEDURE routine2(a int, b int)
  1174  BEGIN
  1175  	SELECT a;
  1176  	SELECT b;
  1177  END`
  1178  	s.dbExec(t, "product", r2dupe)
  1179  	s.handleCommand(t, CodeSuccess, ".", "skeema pull")
  1180  	normalizedContents += "DELIMITER //\nCREATE DEFINER=`root`@`%` PROCEDURE `routine2`(a int, b int)\nBEGIN\n\tSELECT a;\n\tSELECT b;\nEND//\nDELIMITER ;\n"
  1181  	if contents := fs.ReadTestFile(t, "mydb/product/routine2.sql"); contents != normalizedContents {
  1182  		t.Errorf("Unexpected contents after pull; expected:\n%s\nfound:\n%s", normalizedContents, contents)
  1183  	}
  1184  
  1185  	// diff and lint should both be no-ops
  1186  	s.handleCommand(t, CodeSuccess, ".", "skeema diff")
  1187  	s.handleCommand(t, CodeSuccess, ".", "skeema lint")
  1188  
  1189  	// Drop the *function* routine2 and do a push. This should properly recreate
  1190  	// the dropped func.
  1191  	s.dbExec(t, "product", "DROP FUNCTION routine2")
  1192  	s.handleCommand(t, CodeSuccess, ".", "skeema push")
  1193  	for _, otype := range []tengo.ObjectType{tengo.ObjectTypeFunc, tengo.ObjectTypeProc} {
  1194  		exists, phrase, err := s.objectExists("product", otype, "routine2", "")
  1195  		if !exists || err != nil {
  1196  			t.Errorf("Expected %s to exist, instead found %t, err=%v", phrase, exists, err)
  1197  		}
  1198  	}
  1199  }