Repository: ariga/atlas
Pull request #2874 (2024-06-18 13:33:33)

doc/home-announcement-bar
Added announcement bar to main page


Patch:
diff --git a/cmd/atlas/internal/cmdapi/migrate.go b/cmd/atlas/internal/cmdapi/migrate.go
index c9bad882..c2a3f5e9 100644
--- a/cmd/atlas/internal/cmdapi/migrate.go
+++ b/cmd/atlas/internal/cmdapi/migrate.go
@@ -173,8 +173,12 @@ func migrateApplyRun(cmd *cobra.Command, args []string, flags migrateApplyFlags,
 			return fmt.Errorf("cannot apply '%d' migration files", count)
 		}
 	}
+	dirURL, err := url.Parse(flags.dirURL)
+	if err != nil {
+		return fmt.Errorf("parse dir-url: %w", err)
+	}
 	// Open and validate the migration directory.
-	dir, err := cmdmigrate.Dir(ctx, flags.dirURL, false)
+	dir, err := cmdmigrate.DirURL(ctx, dirURL, false)
 	if err != nil {
 		return err
 	}
@@ -217,7 +221,7 @@ func migrateApplyRun(cmd *cobra.Command, args []string, flags migrateApplyFlags,
 		return err
 	}
 	// Setup reporting info.
-	report := cmdlog.NewMigrateApply(ctx, client, dir)
+	report := cmdlog.NewMigrateApply(ctx, client, dirURL)
 	mr.Init(client, report, mrrw)
 	// If cloud reporting is enabled, and we cannot obtain the current
 	// target identifier, abort and report it to the user.
