Repository: ariga/atlas
Pull request #2756 (2024-05-07 19:03:41)

schemahcl: support passing go context to block handlers


Patch:
diff --git a/cmd/atlas/internal/cmdapi/project.go b/cmd/atlas/internal/cmdapi/project.go
index 366dfbf0..2e41f25b 100644
--- a/cmd/atlas/internal/cmdapi/project.go
+++ b/cmd/atlas/internal/cmdapi/project.go
@@ -5,6 +5,7 @@
 package cmdapi
 
 import (
+	"context"
 	"fmt"
 	"net/url"
 	"os"
@@ -403,7 +404,7 @@ func EnvByName(cmd *cobra.Command, name string, vars map[string]cty.Value) (*Pro
 		}
 		return nil, nil, err
 	}
-	project, err := parseConfig(path, name, vars)
+	project, err := parseConfig(cmd.Context(), path, name, vars)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -496,7 +497,7 @@ const (
 	defaultConfigPath = "file://atlas.hcl"
 )
 
-func parseConfig(path, env string, vars map[string]cty.Value) (*Project, error) {
+func parseConfig(ctx context.Context, path, env string, vars map[string]cty.Value) (*Project, error) {
 	pr, err := partialParse(path, env)
 	if err != nil {
 		return nil, err
@@ -512,6 +513,7 @@ func parseConfig(path, env string, vars map[string]cty.Value) (*Project, error)
 		append(
 			append(cmdext.SpecOptions, specOptions...),
 			cloud.InitBlock(),
+			schemahcl.WithContext(ctx),
 			schemahcl.WithScopedEnums("env.migration.format", cmdmigrate.Formats...),
 			schemahcl.WithScopedEnums("env.migration.exec_order", "LINEAR", "LINEAR_SKIP", "NON_LINEAR"),
 			schemahcl.WithScopedEnums("env.lint.review", ReviewModes...),
diff --git a/cmd/atlas/internal/cmdext/cmdext.go b/cmd/atlas/internal/cmdext/cmdext.go
index 7257a085..607822ca 100644
--- a/cmd/atlas/internal/cmdext/cmdext.go
+++ b/cmd/atlas/internal/cmdext/cmdext.go
@@ -11,7 +11,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"io"
 	"io/fs"
 	"net/url"
 	"os"
@@ -27,7 +26,6 @@ import (
 	"ariga.io/atlas/sql/migrate"
 	"ariga.io/atlas/sql/schema"
 	"ariga.io/atlas/sql/sqlclient"
-	"ariga.io/atlas/sql/sqltool"
 	"github.com/aws/aws-sdk-go-v2/config"
 	"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
 	"golang.org/x/oauth2/google"
@@ -74,15 +72,14 @@ var SpecOptions = append(
 //	locals {
 //	  url = "mysql://root:${data.runtimevar.pass}@:3306/"
 //	}
-func RuntimeVar(c *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func RuntimeVar(ctx context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			URL string `hcl:"url"`
 		}
-		ctx    = context.Background()
 		errorf = blockError("data.runtimevar", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, c, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
 	u, err := sqlclient.ParseURL(args.URL)
@@ -129,7 +126,7 @@ func RuntimeVar(c *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 //		username = "admin"
 //		profile  = "prod-ext"
 //	}
-func AWSRDSToken(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func AWSRDSToken(ctx context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			Endpoint string `hcl:"endpoint"`
@@ -139,12 +136,11 @@ func AWSRDSToken(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
 		}
 		errorf = blockError("data.aws_rds_token", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
-	bgctx := context.Background()
 	cfg, err := config.LoadDefaultConfig(
-		bgctx,
+		ctx,
 		config.WithSharedConfigProfile(args.Profile), // Ignored if empty.
 	)
 	if err != nil {
@@ -153,7 +149,7 @@ func AWSRDSToken(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
 	if args.Region == "" {
 		args.Region = cfg.Region
 	}
-	token, err := auth.BuildAuthToken(bgctx, args.Endpoint, args.Region, args.Username, cfg.Credentials)
+	token, err := auth.BuildAuthToken(ctx, args.Endpoint, args.Region, args.Username, cfg.Credentials)
 	if err != nil {
 		return cty.NilVal, errorf("building auth token: %v", err)
 	}
@@ -163,10 +159,9 @@ func AWSRDSToken(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
 // GCPCloudSQLToken exposes a CloudSQL token as a schemahcl datasource.
 //
 //	data "gcp_cloudsql_token" "hello" {}
-func GCPCloudSQLToken(_ *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func GCPCloudSQLToken(ctx context.Context, _ *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	errorf := blockError("data.gcp_cloudsql_token", block)
-	bgctx := context.Background()
-	ts, err := google.DefaultTokenSource(bgctx, sqladmin.SqlserviceAdminScope)
+	ts, err := google.DefaultTokenSource(ctx, sqladmin.SqlserviceAdminScope)
 	if err != nil {
 		return cty.NilVal, errorf("finding default credentials: %v", err)
 	}
@@ -189,7 +184,7 @@ func GCPCloudSQLToken(_ *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, er
 //	  for_each = toset(data.sql.tenants.values)
 //	  url      = urlsetpath(var.url, each.value)
 //	}
-func Query(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func Query(ctx context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			URL    string   `hcl:"url"`
@@ -200,7 +195,7 @@ func Query(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 		values []cty.Value
 		errorf = blockError("data.sql", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
 	attrs, diags := args.Remain.JustAttributes()
@@ -208,7 +203,7 @@ func Query(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 		return cty.NilVal, errorf("getting attributes: %v", diags)
 	}
 	if at, ok := attrs["args"]; ok {
-		switch v, diags := at.Expr.Value(ctx); {
+		switch v, diags := at.Expr.Value(ectx); {
 		case diags.HasErrors():
 			return cty.NilVal, errorf(`evaluating "args": %w`, diags)
 		case !v.CanIterateElements():
@@ -233,12 +228,12 @@ func Query(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	if len(attrs) > 0 {
 		return cty.NilVal, errorf("unexpected attributes: %v", attrs)
 	}
-	c, err := sqlclient.Open(context.Background(), args.URL)
+	c, err := sqlclient.Open(ctx, args.URL)
 	if err != nil {
 		return cty.NilVal, errorf("opening connection: %w", err)
 	}
 	defer c.Close()
-	rows, err := c.QueryContext(context.Background(), args.Query, args.Args...)
+	rows, err := c.QueryContext(ctx, args.Query, args.Args...)
 	if err != nil {
 		return cty.NilVal, errorf("executing query: %w", err)
 	}
@@ -291,7 +286,7 @@ func Query(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 //	    "env_to_json --file=${var.envfile} | jq '...' ",
 //	  ]
 //	}
-func External(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func External(_ context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			Program []string `hcl:"program"`
@@ -300,7 +295,7 @@ func External(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 		}
 		errorf = blockError("data.external", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
 	attrs, diags := args.Remain.JustAttributes()
@@ -344,7 +339,7 @@ func External(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 //	    dir = data.template_dir.name.url
 //	  }
 //	}
-func TemplateDir(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func TemplateDir(_ context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			Path   string   `hcl:"path"`
@@ -353,7 +348,7 @@ func TemplateDir(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
 		vars   = make(map[string]any)
 		errorf = blockError("data.template_dir", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
 	attrs, diags := args.Remain.JustAttributes()
@@ -361,7 +356,7 @@ func TemplateDir(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error
 		return cty.NilVal, errorf("getting attributes: %v", diags)
 	}
 	if vs, ok := attrs["vars"]; ok {
-		switch vs, diags := vs.Expr.Value(ctx); {
+		switch vs, diags := vs.Expr.Value(ectx); {
 		case diags.HasErrors():
 			return cty.NilVal, errorf(`evaluating "vars": %w`, diags)
 		case !vs.CanIterateElements():
@@ -496,7 +491,7 @@ type AtlasConfig struct {
 //	  }
 //	}
 func (c *AtlasConfig) InitBlock() schemahcl.Option {
-	return schemahcl.WithInitBlock("atlas", func(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+	return schemahcl.WithInitBlock("atlas", func(_ context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 		var args struct {
 			Cloud struct {
 				Token   string `hcl:"token"`
@@ -504,7 +499,7 @@ func (c *AtlasConfig) InitBlock() schemahcl.Option {
 				Project string `hcl:"project,optional"`
 			} `hcl:"cloud,block"`
 		}
-		if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+		if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 			return cty.NilVal, fmt.Errorf("atlas.cloud: decoding body: %v", diags)
 		}
 		if args.Cloud.Project == "" {
@@ -519,7 +514,7 @@ func (c *AtlasConfig) InitBlock() schemahcl.Option {
 		})
 		av, diags := (&hclsyntax.ScopeTraversalExpr{
 			Traversal: hcl.Traversal{hcl.TraverseRoot{Name: "atlas", SrcRange: block.Range()}},
-		}).Value(ctx)
+		}).Value(ectx)
 		switch {
 		case !diags.HasErrors():
 			m := av.AsValueMap()
@@ -537,7 +532,7 @@ var clientType = cty.Capsule("client", reflect.TypeOf(cloudapi.Client{}))
 
 // SchemaHCL is a data source that reads an Atlas HCL schema file(s), evaluates it
 // with the given variables and exposes its resulting schema as in-memory HCL file.
-func SchemaHCL(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func SchemaHCL(_ context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			Path   string               `hcl:"path"`
@@ -546,7 +541,7 @@ func SchemaHCL(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error)
 		}
 		errorf = blockError("data.hcl_schema", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
 	attrs, diags := args.Remain.JustAttributes()
@@ -574,7 +569,7 @@ func SchemaHCL(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error)
 }
 
 // SchemaExternal is a data source that reads a SQL schema state from external program.
-func SchemaExternal(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func SchemaExternal(_ context.Context, ectx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
 	var (
 		args struct {
 			Program []string `hcl:"program"`
@@ -583,7 +578,7 @@ func SchemaExternal(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, er
 		}
 		errorf = blockError("data.external_schema", block)
 	)
-	if diags := gohcl.DecodeBody(block.Body, ctx, &args); diags.HasErrors() {
+	if diags := gohcl.DecodeBody(block.Body, ectx, &args); diags.HasErrors() {
 		return cty.NilVal, errorf("decoding body: %v", diags)
 	}
 	attrs, diags := args.Remain.JustAttributes()
@@ -627,37 +622,6 @@ func SchemaExternal(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, er
 	}), nil
 }
 
-// memdir converts the given (cloud) migrate.Dir to a memory migrate.Dir.
-func memdir(client *cloudapi.Client, name, tag string) (string, error) {
-	dir, err := client.Dir(context.Background(), cloudapi.DirInput{
-		Name: name,
-		Tag:  tag,
-	})
-	if err != nil {
-		return "", err
-	}
-	md := migrate.OpenMemDir(name)
-	files, err := dir.Files()
-	if err != nil {
-		return "", err
-	}
-	for _, f := range files {
-		if err := md.WriteFile(f.Name(), f.Bytes()); err != nil {
-			return "", err
-		}
-	}
-	if hf, err := dir.Open(migrate.HashFileName); err == nil {
-		b, err := io.ReadAll(hf)
-		if err != nil {
-			return "", err
-		}
-		if err := md.WriteFile(migrate.HashFileName, b); err != nil {
-			return "", err
-		}
-	}
-	return "mem://" + name, nil
-}
-
 func blockError(name string, b *hclsyntax.Block) func(string, ...any) error {
 	return func(format string, args ...any) error {
 		return fmt.Errorf("%s.%s: %w", name, b.Labels[1], fmt.Errorf(format, args...))
@@ -745,20 +709,3 @@ func (l MemLoader) LoadState(ctx context.Context, config *StateReaderConfig) (*S
 	}
 	return l.states[u].LoadState(ctx, config)
 }
-
-func dirFormatter(dir migrate.Dir) migrate.Formatter {
-	switch dir.(type) {
-	case *sqltool.DBMateDir:
-		return sqltool.DBMateFormatter
-	case *sqltool.GolangMigrateDir:
-		return sqltool.GolangMigrateFormatter
-	case *sqltool.GooseDir:
-		return sqltool.GooseFormatter
-	case *sqltool.FlywayDir:
-		return sqltool.FlywayFormatter
-	case *sqltool.LiquibaseDir:
-		return sqltool.LiquibaseFormatter
-	default:
-		return migrate.DefaultFormatter
-	}
-}
diff --git a/cmd/atlas/internal/cmdext/cmdext_oss.go b/cmd/atlas/internal/cmdext/cmdext_oss.go
index 0b996c7a..8f285558 100644
--- a/cmd/atlas/internal/cmdext/cmdext_oss.go
+++ b/cmd/atlas/internal/cmdext/cmdext_oss.go
@@ -20,12 +20,12 @@ import (
 var specOptions []schemahcl.Option
 
 // RemoteSchema is a data source that for reading remote schemas.
-func RemoteSchema(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error) {
+func RemoteSchema(context.Context, *hcl.EvalContext, *hclsyntax.Block) (cty.Value, error) {
 	return cty.Zero, fmt.Errorf("data.remote_schema is not supported by this release. See: https://atlasgo.io/getting-started")
 }
 
 // RemoteDir is a data source that reads a remote migration directory.
-func RemoteDir(ctx *hcl.EvalContext, block *hclsyntax.Block) (cty.Value, error) {
+func RemoteDir(context.Context, *hcl.EvalContext, *hclsyntax.Block) (cty.Value, error) {
 	return cty.Zero, fmt.Errorf("data.remote_dir is not supported by this release. See: https://atlasgo.io/getting-started")
 }
 
@@ -43,6 +43,6 @@ func (l EntLoader) LoadState(context.Context, *StateReaderConfig) (*StateReadClo
 }
 
 // MigrateDiff returns the diff between ent.Schema and a directory.
-func (l EntLoader) MigrateDiff(ctx context.Context, opts *MigrateDiffOptions) error {
+func (l EntLoader) MigrateDiff(context.Context, *MigrateDiffOptions) error {
 	return fmt.Errorf("ent:// scheme is no longer supported by this release. See: https://atlasgo.io/getting-started")
 }
diff --git a/doc/md/atlas-schema/external.mdx b/doc/md/atlas-schema/external.mdx
index ca4ad2ff..f2e28a79 100644
--- a/doc/md/atlas-schema/external.mdx
+++ b/doc/md/atlas-schema/external.mdx
@@ -138,6 +138,7 @@ crafting them by hand. The supported ORMs are:
 | <span className="badge badge--secondary">Go</span>                                                                    | [Ent](https://entgo.io/docs/versioned-migrations), [Beego](/guides/orms/beego) | <span className="badge badge--primary">MySQL</span> <span className="badge badge--primary">MariaDB</span> <span className="badge badge--primary">PostgreSQL</span> <span className="badge badge--primary">SQLite</span>                                                          |
 | <span className="badge badge--secondary">Java</span>                                                                  | [Hibernate](/guides/orms/hibernate)                                            | <span className="badge badge--primary">MySQL</span> <span className="badge badge--primary">MariaDB</span> <span className="badge badge--primary">PostgreSQL</span> <span className="badge badge--primary">SQLite</span>                                                          |
 | <span className="badge badge--secondary">JavaScript</span> <span className="badge badge--secondary">TypeScript</span> | [Sequelize](/guides/orms/sequelize), [TypeORM](/guides/orms/typeorm)           | <span className="badge badge--primary">MySQL</span> <span className="badge badge--primary">MariaDB</span> <span className="badge badge--primary">PostgreSQL</span> <span className="badge badge--primary">SQLite</span> <span className="badge badge--primary">SQL Server</span> |
+| <span className="badge badge--secondary">PHP</span>                                                                   | [Doctine](/guides/orms/doctrine)                                               | <span className="badge badge--primary">MySQL</span> <span className="badge badge--primary">MariaDB</span> <span className="badge badge--primary">PostgreSQL</span> <span className="badge badge--primary">SQLite</span> <span className="badge badge--primary">SQL Server</span> |
 
 Coming soon are EF, Laravel, and Prisma. If you are using an ORM that is not listed here and would like to see
 it supported, [let us know!](https://github.com/ariga/atlas/issues/new)
diff --git a/doc/md/guides/guides.mdx b/doc/md/guides/guides.mdx
index cc5332d1..8fb7e527 100644
--- a/doc/md/guides/guides.mdx
+++ b/doc/md/guides/guides.mdx
@@ -280,6 +280,12 @@ import { Card, CardImage } from "../components/card";
     url="/guides/orms/django"
     image={CardImage.Tools}
   />
+  <Card
+    name="Doctrine"
+    description="Learn about automatic migration planning for Doctrine using Atlas"
+    url="/guides/orms/doctrine"
+    image={CardImage.Tools}
+  />
 </div>
 
 ## SQL Server
diff --git a/doc/md/guides/orms/doctrine.mdx b/doc/md/guides/orms/doctrine.mdx
new file mode 100644
index 00000000..e79fb1af
--- /dev/null
+++ b/doc/md/guides/orms/doctrine.mdx
@@ -0,0 +1,401 @@
+---
+id: doctrine
+title: Automatic migration planning for Doctrine
+slug: /guides/orms/doctrine
+---
+
+import InstallationInstructions from '../../components/_installation_instructions.mdx'
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+## TL;DR
+* [Doctrine](https://www.doctrine-project.org/) is an ORM library that's widely used in the PHP community.
+* [Atlas](https://atlasgo.io) is an open-source tool for inspecting, planning, linting and
+  executing schema changes to your database.
+* Developers using Doctrine can use Atlas to automatically plan schema migrations
+  for them, based on the desired state of their schema instead of crafting them by hand.
+
+## Automatic migration planning for Doctrine
+Doctrine is a popular ORM widely used in the PHP community, used by projects like [Symfony](https://symfony.com/).
+Doctrine allows users to manage their database schemas using its [orm:schema-tool](https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/reference/tools.html#schematool) Console Command,
+which is usually sufficient during development and in many simple cases.
+
+However, at some point, teams need more control and decide to employ a [versioned migrations](/concepts/declarative-vs-versioned#versioned-migrations) methodology.
+Doctrine has a [migration tool](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.7/index.html) that can be used to manage versioned migrations.
+Doctrine can also [generate a migration](https://www.doctrine-project.org/projects/doctrine-migrations/en/3.7/reference/generating-migrations.html)
+for you by comparing the current state of your database schema to the state defined by using the ORM.
+
+A downside of this approach is that in order for it to work, you must be connected to a pre-existing database with the current version of the schema.
+In many production environments, databases should generally not be reachable from developer workstations,
+which means this comparison is normally done against a local copy of the database which may have
+undergone some changes that aren't reflected in the existing migrations.
+
+More of the limitations of the Doctrine migration tool include:
+
+* **Ensuring Migration Safety.** Migrations are a risky business. If you're not careful, you can easily cause data loss or 
+  a production outage. The Doctrine migration tool does not provide a native way to ensure that a migration is safe to apply.
+* **Modern Deployments.**  Doctrine does not provide native integrations with modern deployment practices such as [GitOps](/blog/2023/12/06/gitops-for-databases-part-1)
+  or [Infrastructure-as-Code](/blog/2023/07/19/bridging-the-gap-between-iac-and-schema-management).
+
+Using Atlas, you can enjoy automatic migration planning, automatic code review and integrations with your favorite CI/CD tools.
+
+Atlas works in a similar way to the Doctrine migration tool, by calculating the diff between the _current_ state of the database.
+
+In the context of [versioned migrations](/concepts/declarative-vs-versioned#versioned-migrations),
+the current state can be thought of as the database schema that would have
+been created by applying all previous migration scripts.
+
+The desired schema of your application can be provided to Atlas via an [External Schema Datasource](/atlas-schema/projects#data-source-external_schema),
+which is any program that can output a SQL schema definition to stdout.
+
+To use Atlas with Doctrine, users can utilize the [Doctrine Atlas Provider](https://github.com/ariga/atlas-provider-doctrine),
+which is a small program that loads the schema of a Doctrine project into Atlas.
+
+In this guide, we will show how to use Atlas to automatically plan schema migrations for Doctrine users.
+
+## Prerequisites
+
+* A local [Doctrine](https://www.doctrine-project.org/projects/doctrine-orm/en/3.1/index.html) project.
+
+If you don't have a Doctrine project, you can use [symfony/demo](https://github.com/symfony/demo) as a starting point:
+
+```bash
+git clone git@github.com:symfony/demo.git
+```
+
+## Using the Atlas Doctrine Provider
+
+In this guide, we will use the [Doctrine Atlas Provider](https://github.com/ariga/atlas-provider-doctrine)
+to automatically plan schema migrations for a Doctrine project.
+
+### Installation
+
+<InstallationInstructions />
+
+Install the provider, depending on the [`doctrine/dbal`](https://www.doctrine-project.org/projects/dbal.html)
+version you are using in the `composer.json` file:
+<Tabs>
+<TabItem value="v4" label="Doctrine DBAL v4" default>
+
+```bash
+composer require ariga/atlas-provider-doctrine:^4
+```
+</TabItem>
+<TabItem value="v3" label="Dcotrine DBAL v3">
+
+```bash
+composer require ariga/atlas-provider-doctrine:^3
+```
+
+</TabItem>
+</Tabs>
+
+### Configuration
+
+The Atlas Doctrine Provider can be used in three modes:
+
+* **Doctrine Command** - If all of your Doctrine entities exist under a single directory,
+you can add the `atlas-provider` command to the [Doctrine Console](https://www.doctrine-project.org/projects/doctrine-orm/en/3.0/reference/tools.html) file.
+* **Symfony Bundle** - If you use a [Symfony](https://symfony.com/) project, you can register the provider as a Symfony bundle.
+* **Script** - In other cases, you can use the provider as a PHP script to load your Doctrine schema into Atlas.
+
+### Using the Doctrine Command
+
+Add the `atlas-provider` command to the Doctrine Console file:
+
+```diff
+#!/usr/bin/env php
+<?php
+
+use Doctrine\ORM\Tools\Console\ConsoleRunner;
+use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
+
+require 'bootstrap.php';
++ require "vendor/ariga/atlas-provider-doctrine/src/Command.php";
+
+ConsoleRunner::run(
+    new SingleManagerProvider($entityManager),
++   [new AtlasCommand()]
+);
+```
+
+Next, in your project directory, create a new file named `atlas.hcl` with the following contents:
+
+```hcl
+data "external_schema" "doctrine" {
+  program = [
+    "php",
+    "bin/doctrine", // path to your Doctrine Console file
+    "atlas:schema",
+    "--path", "./path/to/entities",
+    "--dialect", "mysql" // mariadb | postgres | sqlite | sqlserver
+  ]
+}
+
+env "doctrine" {
+  src = data.external_schema.doctrine.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+### As Symfony Bundle
+
+Add the following bundle to your `config/bundles.php` file:
+
+```diff
+<?php
+
+require "vendor/autoload.php";
+
+return [
+    ...
++  Ariga\AtlasDoctrineBundle::class => ['all' => true],
+];
+
+```
+
+Next, in your project directory, create a new file named `atlas.hcl` with the following contents:
+
+```hcl
+data "external_schema" "doctrine" {
+  program = [
+    "php",
+    "bin/console",
+    "atlas:schema"
+  ]
+}
+
+env "doctrine" {
+  src = data.external_schema.doctrine.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+:::note
+The provider does not support loading entities via [entity listeners](https://symfony.com/bundles/DoctrineBundle/current/entity-listeners.html).
+To load Symfony schema with entity listeners, use the [Doctrine Bundle](https://symfony.com/doc/current/reference/configuration/doctrine.html) directly:
+
+```diff
+data "external_schema" "doctrine" {
+  program = [
+    "php",
+    "bin/console",
+-   "atlas:schema"
++   "doctrine:schema:create",
++   "--dump-sql"
+  ]
+}
+```
+:::
+
+
+### As a PHP Script
+
+Create a new file named `atlas.php` with the following contents:
+
+```php
+<?php
+
+require "vendor/autoload.php";
+require "vendor/ariga/atlas-provider-doctrine/src/LoadEntities.php";
+
+print (DumpDDL(["./path/to/first/entities", "./path/to/more/entities"], "mysql"));
+```
+
+Next, in your project directory, create a new file named `atlas.hcl` with the following contents:
+
+```hcl
+data "external_schema" "doctrine" {
+  program = [
+    "php",
+    "atlas.php"
+  ]
+}
+
+env "doctrine" {
+  src = data.external_schema.doctrine.url
+  dev = "docker://mysql/8/dev"
+  migration {
+    dir = "file://migrations"
+  }
+  format {
+    migrate {
+      diff = "{{ sql . \"  \" }}"
+    }
+  }
+}
+```
+
+## Usage
+
+Atlas supports a versioned migrations workflow, where each change to the database is versioned and recorded in a migration file. You can use the
+`atlas migrate diff` command to automatically generate a migration file that will migrate the database
+from its latest revision to the current Doctrine schema.
+
+Suppose we have the following files in our `src/entities` directory:
+
+<Tabs>
+<TabItem value="user" label="User.php" default>
+
+```php
+<?php
+
+namespace entities;
+
+use Bug;
+use Doctrine\Common\Collections\Collection;
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
+#[ORM\Table(name: 'users')]
+class User
+{
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column(type: 'integer')]
+    private int|null $id = null;
+
+    #[ORM\Column(type: 'string')]
+    private string $name;
+
+    #[ORM\OneToMany(targetEntity: Bug::class, mappedBy: 'reporter')]
+    private Collection $reportedBugs;
+
+    #[ORM\OneToMany(targetEntity: Bug::class, mappedBy: 'engineer')]
+    private $assignedBugs;
+}
+
+```
+</TabItem>
+<TabItem value="bug" label="Bug.php">
+
+```php
+<?php
+
+namespace entities;
+
+use Doctrine\ORM\Mapping as ORM;
+use User;
+
+#[ORM\Entity]
+#[ORM\Table(name: 'bugs')]
+class Bug
+{
+    #[ORM\Id]
+    #[ORM\Column(type: 'integer')]
+    #[ORM\GeneratedValue]
+    private int|null $id = null;
+
+    #[ORM\Column(type: 'string')]
+    private string $description;
+
+    #[ORM\Column(type: 'datetime')]
+    private DateTime $created;
+
+    #[ORM\Column(type: 'string')]
+    private string $status;
+
+    #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'assignedBugs')]
+    private User|null $engineer = null;
+
+    #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'reportedBugs')]
+    private User|null $reporter;
+}
+
+```
+</TabItem>
+</Tabs>
+
+We can generate a migration file by running the following command:
+
+```bash
+atlas migrate diff --env doctrine
+```
+
+Running this command will generate files in the `migrations` directory, similarly to:
+
+```
+migrations
+|-- 20240310094824.sql
+`-- atlas.sum
+
+0 directories, 2 files
+```
+
+Examining the contents of the `20240310094824.sql`:
+
+
+```sql
+-- Create "users" table
+CREATE TABLE `users` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  PRIMARY KEY (`id`)
+) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+-- Create "bugs" table
+CREATE TABLE `bugs` (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `description` varchar(255) NOT NULL,
+  `created` datetime NOT NULL,
+  `status` varchar(255) NOT NULL,
+  `engineer_id` int NULL,
+  `reporter_id` int NULL,
+  PRIMARY KEY (`id`),
+  INDEX `IDX_1E197C9E1CFE6F5` (`reporter_id`),
+  INDEX `IDX_1E197C9F8D8CDF1` (`engineer_id`),
+  CONSTRAINT `FK_1E197C9E1CFE6F5` FOREIGN KEY (`reporter_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION,
+  CONSTRAINT `FK_1E197C9F8D8CDF1` FOREIGN KEY (`engineer_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION ON DELETE NO ACTION
+) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
+
+```
+
+Amazing! Atlas automatically generated a migration file that will create the `users` and `bugs` tables in our database.
+
+Next, alter the `User` entity to include a `$name` property.
+
+```diff
+    #[ORM\Id]
+    #[ORM\GeneratedValue]
+    #[ORM\Column(type: 'integer')]
+    private int|null $id = null;
+
++   #[ORM\Column(type: 'string')]
++   private string $name;
+```
+
+Re-run this command:
+
+```bash
+atlas migrate diff --env doctrine
+```
+
+Observe that a new migration file is generated:
+
+```sql
+-- Modify "users" table
+ALTER TABLE `users` ADD COLUMN `name` varchar(255) NOT NULL;
+```
+
+## Conclusion
+
+In this guide, we demonstrated how projects using Dcotrine can use Atlas to automatically
+plan schema migrations based only on their data model. To learn more about executing
+migrations against your production database, read the documentation about the
+[`migrate apply`](/versioned/apply) command.
+
+Have questions? Feedback? Find our team [on our Discord server](https://discord.gg/zZ6sWVg6NT)
diff --git a/doc/website/docusaurus.config.js b/doc/website/docusaurus.config.js
index 46107418..8da68f5f 100644
--- a/doc/website/docusaurus.config.js
+++ b/doc/website/docusaurus.config.js
@@ -11,7 +11,7 @@ module.exports = {
   themeConfig: {
     image: "https://og.atlasgo.io/image?title=Schema%20Migration%20Tool%20for%20any%20Language",
     prism: {
-      additionalLanguages: ['hcl', 'java', 'properties', 'gradle'],
+      additionalLanguages: ['hcl', 'java', 'properties', 'gradle', 'php'],
       magicComments: [
         {
           className: 'theme-code-block-highlighted-line',
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index f9ec5cd7..4f266d25 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -348,6 +348,11 @@ module.exports = {
                     id: 'guides/orms/django',
                     label: 'Django'
                 },
+                {
+                    type: 'doc',
+                    id: 'guides/orms/doctrine',
+                    label: 'Doctrine'
+                },
                 {
                     type: 'doc',
                     id: 'guides/frameworks/sqlc-declarative',
diff --git a/schemahcl/context.go b/schemahcl/context.go
index a90d1e84..c49978d6 100644
--- a/schemahcl/context.go
+++ b/schemahcl/context.go
@@ -5,6 +5,7 @@
 package schemahcl
 
 import (
+	"context"
 	"fmt"
 	"reflect"
 	"strconv"
@@ -81,10 +82,14 @@ func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error
 	}
 	var (
 		initblk []*node
+		goctx   = s.config.ctx
 		typeblk = make(map[string]bool)
 		nodes   = make(map[[3]string]*node)
 		blocks  = make(hclsyntax.Blocks, 0, len(body.Blocks))
 	)
+	if goctx == nil {
+		goctx = context.Background()
+	}
 	for _, b := range body.Blocks {
 		switch b := b; {
 		case b.Type == BlockData:
@@ -100,7 +105,7 @@ func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error
 			addr := [3]string{RefData, b.Labels[0], b.Labels[1]}
 			nodes[addr] = &node{
 				addr:  addr,
-				value: func() (cty.Value, error) { return h(ctx, b) },
+				value: func() (cty.Value, error) { return h(goctx, ctx, b) },
 				edges: func() []hcl.Traversal { return bodyVars(b.Body) },
 			}
 		case b.Type == BlockLocals:
@@ -132,7 +137,7 @@ func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error
 			h := s.config.initblk[b.Type]
 			n := &node{
 				addr:  addr,
-				value: func() (cty.Value, error) { return h(ctx, b) },
+				value: func() (cty.Value, error) { return h(goctx, ctx, b) },
 				edges: func() []hcl.Traversal { return bodyVars(b.Body) },
 			}
 			nodes[addr] = n
@@ -152,7 +157,7 @@ func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error
 			addr := [3]string{b.Type, b.Labels[0], b.Labels[1]}
 			nodes[addr] = &node{
 				addr:  addr,
-				value: func() (cty.Value, error) { return h(ctx, b) },
+				value: func() (cty.Value, error) { return h(goctx, ctx, b) },
 				edges: func() []hcl.Traversal { return bodyVars(b.Body) },
 			}
 		default:
diff --git a/schemahcl/schemahcl.go b/schemahcl/schemahcl.go
index d7be1dfe..5e23e983 100644
--- a/schemahcl/schemahcl.go
+++ b/schemahcl/schemahcl.go
@@ -6,6 +6,7 @@ package schemahcl
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"reflect"
 	"sort"
@@ -29,8 +30,11 @@ type (
 		pathVars         map[string]map[string]cty.Value
 		pathFuncs        map[string]map[string]function.Function
 		validator        func() SchemaValidator
-		datasrc, initblk map[string]func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error)
-		typedblk         map[string]map[string]func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error)
+		datasrc, initblk map[string]BlockFunc
+		typedblk         map[string]map[string]BlockFunc
+		// Optional context to pass to dynamic block handlers,
+		// such as data-sources, type-blocks, etc.
+		ctx context.Context
 	}
 	// Option configures a Config.
 	Option func(*Config)
@@ -61,6 +65,15 @@ func New(opts ...Option) *State {
 	}
 }
 
+// WithContext allows passing a context to Eval stage.
+// If provided, data source and type-block handlers will
+// have access to this context.
+func WithContext(ctx context.Context) Option {
+	return func(c *Config) {
+		c.ctx = ctx
+	}
+}
+
 // WithScopedEnums configured a list of allowed ENUMs to be used in
 // the given context, block or attribute. For example, the following
 // option allows setting HASH or BTREE to the "using" attribute in
@@ -109,6 +122,9 @@ func WithFunctions(funcs map[string]function.Function) Option {
 	}
 }
 
+// BlockFunc is the function signature for evaluating a dynamic block.
+type BlockFunc func(context.Context, *hcl.EvalContext, *hclsyntax.Block) (cty.Value, error)
+
 // WithDataSource registers a data source name and its corresponding handler.
 // e.g., the example below registers a data source named "text" that returns
 // the string defined in the data source block.
@@ -128,10 +144,10 @@ func WithFunctions(funcs map[string]function.Function) Option {
 //	data "text" "hello" {
 //	  value = "hello world"
 //	}
-func WithDataSource(name string, h func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error)) Option {
+func WithDataSource(name string, h BlockFunc) Option {
 	return func(c *Config) {
 		if c.datasrc == nil {
-			c.datasrc = make(map[string]func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error))
+			c.datasrc = make(map[string]BlockFunc)
 		}
 		c.datasrc[name] = h
 	}
@@ -156,13 +172,13 @@ func WithDataSource(name string, h func(*hcl.EvalContext, *hclsyntax.Block) (cty
 //	driver "remote" "hello" {
 //	  token = "hello world"
 //	}
-func WithTypeLabelBlock(name, label string, h func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error)) Option {
+func WithTypeLabelBlock(name, label string, h BlockFunc) Option {
 	return func(c *Config) {
 		if c.typedblk == nil {
-			c.typedblk = make(map[string]map[string]func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error))
+			c.typedblk = make(map[string]map[string]BlockFunc)
 		}
 		if c.typedblk[name] == nil {
-			c.typedblk[name] = make(map[string]func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error))
+			c.typedblk[name] = make(map[string]BlockFunc)
 		}
 		c.typedblk[name][label] = h
 	}
@@ -182,10 +198,10 @@ func WithTypeLabelBlock(name, label string, h func(*hcl.EvalContext, *hclsyntax.
 //		}
 //		return cty.ObjectVal(map[string]cty.Value{"modules": v}), nil
 //	})
-func WithInitBlock(name string, h func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error)) Option {
+func WithInitBlock(name string, h BlockFunc) Option {
 	return func(c *Config) {
 		if c.initblk == nil {
-			c.initblk = make(map[string]func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error))
+			c.initblk = make(map[string]BlockFunc)
 		}
 		c.initblk[name] = h
 	}


Test patch:
diff --git a/schemahcl/schemahcl_test.go b/schemahcl/schemahcl_test.go
index c08a92a4..b1fe830a 100644
--- a/schemahcl/schemahcl_test.go
+++ b/schemahcl/schemahcl_test.go
@@ -5,6 +5,7 @@
 package schemahcl
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"log"
@@ -439,12 +440,12 @@ env "dev" {
 `)
 	)
 	require.NoError(t, New(
-		WithDataSource("sql", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+		WithDataSource("sql", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 			attrs, diags := b.Body.JustAttributes()
 			if diags.HasErrors() {
 				return cty.NilVal, diags
 			}
-			v, diags := attrs["query"].Expr.Value(ctx)
+			v, diags := attrs["query"].Expr.Value(ectx)
 			if diags.HasErrors() {
 				return cty.NilVal, diags
 			}
@@ -552,30 +553,30 @@ table "s2" {
 func TestDataLocalsRefs(t *testing.T) {
 	var (
 		opts = []Option{
-			WithDataSource("sql", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithDataSource("sql", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 				attrs, diags := b.Body.JustAttributes()
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				v, diags := attrs["result"].Expr.Value(ctx)
+				v, diags := attrs["result"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
 				return cty.ObjectVal(map[string]cty.Value{"output": v}), nil
 			}),
-			WithDataSource("text", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithDataSource("text", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 				attrs, diags := b.Body.JustAttributes()
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				v, diags := attrs["value"].Expr.Value(ctx)
+				v, diags := attrs["value"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
 				return cty.ObjectVal(map[string]cty.Value{"output": v}), nil
 			}),
-			WithInitBlock("atlas", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
-				org, diags := b.Body.Attributes["org"].Expr.Value(ctx)
+			WithInitBlock("atlas", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+				org, diags := b.Body.Attributes["org"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
@@ -586,7 +587,7 @@ func TestDataLocalsRefs(t *testing.T) {
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				host, diags := attrs["host"].Expr.Value(ctx)
+				host, diags := attrs["host"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
@@ -597,12 +598,12 @@ func TestDataLocalsRefs(t *testing.T) {
 					}),
 				}), nil
 			}),
-			WithDataSource("remote_dir", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithDataSource("remote_dir", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 				attrs, diags := b.Body.JustAttributes()
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				host, diags := attrs["host"].Expr.Value(ctx)
+				host, diags := attrs["host"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
@@ -611,7 +612,7 @@ func TestDataLocalsRefs(t *testing.T) {
 						hcl.TraverseRoot{Name: "atlas", SrcRange: b.Range()},
 						hcl.TraverseAttr{Name: "org", SrcRange: b.Range()},
 					},
-				}).Value(ctx)
+				}).Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
@@ -734,19 +735,19 @@ out = data.unknown.a.output
 func TestSkippedDataSrc(t *testing.T) {
 	var (
 		opts = []Option{
-			WithDataSource("dynamic", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithDataSource("dynamic", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 				attrs, diags := b.Body.JustAttributes()
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				s, diags := attrs["skip"].Expr.Value(ctx)
+				s, diags := attrs["skip"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
 				if s.True() {
 					return cty.NilVal, fmt.Errorf("data source should be skipped, but was called with %q", b.Labels)
 				}
-				v, diags := attrs["v"].Expr.Value(ctx)
+				v, diags := attrs["v"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
@@ -867,28 +868,28 @@ func TestTypeLabelBlock(t *testing.T) {
 	var (
 		callD, callT int
 		opts         = []Option{
-			WithTypeLabelBlock("driver", "remote", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithTypeLabelBlock("driver", "remote", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 				attrs, diags := b.Body.JustAttributes()
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				v, diags := attrs["name"].Expr.Value(ctx)
+				v, diags := attrs["name"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
 				callT++
 				return cty.ObjectVal(map[string]cty.Value{"url": cty.StringVal("driver://" + v.AsString())}), nil
 			}),
-			WithTypeLabelBlock("driver", "not_called", func(*hcl.EvalContext, *hclsyntax.Block) (cty.Value, error) {
+			WithTypeLabelBlock("driver", "not_called", func(context.Context, *hcl.EvalContext, *hclsyntax.Block) (cty.Value, error) {
 				t.Fatal("should not be called")
 				return cty.NilVal, nil
 			}),
-			WithDataSource("text", func(ctx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
+			WithDataSource("text", func(_ context.Context, ectx *hcl.EvalContext, b *hclsyntax.Block) (cty.Value, error) {
 				attrs, diags := b.Body.JustAttributes()
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}
-				v, diags := attrs["value"].Expr.Value(ctx)
+				v, diags := attrs["value"].Expr.Value(ectx)
 				if diags.HasErrors() {
 					return cty.NilVal, diags
 				}