github.com/iasthc/atlas/cmd/atlas@v0.0.0-20230523071841-73246df3f88d/internal/cmdapi/migrate_test.go (about) 1 // Copyright 2021-present The Atlas Authors. All rights reserved. 2 // This source code is licensed under the Apache 2.0 license found 3 // in the LICENSE file in the root directory of this source tree. 4 5 package cmdapi 6 7 import ( 8 "context" 9 "database/sql" 10 "encoding/base64" 11 "encoding/json" 12 "errors" 13 "fmt" 14 "io" 15 "net/http" 16 "net/http/httptest" 17 "net/url" 18 "os" 19 "os/exec" 20 "path/filepath" 21 "runtime" 22 "strings" 23 "testing" 24 "time" 25 26 "github.com/iasthc/atlas/cmd/atlas/internal/cloudapi" 27 "github.com/iasthc/atlas/cmd/atlas/internal/cmdlog" 28 migrate2 "github.com/iasthc/atlas/cmd/atlas/internal/migrate" 29 "github.com/iasthc/atlas/sql/migrate" 30 "github.com/iasthc/atlas/sql/schema" 31 "github.com/iasthc/atlas/sql/sqlclient" 32 "github.com/iasthc/atlas/sql/sqlite" 33 _ "github.com/iasthc/atlas/sql/sqlite" 34 _ "github.com/iasthc/atlas/sql/sqlite/sqlitecheck" 35 36 "github.com/fatih/color" 37 "github.com/google/uuid" 38 _ "github.com/mattn/go-sqlite3" 39 "github.com/stretchr/testify/require" 40 ) 41 42 func TestMigrate(t *testing.T) { 43 _, err := runCmd(migrateCmd()) 44 require.NoError(t, err) 45 } 46 47 func TestMigrate_Import(t *testing.T) { 48 for _, tool := range []string{"dbmate", "flyway", "golang-migrate", "goose", "liquibase"} { 49 p := t.TempDir() 50 t.Run(tool, func(t *testing.T) { // remove this once --dir-format is removed. Test is kept to ensure BC. 51 path := filepath.FromSlash("testdata/import/" + tool) 52 out, err := runCmd( 53 migrateImportCmd(), 54 "--from", "file://"+path, 55 "--to", "file://"+p, 56 "--dir-format", tool, 57 ) 58 require.NoError(t, err) 59 require.Zero(t, out) 60 61 path += "_gold" 62 ex, err := os.ReadDir(path) 63 require.NoError(t, err) 64 ac, err := os.ReadDir(p) 65 require.NoError(t, err) 66 require.Equal(t, len(ex)+1, len(ac)) // sum file 67 68 for i := range ex { 69 e, err := os.ReadFile(filepath.Join(path, ex[i].Name())) 70 require.NoError(t, err) 71 a, err := os.ReadFile(filepath.Join(p, ex[i].Name())) 72 require.NoError(t, err) 73 require.Equal(t, string(e), string(a)) 74 } 75 }) 76 p = t.TempDir() 77 t.Run(tool, func(t *testing.T) { 78 path := filepath.FromSlash("testdata/import/" + tool) 79 out, err := runCmd( 80 migrateImportCmd(), 81 "--from", fmt.Sprintf("file://%s?format=%s", path, tool), 82 "--to", "file://"+p, 83 ) 84 require.NoError(t, err) 85 require.Zero(t, out) 86 87 path += "_gold" 88 ex, err := os.ReadDir(path) 89 require.NoError(t, err) 90 ac, err := os.ReadDir(p) 91 require.NoError(t, err) 92 require.Equal(t, len(ex)+1, len(ac)) // sum file 93 94 for i := range ex { 95 e, err := os.ReadFile(filepath.Join(path, ex[i].Name())) 96 require.NoError(t, err) 97 a, err := os.ReadFile(filepath.Join(p, ex[i].Name())) 98 require.NoError(t, err) 99 require.Equal(t, string(e), string(a)) 100 } 101 }) 102 } 103 } 104 105 func TestMigrate_Apply(t *testing.T) { 106 var ( 107 p = t.TempDir() 108 ctx = context.Background() 109 ) 110 // Disable text coloring in testing 111 // to assert on string matching. 112 color.NoColor = true 113 114 // Fails on empty directory. 115 s, err := runCmd( 116 migrateApplyCmd(), 117 "--dir", "file://"+p, 118 "-u", openSQLite(t, ""), 119 ) 120 require.NoError(t, err) 121 require.Equal(t, "No migration files to execute\n", s) 122 123 // Fails on directory without sum file. 124 require.NoError(t, os.Rename( 125 filepath.FromSlash("testdata/sqlite/atlas.sum"), 126 filepath.FromSlash("testdata/sqlite/atlas.sum.bak"), 127 )) 128 t.Cleanup(func() { 129 os.Rename(filepath.FromSlash("testdata/sqlite/atlas.sum.bak"), filepath.FromSlash("testdata/sqlite/atlas.sum")) 130 }) 131 132 _, err = runCmd( 133 migrateApplyCmd(), 134 "--dir", "file://testdata/sqlite", 135 "--url", openSQLite(t, ""), 136 ) 137 require.ErrorIs(t, err, migrate.ErrChecksumNotFound) 138 require.NoError(t, os.Rename( 139 filepath.FromSlash("testdata/sqlite/atlas.sum.bak"), 140 filepath.FromSlash("testdata/sqlite/atlas.sum"), 141 )) 142 143 // A lock will prevent execution. 144 sqlclient.Register( 145 "sqlitelockapply", 146 sqlclient.OpenerFunc(func(ctx context.Context, u *url.URL) (*sqlclient.Client, error) { 147 client, err := sqlclient.Open(ctx, strings.Replace(u.String(), u.Scheme, "sqlite", 1)) 148 if err != nil { 149 return nil, err 150 } 151 client.Driver = &sqliteLockerDriver{client.Driver} 152 return client, nil 153 }), 154 sqlclient.RegisterDriverOpener(func(db schema.ExecQuerier) (migrate.Driver, error) { 155 drv, err := sqlite.Open(db) 156 if err != nil { 157 return nil, err 158 } 159 return &sqliteLockerDriver{drv}, nil 160 }), 161 ) 162 f, err := os.Create(filepath.Join(p, "test.db")) 163 require.NoError(t, err) 164 require.NoError(t, f.Close()) 165 166 s, err = runCmd( 167 migrateApplyCmd(), 168 "--dir", "file://testdata/sqlite", 169 "--url", fmt.Sprintf("sqlitelockapply://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 170 ) 171 require.ErrorIs(t, err, errLock) 172 require.True(t, strings.HasPrefix(s, "Error: acquiring database lock: "+errLock.Error())) 173 174 // Apply zero throws error. 175 for _, n := range []string{"-1", "0"} { 176 _, err = runCmd( 177 migrateApplyCmd(), 178 "--dir", "file://testdata/sqlite", 179 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 180 "--", n, 181 ) 182 require.EqualError(t, err, fmt.Sprintf("cannot apply '%s' migration files", n)) 183 } 184 185 // Will work and print stuff to the console. 186 s, err = runCmd( 187 migrateApplyCmd(), 188 "--dir", "file://testdata/sqlite", 189 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 190 "1", 191 ) 192 require.NoError(t, err) 193 require.Contains(t, s, "20220318104614") // log to version 194 require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // logs statement 195 require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // does not execute second file 196 require.Contains(t, s, "1 migrations") // logs amount of migrations 197 require.Contains(t, s, "1 sql statements") 198 199 // Transactions will be wrapped per file. If the second file has an error, first still is applied. 200 s, err = runCmd( 201 migrateApplyCmd(), 202 "--dir", "file://testdata/sqlite2", 203 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 204 ) 205 require.Error(t, err) 206 require.Contains(t, s, "20220318104614") // log to version 207 require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // logs statement 208 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // does execute first stmt first second file 209 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;") // does execute second stmt first second file 210 require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // but not third 211 require.Contains(t, s, "1 migrations ok (1 with errors)") // logs amount of migrations 212 require.Contains(t, s, "2 sql statements ok (1 with errors)") // logs amount of statement 213 require.Contains(t, s, "near \"asdasd\": syntax error") // logs error summary 214 215 c, err := sqlclient.Open(ctx, fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db"))) 216 require.NoError(t, err) 217 t.Cleanup(func() { 218 require.NoError(t, c.Close()) 219 }) 220 sch, err := c.InspectSchema(ctx, "", nil) 221 tbl, ok := sch.Table("tbl") 222 require.True(t, ok) 223 _, ok = tbl.Column("col_2") 224 require.False(t, ok) 225 _, ok = tbl.Column("col_3") 226 require.False(t, ok) 227 rrw, err := migrate2.NewEntRevisions(ctx, c) 228 require.NoError(t, err) 229 revs, err := rrw.ReadRevisions(ctx) 230 require.NoError(t, err) 231 require.Len(t, revs, 1) 232 233 // Running again will pick up the failed statement and try it again. 234 s, err = runCmd( 235 migrateApplyCmd(), 236 "--dir", "file://testdata/sqlite2", 237 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 238 ) 239 require.Error(t, err) 240 require.Contains(t, s, "20220318104614") // currently applied version 241 require.Contains(t, s, "20220318104615") // retry second (partially applied) 242 require.NotContains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // will not attempt stmts from first file 243 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // picks up first statement 244 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;") // does execute second stmt first second file 245 require.NotContains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // but not third 246 require.Contains(t, s, "0 migrations ok (1 with errors)") // logs amount of migrations 247 require.Contains(t, s, "1 sql statements ok (1 with errors)") // logs amount of statement 248 require.Contains(t, s, "near \"asdasd\": syntax error") // logs error summary 249 250 // Editing an applied line will raise error. 251 s, err = runCmd( 252 migrateApplyCmd(), 253 "--dir", "file://testdata/sqlite2", 254 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 255 "--tx-mode", "none", 256 ) 257 t.Cleanup(func() { 258 _ = os.RemoveAll("testdata/sqlite3") 259 }) 260 require.NoError(t, exec.Command("cp", "-r", "testdata/sqlite2", "testdata/sqlite3").Run()) 261 sed(t, "s/col_2/col_5/g", "testdata/sqlite3/20220318104615_second.sql") 262 _, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/sqlite3") 263 require.NoError(t, err) 264 s, err = runCmd( 265 migrateApplyCmd(), 266 "--dir", "file://testdata/sqlite3", 267 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 268 ) 269 require.ErrorAs(t, err, &migrate.HistoryChangedError{}) 270 271 // Fixing the migration file will finish without errors. 272 sed(t, "s/col_5/col_2/g", "testdata/sqlite3/20220318104615_second.sql") 273 sed(t, "s/asdasd //g", "testdata/sqlite3/20220318104615_second.sql") 274 _, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/sqlite3") 275 require.NoError(t, err) 276 s, err = runCmd( 277 migrateApplyCmd(), 278 "--dir", "file://testdata/sqlite3", 279 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 280 ) 281 require.NoError(t, err) 282 require.Contains(t, s, "20220318104615") // retry second (partially applied) 283 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_3` bigint;") // does execute second stmt first second file 284 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_4` bigint;") // does execute second stmt first second file 285 require.Contains(t, s, "1 migrations") // logs amount of migrations 286 require.Contains(t, s, "2") // logs amount of statement 287 require.NotContains(t, s, "Error: Execution had errors:") // logs error summary 288 require.NotContains(t, s, "near \"asdasd\": syntax error") // logs error summary 289 290 // Running again will report database being in clean state. 291 s, err = runCmd( 292 migrateApplyCmd(), 293 "--dir", "file://testdata/sqlite3", 294 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 295 ) 296 require.NoError(t, err) 297 require.Equal(t, "No migration files to execute\n", s) 298 299 // Dry run will print the statements in second migration file without executing them. 300 // No changes to the revisions will be done. 301 s, err = runCmd( 302 migrateApplyCmd(), 303 "--dir", "file://testdata/sqlite", 304 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 305 "--dry-run", 306 "1", 307 ) 308 require.NoError(t, err) 309 require.Contains(t, s, "20220318104615") // log to version 310 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // logs statement 311 c1, err := sqlclient.Open(ctx, fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db"))) 312 require.NoError(t, err) 313 t.Cleanup(func() { 314 require.NoError(t, c1.Close()) 315 }) 316 sch, err = c1.InspectSchema(ctx, "", nil) 317 tbl, ok = sch.Table("tbl") 318 require.True(t, ok) 319 _, ok = tbl.Column("col_2") 320 require.False(t, ok) 321 rrw, err = migrate2.NewEntRevisions(ctx, c1) 322 require.NoError(t, err) 323 revs, err = rrw.ReadRevisions(ctx) 324 require.NoError(t, err) 325 require.Len(t, revs, 1) 326 327 // Prerequisites for testing missing migration behavior. 328 c1, err = sqlclient.Open(ctx, fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db"))) 329 require.NoError(t, err) 330 t.Cleanup(func() { 331 require.NoError(t, c1.Close()) 332 }) 333 require.NoError(t, os.Rename( 334 "testdata/sqlite3/20220318104615_second.sql", 335 "testdata/sqlite3/20220318104616_second.sql", 336 )) 337 _, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/sqlite3") 338 require.NoError(t, err) 339 rrw, err = migrate2.NewEntRevisions(ctx, c1) 340 require.NoError(t, err) 341 require.NoError(t, rrw.Migrate(ctx)) 342 343 // No changes if the last revision has a greater version than the last migration. 344 require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "zzz"})) 345 s, err = runCmd( 346 migrateApplyCmd(), 347 "--dir", "file://testdata/sqlite3", 348 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")), 349 ) 350 require.NoError(t, err) 351 require.Equal(t, "No migration files to execute\n", s) 352 353 // If the revision is before the last but after the first migration, only the last one is pending. 354 _, err = c1.ExecContext(ctx, "DROP table `atlas_schema_revisions`") 355 require.NoError(t, err) 356 s, err = runCmd( 357 migrateApplyCmd(), "1", 358 "--dir", "file://testdata/sqlite3", 359 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")), 360 ) 361 require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "20220318104615"})) 362 require.NoError(t, err) 363 s, err = runCmd( 364 migrateApplyCmd(), 365 "--dir", "file://testdata/sqlite3", 366 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")), 367 ) 368 require.NoError(t, err) 369 require.NotContains(t, s, "20220318104614") // log to version 370 require.Contains(t, s, "20220318104616") // log to version 371 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // logs statement 372 373 // If the revision is before every migration file, every file is pending. 374 _, err = c1.ExecContext(ctx, "DROP table `atlas_schema_revisions`; DROP table `tbl`;") 375 require.NoError(t, err) 376 require.NoError(t, rrw.Migrate(ctx)) 377 require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "1"})) 378 require.NoError(t, err) 379 s, err = runCmd( 380 migrateApplyCmd(), 381 "--dir", "file://testdata/sqlite3", 382 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")), 383 ) 384 require.NoError(t, err) 385 require.Contains(t, s, "20220318104614") // log to version 386 require.Contains(t, s, "20220318104616") // log to version 387 require.Contains(t, s, "CREATE TABLE tbl (`col` int NOT NULL);") // logs statement 388 require.Contains(t, s, "ALTER TABLE `tbl` ADD `col_2` bigint;") // logs statement 389 390 // If the revision is partially applied, error out. 391 require.NoError(t, rrw.WriteRevision(ctx, &migrate.Revision{Version: "z", Description: "z", Total: 1})) 392 require.NoError(t, err) 393 _, err = runCmd( 394 migrateApplyCmd(), 395 "--dir", "file://testdata/sqlite3", 396 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")), 397 ) 398 require.EqualError(t, err, migrate.MissingMigrationError{Version: "z", Description: "z"}.Error()) 399 } 400 401 func TestMigrate_ApplyMultiEnv(t *testing.T) { 402 t.Run("FromVars", func(t *testing.T) { 403 p := t.TempDir() 404 h := ` 405 variable "urls" { 406 type = list(string) 407 } 408 409 env "local" { 410 for_each = toset(var.urls) 411 url = each.value 412 dev = "sqlite://ci?mode=memory&cache=shared&_fk=1" 413 migration { 414 dir = "file://testdata/sqlite" 415 } 416 } 417 ` 418 path := filepath.Join(p, "atlas.hcl") 419 err := os.WriteFile(path, []byte(h), 0600) 420 require.NoError(t, err) 421 cmd := migrateCmd() 422 cmd.AddCommand(migrateApplyCmd()) 423 s, err := runCmd( 424 cmd, "apply", 425 "-c", "file://"+path, 426 "--env", "local", 427 "--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")), 428 "--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 429 ) 430 require.NoError(t, err) 431 require.Equal(t, 2, strings.Count(s, "Migrating to version 20220318104615 (2 migrations in total)"), "execution per environment") 432 _, err = os.Stat(filepath.Join(p, "test1.db")) 433 require.NoError(t, err) 434 _, err = os.Stat(filepath.Join(p, "test2.db")) 435 require.NoError(t, err) 436 }) 437 438 t.Run("FromDataSrc", func(t *testing.T) { 439 var ( 440 h = ` 441 variable "url" { 442 type = string 443 } 444 445 locals { 446 string = "%test" 447 bool = true 448 int = 1 449 } 450 451 data "sql" "tenants" { 452 url = var.url 453 query = <<EOS 454 SELECT name FROM tenants 455 WHERE mode LIKE ? AND active = ? AND created = ? 456 EOS 457 # Pass all types of arguments. 458 args = [local.string, local.bool, local.int] 459 } 460 461 env "local" { 462 for_each = toset(data.sql.tenants.values) 463 url = "sqlite://file:${each.value}?cache=shared&_fk=1" 464 dev = "sqlite://ci?mode=memory&cache=shared&_fk=1" 465 migration { 466 dir = "file://testdata/sqlite" 467 } 468 log { 469 migrate { 470 apply = format( 471 "{{ json . | json_merge %q }}", 472 jsonencode({ 473 Tenant: each.value 474 }) 475 ) 476 } 477 } 478 } 479 ` 480 p = t.TempDir() 481 path = filepath.Join(p, "atlas.hcl") 482 dbs = []string{filepath.Join(p, "test1.db"), filepath.Join(p, "test2.db"), filepath.Join(p, "test3.db")} 483 ) 484 err := os.WriteFile(path, []byte(h), 0600) 485 require.NoError(t, err) 486 db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db"))) 487 require.NoError(t, err) 488 _, err = db.Exec("CREATE TABLE `tenants` (`name` TEXT, `mode` TEXT DEFAULT 'test', `active` BOOL DEFAULT TRUE, `created` INT DEFAULT 1);") 489 require.NoError(t, err) 490 _, err = db.Exec("INSERT INTO `tenants` (`name`) VALUES (?), (?), (?)", dbs[0], dbs[1], dbs[2]) 491 require.NoError(t, err) 492 493 cmd := migrateCmd() 494 cmd.AddCommand(migrateApplyCmd()) 495 s, err := runCmd( 496 cmd, "apply", 497 "-c", "file://"+path, 498 "--env", "local", 499 "--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")), 500 ) 501 require.NoError(t, err) 502 require.Equal(t, 3, strings.Count(s, `"Tenant"`)) 503 require.Equal(t, 3, strings.Count(s, `"Applied":[{"Applied":["CREATE TABLE tbl`), "execution per environment") 504 for i := range dbs { 505 _, err = os.Stat(dbs[i]) 506 require.NoError(t, err) 507 } 508 509 cmd = migrateCmd() 510 cmd.AddCommand(migrateApplyCmd()) 511 s, err = runCmd( 512 cmd, "apply", 513 "-c", "file://"+path, 514 "--env", "local", 515 "--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")), 516 ) 517 require.NoError(t, err) 518 for _, s := range strings.Split(s, "\n") { 519 var r struct { 520 Tenant string 521 cmdlog.MigrateApply 522 } 523 require.NoError(t, json.Unmarshal([]byte(s), &r)) 524 require.Empty(t, r.Pending) 525 require.Empty(t, r.Applied) 526 require.NotEmpty(t, r.Tenant) 527 require.Equal(t, "sqlite3", r.Driver) 528 } 529 _, err = db.Exec("INSERT INTO `tenants` (`name`) VALUES (NULL)") 530 require.NoError(t, err) 531 _, err = runCmd( 532 cmd, "apply", 533 "-c", "file://"+path, 534 "--env", "local", 535 "--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")), 536 ) 537 // Rows should represent real and consistent values. 538 require.EqualError(t, err, "data.sql.tenants: unsupported row type: <nil>") 539 540 _, err = db.Exec("DELETE FROM `tenants`") 541 require.NoError(t, err) 542 s, err = runCmd( 543 cmd, "apply", 544 "-c", "file://"+path, 545 "--env", "local", 546 "--var", fmt.Sprintf("url=sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "tenants.db")), 547 ) 548 // Empty list is expanded to zero blocks. 549 require.EqualError(t, err, `env "local" not defined in project file`) 550 }) 551 552 t.Run("TemplateDir", func(t *testing.T) { 553 var ( 554 h = ` 555 variable "path" { 556 type = string 557 } 558 559 data "template_dir" "migrations" { 560 path = var.path 561 vars = { 562 Env = atlas.env 563 } 564 } 565 566 env "dev" { 567 url = "sqlite://${atlas.env}?mode=memory&_fk=1" 568 migration { 569 dir = data.template_dir.migrations.url 570 } 571 } 572 573 env "prod" { 574 url = "sqlite://${atlas.env}?mode=memory&_fk=1" 575 migration { 576 dir = data.template_dir.migrations.url 577 } 578 } 579 ` 580 p = t.TempDir() 581 path = filepath.Join(p, "atlas.hcl") 582 ) 583 err := os.WriteFile(path, []byte(h), 0600) 584 require.NoError(t, err) 585 for _, e := range []string{"dev", "prod"} { 586 cmd := migrateCmd() 587 cmd.AddCommand(migrateApplyCmd()) 588 s, err := runCmd( 589 cmd, "apply", 590 "-c", "file://"+path, 591 "--env", e, 592 "--var", "path=testdata/templatedir", 593 ) 594 require.NoError(t, err) 595 require.Contains(t, s, "Migrating to version 2 (2 migrations in total):") 596 require.Contains(t, s, fmt.Sprintf("create table %s1 (c text);", e)) 597 require.Contains(t, s, fmt.Sprintf("create table %s2 (c text);", e)) 598 require.Contains(t, s, fmt.Sprintf("create table users_%s2 (c text);", e)) 599 } 600 }) 601 } 602 603 func TestMigrate_ApplyTxMode(t *testing.T) { 604 for _, mode := range []string{"none", "file", "all"} { 605 t.Run(mode, func(t *testing.T) { 606 p := t.TempDir() 607 // Apply the first 2 migrations. 608 s, err := runCmd( 609 migrateApplyCmd(), 610 "--dir", "file://testdata/sqlitetx", 611 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 612 "--tx-mode", mode, 613 "2", 614 ) 615 require.NoError(t, err) 616 require.NotEmpty(t, s) 617 db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db"))) 618 require.NoError(t, err) 619 var n int 620 require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n)) 621 require.Equal(t, 2, n) 622 623 // Apply the rest. 624 s, err = runCmd( 625 migrateApplyCmd(), 626 "--dir", "file://testdata/sqlitetx", 627 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 628 "--tx-mode", mode, 629 ) 630 require.NoError(t, err) 631 require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n)) 632 require.Equal(t, 2, n) 633 634 // For transactions check that the foreign keys are checked before the transaction is committed. 635 if mode != "none" { 636 // Apply the first 2 migrations for the faulty one. 637 s, err = runCmd( 638 migrateApplyCmd(), 639 "--dir", "file://testdata/sqlitetx2", 640 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db")), 641 "--tx-mode", mode, 642 "2", 643 ) 644 require.NoError(t, err) 645 require.NotEmpty(t, s) 646 db, err = sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db"))) 647 require.NoError(t, err) 648 require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n)) 649 require.Equal(t, 2, n) 650 651 // Add an existing constraint. 652 c, err := sqlclient.Open(context.Background(), fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db"))) 653 require.NoError(t, err) 654 _, err = c.ExecContext(context.Background(), "PRAGMA foreign_keys = off; INSERT INTO `friendships` (`user_id`, `friend_id`) VALUES (3,3);PRAGMA foreign_keys = on;") 655 require.NoError(t, err) 656 require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n)) 657 require.Equal(t, 3, n) 658 659 // Apply the rest, expect it to fail due to constraint error, but only the new one is reported. 660 s, err = runCmd( 661 migrateApplyCmd(), 662 "--dir", "file://testdata/sqlitetx2", 663 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test_2.db")), 664 "--tx-mode", mode, 665 ) 666 require.EqualError(t, err, "sql/sqlite: foreign key mismatch: [{tbl:friendships ref:users row:4 index:1}]") 667 require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM `friendships`").Scan(&n)) 668 require.Equal(t, 3, n) // was rolled back 669 } 670 }) 671 } 672 } 673 674 func TestMigrate_ApplyTxModeDirective(t *testing.T) { 675 for _, mode := range []string{txModeNone, txModeFile} { 676 u := openSQLite(t, "") 677 _, err := runCmd( 678 migrateApplyCmd(), 679 "--dir", "file://testdata/sqlitetx3", 680 "--url", u, 681 "--tx-mode", mode, 682 ) 683 require.EqualError(t, err, `sql/migrate: execute: executing statement "INSERT INTO t1 VALUES (1), (1);" from version "20220925094021": UNIQUE constraint failed: t1.a`) 684 db, err := sql.Open("sqlite3", strings.TrimPrefix(u, "sqlite://")) 685 require.NoError(t, err) 686 var n int 687 require.NoError(t, db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE name IN ('atlas_schema_revisions', 'users', 't1')").Scan(&n)) 688 require.Equal(t, 3, n) 689 require.NoError(t, db.Close()) 690 } 691 692 _, err := runCmd( 693 migrateApplyCmd(), 694 "--dir", "file://testdata/sqlitetx3", 695 "--url", "sqlite://txmode?mode=memory&_fk=1", 696 "--tx-mode", txModeAll, 697 ) 698 require.EqualError(t, err, `cannot set txmode directive to "none" in "20220925094021_second.sql" when txmode "all" is set globally`) 699 700 s, err := runCmd( 701 migrateApplyCmd(), 702 "--dir", "file://testdata/sqlitetx4", 703 "--url", "sqlite://txmode?mode=memory&_fk=1", 704 "--tx-mode", txModeAll, 705 "--log", "{{ .Error }}", 706 ) 707 require.EqualError(t, err, `unknown txmode "unknown" found in file directive "20220925094021_second.sql"`) 708 // Errors should be attached to the report. 709 require.Equal(t, s, `unknown txmode "unknown" found in file directive "20220925094021_second.sql"`) 710 } 711 712 func TestMigrate_ApplyBaseline(t *testing.T) { 713 t.Run("FromFlags", func(t *testing.T) { 714 p := t.TempDir() 715 // Run migration with baseline should store this revision in the database. 716 s, err := runCmd( 717 migrateApplyCmd(), 718 "--dir", "file://testdata/baseline1", 719 "--baseline", "1", 720 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")), 721 ) 722 require.NoError(t, err) 723 require.Contains(t, s, "No migration files to execute") 724 // Next run without baseline should run the migration from the baseline. 725 s, err = runCmd( 726 migrateApplyCmd(), 727 "--dir", "file://testdata/baseline1", 728 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")), 729 ) 730 require.NoError(t, err) 731 require.Contains(t, s, "No migration files to execute") 732 733 // Multiple migration files with baseline. 734 s, err = runCmd( 735 migrateApplyCmd(), 736 "--dir", "file://testdata/baseline2", 737 "--baseline", "1", 738 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 739 ) 740 require.NoError(t, err) 741 require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)") 742 743 // Run all migration files and skip baseline. 744 s, err = runCmd( 745 migrateApplyCmd(), 746 "--dir", "file://testdata/baseline2", 747 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test3.db")), 748 ) 749 require.NoError(t, err) 750 require.Contains(t, s, "Migrating to version 20220318104615 (3 migrations in total)") 751 }) 752 753 t.Run("FromConfig", func(t *testing.T) { 754 const h = ` 755 env "local" { 756 migration { 757 baseline = "1" 758 } 759 }` 760 p := t.TempDir() 761 path := filepath.Join(p, "atlas.hcl") 762 err := os.WriteFile(path, []byte(h), 0600) 763 require.NoError(t, err) 764 cmd := migrateCmd() 765 cmd.AddCommand(migrateApplyCmd()) 766 s, err := runCmd( 767 cmd, "apply", 768 "-c", "file://"+path, 769 "--env", "local", 770 "--dir", "file://testdata/baseline1", 771 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test1.db")), 772 ) 773 require.NoError(t, err) 774 require.Contains(t, s, "No migration files to execute") 775 776 cmd = migrateCmd() 777 cmd.AddCommand(migrateApplyCmd()) 778 s, err = runCmd( 779 cmd, "apply", 780 "-c", "file://"+path, 781 "--env", "local", 782 "--dir", "file://testdata/baseline2", 783 "--url", fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(p, "test2.db")), 784 ) 785 require.NoError(t, err) 786 require.Contains(t, s, "Migrating to version 20220318104615 from 1 (2 migrations in total)") 787 }) 788 } 789 790 func TestMigrate_ApplyCloudReport(t *testing.T) { 791 var ( 792 dir migrate.MemDir 793 status int 794 report cloudapi.ReportMigrationInput 795 srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 796 var m struct { 797 Query string `json:"query"` 798 Variables struct { 799 Input json.RawMessage `json:"input"` 800 } `json:"variables"` 801 } 802 require.NoError(t, json.NewDecoder(r.Body).Decode(&m)) 803 switch { 804 case strings.Contains(m.Query, "query"): 805 // Checksum before archiving. 806 hf, err := dir.Checksum() 807 require.NoError(t, err) 808 ht, err := hf.MarshalText() 809 require.NoError(t, err) 810 require.NoError(t, dir.WriteFile(migrate.HashFileName, ht)) 811 // Archive and send. 812 arc, err := migrate.ArchiveDir(&dir) 813 require.NoError(t, err) 814 fmt.Fprintf(w, `{"data":{"dir":{"content":%q}}}`, base64.StdEncoding.EncodeToString(arc)) 815 case strings.Contains(m.Query, "mutation"): 816 if status != 0 { 817 w.WriteHeader(status) 818 } 819 require.NoError(t, json.Unmarshal(m.Variables.Input, &report)) 820 default: 821 t.Fatalf("unexpected query: %s", m.Query) 822 } 823 })) 824 h = ` 825 variable "cloud_url" { 826 type = string 827 } 828 829 atlas { 830 cloud { 831 token = "token" 832 url = var.cloud_url 833 project = "example" 834 } 835 } 836 837 data "remote_dir" "migrations" { 838 name = "migrations" 839 } 840 841 env { 842 name = atlas.env 843 migration { 844 dir = data.remote_dir.migrations.url 845 } 846 } 847 ` 848 u = openSQLite(t, "") 849 p = t.TempDir() 850 path = filepath.Join(p, "atlas.hcl") 851 ) 852 t.Cleanup(srv.Close) 853 require.NoError(t, os.WriteFile(path, []byte(h), 0600)) 854 855 t.Run("NoPendingFiles", func(t *testing.T) { 856 cmd := migrateCmd() 857 cmd.AddCommand(migrateApplyCmd()) 858 s, err := runCmd( 859 cmd, "apply", 860 "-c", "file://"+path, 861 "--env", "local", 862 "--url", u, 863 "--var", "cloud_url="+srv.URL, 864 ) 865 require.NoError(t, err) 866 require.Equal(t, "No migration files to execute\n", s) 867 require.NotEmpty(t, report.Target.ID) 868 _, err = uuid.Parse(report.Target.ID) 869 require.NoError(t, err, "target id is not a valid uuid") 870 require.False(t, report.StartTime.IsZero()) 871 require.False(t, report.EndTime.IsZero()) 872 require.Equal(t, cloudapi.ReportMigrationInput{ 873 ProjectName: "example", 874 DirName: "migrations", 875 EnvName: "local", 876 AtlasVersion: "Atlas CLI - development", 877 StartTime: report.StartTime, 878 EndTime: report.EndTime, 879 Files: []cloudapi.DeployedFileInput{}, 880 Target: cloudapi.DeployedTargetInput{ 881 ID: report.Target.ID, // generated uuid 882 Schema: "main", 883 URL: u, 884 }, 885 Log: "No migration files to execute\n", 886 }, report) 887 }) 888 889 t.Run("WithFiles", func(t *testing.T) { 890 require.NoError(t, dir.WriteFile("1.sql", []byte("create table foo (id int)"))) 891 require.NoError(t, dir.WriteFile("2.sql", []byte("create table bar (id int)"))) 892 893 cmd := migrateCmd() 894 cmd.AddCommand(migrateApplyCmd()) 895 s, err := runCmd( 896 cmd, "apply", 897 "-c", "file://"+path, 898 "--env", "local", 899 "--url", u, 900 "--var", "cloud_url="+srv.URL, 901 ) 902 require.NoError(t, err) 903 // Reporting does not affect the output. 904 require.True(t, strings.HasPrefix(s, "Migrating to version 2 (2 migrations in total):")) 905 require.True(t, strings.HasSuffix(s, " -- 2 migrations \n -- 2 sql statements\n")) 906 require.Equal(t, "", report.FromVersion, "from empty database") 907 require.Equal(t, "2", report.ToVersion) 908 require.Equal(t, "2", report.CurrentVersion) 909 require.Len(t, report.Files, 2) 910 for i, n := range []string{"1.sql", "2.sql"} { 911 require.Equal(t, n, report.Files[i].Name) 912 require.Equal(t, 1, report.Files[i].Applied) 913 require.Zero(t, report.Files[i].Skipped) 914 require.Nil(t, report.Files[i].Error) 915 } 916 }) 917 918 t.Run("PrintError", func(t *testing.T) { 919 status = http.StatusInternalServerError 920 require.NoError(t, dir.WriteFile("3.sql", []byte("create table baz (id int)"))) 921 cmd := migrateCmd() 922 cmd.AddCommand(migrateApplyCmd()) 923 s, err := runCmd( 924 cmd, "apply", 925 "-c", "file://"+path, 926 "--env", "local", 927 "--url", u, 928 "--var", "cloud_url="+srv.URL, 929 ) 930 require.NoError(t, err) 931 // Reporting error should not affect the migration execution. 932 require.True(t, strings.HasSuffix(s, " -- 1 migrations \n -- 1 sql statements\nError: unexpected status code: 500\n")) 933 934 // Custom logging. 935 cmd = migrateCmd() 936 cmd.AddCommand(migrateApplyCmd()) 937 s, err = runCmd( 938 cmd, "apply", 939 "-c", "file://"+path, 940 "--env", "local", 941 "--url", u, 942 "--var", "cloud_url="+srv.URL, 943 "--format", "{{ .Env.Driver }}", 944 ) 945 require.NoError(t, err) 946 require.Equal(t, "sqlite3\nError: unexpected status code: 500\n", s) 947 }) 948 } 949 950 func TestMigrate_ApplyCloudReportSet(t *testing.T) { 951 var ( 952 dir migrate.MemDir 953 status int 954 report cloudapi.ReportMigrationSetInput 955 srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 956 var m struct { 957 Query string `json:"query"` 958 Variables struct { 959 Input json.RawMessage `json:"input"` 960 } `json:"variables"` 961 } 962 require.NoError(t, json.NewDecoder(r.Body).Decode(&m)) 963 switch { 964 case strings.Contains(m.Query, "query"): 965 // Checksum before archiving. 966 hf, err := dir.Checksum() 967 require.NoError(t, err) 968 ht, err := hf.MarshalText() 969 require.NoError(t, err) 970 require.NoError(t, dir.WriteFile(migrate.HashFileName, ht)) 971 // Archive and send. 972 arc, err := migrate.ArchiveDir(&dir) 973 require.NoError(t, err) 974 fmt.Fprintf(w, `{"data":{"dir":{"content":%q}}}`, base64.StdEncoding.EncodeToString(arc)) 975 case strings.Contains(m.Query, "mutation"): 976 if status != 0 { 977 w.WriteHeader(status) 978 } 979 require.NoError(t, json.Unmarshal(m.Variables.Input, &report)) 980 default: 981 t.Fatalf("unexpected query: %s", m.Query) 982 } 983 })) 984 h = ` 985 variable "cloud_url" { 986 type = string 987 } 988 989 atlas { 990 cloud { 991 token = "token" 992 url = var.cloud_url 993 project = "example" 994 } 995 } 996 997 data "remote_dir" "migration_set" { 998 name = "migration_set" 999 } 1000 1001 variable "urls" { 1002 type = list(string) 1003 default = [] 1004 } 1005 1006 env { 1007 name = atlas.env 1008 for_each = toset(var.urls) 1009 url = each.value 1010 migration { 1011 dir = data.remote_dir.migration_set.url 1012 } 1013 } 1014 ` 1015 p = t.TempDir() 1016 path = filepath.Join(p, "atlas.hcl") 1017 ) 1018 t.Cleanup(srv.Close) 1019 require.NoError(t, os.WriteFile(path, []byte(h), 0600)) 1020 1021 t.Run("MultipleEnvs", func(t *testing.T) { 1022 cmd := migrateCmd() 1023 cmd.AddCommand(migrateApplyCmd()) 1024 s, err := runCmd( 1025 cmd, "apply", 1026 "-c", "file://"+path, 1027 "--env", "local", 1028 "--var", fmt.Sprintf("urls=%s", openSQLite(t, "")), 1029 "--var", fmt.Sprintf("urls=%s", openSQLite(t, "")), 1030 "--var", "cloud_url="+srv.URL, 1031 ) 1032 require.NoError(t, err) 1033 require.Equal(t, "No migration files to execute\nNo migration files to execute\n", s) 1034 require.NotEmpty(t, report.ID) 1035 _, err = uuid.Parse(report.ID) 1036 require.NoError(t, err, "set id is not a valid uuid") 1037 require.False(t, report.StartTime.IsZero()) 1038 require.False(t, report.EndTime.IsZero()) 1039 require.Equal(t, 2, report.Planned) 1040 require.Len(t, report.Completed, 2) 1041 require.Nil(t, report.Error) 1042 1043 // Verify summary log. 1044 require.Len(t, report.Log, 3) 1045 top := report.Log[0] 1046 require.Equal(t, top.Text, "Start migration for 2 targets") 1047 require.Contains(t, top.Log[0].Text, "sqlite://file") 1048 require.Contains(t, top.Log[1].Text, "sqlite://file") 1049 require.Equal(t, report.Log[1].Text, "Run migration: 1") 1050 require.Contains(t, report.Log[1].Log[0].Text, "Target URL: sqlite://file") 1051 require.Contains(t, report.Log[1].Log[1].Text, "Migration directory: mem://migration_set") 1052 require.Equal(t, report.Log[2].Text, "Run migration: 2") 1053 require.Contains(t, report.Log[2].Log[0].Text, "Target URL: sqlite://file") 1054 require.Contains(t, report.Log[2].Log[1].Text, "Migration directory: mem://migration_set") 1055 }) 1056 1057 t.Run("Error", func(t *testing.T) { 1058 require.NoError(t, dir.WriteFile("1.sql", []byte("create table foo (id int)"))) 1059 require.NoError(t, dir.WriteFile("2.sql", []byte("create table bar (id int)"))) 1060 1061 cmd := migrateCmd() 1062 cmd.AddCommand(migrateApplyCmd()) 1063 s, err := runCmd( 1064 cmd, "apply", 1065 "-c", "file://"+path, 1066 "--env", "local", 1067 "--var", fmt.Sprintf("urls=%s", openSQLite(t, "create table bar (id int)")), 1068 "--var", fmt.Sprintf("urls=%s", openSQLite(t, "create table bar (id int)")), 1069 "--var", "cloud_url="+srv.URL, 1070 "--allow-dirty", 1071 ) 1072 require.EqualError(t, err, `sql/migrate: execute: executing statement "create table bar (id int)" from version "2": table bar already exists`) 1073 require.Contains(t, s, "Migrating to version 2 (2 migrations in total):") 1074 require.Contains(t, s, "Error: sql/migrate:") 1075 require.NotEmpty(t, report.ID) 1076 _, err = uuid.Parse(report.ID) 1077 require.NoError(t, err, "set id is not a valid uuid") 1078 require.False(t, report.StartTime.IsZero()) 1079 require.False(t, report.EndTime.IsZero()) 1080 require.Equal(t, 2, report.Planned) 1081 require.Len(t, report.Completed, 1) 1082 require.NotNil(t, report.Error) 1083 require.Equal(t, `Error: sql/migrate: execute: executing statement "create table bar (id int)" from version "2": table bar already exists`, *report.Error) 1084 1085 // Verify summary log. 1086 require.Len(t, report.Log, 2) 1087 top := report.Log[0] 1088 require.Equal(t, top.Text, "Start migration for 2 targets") 1089 require.Contains(t, top.Log[0].Text, "sqlite://file") 1090 require.Contains(t, top.Log[1].Text, "sqlite://file") 1091 require.Equal(t, report.Log[1].Text, "Run migration: 1") 1092 require.True(t, report.Log[1].Error) 1093 require.Contains(t, report.Log[1].Log[0].Text, "Target URL: sqlite://file") 1094 require.Contains(t, report.Log[1].Log[1].Text, "Migration directory: mem://migration_set") 1095 require.Equal(t, `Error: sql/migrate: execute: executing statement "create table bar (id int)" from version "2": table bar already exists`, report.Log[1].Log[2].Text) 1096 }) 1097 } 1098 1099 func TestMigrate_Diff(t *testing.T) { 1100 p := t.TempDir() 1101 to := hclURL(t) 1102 1103 // Will create migration directory if not existing. 1104 _, err := runCmd( 1105 migrateDiffCmd(), 1106 "name", 1107 "--dir", "file://"+filepath.Join(p, "migrations"), 1108 "--dev-url", openSQLite(t, ""), 1109 "--to", to, 1110 ) 1111 require.NoError(t, err) 1112 require.FileExists(t, filepath.Join(p, "migrations", fmt.Sprintf("%s_name.sql", time.Now().UTC().Format("20060102150405")))) 1113 1114 // Expect no clean dev error. 1115 p = t.TempDir() 1116 s, err := runCmd( 1117 migrateDiffCmd(), 1118 "name", 1119 "--dir", "file://"+p, 1120 "--dev-url", openSQLite(t, "create table t (c int);"), 1121 "--to", to, 1122 ) 1123 require.ErrorAs(t, err, new(*migrate.NotCleanError)) 1124 require.ErrorContains(t, err, "found table \"t\"") 1125 1126 // Works (on empty directory). 1127 s, err = runCmd( 1128 migrateDiffCmd(), 1129 "name", 1130 "--dir", "file://"+p, 1131 "--dev-url", openSQLite(t, ""), 1132 "--to", to, 1133 ) 1134 require.NoError(t, err) 1135 require.Zero(t, s) 1136 require.FileExists(t, filepath.Join(p, fmt.Sprintf("%s_name.sql", time.Now().UTC().Format("20060102150405")))) 1137 require.FileExists(t, filepath.Join(p, "atlas.sum")) 1138 1139 // A lock will prevent diffing. 1140 sqlclient.Register("sqlitelockdiff", sqlclient.OpenerFunc(func(ctx context.Context, u *url.URL) (*sqlclient.Client, error) { 1141 u.Scheme = "sqlite" 1142 client, err := sqlclient.OpenURL(ctx, u) 1143 if err != nil { 1144 return nil, err 1145 } 1146 client.Driver = &sqliteLockerDriver{Driver: client.Driver} 1147 return client, nil 1148 })) 1149 f, err := os.Create(filepath.Join(p, "test.db")) 1150 require.NoError(t, err) 1151 require.NoError(t, f.Close()) 1152 s, err = runCmd( 1153 migrateDiffCmd(), 1154 "name", 1155 "--dir", "file://"+t.TempDir(), 1156 "--dev-url", fmt.Sprintf("sqlitelockdiff://file:%s?cache=shared&_fk=1", filepath.Join(p, "test.db")), 1157 "--to", to, 1158 ) 1159 require.True(t, strings.HasPrefix(s, "Error: acquiring database lock: "+errLock.Error())) 1160 require.ErrorIs(t, err, errLock) 1161 1162 t.Run("Edit", func(t *testing.T) { 1163 p := t.TempDir() 1164 require.NoError(t, os.Setenv("EDITOR", "echo '-- Comment' >>")) 1165 t.Cleanup(func() { require.NoError(t, os.Unsetenv("EDITOR")) }) 1166 args := []string{ 1167 "--edit", 1168 "--dir", "file://" + p, 1169 "--dev-url", openSQLite(t, ""), 1170 "--to", to, 1171 } 1172 _, err := runCmd(migrateDiffCmd(), args...) 1173 files, err := os.ReadDir(p) 1174 require.NoError(t, err) 1175 require.Len(t, files, 2) 1176 b, err := os.ReadFile(filepath.Join(p, files[0].Name())) 1177 require.NoError(t, err) 1178 require.Contains(t, string(b), "CREATE") 1179 require.True(t, strings.HasSuffix(string(b), "-- Comment\n")) 1180 require.Equal(t, "atlas.sum", files[1].Name()) 1181 1182 // Second run will have no effect. 1183 _, err = runCmd(migrateDiffCmd(), args...) 1184 require.NoError(t, err) 1185 files, err = os.ReadDir(p) 1186 require.NoError(t, err) 1187 require.Len(t, files, 2) 1188 }) 1189 1190 t.Run("Format", func(t *testing.T) { 1191 for f, out := range map[string]string{ 1192 "{{sql .}}": "CREATE TABLE `t` (`c` int NULL);", 1193 `{{- sql . " " -}}`: "CREATE TABLE `t` (\n `c` int NULL\n);", 1194 "{{ sql . \"\t\" }}": "CREATE TABLE `t` (\n\t`c` int NULL\n);", 1195 "{{sql $ \" \t \"}}": "CREATE TABLE `t` (\n \t `c` int NULL\n);", 1196 } { 1197 p := t.TempDir() 1198 d, err := migrate.NewLocalDir(p) 1199 require.NoError(t, err) 1200 // Works with indentation. 1201 s, err = runCmd( 1202 migrateDiffCmd(), 1203 "name", 1204 "--dir", "file://"+p, 1205 "--dev-url", openSQLite(t, ""), 1206 "--to", openSQLite(t, "create table t (c int);"), 1207 "--format", f, 1208 ) 1209 require.NoError(t, err) 1210 require.Zero(t, s) 1211 files, err := d.Files() 1212 require.NoError(t, err) 1213 require.Len(t, files, 1) 1214 require.Equal(t, "-- Create \"t\" table\n"+out+"\n", string(files[0].Bytes())) 1215 } 1216 1217 // Invalid use of sql. 1218 s, err = runCmd( 1219 migrateDiffCmd(), 1220 "name", 1221 "--dir", "file://"+p, 1222 "--dev-url", openSQLite(t, ""), 1223 "--to", openSQLite(t, "create table t (c int);"), 1224 "--format", `{{ if . }}{{ sql . " " }}{{ end }}`, 1225 ) 1226 require.EqualError(t, err, `'sql' can only be used to indent statements. got: {{if .}}{{sql . " "}}{{end}}`) 1227 1228 // Valid template. 1229 p := t.TempDir() 1230 d, err := migrate.NewLocalDir(p) 1231 require.NoError(t, err) 1232 s, err = runCmd( 1233 migrateDiffCmd(), 1234 "name", 1235 "--dir", "file://"+p, 1236 "--dev-url", openSQLite(t, ""), 1237 "--to", openSQLite(t, "create table t (c int);"), 1238 "--format", `{{ range .Changes }}{{ .Cmd }}{{ end }}`, 1239 ) 1240 require.NoError(t, err) 1241 files, err := d.Files() 1242 require.NoError(t, err) 1243 require.Len(t, files, 1) 1244 require.Equal(t, "CREATE TABLE `t` (`c` int NULL)", string(files[0].Bytes())) 1245 }) 1246 1247 t.Run("ProjectFile", func(t *testing.T) { 1248 p := t.TempDir() 1249 h := ` 1250 variable "schema" { 1251 type = string 1252 } 1253 1254 variable "dir" { 1255 type = string 1256 } 1257 1258 variable "destructive" { 1259 type = bool 1260 default = false 1261 } 1262 1263 env "local" { 1264 src = "file://${var.schema}" 1265 dev = "sqlite://ci?mode=memory&_fk=1" 1266 migration { 1267 dir = "file://${var.dir}" 1268 } 1269 diff { 1270 skip { 1271 drop_column = !var.destructive 1272 } 1273 } 1274 } 1275 ` 1276 pathC := filepath.Join(p, "atlas.hcl") 1277 require.NoError(t, os.WriteFile(pathC, []byte(h), 0600)) 1278 pathS := filepath.Join(p, "schema.sql") 1279 require.NoError(t, os.WriteFile(pathS, []byte(`CREATE TABLE t(c1 int, c2 int);`), 0600)) 1280 pathD := t.TempDir() 1281 cmd := migrateCmd() 1282 cmd.AddCommand(migrateDiffCmd()) 1283 s, err := runCmd( 1284 cmd, "diff", "initial", 1285 "-c", "file://"+pathC, 1286 "--env", "local", 1287 "--var", "schema="+pathS, 1288 "--var", "dir="+pathD, 1289 ) 1290 require.NoError(t, err) 1291 require.Empty(t, s) 1292 d, err := migrate.NewLocalDir(pathD) 1293 require.NoError(t, err) 1294 files, err := d.Files() 1295 require.NoError(t, err) 1296 require.Len(t, files, 1) 1297 require.Equal(t, "-- Create \"t\" table\nCREATE TABLE `t` (`c1` int NULL, `c2` int NULL);\n", string(files[0].Bytes())) 1298 1299 // Drop column should be skipped. 1300 require.NoError(t, os.WriteFile(pathS, []byte(`CREATE TABLE t(c1 int);`), 0600)) 1301 cmd = migrateCmd() 1302 cmd.AddCommand(migrateDiffCmd()) 1303 s, err = runCmd( 1304 cmd, "diff", "no_change", 1305 "-c", "file://"+pathC, 1306 "--env", "local", 1307 "--var", "schema="+pathS, 1308 "--var", "dir="+pathD, 1309 ) 1310 require.NoError(t, err) 1311 require.Equal(t, "The migration directory is synced with the desired state, no changes to be made\n", s) 1312 files, err = d.Files() 1313 require.NoError(t, err) 1314 require.Len(t, files, 1) 1315 1316 // Column is dropped when destructive is true. 1317 cmd = migrateCmd() 1318 cmd.AddCommand(migrateDiffCmd()) 1319 s, err = runCmd( 1320 cmd, "diff", "second", 1321 "-c", "file://"+pathC, 1322 "--env", "local", 1323 "--var", "schema="+pathS, 1324 "--var", "dir="+pathD, 1325 "--var", "destructive=true", 1326 ) 1327 require.NoError(t, err) 1328 require.Empty(t, s) 1329 files, err = d.Files() 1330 require.NoError(t, err) 1331 require.Len(t, files, 2) 1332 }) 1333 } 1334 1335 func TestMigrate_StatusJSON(t *testing.T) { 1336 p := t.TempDir() 1337 s, err := runCmd( 1338 migrateStatusCmd(), 1339 "--dir", "file://"+p, 1340 "-u", openSQLite(t, ""), 1341 "--format", "{{ json .Env.Driver }}", 1342 ) 1343 require.NoError(t, err) 1344 require.Equal(t, `"sqlite3"`, s) 1345 } 1346 1347 func TestMigrate_Set(t *testing.T) { 1348 u := fmt.Sprintf("sqlite://file:%s?_fk=1", filepath.Join(t.TempDir(), "test.db")) 1349 _, err := runCmd( 1350 migrateApplyCmd(), 1351 "--dir", "file://testdata/sqlite", 1352 "--url", u, 1353 ) 1354 require.NoError(t, err) 1355 1356 s, err := runCmd( 1357 migrateSetCmd(), 1358 "--dir", "file://testdata/sqlite", 1359 "-u", u, 1360 "20220318104614", 1361 ) 1362 require.NoError(t, err) 1363 require.Equal(t, `Current version is 20220318104614 (1 removed): 1364 1365 - 20220318104615 (second) 1366 1367 `, s) 1368 s, err = runCmd( 1369 migrateSetCmd(), 1370 "--dir", "file://testdata/sqlite", 1371 "-u", u, 1372 "20220318104615", 1373 ) 1374 require.NoError(t, err) 1375 require.Equal(t, `Current version is 20220318104615 (1 set): 1376 1377 + 20220318104615 (second) 1378 1379 `, s) 1380 1381 s, err = runCmd( 1382 migrateSetCmd(), 1383 "--dir", "file://testdata/baseline1", 1384 "-u", u, 1385 ) 1386 require.NoError(t, err) 1387 require.Equal(t, `Current version is 1 (1 set, 2 removed): 1388 1389 + 1 (baseline) 1390 - 20220318104614 (initial) 1391 - 20220318104615 (second) 1392 1393 `, s) 1394 1395 s, err = runCmd( 1396 migrateSetCmd(), 1397 "--dir", filepath.Join("file://", t.TempDir()), // empty dir. 1398 "-u", u, 1399 ) 1400 require.NoError(t, err) 1401 require.Equal(t, `All revisions deleted (1 in total): 1402 1403 - 1 (baseline) 1404 1405 `, s) 1406 1407 // Empty database. 1408 u = fmt.Sprintf("sqlite://file:%s?_fk=1", filepath.Join(t.TempDir(), "test.db")) 1409 _, err = runCmd( 1410 migrateSetCmd(), 1411 "--dir", "file://testdata/sqlite", 1412 "-u", u, 1413 ) 1414 require.EqualError(t, err, "accepts 1 arg(s), received 0") 1415 1416 s, err = runCmd( 1417 migrateSetCmd(), 1418 "--dir", "file://testdata/sqlite", 1419 "-u", u, 1420 "20220318104614", 1421 ) 1422 require.NoError(t, err) 1423 require.Equal(t, `Current version is 20220318104614 (1 set): 1424 1425 + 20220318104614 (initial) 1426 1427 `, s) 1428 } 1429 1430 func TestMigrate_New(t *testing.T) { 1431 var ( 1432 p = t.TempDir() 1433 v = time.Now().UTC().Format("20060102150405") 1434 ) 1435 1436 s, err := runCmd(migrateNewCmd(), "--dir", "file://"+p) 1437 require.Zero(t, s) 1438 require.NoError(t, err) 1439 require.FileExists(t, filepath.Join(p, v+".sql")) 1440 require.FileExists(t, filepath.Join(p, "atlas.sum")) 1441 require.Equal(t, 2, countFiles(t, p)) 1442 1443 s, err = runCmd(migrateNewCmd(), "my-migration-file", "--dir", "file://"+p) 1444 require.Zero(t, s) 1445 require.NoError(t, err) 1446 require.FileExists(t, filepath.Join(p, v+"_my-migration-file.sql")) 1447 require.FileExists(t, filepath.Join(p, "atlas.sum")) 1448 require.Equal(t, 3, countFiles(t, p)) 1449 1450 p = t.TempDir() 1451 s, err = runCmd(migrateNewCmd(), "golang-migrate", "--dir", "file://"+p, "--dir-format", formatGolangMigrate) 1452 require.Zero(t, s) 1453 require.NoError(t, err) 1454 require.FileExists(t, filepath.Join(p, v+"_golang-migrate.up.sql")) 1455 require.FileExists(t, filepath.Join(p, v+"_golang-migrate.down.sql")) 1456 require.Equal(t, 3, countFiles(t, p)) 1457 1458 p = t.TempDir() 1459 s, err = runCmd(migrateNewCmd(), "goose", "--dir", "file://"+p+"?format="+formatGoose) 1460 require.Zero(t, s) 1461 require.NoError(t, err) 1462 require.FileExists(t, filepath.Join(p, v+"_goose.sql")) 1463 require.Equal(t, 2, countFiles(t, p)) 1464 1465 p = t.TempDir() 1466 s, err = runCmd(migrateNewCmd(), "flyway", "--dir", "file://"+p+"?format="+formatFlyway) 1467 require.Zero(t, s) 1468 require.NoError(t, err) 1469 require.FileExists(t, filepath.Join(p, fmt.Sprintf("V%s__%s.sql", v, formatFlyway))) 1470 require.FileExists(t, filepath.Join(p, fmt.Sprintf("U%s__%s.sql", v, formatFlyway))) 1471 require.Equal(t, 3, countFiles(t, p)) 1472 1473 p = t.TempDir() 1474 s, err = runCmd(migrateNewCmd(), "liquibase", "--dir", "file://"+p+"?format="+formatLiquibase) 1475 require.Zero(t, s) 1476 require.NoError(t, err) 1477 require.FileExists(t, filepath.Join(p, v+"_liquibase.sql")) 1478 require.Equal(t, 2, countFiles(t, p)) 1479 1480 p = t.TempDir() 1481 s, err = runCmd(migrateNewCmd(), "dbmate", "--dir", "file://"+p+"?format="+formatDBMate) 1482 require.Zero(t, s) 1483 require.NoError(t, err) 1484 require.FileExists(t, filepath.Join(p, v+"_dbmate.sql")) 1485 require.Equal(t, 2, countFiles(t, p)) 1486 1487 f := filepath.Join("testdata", "mysql", "new.sql") 1488 require.NoError(t, os.WriteFile(f, []byte("contents"), 0600)) 1489 t.Cleanup(func() { os.Remove(f) }) 1490 s, err = runCmd(migrateNewCmd(), "--dir", "file://testdata/mysql") 1491 require.NotZero(t, s) 1492 require.Error(t, err) 1493 1494 t.Run("Edit", func(t *testing.T) { 1495 p := t.TempDir() 1496 require.NoError(t, os.Setenv("EDITOR", "echo 'contents' >")) 1497 t.Cleanup(func() { require.NoError(t, os.Unsetenv("EDITOR")) }) 1498 s, err = runCmd(migrateNewCmd(), "--dir", "file://"+p, "--edit") 1499 files, err := os.ReadDir(p) 1500 require.NoError(t, err) 1501 require.Len(t, files, 2) 1502 b, err := os.ReadFile(filepath.Join(p, files[0].Name())) 1503 require.NoError(t, err) 1504 require.Equal(t, "contents\n", string(b)) 1505 require.Equal(t, "atlas.sum", files[1].Name()) 1506 }) 1507 } 1508 1509 func TestMigrate_Validate(t *testing.T) { 1510 // Without re-playing. 1511 s, err := runCmd(migrateValidateCmd(), "--dir", "file://testdata/mysql") 1512 require.Zero(t, s) 1513 require.NoError(t, err) 1514 1515 f := filepath.Join("testdata", "mysql", "new.sql") 1516 require.NoError(t, os.WriteFile(f, []byte("contents"), 0600)) 1517 t.Cleanup(func() { os.Remove(f) }) 1518 s, err = runCmd(migrateValidateCmd(), "--dir", "file://testdata/mysql") 1519 require.NotZero(t, s) 1520 require.Error(t, err) 1521 require.NoError(t, os.Remove(f)) 1522 1523 // Replay migration files if a dev-url is given. 1524 p := t.TempDir() 1525 require.NoError(t, os.WriteFile(filepath.Join(p, "1_initial.sql"), []byte("create table t1 (c1 int)"), 0644)) 1526 require.NoError(t, os.WriteFile(filepath.Join(p, "2_second.sql"), []byte("create table t2 (c2 int)"), 0644)) 1527 _, err = runCmd(migrateHashCmd(), "--dir", "file://"+p) 1528 require.NoError(t, err) 1529 s, err = runCmd( 1530 migrateValidateCmd(), 1531 "--dir", "file://"+p, 1532 "--dev-url", openSQLite(t, ""), 1533 ) 1534 require.Zero(t, s) 1535 require.NoError(t, err) 1536 1537 // Should fail since the files are not compatible with SQLite. 1538 _, err = runCmd(migrateValidateCmd(), "--dir", "file://testdata/mysql", "--dev-url", openSQLite(t, "")) 1539 require.Error(t, err) 1540 } 1541 1542 func TestMigrate_Hash(t *testing.T) { 1543 s, err := runCmd(migrateHashCmd(), "--dir", "file://testdata/mysql") 1544 require.Zero(t, s) 1545 require.NoError(t, err) 1546 1547 // Prints a warning if --force flag is still used. 1548 s, err = runCmd(migrateHashCmd(), "--dir", "file://testdata/mysql", "--force") 1549 require.NoError(t, err) 1550 require.Equal(t, "Flag --force has been deprecated, you can safely omit it.\n", s) 1551 1552 p := t.TempDir() 1553 err = copyFile(filepath.Join("testdata", "mysql", "20220318104614_initial.sql"), filepath.Join(p, "20220318104614_initial.sql")) 1554 require.NoError(t, err) 1555 1556 s, err = runCmd(migrateHashCmd(), "--dir", "file://"+p) 1557 require.Zero(t, s) 1558 require.NoError(t, err) 1559 require.FileExists(t, filepath.Join(p, "atlas.sum")) 1560 d, err := os.ReadFile(filepath.Join(p, "atlas.sum")) 1561 require.NoError(t, err) 1562 dir, err := migrate.NewLocalDir(p) 1563 require.NoError(t, err) 1564 sum, err := dir.Checksum() 1565 require.NoError(t, err) 1566 b, err := sum.MarshalText() 1567 require.NoError(t, err) 1568 require.Equal(t, d, b) 1569 1570 p = t.TempDir() 1571 require.NoError(t, copyFile( 1572 filepath.Join("testdata", "mysql", "20220318104614_initial.sql"), 1573 filepath.Join(p, "20220318104614_initial.sql"), 1574 )) 1575 s, err = runCmd(migrateHashCmd(), "--dir", "file://"+os.Getenv("MIGRATION_DIR")) 1576 require.NotZero(t, s) 1577 require.Error(t, err) 1578 } 1579 1580 func TestMigrate_Lint(t *testing.T) { 1581 p := t.TempDir() 1582 s, err := runCmd( 1583 migrateLintCmd(), 1584 "--dir", "file://"+p, 1585 "--dev-url", openSQLite(t, ""), 1586 "--latest", "1", 1587 ) 1588 require.NoError(t, err) 1589 require.Empty(t, s) 1590 1591 err = os.WriteFile(filepath.Join(p, "1.sql"), []byte("CREATE TABLE t(c int);"), 0600) 1592 require.NoError(t, err) 1593 err = os.WriteFile(filepath.Join(p, "2.sql"), []byte("DROP TABLE t;"), 0600) 1594 require.NoError(t, err) 1595 s, err = runCmd( 1596 migrateLintCmd(), 1597 "--dir", "file://"+p, 1598 "--dev-url", openSQLite(t, ""), 1599 "--latest", "1", 1600 ) 1601 require.Error(t, err) 1602 require.Equal(t, "2.sql: destructive changes detected:\n\n\tL1: Dropping table \"t\"\n\n", s) 1603 s, err = runCmd( 1604 migrateLintCmd(), 1605 "--dir", "file://"+p, 1606 "--dev-url", openSQLite(t, ""), 1607 "--latest", "1", 1608 "--log", "{{ range .Files }}{{ .Name }}{{ end }}", // Backward compatibility with old flag name. 1609 ) 1610 require.Error(t, err) 1611 require.Equal(t, "2.sql", s) 1612 1613 t.Run("FromConfig", func(t *testing.T) { 1614 cfg := filepath.Join(p, "atlas.hcl") 1615 err := os.WriteFile(cfg, []byte(` 1616 variable "error" { 1617 type = bool 1618 default = false 1619 } 1620 1621 lint { 1622 latest = 1 1623 destructive { 1624 error = var.error 1625 } 1626 } 1627 `), 0600) 1628 require.NoError(t, err) 1629 cmd := migrateCmd() 1630 cmd.AddCommand(migrateLintCmd()) 1631 s, err := runCmd( 1632 cmd, "lint", 1633 "--dir", "file://"+p, 1634 "--dev-url", openSQLite(t, ""), 1635 "-c", "file://"+cfg, 1636 ) 1637 require.NoError(t, err) 1638 require.Equal(t, "2.sql: destructive changes detected:\n\n\tL1: Dropping table \"t\"\n\n", s) 1639 1640 cmd = migrateCmd() 1641 cmd.AddCommand(migrateLintCmd()) 1642 s, err = runCmd( 1643 cmd, "lint", 1644 "--dir", "file://"+p, 1645 "--dev-url", openSQLite(t, ""), 1646 "-c", "file://"+cfg, 1647 "--var", "error=true", 1648 ) 1649 require.Error(t, err) 1650 require.Equal(t, "2.sql: destructive changes detected:\n\n\tL1: Dropping table \"t\"\n\n", s) 1651 }) 1652 1653 // Change files to golang-migrate format. 1654 require.NoError(t, os.Rename(filepath.Join(p, "1.sql"), filepath.Join(p, "1.up.sql"))) 1655 require.NoError(t, os.Rename(filepath.Join(p, "2.sql"), filepath.Join(p, "1.down.sql"))) 1656 s, err = runCmd( 1657 migrateLintCmd(), 1658 "--dir", "file://"+p+"?format="+formatGolangMigrate, 1659 "--dev-url", openSQLite(t, ""), 1660 "--latest", "2", 1661 "--format", "{{ range .Files }}{{ .Name }}:{{ len .Reports }}{{ end }}", 1662 ) 1663 require.NoError(t, err) 1664 require.Equal(t, "1.up.sql:0", s) 1665 s, err = runCmd( 1666 migrateLintCmd(), 1667 "--dir", "file://"+p+"?format="+formatGolangMigrate, 1668 "--dev-url", openSQLite(t, ""), 1669 "--latest", "2", 1670 "--format", "{{ range .Files }}{{ .Name }}:{{ len .Reports }}{{ end }}", 1671 "--dir-format", formatGolangMigrate, 1672 ) 1673 require.NoError(t, err) 1674 require.Equal(t, "1.up.sql:0", s) 1675 1676 // Invalid files. 1677 err = os.WriteFile(filepath.Join(p, "2.up.sql"), []byte("BORING"), 0600) 1678 require.NoError(t, err) 1679 s, err = runCmd( 1680 migrateLintCmd(), 1681 "--dir", "file://"+p+"?format="+formatGolangMigrate, 1682 "--dev-url", openSQLite(t, ""), 1683 "--latest", "1", 1684 ) 1685 require.Error(t, err) 1686 require.Equal(t, "2.up.sql: executing statement: near \"BORING\": syntax error\n", s) 1687 } 1688 1689 const testSchema = ` 1690 schema "main" { 1691 } 1692 1693 table "table" { 1694 schema = schema.main 1695 column "col" { 1696 type = int 1697 comment = "column comment" 1698 } 1699 column "age" { 1700 type = int 1701 } 1702 column "price1" { 1703 type = int 1704 } 1705 column "price2" { 1706 type = int 1707 } 1708 column "account_name" { 1709 type = varchar(32) 1710 null = true 1711 } 1712 column "created_at" { 1713 type = datetime 1714 default = sql("current_timestamp") 1715 } 1716 primary_key { 1717 columns = [table.table.column.col] 1718 } 1719 index "index" { 1720 unique = true 1721 columns = [ 1722 table.table.column.col, 1723 table.table.column.age, 1724 ] 1725 comment = "index comment" 1726 } 1727 foreign_key "accounts" { 1728 columns = [ 1729 table.table.column.account_name, 1730 ] 1731 ref_columns = [ 1732 table.accounts.column.name, 1733 ] 1734 on_delete = SET_NULL 1735 on_update = "NO_ACTION" 1736 } 1737 check "positive price" { 1738 expr = "price1 > 0" 1739 } 1740 check { 1741 expr = "price1 <> price2" 1742 enforced = true 1743 } 1744 check { 1745 expr = "price2 <> price1" 1746 enforced = false 1747 } 1748 comment = "table comment" 1749 } 1750 1751 table "accounts" { 1752 schema = schema.main 1753 column "name" { 1754 type = varchar(32) 1755 } 1756 column "unsigned_float" { 1757 type = float(10) 1758 unsigned = true 1759 } 1760 column "unsigned_decimal" { 1761 type = decimal(10, 2) 1762 unsigned = true 1763 } 1764 primary_key { 1765 columns = [table.accounts.column.name] 1766 } 1767 }` 1768 1769 func hclURL(t *testing.T) string { 1770 p := t.TempDir() 1771 require.NoError(t, os.WriteFile(filepath.Join(p, "schema.hcl"), []byte(testSchema), 0600)) 1772 return "file://" + filepath.Join(p, "schema.hcl") 1773 } 1774 1775 func copyFile(src, dst string) error { 1776 sf, err := os.Open(src) 1777 if err != nil { 1778 return err 1779 } 1780 defer sf.Close() 1781 df, err := os.Create(dst) 1782 if err != nil { 1783 return err 1784 } 1785 defer df.Close() 1786 _, err = io.Copy(df, sf) 1787 return err 1788 } 1789 1790 type sqliteLockerDriver struct{ migrate.Driver } 1791 1792 var errLock = errors.New("lockErr") 1793 1794 func (d *sqliteLockerDriver) Lock(context.Context, string, time.Duration) (schema.UnlockFunc, error) { 1795 return func() error { return nil }, errLock 1796 } 1797 1798 func (d *sqliteLockerDriver) Snapshot(ctx context.Context) (migrate.RestoreFunc, error) { 1799 return d.Driver.(migrate.Snapshoter).Snapshot(ctx) 1800 } 1801 1802 func (d *sqliteLockerDriver) CheckClean(ctx context.Context, revT *migrate.TableIdent) error { 1803 return d.Driver.(migrate.CleanChecker).CheckClean(ctx, revT) 1804 } 1805 1806 func countFiles(t *testing.T, p string) int { 1807 files, err := os.ReadDir(p) 1808 require.NoError(t, err) 1809 return len(files) 1810 } 1811 1812 func sed(t *testing.T, r, p string) { 1813 args := []string{"-i"} 1814 if runtime.GOOS == "darwin" { 1815 args = append(args, ".bk") 1816 } 1817 buf, err := exec.Command("sed", append(args, r, p)...).CombinedOutput() 1818 require.NoError(t, err, string(buf)) 1819 }