github.com/iasthc/atlas/cmd/atlas@v0.0.0-20230523071841-73246df3f88d/internal/cmdapi/schema_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/json" 11 "fmt" 12 "net/url" 13 "os" 14 "path" 15 "path/filepath" 16 "strings" 17 "testing" 18 19 "github.com/iasthc/atlas/cmd/atlas/internal/cmdlog" 20 "github.com/iasthc/atlas/sql/migrate" 21 "github.com/iasthc/atlas/sql/schema" 22 "github.com/iasthc/atlas/sql/sqlclient" 23 "github.com/stretchr/testify/require" 24 ) 25 26 const ( 27 unformatted = `block "x" { 28 x = 1 29 y = 2 30 } 31 ` 32 formatted = `block "x" { 33 x = 1 34 y = 2 35 } 36 ` 37 ) 38 39 func TestSchema_Diff(t *testing.T) { 40 // Creates the missing table. 41 s, err := runCmd( 42 schemaDiffCmd(), 43 "--from", openSQLite(t, ""), 44 "--to", openSQLite(t, "create table t1 (id int);"), 45 ) 46 require.NoError(t, err) 47 require.EqualValues(t, "-- Create \"t1\" table\nCREATE TABLE `t1` (`id` int NULL);\n", s) 48 49 // Format indentation one table. 50 s, err = runCmd( 51 schemaDiffCmd(), 52 "--from", openSQLite(t, ""), 53 "--to", openSQLite(t, "create table t1 (id int);"), 54 "--format", `{{ sql . " " }}`, 55 ) 56 require.NoError(t, err) 57 require.EqualValues(t, "-- Create \"t1\" table\nCREATE TABLE `t1` (\n `id` int NULL\n);\n", s) 58 59 // No changes. 60 s, err = runCmd( 61 schemaDiffCmd(), 62 "--from", openSQLite(t, ""), 63 "--to", openSQLite(t, ""), 64 ) 65 require.NoError(t, err) 66 require.EqualValues(t, "Schemas are synced, no changes to be made.\n", s) 67 68 // Format no changes. 69 s, err = runCmd( 70 schemaDiffCmd(), 71 "--from", openSQLite(t, ""), 72 "--to", openSQLite(t, ""), 73 "--format", `{{ sql . " " }}`, 74 ) 75 require.NoError(t, err) 76 require.Empty(t, s) 77 78 // Desired state from migration directory requires dev database. 79 _, err = runCmd( 80 schemaDiffCmd(), 81 "--from", "file://testdata/sqlite", 82 "--to", openSQLite(t, ""), 83 ) 84 require.EqualError(t, err, "--dev-url cannot be empty") 85 86 // Desired state from migration directory. 87 s, err = runCmd( 88 schemaDiffCmd(), 89 "--from", openSQLite(t, ""), 90 "--to", "file://testdata/sqlite", 91 "--dev-url", openSQLite(t, ""), 92 ) 93 require.NoError(t, err) 94 require.EqualValues(t, "-- Create \"tbl\" table\nCREATE TABLE `tbl` (`col` int NOT NULL, `col_2` bigint NULL);\n", s) 95 96 // Desired state from migration directory. 97 s, err = runCmd( 98 schemaDiffCmd(), 99 "--from", openSQLite(t, ""), 100 "--to", "file://testdata/sqlite", 101 "--dev-url", openSQLite(t, ""), 102 ) 103 require.NoError(t, err) 104 require.EqualValues(t, "-- Create \"tbl\" table\nCREATE TABLE `tbl` (`col` int NOT NULL, `col_2` bigint NULL);\n", s) 105 106 // Current state from migration directory, desired state from HCL - synced. 107 p := filepath.Join(t.TempDir(), "schema.hcl") 108 require.NoError(t, os.WriteFile(p, []byte(`schema "main" {} 109 table "tbl" { 110 schema = schema.main 111 column "col" { 112 type = int 113 } 114 column "col_2" { 115 type = bigint 116 null = true 117 } 118 }`), 0644)) 119 s, err = runCmd( 120 schemaDiffCmd(), 121 "--from", "file://testdata/sqlite", 122 "--to", "file://"+p, 123 "--dev-url", openSQLite(t, ""), 124 ) 125 require.NoError(t, err) 126 require.EqualValues(t, "Schemas are synced, no changes to be made.\n", s) 127 128 // Current state from migration directory, desired state from HCL - missing column. 129 p = filepath.Join(t.TempDir(), "schema.hcl") 130 require.NoError(t, os.WriteFile(p, []byte(`schema "main" {} 131 table "tbl" { 132 schema = schema.main 133 column "col" { 134 type = int 135 } 136 column "col_2" { 137 type = bigint 138 null = true 139 } 140 column "col_3" { 141 type = text 142 } 143 }`), 0644)) 144 s, err = runCmd( 145 schemaDiffCmd(), 146 "--from", "file://testdata/sqlite", 147 "--to", "file://"+p, 148 "--dev-url", openSQLite(t, ""), 149 ) 150 require.NoError(t, err) 151 require.EqualValues( 152 t, 153 "-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n", 154 s, 155 ) 156 157 // Current state from migration directory with version, desired state from HCL - two missing columns. 158 s, err = runCmd( 159 schemaDiffCmd(), 160 "--from", "file://testdata/sqlite?version=20220318104614", 161 "--to", "file://"+p, 162 "--dev-url", openSQLite(t, ""), 163 ) 164 require.NoError(t, err) 165 require.EqualValues( 166 t, 167 "-- Add column \"col_2\" to table: \"tbl\"\n"+ 168 "ALTER TABLE `tbl` ADD COLUMN `col_2` bigint NULL;\n"+ 169 "-- Add column \"col_3\" to table: \"tbl\"\n"+ 170 "ALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n", 171 s, 172 ) 173 174 // Current state from migration directory, desired state from multi file HCL - missing column. 175 p = t.TempDir() 176 var ( 177 one = filepath.Join(p, "one.hcl") 178 two = filepath.Join(p, "two.hcl") 179 ) 180 require.NoError(t, os.WriteFile(one, []byte(`table "tbl" { 181 schema = schema.main 182 column "col" { 183 type = int 184 } 185 column "col_2" { 186 type = bigint 187 null = true 188 } 189 column "col_3" { 190 type = text 191 } 192 }`), 0644)) 193 require.NoError(t, os.WriteFile(two, []byte(`schema "main" {}`), 0644)) 194 s, err = runCmd( 195 schemaDiffCmd(), 196 "--from", "file://testdata/sqlite", 197 "--to", "file://"+p, 198 "--dev-url", openSQLite(t, ""), 199 ) 200 require.NoError(t, err) 201 require.EqualValues( 202 t, 203 "-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n", 204 s, 205 ) 206 s, err = runCmd( 207 schemaDiffCmd(), 208 "--from", "file://testdata/sqlite", 209 "--to", "file://"+one, 210 "--to", "file://"+two, 211 "--dev-url", openSQLite(t, ""), 212 ) 213 require.NoError(t, err) 214 require.EqualValues( 215 t, 216 "-- Add column \"col_3\" to table: \"tbl\"\nALTER TABLE `tbl` ADD COLUMN `col_3` text NOT NULL;\n", 217 s, 218 ) 219 220 t.Run("FromConfig", func(t *testing.T) { 221 var ( 222 p = t.TempDir() 223 cp = filepath.Join(p, "atlas.hcl") 224 sp = filepath.Join(p, "schema.hcl") 225 cfg = fmt.Sprintf(` 226 env "local" { 227 dev = "%s" 228 format { 229 schema { 230 diff = "{{ sql . \"\t\" }}" 231 } 232 } 233 }`, openSQLite(t, "")) 234 ) 235 require.NoError(t, os.WriteFile(cp, []byte(cfg), 0600)) 236 require.NoError(t, os.WriteFile(sp, []byte(` 237 schema "main" {} 238 table "users" { 239 schema = schema.main 240 column "id" { 241 type = int 242 } 243 } 244 `), 0600)) 245 246 cmd := schemaCmd() 247 cmd.AddCommand(schemaDiffCmd()) 248 s, err := runCmd( 249 cmd, "diff", 250 "-c", "file://"+cp, 251 "--env", "local", 252 "--to", "file://"+sp, 253 "--from", openSQLite(t, ""), 254 ) 255 require.NoError(t, err) 256 require.Equal(t, "-- Create \"users\" table\nCREATE TABLE `users` (\n\t`id` int NOT NULL\n);\n", s) 257 }) 258 259 t.Run("SkipChanges", func(t *testing.T) { 260 var ( 261 p = t.TempDir() 262 cfg = filepath.Join(p, "atlas.hcl") 263 ) 264 err = os.WriteFile(cfg, []byte(` 265 variable "destructive" { 266 type = bool 267 default = false 268 } 269 270 env "local" { 271 diff { 272 skip { 273 drop_table = !var.destructive 274 } 275 } 276 } 277 `), 0600) 278 require.NoError(t, err) 279 280 // Skip destructive changes. 281 cmd := schemaCmd() 282 cmd.AddCommand(schemaDiffCmd()) 283 s, err := runCmd( 284 cmd, "diff", 285 "-c", "file://"+cfg, 286 "--from", openSQLite(t, "create table users (id int);"), 287 "--to", openSQLite(t, ""), 288 "--env", "local", 289 ) 290 require.NoError(t, err) 291 require.Equal(t, "Schemas are synced, no changes to be made.\n", s) 292 293 // Apply destructive changes. 294 cmd = schemaCmd() 295 cmd.AddCommand(schemaDiffCmd()) 296 s, err = runCmd( 297 cmd, "diff", 298 "-c", "file://"+cfg, 299 "--from", openSQLite(t, "create table users (id int);"), 300 "--to", openSQLite(t, ""), 301 "--env", "local", 302 "--var", "destructive=true", 303 ) 304 require.NoError(t, err) 305 lines := strings.Split(strings.TrimSpace(s), "\n") 306 require.Equal(t, []string{ 307 "-- Disable the enforcement of foreign-keys constraints", 308 "PRAGMA foreign_keys = off;", 309 `-- Drop "users" table`, 310 "DROP TABLE `users`;", 311 "-- Enable back the enforcement of foreign-keys constraints", 312 "PRAGMA foreign_keys = on;", 313 }, lines) 314 }) 315 } 316 317 func TestSchema_Apply(t *testing.T) { 318 const drvName = "checknormalizer" 319 // If no dev-database is given, there must not be a call to Driver.Normalize. 320 sqlclient.Register( 321 drvName, 322 sqlclient.OpenerFunc(func(ctx context.Context, url *url.URL) (*sqlclient.Client, error) { 323 url.Scheme = "sqlite" 324 c, err := sqlclient.OpenURL(ctx, url) 325 if err != nil { 326 return nil, err 327 } 328 c.Driver = &assertNormalizerDriver{t: t, Driver: c.Driver} 329 return c, nil 330 }), 331 ) 332 333 p := filepath.Join(t.TempDir(), "schema.hcl") 334 require.NoError(t, os.WriteFile(p, []byte(`schema "my_schema" {}`), 0644)) 335 _, _ = runCmd( 336 schemaApplyCmd(), 337 "--url", drvName+"://?mode=memory", 338 "-f", p, 339 ) 340 } 341 342 func TestSchema_ApplyMultiEnv(t *testing.T) { 343 p := t.TempDir() 344 cfg := filepath.Join(p, "atlas.hcl") 345 src := filepath.Join(p, "schema.hcl") 346 err := os.WriteFile(cfg, []byte(` 347 variable "urls" { 348 type = list(string) 349 } 350 351 variable "src" { 352 type = string 353 } 354 355 env "local" { 356 for_each = toset(var.urls) 357 url = each.value 358 src = var.src 359 }`), 0600) 360 require.NoError(t, err) 361 err = os.WriteFile(src, []byte(` 362 schema "main" {} 363 364 table "users" { 365 schema = schema.main 366 column "id" { 367 type = int 368 } 369 } 370 `), 0600) 371 require.NoError(t, err) 372 db1, db2 := filepath.Join(p, "test1.db"), filepath.Join(p, "test2.db") 373 cmd := schemaCmd() 374 cmd.AddCommand(schemaApplyCmd()) 375 s, err := runCmd( 376 cmd, "apply", 377 "-c", "file://"+cfg, 378 "--env", "local", 379 "--var", fmt.Sprintf("src=file://%s", src), 380 "--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db1), 381 "--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db2), 382 "--auto-approve", 383 ) 384 require.NoError(t, err) 385 require.Equal(t, 2, strings.Count(s, "CREATE TABLE `users` (`id` int NOT NULL)")) 386 _, err = os.Stat(db1) 387 require.NoError(t, err) 388 _, err = os.Stat(db2) 389 require.NoError(t, err) 390 391 cmd = schemaCmd() 392 cmd.AddCommand(schemaApplyCmd()) 393 s, err = runCmd( 394 cmd, "apply", 395 "-c", "file://"+cfg, 396 "--env", "local", 397 "--var", fmt.Sprintf("src=file://%s", src), 398 "--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db1), 399 "--var", fmt.Sprintf("urls=sqlite://file:%s?cache=shared&_fk=1", db2), 400 "--auto-approve", 401 ) 402 require.NoError(t, err) 403 require.Equal(t, 2, strings.Count(s, "Schema is synced, no changes to be made")) 404 } 405 406 func TestSchema_ApplyLog(t *testing.T) { 407 t.Run("DryRun", func(t *testing.T) { 408 db := openSQLite(t, "") 409 cmd := schemaCmd() 410 cmd.AddCommand(schemaApplyCmd()) 411 s, err := runCmd( 412 cmd, "apply", 413 "-u", db, 414 "--to", openSQLite(t, ""), 415 "--dry-run", 416 "--format", "{{ json .Changes }}", 417 ) 418 require.NoError(t, err) 419 require.Equal(t, "{}", s) 420 421 cmd = schemaCmd() 422 cmd.AddCommand(schemaApplyCmd()) 423 s, err = runCmd( 424 cmd, "apply", 425 "-u", db, 426 "--to", openSQLite(t, "create table t1 (id int);"), 427 "--dry-run", 428 "--format", "{{ json .Changes }}", 429 ) 430 require.NoError(t, err) 431 require.Equal(t, "{\"Pending\":[\"CREATE TABLE `t1` (`id` int NULL)\"]}", s) 432 }) 433 434 t.Run("AutoApprove", func(t *testing.T) { 435 db := openSQLite(t, "") 436 cmd := schemaCmd() 437 cmd.AddCommand(schemaApplyCmd()) 438 s, err := runCmd( 439 cmd, "apply", 440 "-u", db, 441 "--to", openSQLite(t, "create table t1 (id int);"), 442 "--auto-approve", 443 "--format", "{{ json .Changes }}", 444 ) 445 require.NoError(t, err) 446 require.Equal(t, "{\"Applied\":[\"CREATE TABLE `t1` (`id` int NULL)\"]}", s) 447 448 cmd = schemaCmd() 449 cmd.AddCommand(schemaApplyCmd()) 450 s, err = runCmd( 451 cmd, "apply", 452 "-u", db, 453 "--to", openSQLite(t, "create table t1 (id int);"), 454 "--auto-approve", 455 "--format", "{{ json .Changes }}", 456 ) 457 require.NoError(t, err) 458 require.Equal(t, "{}", s) 459 460 cmd = schemaCmd() 461 cmd.AddCommand(schemaApplyCmd()) 462 s, err = runCmd( 463 cmd, "apply", 464 "-u", db, 465 "--to", openSQLite(t, "create table t2 (id int);"), 466 "--auto-approve", 467 "--format", "{{ json .Changes }}", 468 ) 469 require.NoError(t, err) 470 require.Equal(t, "{\"Applied\":[\"PRAGMA foreign_keys = off\",\"DROP TABLE `t1`\",\"CREATE TABLE `t2` (`id` int NULL)\",\"PRAGMA foreign_keys = on\"]}", s) 471 472 // Simulate a failed execution. 473 conn, err := sql.Open("sqlite3", strings.TrimPrefix(db, "sqlite://")) 474 require.NoError(t, err) 475 _, err = conn.Exec("INSERT INTO t2 (`id`) VALUES (1), (1)") 476 require.NoError(t, err) 477 478 cmd = schemaCmd() 479 cmd.AddCommand(schemaApplyCmd()) 480 s, err = runCmd( 481 cmd, "apply", 482 "-u", db, 483 "--to", openSQLite(t, "create table t2 (id int, c int);create unique index t2_id on t2 (id);"), 484 "--auto-approve", 485 "--format", "{{ json .Changes }}\n", 486 ) 487 require.EqualError(t, err, `create index "t2_id" to table: "t2": UNIQUE constraint failed: t2.id`) 488 var out struct { 489 Applied []string 490 Pending []string 491 Error cmdlog.StmtError 492 } 493 require.NoError(t, json.NewDecoder(strings.NewReader(s)).Decode(&out)) 494 require.Equal(t, []string{"ALTER TABLE `t2` ADD COLUMN `c` int NULL"}, out.Applied) 495 require.Equal(t, []string{"CREATE UNIQUE INDEX `t2_id` ON `t2` (`id`)"}, out.Pending) 496 require.Equal(t, out.Pending[0], out.Error.Stmt) 497 require.Equal(t, `create index "t2_id" to table: "t2": UNIQUE constraint failed: t2.id`, out.Error.Text) 498 }) 499 } 500 501 func TestSchema_ApplySchemaMismatch(t *testing.T) { 502 var ( 503 p = t.TempDir() 504 src = filepath.Join(p, "schema.hcl") 505 ) 506 // SQLite always has a schema called "main". 507 err := os.WriteFile(src, []byte(` 508 schema "hello" {} 509 `), 0600) 510 require.NoError(t, err) 511 cmd := schemaCmd() 512 cmd.AddCommand(schemaApplyCmd()) 513 _, err = runCmd( 514 cmd, "apply", 515 "-u", openSQLite(t, ""), 516 "-f", src, 517 ) 518 require.EqualError(t, err, `mismatched HCL and database schemas: "main" <> "hello"`) 519 } 520 521 func TestSchema_ApplySkip(t *testing.T) { 522 var ( 523 p = t.TempDir() 524 cfg = filepath.Join(p, "atlas.hcl") 525 src = filepath.Join(p, "schema.hcl") 526 ) 527 err := os.WriteFile(src, []byte(` 528 schema "main" {} 529 530 table "users" { 531 schema = schema.main 532 column "id" { 533 type = int 534 } 535 } 536 `), 0600) 537 require.NoError(t, err) 538 err = os.WriteFile(cfg, []byte(` 539 variable "schema" { 540 type = string 541 default = "dev" 542 } 543 544 variable "destructive" { 545 type = bool 546 default = false 547 } 548 549 env "local" { 550 src = var.schema 551 dev_url = "sqlite://dev?mode=memory&_fk=1" 552 } 553 554 diff { 555 skip { 556 drop_table = !var.destructive 557 } 558 } 559 `), 0600) 560 require.NoError(t, err) 561 562 // Skip destructive changes. 563 cmd := schemaCmd() 564 cmd.AddCommand(schemaApplyCmd()) 565 s, err := runCmd( 566 cmd, "apply", 567 "-u", openSQLite(t, "create table pets (id int);"), 568 "-c", "file://"+cfg, 569 "--var", "schema=file://"+src, 570 "--env", "local", 571 "--auto-approve", 572 ) 573 require.NoError(t, err) 574 lines := strings.Split(strings.TrimSpace(s), "\n") 575 require.Equal(t, []string{ 576 "-- Planned Changes:", 577 `-- Create "users" table`, 578 "CREATE TABLE `users` (`id` int NOT NULL);", 579 }, lines) 580 581 // Skip destructive changes by using project-level policy (no --env was passed). 582 cmd = schemaCmd() 583 cmd.AddCommand(schemaApplyCmd()) 584 s, err = runCmd( 585 cmd, "apply", 586 "-u", openSQLite(t, "create table pets (id int);"), 587 "-c", "file://"+cfg, // Using the project-level policy. 588 "--to", "file://"+src, 589 "--dev-url", "sqlite://dev?mode=memory&_fk=1", 590 "--auto-approve", 591 ) 592 require.NoError(t, err) 593 lines = strings.Split(strings.TrimSpace(s), "\n") 594 require.Equal(t, []string{ 595 "-- Planned Changes:", 596 `-- Create "users" table`, 597 "CREATE TABLE `users` (`id` int NOT NULL);", 598 }, lines) 599 600 // Apply destructive changes. 601 cmd = schemaCmd() 602 cmd.AddCommand(schemaApplyCmd()) 603 s, err = runCmd( 604 cmd, "apply", 605 "-u", openSQLite(t, "create table pets (id int);"), 606 "-c", "file://"+cfg, 607 "--var", "schema=file://"+src, 608 "--var", "destructive=true", 609 "--env", "local", 610 "--auto-approve", 611 ) 612 require.NoError(t, err) 613 lines = strings.Split(strings.TrimSpace(s), "\n") 614 require.Equal(t, []string{ 615 "-- Planned Changes:", 616 "-- Disable the enforcement of foreign-keys constraints", 617 "PRAGMA foreign_keys = off;", 618 `-- Drop "pets" table`, 619 "DROP TABLE `pets`;", 620 `-- Create "users" table`, 621 "CREATE TABLE `users` (`id` int NOT NULL);", 622 "-- Enable back the enforcement of foreign-keys constraints", 623 "PRAGMA foreign_keys = on;", 624 }, lines) 625 } 626 627 func TestSchema_ApplySources(t *testing.T) { 628 var ( 629 p = t.TempDir() 630 cfg = filepath.Join(p, "atlas.hcl") 631 src = []string{filepath.Join(p, "one.hcl"), filepath.Join(p, "two.hcl")} 632 ) 633 err := os.WriteFile(src[0], []byte(` 634 schema "main" {} 635 636 table "one" { 637 schema = schema.main 638 column "id" { 639 type = int 640 } 641 } 642 `), 0600) 643 require.NoError(t, err) 644 err = os.WriteFile(src[1], []byte(` 645 table "two" { 646 schema = schema.main 647 column "id" { 648 type = int 649 } 650 } 651 `), 0600) 652 require.NoError(t, err) 653 err = os.WriteFile(cfg, []byte(fmt.Sprintf(` 654 env "local" { 655 src = [%q, %q] 656 }`, src[0], src[1])), 0600) 657 require.NoError(t, err) 658 659 cmd := schemaCmd() 660 cmd.AddCommand(schemaApplyCmd()) 661 s, err := runCmd( 662 cmd, "apply", 663 "-u", openSQLite(t, ""), 664 "-c", "file://"+cfg, 665 "--env", "local", 666 "--auto-approve", 667 ) 668 require.NoError(t, err) 669 lines := strings.Split(strings.TrimSpace(s), "\n") 670 require.Equal(t, []string{ 671 "-- Planned Changes:", 672 `-- Create "one" table`, 673 "CREATE TABLE `one` (`id` int NOT NULL);", 674 `-- Create "two" table`, 675 "CREATE TABLE `two` (`id` int NOT NULL);", 676 }, lines) 677 } 678 679 func TestSchema_ApplySQL(t *testing.T) { 680 t.Run("File", func(t *testing.T) { 681 db := openSQLite(t, "") 682 p := filepath.Join(t.TempDir(), "schema.sql") 683 require.NoError(t, os.WriteFile(p, []byte(`create table t1 (id int NOT NULL);`), 0600)) 684 s, err := runCmd( 685 schemaApplyCmd(), 686 "-u", db, 687 "--dev-url", openSQLite(t, ""), 688 "--to", "file://"+p, 689 "--auto-approve", 690 ) 691 require.NoError(t, err) 692 require.Equal(t, "-- Planned Changes:\n-- Create \"t1\" table\nCREATE TABLE `t1` (`id` int NOT NULL);\n", s) 693 694 s, err = runCmd( 695 schemaApplyCmd(), 696 "-u", db, 697 "--dev-url", openSQLite(t, ""), 698 "--to", "file://"+p, 699 "--auto-approve", 700 ) 701 require.NoError(t, err) 702 require.Equal(t, "Schema is synced, no changes to be made.\n", s) 703 }) 704 t.Run("Dir", func(t *testing.T) { 705 db := openSQLite(t, "") 706 s, err := runCmd( 707 schemaApplyCmd(), 708 "-u", db, 709 "--dev-url", openSQLite(t, ""), 710 "--to", "file://testdata/sqlite", 711 "--auto-approve", 712 ) 713 require.NoError(t, err) 714 require.Equal(t, "-- Planned Changes:\n-- Create \"tbl\" table\nCREATE TABLE `tbl` (`col` int NOT NULL, `col_2` bigint NULL);\n", s) 715 716 s, err = runCmd( 717 schemaApplyCmd(), 718 "-u", db, 719 "--dev-url", openSQLite(t, ""), 720 "--to", "file://testdata/sqlite", 721 "--auto-approve", 722 ) 723 require.NoError(t, err) 724 require.Equal(t, "Schema is synced, no changes to be made.\n", s) 725 }) 726 t.Run("Error", func(t *testing.T) { 727 _, err := runCmd( 728 schemaApplyCmd(), 729 "-u", openSQLite(t, ""), 730 "--dev-url", openSQLite(t, ""), 731 "--to", "file://testdata/sqlite", 732 "--to", "file://testdata/sqlite2", 733 ) 734 require.EqualError(t, err, `the provided SQL state must be either a single schema file or a migration directory, but 2 paths were found`) 735 736 _, err = runCmd( 737 schemaApplyCmd(), 738 "-u", openSQLite(t, ""), 739 "--dev-url", openSQLite(t, ""), 740 "--to", "file://"+t.TempDir(), 741 ) 742 require.ErrorContains(t, err, `contains neither SQL nor HCL files`) 743 744 p := t.TempDir() 745 require.NoError(t, os.WriteFile(filepath.Join(p, "schema.sql"), []byte(`create table t1 (id int NOT NULL);`), 0600)) 746 require.NoError(t, os.WriteFile(filepath.Join(p, "schema.hcl"), []byte(`schema "main" {}`), 0600)) 747 _, err = runCmd( 748 schemaApplyCmd(), 749 "-u", openSQLite(t, ""), 750 "--dev-url", openSQLite(t, ""), 751 "--to", "file://"+p, 752 ) 753 require.EqualError(t, err, `ambiguous schema: both SQL and HCL files found: "schema.hcl", "schema.sql"`) 754 755 _, err = runCmd( 756 schemaApplyCmd(), 757 "-u", openSQLite(t, ""), 758 "--dev-url", openSQLite(t, ""), 759 "--to", "testdata/sqlite", 760 ) 761 require.EqualError(t, err, "missing scheme. Did you mean file://testdata/sqlite?") 762 }) 763 } 764 765 func TestSchema_InspectLog(t *testing.T) { 766 db := openSQLite(t, "create table t1 (id integer primary key);create table t2 (name text);") 767 cmd := schemaCmd() 768 cmd.AddCommand(schemaInspectCmd()) 769 s, err := runCmd( 770 cmd, "inspect", 771 "-u", db, 772 "--format", "{{ json . }}", 773 ) 774 require.NoError(t, err) 775 require.Equal(t, `{"schemas":[{"name":"main","tables":[{"name":"t1","columns":[{"name":"id","type":"INTEGER","null":true}],"primary_key":{"parts":[{"column":"id"}]}},{"name":"t2","columns":[{"name":"name","type":"TEXT","null":true}]}]}]}`, s) 776 } 777 778 func TestFmt(t *testing.T) { 779 for _, tt := range []struct { 780 name string 781 inputDir map[string]string 782 expectedDir map[string]string 783 expectedFile string 784 expectedOut string 785 args []string 786 expectedPrint bool 787 }{ 788 { 789 name: "specific file", 790 inputDir: map[string]string{ 791 "test.hcl": unformatted, 792 }, 793 expectedDir: map[string]string{ 794 "test.hcl": formatted, 795 }, 796 args: []string{"test.hcl"}, 797 expectedOut: "test.hcl\n", 798 }, 799 { 800 name: "current dir", 801 inputDir: map[string]string{ 802 "test.hcl": unformatted, 803 }, 804 expectedDir: map[string]string{ 805 "test.hcl": formatted, 806 }, 807 expectedOut: "test.hcl\n", 808 }, 809 { 810 name: "multi path implicit", 811 inputDir: map[string]string{ 812 "test.hcl": unformatted, 813 "test2.hcl": unformatted, 814 }, 815 expectedDir: map[string]string{ 816 "test.hcl": formatted, 817 "test2.hcl": formatted, 818 }, 819 expectedOut: "test.hcl\ntest2.hcl\n", 820 }, 821 { 822 name: "multi path explicit", 823 inputDir: map[string]string{ 824 "test.hcl": unformatted, 825 "test2.hcl": unformatted, 826 }, 827 expectedDir: map[string]string{ 828 "test.hcl": formatted, 829 "test2.hcl": formatted, 830 }, 831 args: []string{"test.hcl", "test2.hcl"}, 832 expectedOut: "test.hcl\ntest2.hcl\n", 833 }, 834 { 835 name: "formatted", 836 inputDir: map[string]string{ 837 "test.hcl": formatted, 838 }, 839 expectedDir: map[string]string{ 840 "test.hcl": formatted, 841 }, 842 }, 843 } { 844 t.Run(tt.name, func(t *testing.T) { 845 dir := setupFmtTest(t, tt.inputDir) 846 out, err := runCmd(schemaFmtCmd(), tt.args...) 847 require.NoError(t, err) 848 assertDir(t, dir, tt.expectedDir) 849 require.EqualValues(t, tt.expectedOut, out) 850 }) 851 } 852 } 853 854 func TestSchema_Clean(t *testing.T) { 855 var ( 856 u = fmt.Sprintf("sqlite://file:%s?cache=shared&_fk=1", filepath.Join(t.TempDir(), "test.db")) 857 c, err = sqlclient.Open(context.Background(), u) 858 ) 859 require.NoError(t, err) 860 861 // Apply migrations onto database. 862 _, err = runCmd(migrateApplyCmd(), "--dir", "file://testdata/sqlite", "--url", u) 863 require.NoError(t, err) 864 865 // Run clean and expect to be clean. 866 _, err = runCmd(migrateApplyCmd(), "--dir", "file://testdata/sqlite", "--url", u) 867 require.NoError(t, err) 868 s, err := runCmd(schemaCleanCmd(), "--url", u, "--auto-approve") 869 require.NoError(t, err) 870 require.NotZero(t, s) 871 require.NoError(t, c.Driver.(migrate.CleanChecker).CheckClean(context.Background(), nil)) 872 } 873 874 func assertDir(t *testing.T, dir string, expected map[string]string) { 875 act := make(map[string]string) 876 files, err := os.ReadDir(dir) 877 require.NoError(t, err) 878 for _, f := range files { 879 if f.IsDir() { 880 continue 881 } 882 contents, err := os.ReadFile(filepath.Join(dir, f.Name())) 883 require.NoError(t, err) 884 act[f.Name()] = string(contents) 885 } 886 require.EqualValues(t, expected, act) 887 } 888 889 func setupFmtTest(t *testing.T, inputDir map[string]string) string { 890 wd, err := os.Getwd() 891 require.NoError(t, err) 892 dir, err := os.MkdirTemp(os.TempDir(), "fmt-test-") 893 require.NoError(t, err) 894 err = os.Chdir(dir) 895 require.NoError(t, err) 896 t.Cleanup(func() { 897 os.RemoveAll(dir) 898 os.Chdir(wd) //nolint:errcheck 899 }) 900 for name, contents := range inputDir { 901 file := path.Join(dir, name) 902 err = os.WriteFile(file, []byte(contents), 0600) 903 } 904 require.NoError(t, err) 905 return dir 906 } 907 908 type assertNormalizerDriver struct { 909 migrate.Driver 910 t *testing.T 911 } 912 913 // NormalizeSchema returns the normal representation of a schema. 914 func (d *assertNormalizerDriver) NormalizeSchema(context.Context, *schema.Schema) (*schema.Schema, error) { 915 d.t.Fatal("did not expect a call to NormalizeSchema") 916 return nil, nil 917 } 918 919 // NormalizeRealm returns the normal representation of a database. 920 func (d *assertNormalizerDriver) NormalizeRealm(context.Context, *schema.Realm) (*schema.Realm, error) { 921 d.t.Fatal("did not expect a call to NormalizeRealm") 922 return nil, nil 923 }