Repository: ariga/atlas
Pull request #1749 (2023-06-16 18:56:13)

doc/md/integrations: operator support versioned migration


Patch:
diff --git a/cmd/atlas/internal/cmdapi/project.go b/cmd/atlas/internal/cmdapi/project.go
index 7c462b95..e9f2421f 100644
--- a/cmd/atlas/internal/cmdapi/project.go
+++ b/cmd/atlas/internal/cmdapi/project.go
@@ -451,11 +451,9 @@ func partialParse(path, env string) (*hclparse.Parser, error) {
 	if err != nil {
 		return nil, err
 	}
-	var used, datasrc []*hclsyntax.Block
+	var used []*hclsyntax.Block
 	for _, b := range fi.Body.(*hclsyntax.Body).Blocks {
 		switch b.Type {
-		case schemahcl.BlockData:
-			datasrc = append(datasrc, b)
 		case blockEnv:
 			switch n := len(b.Labels); {
 			// No env was selected.
@@ -474,14 +472,9 @@ func partialParse(path, env string) (*hclparse.Parser, error) {
 			used = append(used, b)
 		}
 	}
-	body := &hclsyntax.Body{
+	fi.Body = &hclsyntax.Body{
 		Blocks:     used,
 		Attributes: fi.Body.(*hclsyntax.Body).Attributes,
 	}
-	// If no attributes, locals, variables, or blocks reference data sources, we skip
-	// loading. Otherwise, body stays unchanged as data sources may depend on others.
-	if len(schemahcl.DynamicRefs(body).Data) == 0 {
-		fi.Body = body
-	}
 	return parser, nil
 }
diff --git a/doc/md/integrations/kubernetes-operator.mdx b/doc/md/integrations/kubernetes/operator.mdx
similarity index 59%
rename from doc/md/integrations/kubernetes-operator.mdx
rename to doc/md/integrations/kubernetes/operator.mdx
index 0193821b..e2b2320f 100644
--- a/doc/md/integrations/kubernetes-operator.mdx
+++ b/doc/md/integrations/kubernetes/operator.mdx
@@ -1,44 +1,72 @@
 ---
 title: The Atlas Kubernetes Operator
-id: kubernetes-operator
+id: operator
 slug: /integrations/kubernetes/operator
 ---
 
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 
-## Background
+## What are Kubernetes Operators?
 
-### What are Kubernetes Operators?
-
-For many stateful resources, reconciling the desired state of a database with its actual state
-can be a complex task that requires a lot of domain knowledge. [Kubernetes Operators](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
+Kubernetes is an excellent solution for managing stateless resources. But for many stateful resources, such as databases,
+reconciling the desired state of a database with its actual state can be a complex task that requires specific domain
+knowledge. [Kubernetes Operators](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/)
 were introduced to the Kubernetes ecosystem to help users manage complex stateful resources by codifying
 this domain knowledge into a Kubernetes controller.
 
-### What is the Atlas Kubernetes Operator?
+## What is the Atlas Kubernetes Operator?
 
 The Atlas Kubernetes Operator is a Kubernetes controller that uses Atlas to manage
 your database schema. The Atlas Kubernetes Operator allows you to define the desired schema 
 and apply it to your database using the Kubernetes API.
 
-### Supported workflows
+## Supported workflows
 
 * [Declarative schema migrations](#declarative-schema-migrations)
-* Versioned schema migrations - coming soon. 
+* [Versioned schema migrations](#versioned-schema-migrations)
 
 ### Declarative schema migrations
 
-![](https://user-images.githubusercontent.com/1522681/236139615-1d10feea-8b19-46a2-905b-b614883c48c8.png)
+![](https://atlasgo.io/uploads/k8s/operator/declarative-workflow.png)
 
-The Atlas Kubernetes Operator supports [declarative migrations](https://atlasgo.io/concepts/declarative-vs-versioned#declarative-migrations).
+The Atlas Kubernetes Operator supports [declarative migrations](/concepts/declarative-vs-versioned#declarative-migrations).
 In declarative migrations, the desired state of the database is defined by the user and the operator is responsible
 for reconciling the desired state with the actual state of the database (planning and executing `CREATE`, `ALTER`
 and `DROP` statements).
 
-## Getting started
+In this workflow, after installing the Atlas Kubernetes Operator, the user defines the desired state of the database
+as an `AtlasSchema` resource which either includes the desired schema directly or references a `ConfigMap` that contains
+it.
+
+The operator then reconciles the desired state with the actual state of the database by planning and executing
+`CREATE`, `ALTER` and `DROP` statements.
+
+To ensure that the operator does not accidentally introduce destructive changes to the database, the user can
+specify linting or diffing policies that will be applied to the computed plan before it is applied to the database.
+
+To learn more about getting started with the Atlas Operator and declarative migrations, see the
+[announcement blog post](/blog/2023/05/08/atlas-v011-kubernetes-operator).
+
+### Versioned schema migrations
+
+![](https://atlasgo.io/uploads/k8s/operator/versioned-flow.png)
+
+The Atlas Kubernetes Operator also supports [versioned migrations](https://atlasgo.io/concepts/declarative-vs-versioned#versioned-migrations).
+In versioned migrations, the database schema is defined by a series of SQL scripts ("migrations") that are applied
+in lexicographical order. The user can specify the version and migration directory to run, which can be located
+on the [Atlas Cloud](https://atlasgo.io/cloud/getting-started) or stored as a `ConfigMap` in your Kubernetes
+cluster.
 
-### Installation
+In this workflow, after installing the Atlas Kubernetes Operator, the user defines the desired state of the database
+as an `AtlasMigration` resource which connects between a target database and a migration directory. The migration directory
+may be configured as a remote directory in [Atlas Cloud](/cloud/directories) or as a `ConfigMap` in your Kubernetes
+cluster.
+
+The operator then reconciles the desired state with the actual state of the database by applying any pending migrations on the
+target database.
+
+## Installation
 
 The Atlas Kubernetes Operator is available as a Helm Chart. To install the chart with the release name `atlas-operator`:
 
@@ -46,7 +74,7 @@ The Atlas Kubernetes Operator is available as a Helm Chart. To install the chart
 helm install atlas-operator oci://ghcr.io/ariga/charts/atlas-operator
 ```
 
-### Credentials
+## Providing credentials
 
 In order to manage the schema of your database, you must provide the Atlas Kubernetes Operator with an [Atlas URL](https://atlasgo.io/concepts/url)
 to your database. The Atlas Kubernetes Operator will use this URL to connect to your database and apply changes to the schema.
@@ -124,12 +152,7 @@ stringData:
 </TabItem>
 </Tabs>
 
-Apply this file to your cluster:
-```
-kubectl apply -f db-credentials.yaml
-```
-
-### Defining the desired schema
+## Declarative migrations
 
 The Atlas Operator relies on a Kubernetes custom resource called `AtlasSchema` to define the desired schema of your database.
 You can find the full definition for this resource on the [ariga/atlas-operator repo](https://github.com/ariga/atlas-operator/blob/master/charts/atlas-operator/templates/crds/crd.yaml).
@@ -176,17 +199,6 @@ spec:
     - external_table_managed_elsewhere
 ```
 
-Apply this file to your cluster:
-```
-kubectl apply -f atlas-schema.yaml
-```
-
-Wait for the operator to reconcile the desired state with the actual state of the database:
-
-```
-kubectl wait --for=condition=Ready atlasschema/myapp --timeout=60s
-```
-
 Alternatively, you can use a `ConfigMap` to store the desired schema of your database. To use
 this approach, create a ConfigMap with the desired schema:
 
@@ -208,7 +220,7 @@ data:
 
 Then, update the `AtlasSchema` resource to reference the config map key:
 
-```yaml title="atlas-schema.yaml"
+```yaml title="atlas-schema.yaml" {7-9}
 apiVersion: db.atlasgo.io/v1alpha1
 kind: AtlasSchema
 metadata:
@@ -221,7 +233,7 @@ spec:
   # Skipping all other fields for brevity.
 ```
 
-## Configuration for the `AtlasSchema` resource
+## `AtlasSchema` API reference
 
 The `AtlasSchema` resource is used to define the desired state of a target database.
 
@@ -344,3 +356,165 @@ schemas:
 - schema1
 - schema2
 ```
+
+
+## Versioned migrations
+
+### ConfigMap
+
+The Atlas Operator supports defining the migration directory as `ConfigMap`
+with key-value pairs where the key is the file name and the value is the content of the migration file.
+
+```yaml title="atlas-migration-configmap.yaml"
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: migration-dir
+data:
+  20230316085611.sql: |
+    create table users (
+      id int not null auto_increment,
+      name varchar(255) not null,
+      email varchar(255) unique not null,
+      short_bio varchar(255) not null,
+      primary key (id)
+    );
+  atlas.sum: |
+    h1:W/ujmfqFLOAFDscMEVJ2wrbh4MrpVHENKEP9bVH3mlc=
+    20230316085611.sql h1:Bb6K2XoJavRLhMIsITnxtWLiBhe/Dfp5X5yNxBYP++I=
+```
+
+Then, update the `AtlasMigration` resource to reference the config map key:
+
+```yaml title="atlas-migration.yaml"
+apiVersion: db.atlasgo.io/v1alpha1
+kind: AtlasMigration
+metadata:
+  name: atlasmigration-sample
+spec:
+  urlFrom:
+    secretKeyRef:
+      key: url
+      name: db-credentials
+  dir:
+    configMapRef:
+      name: "migration-dir" # ConfigMap name of atlas-migration-configmap.yaml
+```
+
+### Remote Directory (Atlas Cloud)
+
+If your migration directory is connected to [Atlas Cloud](/cloud/directories/)
+you can configure the operator to read it from your cloud account.
+
+To configure your `AtlasMigration` to read the directory from your cloud account, set the `cloud` and `dir`
+attributes.
+
+```yaml title="atlas-migration-dir.yaml"
+apiVersion: db.atlasgo.io/v1alpha1
+kind: AtlasMigration
+metadata:
+  name: atlasmigration-sample
+spec:
+  urlFrom:
+    secretKeyRef:
+      key: url
+      name: db-credentials
+  cloud:
+    project: "cloud" # Atlas Cloud project name
+    tokenFrom:
+      secretKeyRef:
+        key: token
+        name: atlas-credentials
+  dir:
+    remote:
+      name: "atlas" # Migration directory name in your atlas cloud project
+      tag: "commit-id" # When tag is not specified, the latest tag will be used
+```
+
+
+## `AtlasMigration` API reference
+
+### `envName`
+
+The `envName` sets the environment name used for reporting runs to Atlas Cloud.
+By default, the environment name is set to `Kubernetes`.
+
+```yaml
+envName: "production"
+```
+
+### `url`
+
+The `url` field is used to define the URL of the target database directly.
+This is not recommended, but may be useful for testing purposes.
+
+```yaml
+url: "mysql://user:pass@localhost:3306/myapp"
+```
+
+#### `urlFrom`
+
+The `urlFrom` field is used to load the URL of the target database from another resource such as a
+Kubernetes secret.
+
+```yaml
+urlFrom:
+    secretKeyRef:
+      key: url
+      name: db-credentials
+```
+
+### `cloud`
+
+Cloud is used to define the Atlas Cloud configuration.
+
+#### `project`
+
+The `project` field is used to define the project name.
+
+```yaml
+cloud:
+  project: "cloud"
+```
+
+#### `tokenFrom`
+
+The `tokenFrom` field is used to load the Atlas Cloud token from another resource such as a Kubernetes secret.
+
+```yaml
+cloud:
+  tokenFrom:
+    secretKeyRef:
+      key: token
+      name: atlas-credentials
+```
+
+### `dir`
+
+The `dir` field is used to define the migration directory.
+For local directory, use `configMapRef` to reference the `ConfigMap` that contains the migration files.
+
+```yaml
+dir:
+  configMapRef:
+    name: "migration-dir" # ConfigMap name of atlas-migration-configmap.yaml
+```
+
+For remote directory, use `remote` to define the migration directory in Atlas Cloud.
+
+```yaml
+dir:
+  remote:
+    name: "atlas"
+    tag: "commit-id"
+```
+
+### `revisionsSchema`
+
+The `revisionsSchema` field is used to define the schema name for the revisions table.
+Details about the revisions table can be found in the [Revisions Schema](/versioned/apply#schema-revision-information).
+
+```yaml
+revisionsSchema: "atlas"
+```
+
diff --git a/doc/website/sidebars.js b/doc/website/sidebars.js
index 074d3cf5..7b572b0a 100644
--- a/doc/website/sidebars.js
+++ b/doc/website/sidebars.js
@@ -98,7 +98,12 @@ module.exports = {
             label: 'Integrations',
             collapsed: false,
             items: [
-                {type: 'doc', id: 'integrations/kubernetes-operator', label: 'Kubernetes Operator'},
+                {
+                    type: 'doc',
+                    label: 'Kubernetes Operator',
+                    type: 'doc',
+                    id: 'integrations/kubernetes/operator'
+                },
                 {type: 'doc', id: 'integrations/github-actions', label: 'GitHub Actions'},
                 {type: 'doc', id: 'integrations/terraform-provider', label: 'Terraform Provider'},
                 {type: 'doc', id: 'integrations/go-api', label: 'Go API'},
diff --git a/schemahcl/context.go b/schemahcl/context.go
index 5fd2b464..c37a57b5 100644
--- a/schemahcl/context.go
+++ b/schemahcl/context.go
@@ -72,7 +72,7 @@ func (s *State) setInputVals(ctx *hcl.EvalContext, body hcl.Body, input map[stri
 	return nil
 }
 
-// evalReferences evaluates data blocks.
+// evalReferences evaluates local and data blocks.
 func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error {
 	type node struct {
 		addr  [3]string
@@ -141,7 +141,7 @@ func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error
 		}
 	}
 	var (
-		visit    func(n *node) error
+		visit    func(*node) error
 		visited  = make(map[*node]bool)
 		progress = make(map[*node]bool)
 	)
@@ -212,7 +212,25 @@ func (s *State) evalReferences(ctx *hcl.EvalContext, body *hclsyntax.Body) error
 			return err
 		}
 	}
+	dataref := dataRefs(body)
 	for _, n := range nodes {
+		// Evaluate data sources only if they were referenced by other top-level
+		// blocks/attributes or if they reference other evaluated data sources.
+		if n.addr[0] == BlockData {
+			exists := func() bool {
+				for _, r := range dataref {
+					t, ok1 := r[1].(hcl.TraverseAttr)
+					l, ok2 := r[2].(hcl.TraverseAttr)
+					if ok1 && ok2 && t.Name == n.addr[1] && l.Name == n.addr[2] {
+						return true
+					}
+				}
+				return false
+			}()
+			if !exists {
+				continue
+			}
+		}
 		if err := visit(n); err != nil {
 			return err
 		}
@@ -391,43 +409,6 @@ type Variables struct {
 	Data       map[string]map[string]bool
 }
 
-// DynamicRefs returns the references to dynamic variables.
-// i.e, variables, locals, and data sources.
-func DynamicRefs(b *hclsyntax.Body) *Variables {
-	var (
-		vars = &Variables{
-			Var:   make(map[string]bool),
-			Local: make(map[string]bool),
-			Data:  make(map[string]map[string]bool),
-		}
-		visit func(b *hclsyntax.Body, vars *Variables)
-	)
-	visit = func(b *hclsyntax.Body, vars *Variables) {
-		for _, a := range b.Attributes {
-			for _, v := range hclsyntax.Variables(a.Expr) {
-				switch {
-				case len(v) == 0:
-				case v.RootName() == RefVar && len(v) > 1:
-					vars.Var[v[1].(hcl.TraverseAttr).Name] = true
-				case v.RootName() == RefLocal && len(v) > 1:
-					vars.Local[v[1].(hcl.TraverseAttr).Name] = true
-				case v.RootName() == RefData && len(v) > 2:
-					n1, n2 := v[1].(hcl.TraverseAttr).Name, v[2].(hcl.TraverseAttr).Name
-					if _, ok := vars.Data[n1]; !ok {
-						vars.Data[n1] = make(map[string]bool)
-					}
-					vars.Data[n1][n2] = true
-				}
-			}
-		}
-		for _, b := range b.Blocks {
-			visit(b.Body, vars)
-		}
-	}
-	visit(b, vars)
-	return vars
-}
-
 // defRegistry returns a tree of blockDef structs representing the schema of the
 // blocks in the *hclsyntax.Body. The returned fields and children of each type
 // are an intersection of all existing blocks of the same type.
@@ -499,11 +480,28 @@ func extractDef(blk *hclsyntax.Block, parent *blockDef) *blockDef {
 }
 
 func bodyVars(b *hclsyntax.Body) (vars []hcl.Traversal) {
-	for _, attr := range b.Attributes {
-		vars = append(vars, hclsyntax.Variables(attr.Expr)...)
+	for _, a := range b.Attributes {
+		vars = append(vars, hclsyntax.Variables(a.Expr)...)
 	}
 	for _, b := range b.Blocks {
 		vars = append(vars, bodyVars(b.Body)...)
 	}
 	return
 }
+
+// dataRefs returns all data source referenced in the body.
+func dataRefs(b *hclsyntax.Body) (refs []hcl.Traversal) {
+	for _, a := range b.Attributes {
+		for _, v := range hclsyntax.Variables(a.Expr) {
+			if v.RootName() == RefData {
+				refs = append(refs, v)
+			}
+		}
+	}
+	for _, b := range b.Blocks {
+		if b.Type != BlockData {
+			refs = append(refs, dataRefs(b.Body)...)
+		}
+	}
+	return
+}


Test patch:
diff --git a/cmd/atlas/internal/cmdext/cmdext_test.go b/cmd/atlas/internal/cmdext/cmdext_test.go
index a8f08fe5..fa381c0e 100644
--- a/cmd/atlas/internal/cmdext/cmdext_test.go
+++ b/cmd/atlas/internal/cmdext/cmdext_test.go
@@ -440,6 +440,8 @@ func TestRemoteDir(t *testing.T) {
 data "remote_dir" "hello" {
   name  = "atlas"
 }
+
+dir = data.remote_dir.hello.url
 `), &v, map[string]cty.Value{"cloud_url": cty.StringVal(srv.URL)})
 	require.EqualError(t, err, "data.remote_dir.hello: missing atlas cloud config")
 
diff --git a/internal/integration/cockroach_test.go b/internal/integration/cockroach_test.go
index 41dc0b0d..293fca56 100644
--- a/internal/integration/cockroach_test.go
+++ b/internal/integration/cockroach_test.go
@@ -809,6 +809,7 @@ func (t *crdbTest) valueByVersion(values map[string]string, defaults string) str
 func (t *crdbTest) loadRealm() *schema.Realm {
 	r, err := t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{
 		Schemas: []string{"public"},
+		Mode:    ^schema.InspectViews,
 	})
 	require.NoError(t, err)
 	return r
diff --git a/internal/integration/mysql_test.go b/internal/integration/mysql_test.go
index 50548e61..13348b7d 100644
--- a/internal/integration/mysql_test.go
+++ b/internal/integration/mysql_test.go
@@ -1478,6 +1478,7 @@ func (t *myTest) quoted(s string) string {
 func (t *myTest) loadRealm() *schema.Realm {
 	r, err := t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{
 		Schemas: []string{"test"},
+		Mode:    ^schema.InspectViews,
 	})
 	require.NoError(t, err)
 	return r
diff --git a/internal/integration/postgres_test.go b/internal/integration/postgres_test.go
index 4f2795b0..1948d924 100644
--- a/internal/integration/postgres_test.go
+++ b/internal/integration/postgres_test.go
@@ -1446,6 +1446,7 @@ func (t *pgTest) valueByVersion(values map[string]string, defaults string) strin
 func (t *pgTest) loadRealm() *schema.Realm {
 	r, err := t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{
 		Schemas: []string{"public"},
+		Mode:    ^schema.InspectViews,
 	})
 	require.NoError(t, err)
 	return r
diff --git a/internal/integration/sqlite_test.go b/internal/integration/sqlite_test.go
index b222666b..108eefa6 100644
--- a/internal/integration/sqlite_test.go
+++ b/internal/integration/sqlite_test.go
@@ -819,6 +819,7 @@ func (t *liteTest) applyHcl(spec string) {
 func (t *liteTest) loadRealm() *schema.Realm {
 	r, err := t.drv.InspectRealm(context.Background(), &schema.InspectRealmOption{
 		Schemas: []string{"main"},
+		Mode:    ^schema.InspectViews,
 	})
 	require.NoError(t, err)
 	return r
diff --git a/schemahcl/context_test.go b/schemahcl/context_test.go
index 4e3c5eb0..2b3fa45b 100644
--- a/schemahcl/context_test.go
+++ b/schemahcl/context_test.go
@@ -8,8 +8,6 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/hashicorp/hcl/v2/hclparse"
-	"github.com/hashicorp/hcl/v2/hclsyntax"
 	"github.com/stretchr/testify/require"
 	"github.com/zclconf/go-cty/cty"
 )
@@ -614,77 +612,3 @@ variable "name" {
 	err = New().EvalBytes([]byte(h), &struct{}{}, nil)
 	require.EqualError(t, err, `invalid type "boring" for variable "name". Valid types are: string, number, bool, list, map, or set`)
 }
-
-func TestDynamicRefs(t *testing.T) {
-	var (
-		h = `
-variable "name" {
-  type = string
-}
-
-locals {
-  name = "a8m"
-}
-
-data "remote_dir" "example1" {
-  name = "ent"
-}
-
-data "remote_dir" "example2" {
-  name = "atlas"
-}
-
-data "template_dir" "example1" {
-  name = "atlas"
-}
-
-env "var_only" {
-  v = var.name
-}
-
-env "local_only" {
-  v = local.name
-}
-
-env "data_only" {
-  v = data.remote_dir.example1.url
-}
-
-env "var_local" {
-  v = "${var.name}-${local.name}"
-}
-
-env "var_data" {
-  v = "${var.name}-${data.remote_dir.example1.url}"
-}
-
-env "all" {
-  v = "${var.name}-${local.name}-${data.remote_dir.example1.url}-${data.remote_dir.example2.url}-${data.template_dir.example1.url}"
-}
-`
-		zero1, zero2 = make(map[string]bool), make(map[string]map[string]bool)
-	)
-	for name, vars := range map[string]Variables{
-		"var_only":   {Var: map[string]bool{"name": true}, Local: zero1, Data: zero2},
-		"local_only": {Var: zero1, Local: map[string]bool{"name": true}, Data: zero2},
-		"data_only":  {Var: zero1, Local: zero1, Data: map[string]map[string]bool{"remote_dir": {"example1": true}}},
-		"var_local":  {Var: map[string]bool{"name": true}, Local: map[string]bool{"name": true}, Data: zero2},
-		"var_data":   {Var: map[string]bool{"name": true}, Local: zero1, Data: map[string]map[string]bool{"remote_dir": {"example1": true}}},
-		"all":        {Var: map[string]bool{"name": true}, Local: map[string]bool{"name": true}, Data: map[string]map[string]bool{"remote_dir": {"example1": true, "example2": true}, "template_dir": {"example1": true}}},
-	} {
-		parser := hclparse.NewParser()
-		fi, diags := parser.ParseHCL([]byte(h), "test")
-		require.False(t, diags.HasErrors())
-		env := func() *hclsyntax.Body {
-			for _, b := range fi.Body.(*hclsyntax.Body).Blocks {
-				if b.Type == "env" && b.Labels[0] == name {
-					return b.Body
-				}
-			}
-			require.Failf(t, "env %q block not found", name)
-			return nil
-		}()
-		got := DynamicRefs(env)
-		require.Equal(t, vars, *got)
-	}
-}
diff --git a/schemahcl/hcl_test.go b/schemahcl/hcl_test.go
index 1e703c2e..47431685 100644
--- a/schemahcl/hcl_test.go
+++ b/schemahcl/hcl_test.go
@@ -639,3 +639,62 @@ locals {
 `)
 	require.Error(t, New(opts...).EvalBytes(b, &doc, nil), `cyclic reference to "data.text.a"`)
 }
+
+func TestSkippedDataSrc(t *testing.T) {
+	var (
+		opts = []Option{
+			WithDataSource("dynamic", func(ctx *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)
+				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)
+				if diags.HasErrors() {
+					return cty.NilVal, diags
+				}
+				return cty.ObjectVal(map[string]cty.Value{
+					"v": v,
+				}), nil
+			}),
+		}
+		v struct {
+			V2 string `spec:"v2"`
+			V3 string `spec:"v3"`
+		}
+		b = []byte(`
+data "dynamic" "skipped1" {
+  v = "value is irrelevant"
+  // This attribute has no meaning, besides indicating
+  // to the test that the data source should be skipped.
+  skip = true
+}
+data "dynamic" "skipped2" {
+  v = data.dynamic.skipped1.v
+  skip = true
+}
+data "dynamic" "evaluated1" {
+  v = "v1"
+  skip = false
+}
+data "dynamic" "evaluated2" {
+  v = "v2"
+  skip = false
+}
+data "dynamic" "evaluated3" {
+  v = data.dynamic.evaluated1.v
+  skip = false
+}
+
+v2 = data.dynamic.evaluated2.v
+v3 = data.dynamic.evaluated3.v
+`)
+	)
+	require.NoError(t, New(opts...).EvalBytes(b, &v, nil))
+}