Repository: ariga/atlas
Pull request #280 (2021-12-05 14:45:16)

sql/mysql: initial support for mariadb databases


Patch:
diff --git a/schema/schemaspec/schemahcl/context.go b/schema/schemaspec/schemahcl/context.go
index 00a97e1d..56c8d8df 100644
--- a/schema/schemaspec/schemahcl/context.go
+++ b/schema/schemaspec/schemahcl/context.go
@@ -27,7 +27,7 @@ import (
 //		title = "{greeting.hebrew.word}, world!"
 //	}
 //
-func evalCtx(f *hcl.File) (*hcl.EvalContext, error) {
+func evalCtx(ctx *hcl.EvalContext, f *hcl.File) (*hcl.EvalContext, error) {
 	c := &container{}
 	if diag := gohcl.DecodeBody(f.Body, &hcl.EvalContext{}, c); diag.HasErrors() {
 		return nil, diag
@@ -41,9 +41,10 @@ func evalCtx(f *hcl.File) (*hcl.EvalContext, error) {
 	if err != nil {
 		return nil, err
 	}
-	return &hcl.EvalContext{
-		Variables: vars,
-	}, nil
+	for k, v := range vars {
+		ctx.Variables[k] = v
+	}
+	return ctx, nil
 }
 
 func blockVars(b *hclsyntax.Body, parentAddr string, defs *blockDef) (map[string]cty.Value, error) {
diff --git a/schema/schemaspec/schemahcl/hcl.go b/schema/schemaspec/schemahcl/hcl.go
index d06495a9..3adfe6ba 100644
--- a/schema/schemaspec/schemahcl/hcl.go
+++ b/schema/schemaspec/schemahcl/hcl.go
@@ -30,24 +30,34 @@ var (
 
 var (
 	// Unmarshal parses the Atlas HCL-encoded data and stores the result in the target.
-	Unmarshal = schemaspec.UnmarshalerFunc(func(data []byte, v interface{}) error {
-		spec, err := decode(data)
-		if err != nil {
-			return fmt.Errorf("schemahcl: failed decoding: %w", err)
-		}
-		if err := spec.As(v); err != nil {
-			return fmt.Errorf("schemahcl: failed reading spec as %T: %w", v, err)
-		}
-		return nil
-	})
+	Unmarshal = schemaspec.UnmarshalerFunc(NewUnmarshaler().UnmarshalSpec)
 )
 
-type container struct {
-	Body hcl.Body `hcl:",remain"`
+type (
+	container struct {
+		Body hcl.Body `hcl:",remain"`
+	}
+
+	// Unmarshaler implements schemaspec.Unmarshaler
+	Unmarshaler struct {
+		config *Config
+	}
+)
+
+// UnmarshalSpec implements schemaspec.Unmarshaler.
+func (u *Unmarshaler) UnmarshalSpec(data []byte, v interface{}) error {
+	spec, err := decode(u.config.ctx, data)
+	if err != nil {
+		return fmt.Errorf("schemahcl: failed decoding: %w", err)
+	}
+	if err := spec.As(v); err != nil {
+		return fmt.Errorf("schemahcl: failed reading spec as %T: %w", v, err)
+	}
+	return nil
 }
 
 // decode decodes the input Atlas HCL document and returns a *schemaspec.Resource representing it.
-func decode(body []byte) (*schemaspec.Resource, error) {
+func decode(ctx *hcl.EvalContext, body []byte) (*schemaspec.Resource, error) {
 	parser := hclparse.NewParser()
 	srcHCL, diag := parser.ParseHCL(body, "")
 	if diag.HasErrors() {
@@ -57,7 +67,7 @@ func decode(body []byte) (*schemaspec.Resource, error) {
 		return nil, fmt.Errorf("schemahcl: no HCL syntax found in body")
 	}
 	c := &container{}
-	ctx, err := evalCtx(srcHCL)
+	ctx, err := evalCtx(ctx, srcHCL)
 	if err != nil {
 		return nil, err
 	}
diff --git a/schema/schemaspec/schemahcl/opts.go b/schema/schemaspec/schemahcl/opts.go
new file mode 100644
index 00000000..dfdd7d13
--- /dev/null
+++ b/schema/schemaspec/schemahcl/opts.go
@@ -0,0 +1,36 @@
+package schemahcl
+
+import (
+	"github.com/hashicorp/hcl/v2"
+	"github.com/zclconf/go-cty/cty"
+)
+
+type (
+	// Config configures an unmarshaling.
+	Config struct {
+		ctx *hcl.EvalContext
+	}
+
+	// Option configures a Config.
+	Option func(*Config)
+)
+
+// NewUnmarshaler returns a schemaspec.Unmarshaler configured with options.
+func NewUnmarshaler(opts ...Option) *Unmarshaler {
+	cfg := &Config{
+		ctx: &hcl.EvalContext{
+			Variables: make(map[string]cty.Value),
+		},
+	}
+	for _, opt := range opts {
+		opt(cfg)
+	}
+	return &Unmarshaler{config: cfg}
+}
+
+// EvalContext configures an unmarshaler to decode with an *hcl.EvalContext.
+func EvalContext(ctx *hcl.EvalContext) Option {
+	return func(config *Config) {
+		config.ctx = ctx
+	}
+}
diff --git a/sql/mysql/diff.go b/sql/mysql/diff.go
index eb7f8088..e7b5c208 100644
--- a/sql/mysql/diff.go
+++ b/sql/mysql/diff.go
@@ -12,8 +12,6 @@ import (
 
 	"ariga.io/atlas/sql/internal/sqlx"
 	"ariga.io/atlas/sql/schema"
-
-	"golang.org/x/mod/semver"
 )
 
 // A diff provides a MySQL implementation for sqlx.DiffDriver.
@@ -243,7 +241,7 @@ func (d *diff) typeChanged(from, to *schema.Column) (bool, error) {
 		toT := toT.(*schema.IntegerType)
 		// MySQL v8.0.19 dropped the display width
 		// information from the information schema.
-		if semver.Compare("v"+d.version, "v8.0.19") == -1 {
+		if d.supportsDisplayWidth() {
 			ft, _, _, err := parseColumn(fromT.T)
 			if err != nil {
 				return false, err
diff --git a/sql/mysql/driver.go b/sql/mysql/driver.go
index c61a994b..6f9446fd 100644
--- a/sql/mysql/driver.go
+++ b/sql/mysql/driver.go
@@ -114,10 +114,8 @@ func (d *Driver) inspectTable(ctx context.Context, name string, opts *schema.Ins
 	if err := d.fks(ctx, t); err != nil {
 		return nil, err
 	}
-	if d.supportsCheck() {
-		if err := d.checks(ctx, t); err != nil {
-			return nil, err
-		}
+	if err := d.checks(ctx, t); err != nil {
+		return nil, err
 	}
 	return t, nil
 }
@@ -501,20 +499,24 @@ func (d *Driver) fks(ctx context.Context, t *schema.Table) error {
 
 // checks queries and appends the check constraints of the given table.
 func (d *Driver) checks(ctx context.Context, t *schema.Table) error {
-	rows, err := d.QueryContext(ctx, checksQuery, t.Schema.Name, t.Name)
+	query, ok := d.supportsCheck()
+	if !ok {
+		return nil
+	}
+	rows, err := d.QueryContext(ctx, query, t.Schema.Name, t.Name)
 	if err != nil {
 		return fmt.Errorf("mysql: querying %q check constraints: %w", t.Name, err)
 	}
 	defer rows.Close()
 	for rows.Next() {
-		var name, enforced, clause string
-		if err := rows.Scan(&name, &enforced, &clause); err != nil {
+		var name, clause, enforced sql.NullString
+		if err := rows.Scan(&name, &clause, &enforced); err != nil {
 			return fmt.Errorf("mysql: %w", err)
 		}
 		t.Attrs = append(t.Attrs, &Check{
-			Name:     name,
-			Clause:   unescape(clause),
-			Enforced: clause != "NO",
+			Name:     name.String,
+			Clause:   unescape(clause.String),
+			Enforced: clause.String != "NO",
 		})
 
 	}
@@ -541,11 +543,44 @@ func (d *Driver) tableNames(ctx context.Context, schema string, opts *schema.Ins
 	return names, nil
 }
 
-// supportsCheck reports if connected MySQL database supports the CHECK clause.
-func (d *Driver) supportsCheck() bool { return semver.Compare("v"+d.version, "v8.0.16") != -1 }
+// supportsCheck reports if the connected database supports
+// the CHECK clause, and return the querying for getting them.
+func (d *Driver) supportsCheck() (string, bool) {
+	v, q := "8.0.16", myChecksQuery
+	if d.mariadb() {
+		v, q = "10.2.1", marChecksQuery
+	}
+	return q, d.compareV(v) != -1
+}
 
-// supportsIndexExpr reports if connected MySQL database supports index expressions (functional key part).
-func (d *Driver) supportsIndexExpr() bool { return semver.Compare("v"+d.version, "v8.0.13") != -1 }
+// supportsIndexExpr reports if the connected database supports
+// index expressions (functional key part).
+func (d *Driver) supportsIndexExpr() bool {
+	return !d.mariadb() && d.compareV("8.0.13") != -1
+}
+
+// supportsDisplayWidth reports if the connected database supports
+// getting the display width information from the information schema.
+func (d *Driver) supportsDisplayWidth() bool {
+	// MySQL v8.0.19 dropped the display width
+	// information from the information schema
+	return d.mariadb() || d.compareV("8.0.19") == -1
+}
+
+// mariadb reports if the Driver is connected to a MariaDB database.
+func (d *Driver) mariadb() bool {
+	return strings.Index(d.version, "MariaDB") > 0
+}
+
+// compareV returns an integer comparing two versions according to
+// semantic version precedence.
+func (d *Driver) compareV(w string) int {
+	v := d.version
+	if d.mariadb() {
+		v = v[:strings.Index(v, "MariaDB")-1]
+	}
+	return semver.Compare("v"+v, "v"+w)
+}
 
 // parseColumn returns column parts, size and signed-info from a MySQL type.
 func parseColumn(typ string) (parts []string, size int64, unsigned bool, err error) {
@@ -636,11 +671,9 @@ WHERE
 `
 
 	// Query to list table check constraints.
-	checksQuery = `
-SELECT
-	t1.CONSTRAINT_NAME,
-	t1.ENFORCED,
-	t2.CHECK_CLAUSE
+	myChecksQuery  = `SELECT t1.CONSTRAINT_NAME, t2.CHECK_CLAUSE, t1.ENFORCED` + checksQuery
+	marChecksQuery = `SELECT t1.CONSTRAINT_NAME, t2.CHECK_CLAUSE, "YES" AS ENFORCED` + checksQuery
+	checksQuery    = `
 FROM
 	INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS t1
 	JOIN INFORMATION_SCHEMA.CHECK_CONSTRAINTS AS t2


Test patch:
diff --git a/schema/schemaspec/schemahcl/context_test.go b/schema/schemaspec/schemahcl/context_test.go
index 6eea3f0f..7a9f58f4 100644
--- a/schema/schemaspec/schemahcl/context_test.go
+++ b/schema/schemaspec/schemahcl/context_test.go
@@ -4,7 +4,9 @@ import (
 	"testing"
 
 	"ariga.io/atlas/schema/schemaspec"
+	"github.com/hashicorp/hcl/v2"
 	"github.com/stretchr/testify/require"
+	"github.com/zclconf/go-cty/cty"
 )
 
 func TestReferences(t *testing.T) {
@@ -328,3 +330,22 @@ point {
 `
 	require.Equal(t, expected, string(b))
 }
+
+func TestEvalContext(t *testing.T) {
+	f := `
+x = from_ctx
+`
+	var test struct {
+		X int `spec:"x"`
+	}
+	unmarshaler := NewUnmarshaler(
+		EvalContext(&hcl.EvalContext{
+			Variables: map[string]cty.Value{
+				"from_ctx": cty.NumberIntVal(42),
+			},
+		}),
+	)
+	err := unmarshaler.UnmarshalSpec([]byte(f), &test)
+	require.NoError(t, err)
+	require.EqualValues(t, 42, test.X)
+}
diff --git a/sql/mysql/driver_test.go b/sql/mysql/driver_test.go
index 85a1afb9..98d26f27 100644
--- a/sql/mysql/driver_test.go
+++ b/sql/mysql/driver_test.go
@@ -143,6 +143,39 @@ func TestDriver_InspectTable(t *testing.T) {
 				}, t.Columns)
 			},
 		},
+		{
+			name: "maria/types",
+			before: func(m mock) {
+				m.version("10.7.1-MariaDB")
+				m.tableExists("public", "users", true)
+				m.ExpectQuery(sqltest.Escape(columnsQuery)).
+					WithArgs("public", "users").
+					WillReturnRows(sqltest.Rows(`
++---------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
+| column_name   | column_type                  | column_comment       | is_nullable | column_key | column_default | extra          | character_set_name | collation_name |
++---------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
+| id            | bigint(20)                   |                      | NO          | PRI        | NULL           | auto_increment | NULL               | NULL           |
+| tiny_int      | tinyint(1)                   |                      | NO          |            | NULL           |                | NULL               | NULL           |
++---------------+------------------------------+----------------------+-------------+------------+----------------+----------------+--------------------+----------------+
+`))
+				m.ExpectQuery(sqltest.Escape(indexesQuery)).
+					WillReturnRows(sqlmock.NewRows([]string{"index_name", "column_name", "non_unique", "key_part", "expression"}))
+				m.noFKs()
+				m.ExpectQuery(sqltest.Escape(marChecksQuery)).
+					WithArgs("public", "users").
+					WillReturnRows(sqlmock.NewRows([]string{"CONSTRAINT_NAME", "CHECK_CLAUSE", "ENFORCED"}))
+			},
+			expect: func(require *require.Assertions, t *schema.Table, err error) {
+				require.NoError(err)
+				require.Equal("users", t.Name)
+				require.Len(t.PrimaryKey.Parts, 1)
+				require.True(t.PrimaryKey.Parts[0].C == t.Columns[0])
+				require.EqualValues([]*schema.Column{
+					{Name: "id", Type: &schema.ColumnType{Raw: "bigint(20)", Type: &schema.IntegerType{T: "bigint"}}, Attrs: []schema.Attr{&AutoIncrement{A: "auto_increment"}}},
+					{Name: "tiny_int", Type: &schema.ColumnType{Raw: "tinyint(1)", Type: &schema.BoolType{T: "tinyint"}}},
+				}, t.Columns)
+			},
+		},
 		{
 			name: "decimal types",
 			before: func(m mock) {
@@ -532,14 +565,14 @@ func TestDriver_InspectTable(t *testing.T) {
 `))
 				m.noIndexes()
 				m.noFKs()
-				m.ExpectQuery(sqltest.Escape(checksQuery)).
+				m.ExpectQuery(sqltest.Escape(myChecksQuery)).
 					WithArgs("public", "users").
 					WillReturnRows(sqltest.Rows(`
-+-------------------+----------+-----------------------------------------------------------------+
-| CONSTRAINT_NAME   | ENFORCED | CHECK_CLAUSE                                                    |
-+-------------------+----------+-----------------------------------------------------------------+
-| users_chk_1       | YES      | (` + "`c6`" + ` <>_latin1\'foo\\\'s\')                          |
-+-------------------+----------+-----------------------------------------------------------------+
++-------------------+-------------------------------------------+------------+
+| CONSTRAINT_NAME   | CHECK_CLAUSE                              |  ENFORCED  |
++-------------------+-------------------------------------------+------------+
+| users_chk_1       | (` + "`c6`" + ` <>_latin1\'foo\\\'s\')    |  YES       |
++-------------------+-------------------------------------------+------------+
 `))
 			},
 			expect: func(require *require.Assertions, t *schema.Table, err error) {