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 }