Repository: ariga/atlas
Pull request #2020 (2023-08-25 19:40:07)

cmd/atlas: attach attributes to json format
Fixed https://github.com/ariga/atlas/issues/1752


Patch:
diff --git a/cmd/atlas/internal/cmdlog/cmdlog.go b/cmd/atlas/internal/cmdlog/cmdlog.go
index 07a1ca21..4e5cbeed 100644
--- a/cmd/atlas/internal/cmdlog/cmdlog.go
+++ b/cmd/atlas/internal/cmdlog/cmdlog.go
@@ -550,10 +550,16 @@ func (s *SchemaInspect) MarshalHCL() (string, error) {
 // MarshalJSON implements json.Marshaler.
 func (s *SchemaInspect) MarshalJSON() ([]byte, error) {
 	type (
+		Attrs struct {
+			Comment string `json:"comment,omitempty"`
+			Charset string `json:"charset,omitempty"`
+			Collate string `json:"collate,omitempty"`
+		}
 		Column struct {
 			Name string `json:"name"`
 			Type string `json:"type,omitempty"`
 			Null bool   `json:"null,omitempty"`
+			Attrs
 		}
 		IndexPart struct {
 			Desc   bool   `json:"desc,omitempty"`
@@ -579,25 +585,45 @@ func (s *SchemaInspect) MarshalJSON() ([]byte, error) {
 			Indexes     []Index      `json:"indexes,omitempty"`
 			PrimaryKey  *Index       `json:"primary_key,omitempty"`
 			ForeignKeys []ForeignKey `json:"foreign_keys,omitempty"`
+			Attrs
 		}
 		Schema struct {
 			Name   string  `json:"name"`
 			Tables []Table `json:"tables,omitempty"`
+			Attrs
+		}
+	)
+	var (
+		realm struct {
+			Schemas []Schema `json:"schemas,omitempty"`
+		}
+		setAttrs = func(from []schema.Attr, to *Attrs) {
+			for i := range from {
+				switch a := from[i].(type) {
+				case *schema.Comment:
+					to.Comment = a.Text
+				case *schema.Charset:
+					to.Charset = a.V
+				case *schema.Collation:
+					to.Collate = a.V
+				}
+			}
 		}
 	)
-	var realm struct {
-		Schemas []Schema `json:"schemas,omitempty"`
-	}
 	for _, s1 := range s.Realm.Schemas {
 		s2 := Schema{Name: s1.Name}
+		setAttrs(s1.Attrs, &s2.Attrs)
 		for _, t1 := range s1.Tables {
 			t2 := Table{Name: t1.Name}
+			setAttrs(t1.Attrs, &t2.Attrs)
 			for _, c1 := range t1.Columns {
-				t2.Columns = append(t2.Columns, Column{
+				c2 := Column{
 					Name: c1.Name,
 					Type: c1.Type.Raw,
 					Null: c1.Type.Null,
-				})
+				}
+				setAttrs(c1.Attrs, &c2.Attrs)
+				t2.Columns = append(t2.Columns, c2)
 			}
 			idxParts := func(idx *schema.Index) (parts []IndexPart) {
 				for _, p1 := range idx.Parts {
diff --git a/doc/md/getting-started/getting-started.mdx b/doc/md/getting-started/getting-started.mdx
index 607d8148..d43de151 100644
--- a/doc/md/getting-started/getting-started.mdx
+++ b/doc/md/getting-started/getting-started.mdx
@@ -116,7 +116,9 @@ Now, consider we want to add a `blog_posts` table and have our schema represent
 blogging system.
 
 
-<p style={{textAlign: "center"}}><a href="https://gh.atlasgo.cloud/explore/9717d499"><img src="https://atlasgo.io/uploads/images/blog-erd.png" alt="Blog ERD"/></a></p>
+<p style={{textAlign: "center"}}>
+    <a href="https://gh.atlasgo.cloud/explore/9717d499"><img src="https://atlasgo.io/uploads/users-blogs-explore.jpg" alt="Blog ERD"/></a>
+</p>
 
 Let's add the following to our inspected schema, and use Atlas to plan and apply the changes to our database.
 
@@ -221,23 +223,18 @@ Use the arrow keys to navigate: ↓ ↑ → ←
 ```
 Apply the changes, and that's it! You have successfully run a declarative migration.
 
-You can reinspect the database by running the `inspect` command again to ensure that the changes have been made to the
-schema:
+To ensure that the changes have been made to the schema, you can run the `inspect` command again. This time, we use
+the `--web`/`-w` flag to open the Atlas Web UI and view the schema.
 
-<Tabs>
-<TabItem value="hcl" label="Atlas DDL (HCL)" default>
-
-```shell
-atlas schema inspect -u "mysql://root:pass@localhost:3306/example"
+```shell {3}
+atlas schema inspect \
+  -u "mysql://root:pass@localhost:3306/example" \
+  --web
 ```
-</TabItem>
-<TabItem value="sql" label="SQL">
 
-```shell
-atlas schema inspect -u "mysql://root:pass@localhost:3306/example" --format '{{ sql . }}'
-```
-</TabItem>
-</Tabs>
+:::note
+If you are using an old version of Atlas, you may need to replace the `--web` flag with `--visualize`.
+:::
 
 ### Versioned Migrations
 Alternatively, the versioned migration workflow, sometimes called "change-based migrations", allows each change to the
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index 681991b4..be126ac2 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -9,7 +9,7 @@ module.exports = {
   organizationName: 'ariga',
   projectName: 'atlas',
   themeConfig: {
-    image: "https://atlas-og-img.vercel.app/**Atlas%20%7C**%20Open-source%20database%20schema%20management.png?theme=dark",
+    image: "https://og.atlasgo.io/image?title=Schema%20Migration%20Tool%20for%20any%20Language",
     prism: {
       additionalLanguages: ['hcl'],
       magicComments: [
diff --git a/sql/mysql/convert.go b/sql/mysql/convert.go
index 1c882978..7d1bd4c5 100644
--- a/sql/mysql/convert.go
+++ b/sql/mysql/convert.go
@@ -39,13 +39,13 @@ func FormatType(t schema.Type) (string, error) {
 		}
 	case *schema.DecimalType:
 		if f = strings.ToLower(t.T); f != TypeDecimal && f != TypeNumeric {
-			return "", fmt.Errorf("mysql: unexpected decimal type: %q", t.T)
+			return "", fmt.Errorf("unexpected decimal type: %q", t.T)
 		}
 		switch p, s := t.Precision, t.Scale; {
 		case p < 0 || s < 0:
-			return "", fmt.Errorf("mysql: decimal type must have precision > 0 and scale >= 0: %d, %d", p, s)
+			return "", fmt.Errorf("decimal type must have precision > 0 and scale >= 0: %d, %d", p, s)
 		case p < s:
-			return "", fmt.Errorf("mysql: decimal type must have precision >= scale: %d < %d", p, s)
+			return "", fmt.Errorf("decimal type must have precision >= scale: %d < %d", p, s)
 		case p == 0 && s == 0:
 			// The default value for precision is 10 (i.e. decimal(0,0) = decimal(10)).
 			p = 10
@@ -205,7 +205,7 @@ func ParseType(raw string) (schema.Type, error) {
 		// github.com/mysql/mysql-server/blob/8.0/sql/field.cc#Field_enum::sql_type
 		rv := strings.TrimSuffix(strings.TrimPrefix(raw, t+"("), ")")
 		if rv == "" {
-			return nil, fmt.Errorf("mysql: unexpected enum type: %q", raw)
+			return nil, fmt.Errorf("unexpected enum type: %q", raw)
 		}
 		values := strings.Split(rv, "','")
 		for i := range values {
diff --git a/sql/mysql/inspect.go b/sql/mysql/inspect.go
index 8ecb26f1..2c785468 100644
--- a/sql/mysql/inspect.go
+++ b/sql/mysql/inspect.go
@@ -262,7 +262,7 @@ func (i *inspect) addColumn(s *schema.Schema, rows *sql.Rows) error {
 	}
 	ct, err := ParseType(c.Type.Raw)
 	if err != nil {
-		return err
+		return fmt.Errorf("parse %q.%q type %q: %w", t.Name, c.Name, c.Type.Raw, err)
 	}
 	c.Type.Type = ct
 	attr, err := parseExtra(extra.String)
@@ -451,6 +451,9 @@ func (i *inspect) checks(ctx context.Context, s *schema.Schema) error {
 				// Unset the inspected CHARSET/COLLATE attributes
 				// as they are valid only for character types.
 				c.UnsetCharset().UnsetCollation()
+				// Skip adding this CHECK constraint to the table definition
+				// as it is implicitly created by MariaDB for this JSON column.
+				continue
 			}
 		} else if enforced.String == "NO" {
 			// The ENFORCED attribute is not supported by MariaDB.


Test patch:
diff --git a/cmd/atlas/internal/cmdlog/cmdlog_test.go b/cmd/atlas/internal/cmdlog/cmdlog_test.go
index 2cef48b8..459085b1 100644
--- a/cmd/atlas/internal/cmdlog/cmdlog_test.go
+++ b/cmd/atlas/internal/cmdlog/cmdlog_test.go
@@ -26,8 +26,10 @@ func TestSchemaInspect_MarshalJSON(t *testing.T) {
 	report := &cmdlog.SchemaInspect{
 		Realm: schema.NewRealm(
 			schema.New("test").
+				SetComment("schema comment").
 				AddTables(
 					schema.NewTable("users").
+						SetCharset("charset").
 						AddColumns(
 							&schema.Column{
 								Name: "id",
@@ -36,6 +38,9 @@ func TestSchemaInspect_MarshalJSON(t *testing.T) {
 							&schema.Column{
 								Name: "name",
 								Type: &schema.ColumnType{Raw: "varchar(255)"},
+								Attrs: []schema.Attr{
+									&schema.Collation{V: "collate"},
+								},
 							},
 						),
 					schema.NewTable("posts").
@@ -71,9 +76,11 @@ func TestSchemaInspect_MarshalJSON(t *testing.T) {
             },
             {
               "name": "name",
-              "type": "varchar(255)"
+              "type": "varchar(255)",
+              "collate": "collate"
             }
-          ]
+          ],
+          "charset": "charset"
         },
         {
           "name": "posts",
@@ -88,7 +95,8 @@ func TestSchemaInspect_MarshalJSON(t *testing.T) {
             }
           ]
         }
-      ]
+      ],
+      "comment": "schema comment"
     },
     {
       "name": "temp"
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index f92afc43..509d4d3d 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -1268,21 +1268,11 @@ create table atlas_types_sanity
 			require.True(t, ok)
 			expected := schema.Table{
 				Name: n,
-				Attrs: func() []schema.Attr {
-					if t.version == "maria107" {
-						return []schema.Attr{
-							&schema.Charset{V: "latin1"},
-							&schema.Collation{V: "latin1_swedish_ci"},
-							&mysql.Engine{V: "InnoDB", Default: true},
-							&schema.Check{Name: "tJSON", Expr: "json_valid(`tJSON`)"},
-						}
-					}
-					return []schema.Attr{
-						&schema.Charset{V: "latin1"},
-						&schema.Collation{V: "latin1_swedish_ci"},
-						&mysql.Engine{V: "InnoDB", Default: true},
-					}
-				}(),
+				Attrs: []schema.Attr{
+					&schema.Charset{V: "latin1"},
+					&schema.Collation{V: "latin1_swedish_ci"},
+					&mysql.Engine{V: "InnoDB", Default: true},
+				},
 				Schema: realm.Schemas[0],
 				Columns: []*schema.Column{
 					func() *schema.Column {
diff --git a/internal/integration/testdata/mysql/column-json.txt b/internal/integration/testdata/mysql/column-json.txt
new file mode 100644
index 00000000..62407968
--- /dev/null
+++ b/internal/integration/testdata/mysql/column-json.txt
@@ -0,0 +1,53 @@
+only maria*
+
+apply 1.hcl
+cmpshow users 1.sql
+
+# The CHECK "json_valid(`name`)" should not be present in the HCL
+# description because the "longtext" is converted to "json" type.
+cmphcl 1.inspect.hcl
+
+-- 1.hcl --
+schema "script_column_json" {}
+
+table "users" {
+  schema = schema.script_column_json
+  column "id" {
+    null = false
+    type = int
+  }
+  column "name" {
+    null = false
+    type = json
+  }
+  primary_key {
+    columns = [column.id]
+  }
+}
+
+-- 1.sql --
+CREATE TABLE `users` (
+  `id` int(11) NOT NULL,
+  `name` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL CHECK (json_valid(`name`)),
+  PRIMARY KEY (`id`)
+)
+
+-- 1.inspect.hcl --
+table "users" {
+  schema = schema.script_column_json
+  column "id" {
+    null = false
+    type = int
+  }
+  column "name" {
+    null = false
+    type = json
+  }
+  primary_key {
+    columns = [column.id]
+  }
+}
+schema "script_column_json" {
+  charset = "$charset"
+  collate = "$collate"
+}
\ No newline at end of file
diff --git a/sql/mysql/inspect_test.go b/sql/mysql/inspect_test.go
index dc2f6ac1..b2d4126d 100644
--- a/sql/mysql/inspect_test.go
+++ b/sql/mysql/inspect_test.go
@@ -194,7 +194,6 @@ func TestDriver_InspectTable(t *testing.T) {
 					{Name: "jsonc", Type: &schema.ColumnType{Raw: "json", Type: &schema.JSONType{T: "json"}}},
 				}, t.Columns)
 				require.EqualValues([]schema.Attr{
-					&schema.Check{Name: "jsonc", Expr: "json_valid(`jsonc`)"},
 					&schema.Check{Name: "users_chk_1", Expr: `longtext <> '\'\'""'`},
 				}, t.Attrs)
 			},