@@ -1298,7 +1302,11 @@ func migrateStatusCmd() *cobra.Command {
 
 func migrateStatusRun(cmd *cobra.Command, _ []string, flags migrateStatusFlags) error {
 	ctx := cmd.Context()
-	dir, err := cmdmigrate.Dir(ctx, flags.dirURL, false)
+	dirURL, err := url.Parse(flags.dirURL)
+	if err != nil {
+		return fmt.Errorf("parse dir-url: %w", err)
+	}
+	dir, err := cmdmigrate.DirURL(ctx, dirURL, false)
 	if err != nil {
 		return err
 	}
@@ -1313,6 +1321,7 @@ func migrateStatusRun(cmd *cobra.Command, _ []string, flags migrateStatusFlags)
 	report, err := (&cmdmigrate.StatusReporter{
 		Client: client,
 		Dir:    dir,
+		DirURL: dirURL,
 		Schema: revisionSchemaName(client, flags.revisionSchema),
 	}).Report(ctx)
 	if err != nil {
diff --git a/cmd/atlas/internal/cmdext/reader.go b/cmd/atlas/internal/cmdext/reader.go
index c7d6a6ef..1f058eed 100644
--- a/cmd/atlas/internal/cmdext/reader.go
+++ b/cmd/atlas/internal/cmdext/reader.go
@@ -121,7 +121,7 @@ func stateSchemaSQL(ctx context.Context, cfg *StateReaderConfig, dir migrate.Dir
 	if cfg.Dev == nil {
 		return nil, errNoDevURL
 	}
-	log := cmdlog.NewMigrateApply(ctx, cfg.Dev, dir)
+	log := cmdlog.NewMigrateApply(ctx, cfg.Dev, nil)
 	r, err := stateReaderSQL(ctx, cfg, dir, []migrate.ExecutorOption{migrate.WithLogger(log)}, nil)
 	if n := len(log.Applied); err != nil && n > 0 {
 		if serr := log.Applied[n-1].Error; serr != nil && serr.Stmt != "" && serr.Text != "" {
diff --git a/cmd/atlas/internal/cmdlog/cmdlog.go b/cmd/atlas/internal/cmdlog/cmdlog.go
index a517cf12..18900864 100644
--- a/cmd/atlas/internal/cmdlog/cmdlog.go
+++ b/cmd/atlas/internal/cmdlog/cmdlog.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"net/url"
 	"slices"
 	"sort"
 	"strings"
@@ -102,13 +103,13 @@ func (f Files) MarshalJSON() ([]byte, error) {
 }
 
 // NewEnv returns an initialized Env.
-func NewEnv(c *sqlclient.Client, dir migrate.Dir) Env {
+func NewEnv(c *sqlclient.Client, dirURL *url.URL) Env {
 	e := Env{
 		Driver: c.Name,
 		URL:    c.URL,
 	}
-	if p, ok := dir.(interface{ Path() string }); ok {
-		e.Dir = p.Path()
+	if dirURL != nil {
+		e.Dir = dirURL.Redacted()
 	}
 	return e
 }
@@ -361,10 +362,10 @@ type (
 )
 
 // NewMigrateApply returns an MigrateApply.
-func NewMigrateApply(ctx context.Context, client *sqlclient.Client, dir migrate.Dir) *MigrateApply {
+func NewMigrateApply(ctx context.Context, client *sqlclient.Client, dirURL *url.URL) *MigrateApply {
 	return &MigrateApply{
 		ctx:   ctx,
-		Env:   NewEnv(client, dir),
+		Env:   NewEnv(client, dirURL),
 		Start: time.Now(),
 	}
 }
diff --git a/cmd/atlas/internal/migrate/report.go b/cmd/atlas/internal/migrate/report.go
index 04970821..f2b22685 100644
--- a/cmd/atlas/internal/migrate/report.go
+++ b/cmd/atlas/internal/migrate/report.go
@@ -8,6 +8,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"net/url"
 	"strings"
 
 	"ariga.io/atlas/cmd/atlas/internal/cmdlog"
@@ -21,6 +22,8 @@ import (
 type StatusReporter struct {
 	// Client configures the connection to the database to file a MigrateStatus for.
 	Client *sqlclient.Client
+	// DirURL of the migration directory.
+	DirURL *url.URL
 	// Dir is used for scanning and validating the migration directory.
 	Dir migrate.Dir
 	// Schema name the revision table resides in.
@@ -29,7 +32,7 @@ type StatusReporter struct {
 
 // Report creates and writes a MigrateStatus.
 func (r *StatusReporter) Report(ctx context.Context) (*cmdlog.MigrateStatus, error) {
-	rep := &cmdlog.MigrateStatus{Env: cmdlog.NewEnv(r.Client, r.Dir)}
+	rep := &cmdlog.MigrateStatus{Env: cmdlog.NewEnv(r.Client, r.DirURL)}
 	// Check if there already is a revision table in the defined schema.
 	// Inspect schema and check if the table does already exist.
 	sch, err := r.Client.InspectSchema(ctx, r.Schema, &schema.InspectOptions{Tables: []string{revision.Table}})
diff --git a/doc/md/atlas-schema/hcl.mdx b/doc/md/atlas-schema/hcl.mdx
index 786f0b1b..e25297d5 100644
--- a/doc/md/atlas-schema/hcl.mdx
+++ b/doc/md/atlas-schema/hcl.mdx
@@ -622,6 +622,9 @@ primary key.
 
 #### Example
 
+<Tabs>
+<TabItem label="Columns" value="columns">
+
 ```hcl
 primary_key {
   columns = [column.id]
@@ -632,7 +635,36 @@ primary_key {
 
 | Name    | Kind      | Type                     | Description                                                    |
 |---------|-----------|--------------------------|----------------------------------------------------------------|
-| columns | resource  | reference (list)         | A list of references to columns that comprise the primary key. |
+| columns | attribute | reference (list)         | A list of references to columns that comprise the primary key. |
+
+</TabItem>
+<TabItem label="Expressions" value="expr">
+
+:::info
+Note, primary key expressions are supported by ClickHouse.
+::: 
+
+```hcl
+primary_key {
+  on {
+    column = column.id
+  }
+  on {
+    expr = "c1 + c2"
+  }
+}
+```
+
+#### Properties
+
+| Name    | Kind      | Type                     | Description                                                    |
+|---------|-----------|--------------------------|----------------------------------------------------------------|
+| on      | resource  | schema.IndexPart (list)  | The index parts that comprise the index                        |
+
+</TabItem>
+</Tabs>
+
+
 
 ## Foreign Key
 
@@ -1309,6 +1341,56 @@ function "fn_return_table" {
 </TabItem>
 </Tabs>
 
+:::info <a class="sticky-anchor" id={"scope"} href={"#testing-functions"}>Testing Functions</a>
+
+Atlas testing framework allows you to write unit tests for your functions. The following example demonstrates how
+to write tests for the `positive` function defined above. For more detail, read the [schema testing docs](/testing/schema).
+
+
+<Tabs>
+<TabItem label="Simple Test" value="simple">
+
+```hcl title="schema.test.hcl"
+test "schema" "simple_test" {
+  parallel = true
+  assert {
+    sql = "SELECT positive(1)"
+  }
+  log {
+    message = "First assertion passed"
+  }
+  assert {
+    sql = <<SQL
+SELECT NOT positive(0);
+SELECT NOT positive(-1);
+SQL
+  }
+}
+```
+
+</TabItem>
+<TabItem label="Table-driven Test" value="table-driven">
+
+```hcl title="schema.test.hcl"
+test "schema" "simple_test" {
+  parallel = true
+  for_each = [
+    {input: 1, expected: "t"},
+    {input: 0, expected: "f"},
+    {input: -1, expected: "f"},
+  ]
+  exec {
+    sql = "SELECT positive(${each.value.input})"
+    output = each.value.expected
+  }
+}
+```
+
+</TabItem>
+</Tabs>
+
+:::
+
 #### Aggregate Functions
 
 The `aggregate` block defines a function that computes a single result from a set of values. Supported by
diff --git a/doc/md/cloud/deployment.mdx b/doc/md/cloud/deployment.mdx
index 0770abb0..b08f274c 100644
--- a/doc/md/cloud/deployment.mdx
+++ b/doc/md/cloud/deployment.mdx
@@ -29,7 +29,7 @@ with a single command (or using popular CD tools such as Kubernetes and Terrafor
 
 ## Deploying migrations from Atlas Cloud
 
-To read the migrations directory from the [Schema Registry](https://atlasgo.io/cloud/features/registry),
+To read the migration directory from the [Schema Registry](https://atlasgo.io/cloud/features/registry),
 use the `atlas://` scheme in the migration URL as follows:
 
 ```hcl title="atlas.hcl"
@@ -70,13 +70,12 @@ runs in your cloud account. Here's a demonstration of how it looks in action:
 <Tabs>
 <TabItem label="Single Deployment" value="single">
 
-[![](https://atlasgo.io/uploads/cloud/images/public-tenant-deployment.png)](https://gh.atlasgo.cloud/deployments/51539607559)
+[![](https://atlasgo.io/uploads/cloud/images/deployment-2.png)](https://gh.atlasgo.cloud/dirs/4294967383/deployments/51539607593)
 
 </TabItem>
 <TabItem label="Multi-Tenant Deployment" value="mt">
 
-[![](https://atlasgo.io/uploads/cloud/images/multi-tenant-deployment.png)](https://gh.atlasgo.cloud/deployments/sets/94489280524)
+[![](https://atlasgo.io/uploads/cloud/images/deployment-set-2.png)](https://gh.atlasgo.cloud/dirs/4294967347/deployments/sets/94489280524)
 
 </TabItem>
 </Tabs>
-
diff --git a/doc/md/cloud/features/troubleshooting.mdx b/doc/md/cloud/features/troubleshooting.mdx
new file mode 100644
index 00000000..d545429c
--- /dev/null
+++ b/doc/md/cloud/features/troubleshooting.mdx
@@ -0,0 +1,46 @@
+---
+title: Troubleshooting Migrations
+slug: /cloud/features/troubleshooting
+---
+
+Troubleshooting and triaging failures during database migrations can be especially difficult. Errors often stem from data
+and schema changes, making it hard to identify the exact problem.
+
+When an error or migration failure occurs, it is crucial to understand what went wrong and assess the current state of
+the database.
+
+## Drill Down Analysis
+
+When reporting migration runs to Atlas Cloud, the detailed logs allow you to quickly drill down and troubleshoot
+any schema migration failures.
+
+The report shows what happened in the migration, what caused the failure, and the current state of the
+database post-deployment.
+
+For example, in the image below we can see a migration that failed due to a constraint, `number_length`, which
+was violated. This caused the migration to fail and only one of the three intended migration files was executed on
+the database.
+
+[![](https://atlasgo.io/uploads/cloud/images/gh-failed-deployment.png)](https://gh.atlasgo.cloud/dirs/4294967329/deployments/51539607581)
+
+### Database-per-Tenant Migrations
+
+In a database-per-tenant architecture, the same migration is executed on multiple databases. If a migration fails,
+the root cause of the error often involves tenant-specific data and schema changes, making it even more challenging to
+pinpoint issues.
+
+In this scenario, identifying which databases were affected and which remained unaffected is crucial to assess the impact
+and plan the next steps effectively.
+
+In the image below we can see the deployment intended to run on four different databases. The migration failed once it reached
+the third tenant, stopping the deployment entirely before reaching the last tenant's database.
+
+[![](https://atlasgo.io/uploads/cloud/images/multitenant-failed-deployment.png)](https://gh.atlasgo.cloud/dirs/4294967347/deployments/sets/94489280523)
+
+When examining the specific tenant that failed (as shown in the image below), we can see that the failure was caused by
+an attempt to create a table that already existed in the database.
+
+[![](https://atlasgo.io/uploads/cloud/images/tenant-failed-deployment.png)](https://gh.atlasgo.cloud/dirs/4294967347/deployments/51539607566)
+
+The detailed deployment reports provide clarity on migration failures, allowing for efficient resolution and minimizing
+downtime.
diff --git a/doc/md/community-edition.mdx b/doc/md/community-edition.mdx
index da7f407d..63816942 100644
--- a/doc/md/community-edition.mdx
+++ b/doc/md/community-edition.mdx
@@ -70,11 +70,3 @@ If you want to manually install the Atlas CLI, pick one of the below builds suit
 
 </TabItem>
 </Tabs>
-
-## Building from Source
-
-If you would like to build Atlas from source, run:
-
-```
-go get ariga.io/atlas/cmd/atlas
-```
\ No newline at end of file
diff --git a/doc/md/lint/analyzers.mdx b/doc/md/lint/analyzers.mdx
index 102f9823..c6ed289f 100644
--- a/doc/md/lint/analyzers.mdx
+++ b/doc/md/lint/analyzers.mdx
@@ -391,8 +391,8 @@ entity while still pointing to its previous table name. e.g., here is how you ca
 
 2. If renaming is desired but the previous version of the application uses the old table name, a temporary `VIEW`
 can be created to mimic the previous schema version in the deployment phase. However, the downside of this solution is
-that mutations using the old table name will fail. Yet, if Atlas detects a consecutive statement with a
-`CREATE VIEW <old_name>`, it will ignore this check.
+that mutations using the old table name might fail (e.g., if the VIEW is not [Updatable/Insertable](https://dev.mysql.com/doc/refman/8.4/en/view-updatability.html)).
+Yet, if Atlas detects a consecutive statement with a `CREATE VIEW <old_name>`, it will ignore this check.
   ```sql
   ALTER TABLE `users` RENAME TO `Users`;
   CREATE VIEW `users` AS SELECT * FROM `Users`;
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index c87c9674..304f62e5 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -125,6 +125,7 @@ module.exports = {
                         {type: 'doc', id: 'cloud/features/registry', label: 'Schema Registry'},
                         {type: 'doc', id: 'cloud/features/schema-docs', label: 'Schema Docs'},
                         {type: 'doc', id: 'cloud/features/pre-migration-checks', label: 'Pre-migration Checks'},
+                        {type: 'doc', id: 'cloud/features/troubleshooting', label: 'Migration Troubleshooting'},
                     ],
                 },
                 {type: 'doc', id: 'cloud/pricing', label: 'Pricing'},
diff --git a/doc/website/src/css/custom.css b/doc/website/src/css/custom.css
index 7db2fd1e..120c51dd 100644
--- a/doc/website/src/css/custom.css
+++ b/doc/website/src/css/custom.css
@@ -342,3 +342,7 @@ h3 > .login {
 .docs-layout p code {
   @apply rounded-lg border-none p-1 font-roboto;
 }
+
+div[class^='announcementBar'] {
+  @apply border-0;
+}
\ No newline at end of file
diff --git a/doc/website/src/pages/index.js b/doc/website/src/pages/index.js
index e860a499..bd232312 100644
--- a/doc/website/src/pages/index.js
+++ b/doc/website/src/pages/index.js
@@ -1,5 +1,6 @@
 import React from "react";
 import LayoutProvider from "@theme/Layout/Provider";
+import AnnouncementBar from "@theme/AnnouncementBar";
 import { AtlasGoWebsite } from "@ariga/atlas-website";
 import FOOTER from "../constants/footer";
 
@@ -8,6 +9,7 @@ import "@ariga/atlas-website/style.css";
 export default function () {
   return (
     <LayoutProvider>
+      <AnnouncementBar />
       <AtlasGoWebsite
         events={{
           events: [],
diff --git a/schemahcl/context.go b/schemahcl/context.go
index 612af9ec..5790ea9f 100644
--- a/schemahcl/context.go
+++ b/schemahcl/context.go
@@ -443,11 +443,14 @@ func (t *blockDef) addChild(blk *hclsyntax.Block, depth int) {
 	if t.children == nil {
 		t.children = make(map[string]*blockDef)
 	}
-	c := &blockDef{
-		name:   blk.Type,
-		parent: t,
+	c, ok := t.children[blk.Type]
+	if !ok {
+		c = &blockDef{
+			name:   blk.Type,
+			parent: t,
+		}
+		t.children[blk.Type] = c
 	}
-	t.children[blk.Type] = c
 	// Limit depth of children to two as we do not have any
 	// case for more than this atm. e.g., table.column.
 	if depth < 2 {
diff --git a/sql/postgres/diff.go b/sql/postgres/diff.go
index 6e7d59eb..df81284c 100644
--- a/sql/postgres/diff.go
+++ b/sql/postgres/diff.go
@@ -340,6 +340,10 @@ func typeChanged(from, to *schema.Column, ns string) (bool, error) {
 			// In case the type is defined with schema qualifier, but returned without
 			// (inspecting a schema scope), or vice versa, remove before comparing.
 			ns != "" && trimSchema(toT.T, ns) != trimSchema(toT.T, ns)
+	case *CompositeType:
+		toT := toT.(*CompositeType)
+		changed = toT.T != fromT.T ||
+			(toT.Schema != nil && fromT.Schema != nil && toT.Schema.Name != fromT.Schema.Name)
 	case *DomainType:
 		toT := toT.(*DomainType)
 		changed = toT.T != fromT.T ||


Test patch:
diff --git a/doc/md/testing/migrate.mdx b/doc/md/testing/migrate.mdx
index 847ec7f9..99f7c7b4 100644
--- a/doc/md/testing/migrate.mdx
+++ b/doc/md/testing/migrate.mdx
@@ -76,7 +76,7 @@ test "migrate" "20240613061102" {
   }
   # Migrate to version 20240613061102.
   migrate {
-      to = "20240613061102"
+    to = "20240613061102"
   }
   # Verify the correctness of the data migration.
   exec {
@@ -112,7 +112,7 @@ test "migrate" "20240613061102" {
   }
   # Migrate to the tested version.
   migrate {
-      to = "20240613061102"
+    to = "20240613061102"
   }
   # Verify the correctness of the data migration.
   exec {
diff --git a/internal/integration/testdata/mysql/cli-migrate-apply.txt b/internal/integration/testdata/mysql/cli-migrate-apply.txt
index 2b8741cf..a1d96ce9 100644
--- a/internal/integration/testdata/mysql/cli-migrate-apply.txt
+++ b/internal/integration/testdata/mysql/cli-migrate-apply.txt
@@ -49,7 +49,7 @@ atlas migrate apply --url URL --revisions-schema $db --log '{{ json . }}'
 validJSON stdout
 stdout '"Driver":"mysql"'
 stdout '"Scheme":"mysql"'
-stdout '"Dir":"migrations"'
+stdout '"Dir":"file://migrations"'
 stdout '"Target":"3"'
 stdout '"Pending":\[{"Name":"1_first.sql","Version":"1","Description":"first"},{"Name":"2_second.sql","Version":"2","Description":"second"},{"Name":"3_third.sql","Version":"3","Description":"third"}\]'
 stdout '"Applied":\["CREATE TABLE `users` \(`id` bigint NOT NULL AUTO_INCREMENT, `age` bigint NOT NULL, `name` varchar\(255\) NOT NULL, PRIMARY KEY \(`id`\)\) CHARSET utf8mb4 COLLATE utf8mb4_bin;"\]'
diff --git a/schemahcl/schemahcl_test.go b/schemahcl/schemahcl_test.go
index da49632a..f0c11629 100644
--- a/schemahcl/schemahcl_test.go
+++ b/schemahcl/schemahcl_test.go
@@ -1104,3 +1104,46 @@ baz = 1
 	require.Equal(t, 4, rs[1].Children[0].Range().Start.Line)
 	require.Equal(t, 5, rs[1].Children[0].Attrs[0].Range().Start.Line)
 }
+
+func TestExtendedBlockDef(t *testing.T) {
+	var (
+		doc struct {
+			DefaultExtension
+		}
+		b = []byte(`
+schema "public" {}
+table "users" {}
+materialized "users_view2" {
+  schema = schema.public
+  as = "SELECT * FROM script_matview_inspect.users"
+  column "id" {
+    null = false
+  }
+  column "a" {
+    null = false
+  }
+  column "b" {
+    null = false
+  }
+  primary_key {
+    columns = [column.id]
+  }
+  populate = true
+}
+materialized "users_view" {
+  schema = schema.public
+  to = table.users
+  as = "SELECT * FROM script_matview_inspect.users"
+  index "i" {
+    on {
+      expr = "a"
+    }
+  }
+  primary_key {
+    using = index.i # Not a real syntax.
+  }
+}
+`)
+	)
+	require.NoError(t, New().EvalBytes(b, &doc, nil))
+}