Repository: ariga/atlas
Pull request #1216 (2022-10-20 14:56:58)

schemahcl: added `urlsetpath` func to support change DB schema
Signed-off-by: Giau. Tran Minh <hello@giautm.dev>


Patch:
diff --git a/doc/md/atlas-schema/projects.md b/doc/md/atlas-schema/projects.md
index 7cb44137..1f9bb750 100644
--- a/doc/md/atlas-schema/projects.md
+++ b/doc/md/atlas-schema/projects.md
@@ -202,7 +202,7 @@ EOS
 env "prod" {
   // Reference a data source.
   for_each = toset(data.sql.tenants.values)
-  url = "${local.base_url}/${each.value}"
+  url      = urlsetpath(var.url, each.value)
 }
 ```
 
@@ -248,7 +248,7 @@ that `for_each` accepts a map or a set of strings.
 ```hcl
 env "prod" {
   for_each = toset(data.sql.tenants.values)
-  url      = "${local.base_url}/${each.value}"
+  url      = urlsetpath(var.url, each.value)
   migration {
     dir = "file://migrations"
   }
diff --git a/schemahcl/stdlib.go b/schemahcl/stdlib.go
index ba13586a..4614e75e 100644
--- a/schemahcl/stdlib.go
+++ b/schemahcl/stdlib.go
@@ -5,6 +5,7 @@
 package schemahcl
 
 import (
+	"net/url"
 	"strconv"
 
 	"github.com/hashicorp/hcl/v2"
@@ -144,17 +145,18 @@ func stdFuncs() map[string]function.Function {
 		"substr":          stdlib.SubstrFunc,
 		"timeadd":         stdlib.TimeAddFunc,
 		"title":           stdlib.TitleFunc,
-		"tostring":        makeToFunc(cty.String),
-		"tonumber":        makeToFunc(cty.Number),
 		"tobool":          makeToFunc(cty.Bool),
-		"toset":           makeToFunc(cty.Set(cty.DynamicPseudoType)),
 		"tolist":          makeToFunc(cty.List(cty.DynamicPseudoType)),
+		"tonumber":        makeToFunc(cty.Number),
+		"toset":           makeToFunc(cty.Set(cty.DynamicPseudoType)),
+		"tostring":        makeToFunc(cty.String),
 		"trim":            stdlib.TrimFunc,
 		"trimprefix":      stdlib.TrimPrefixFunc,
 		"trimspace":       stdlib.TrimSpaceFunc,
 		"trimsuffix":      stdlib.TrimSuffixFunc,
 		"try":             tryfunc.TryFunc,
 		"upper":           stdlib.UpperFunc,
+		"urlsetpath":      urlSetPathFunc,
 		"values":          stdlib.ValuesFunc,
 		"zipmap":          stdlib.ZipmapFunc,
 	}
@@ -235,3 +237,25 @@ func makeToFunc(wantTy cty.Type) function.Function {
 		},
 	})
 }
+
+var urlSetPathFunc = function.New(&function.Spec{
+	Params: []function.Parameter{
+		{
+			Name: "url",
+			Type: cty.String,
+		},
+		{
+			Name: "path",
+			Type: cty.String,
+		},
+	},
+	Type: function.StaticReturnType(cty.String),
+	Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
+		u, err := url.Parse(args[0].AsString())
+		if err != nil {
+			return cty.NilVal, err
+		}
+		u.Path = args[1].AsString()
+		return cty.StringVal(u.String()), nil
+	},
+})


Test patch:
diff --git a/internal/integration/testdata/mysql/cli-migrate-apply-datasrc.txt b/internal/integration/testdata/mysql/cli-migrate-apply-datasrc.txt
index cbb7bf11..d988572d 100644
--- a/internal/integration/testdata/mysql/cli-migrate-apply-datasrc.txt
+++ b/internal/integration/testdata/mysql/cli-migrate-apply-datasrc.txt
@@ -13,11 +13,6 @@ variable "pattern" {
   type = string
 }
 
-locals {
-  # The database URL without its schema name.
-  base_url = regexreplace(var.url, "/\\w+(\\?parseTime=true)?", "")
-}
-
 data "sql" "tenants" {
   url = var.url
   query = <<EOS
@@ -30,7 +25,7 @@ EOS
 
 env "dev" {
   for_each = toset(data.sql.tenants.values)
-  url = "${local.base_url}/${each.value}"
+  url      = urlsetpath(var.url, each.value)
 }
 
 -- migrations/1_create_users.sql --
diff --git a/schemahcl/stdlib_test.go b/schemahcl/stdlib_test.go
new file mode 100644
index 00000000..361ed4fb
--- /dev/null
+++ b/schemahcl/stdlib_test.go
@@ -0,0 +1,49 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package schemahcl
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/zclconf/go-cty/cty"
+)
+
+func TestURLSetPathFunc(t *testing.T) {
+	tests := []struct {
+		URL  cty.Value
+		Path cty.Value
+		Want cty.Value
+	}{
+		{
+			cty.StringVal("mysql://root:pass@mysql:3306"),
+			cty.StringVal(""),
+			cty.StringVal("mysql://root:pass@mysql:3306"),
+		},
+		{
+			cty.StringVal("mysql://root:pass@mysql:3306?parseTime=true"),
+			cty.StringVal("my-tenant"),
+			cty.StringVal("mysql://root:pass@mysql:3306/my-tenant?parseTime=true"),
+		},
+		{
+			cty.StringVal("mysql://root:pass@mysql:3306/admin?parseTime=true"),
+			cty.StringVal("my-tenant"),
+			cty.StringVal("mysql://root:pass@mysql:3306/my-tenant?parseTime=true"),
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(fmt.Sprintf("(%#v,%#v)", test.URL, test.Path), func(t *testing.T) {
+			got, err := urlSetPathFunc.Call([]cty.Value{test.URL, test.Path})
+
+			if err != nil {
+				t.Fatalf("unexpected error: %s", err)
+			}
+			if !got.RawEquals(test.Want) {
+				t.Errorf("wrong result\ngot:  %#v\nwant: %#v", got, test.Want)
+			}
+		})
+	}
+}