Data Source Read Operation

8 minute read

Read Operation Overview

The Read operation is the only operation data sources implement. It queries the API and populates computed attributes based on search criteria.

Read responsibilities:

  • Parse configuration (search criteria)
  • Query API with filters
  • Handle not-found scenarios
  • Populate all computed attributes
  • Set state
  • Log operations

Basic Read Implementation

func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    // 1. Get configuration
    var config DatabaseDataSourceModel
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // 2. Extract search criteria
    name := config.Name.ValueString()
    
    // 3. Log the operation
    tflog.Debug(ctx, "Reading database data source", map[string]any{
        "name": name,
    })
    
    // 4. Query API
    db, err := d.client.GetDatabaseByName(ctx, name)
    if err != nil {
        resp.Diagnostics.AddError(
            "Failed to Read Database",
            fmt.Sprintf("Could not find database '%s': %s", name, err.Error()),
        )
        return
    }
    
    // 5. Populate computed attributes
    config.ID = types.StringValue(db.ID)
    config.Endpoint = types.StringValue(db.Endpoint)
    config.Port = types.Int64Value(int64(db.Port))
    config.Engine = types.StringValue(db.Engine)
    config.Status = types.StringValue(db.Status)
    
    // 6. Save to state
    diags = resp.State.Set(ctx, &config)
    resp.Diagnostics.Append(diags...)
    
    // 7. Log success
    tflog.Info(ctx, "Successfully read database", map[string]any{
        "id": db.ID,
    })
}

Parsing Configuration

Simple Attributes

// String attribute
name := config.Name.ValueString()

// Int attribute
port := config.Port.ValueInt64()

// Bool attribute
multiAZ := config.MultiAZ.ValueBool()

// Check if null/unknown
if config.Name.IsNull() {
    // Attribute not set
}

if config.Name.IsUnknown() {
    // Value not yet known (rare in data sources)
}

Map Attributes

// Get tags map
var tags map[string]string
if !config.Tags.IsNull() {
    diags := config.Tags.ElementsAs(ctx, &tags, false)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
}

// Use tags in query
db, err := d.client.FindDatabaseByTags(ctx, tags)

List Attributes

// Get list of values
var values []string
if !config.Values.IsNull() {
    diags := config.Values.ElementsAs(ctx, &values, false)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
}

Querying APIs

Lookup by ID

func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var config DatabaseDataSourceModel
    req.Config.Get(ctx, &config)
    
    id := config.ID.ValueString()
    
    // Direct lookup by ID
    db, err := d.client.GetDatabase(ctx, id)
    if err != nil {
        if client.IsNotFoundError(err) {
            resp.Diagnostics.AddError(
                "Database Not Found",
                fmt.Sprintf("No database found with ID: %s", id),
            )
            return
        }
        
        resp.Diagnostics.AddError(
            "API Error",
            fmt.Sprintf("Failed to read database: %s", err.Error()),
        )
        return
    }
    
    // Populate attributes
    config.Name = types.StringValue(db.Name)
    config.Endpoint = types.StringValue(db.Endpoint)
    
    resp.State.Set(ctx, config)
}

Lookup by Name

func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var config DatabaseDataSourceModel
    req.Config.Get(ctx, &config)
    
    name := config.Name.ValueString()
    
    // Search by name
    dbs, err := d.client.ListDatabases(ctx, &client.ListOptions{
        Filters: map[string]string{
            "name": name,
        },
    })
    
    if err != nil {
        resp.Diagnostics.AddError("Query Failed", err.Error())
        return
    }
    
    // Handle results
    if len(dbs) == 0 {
        resp.Diagnostics.AddError(
            "Database Not Found",
            fmt.Sprintf("No database found with name: %s", name),
        )
        return
    }
    
    if len(dbs) > 1 {
        resp.Diagnostics.AddError(
            "Multiple Databases Found",
            fmt.Sprintf(
                "Found %d databases with name '%s'. Expected exactly one.\n\n"+
                "Databases found:\n%s",
                len(dbs), name, formatDatabaseList(dbs),
            ),
        )
        return
    }
    
    // Exactly one result
    db := dbs[0]
    config.ID = types.StringValue(db.ID)
    config.Endpoint = types.StringValue(db.Endpoint)
    
    resp.State.Set(ctx, config)
}

Lookup by Tags

func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    var config DatabaseDataSourceModel
    req.Config.Get(ctx, &config)
    
    // Extract tag filters
    var filterTags map[string]string
    config.Tags.ElementsAs(ctx, &filterTags, false)
    
    tflog.Debug(ctx, "Searching for database", map[string]any{
        "tags": filterTags,
    })
    
    // Query API with tag filters
    dbs, err := d.client.ListDatabasesByTags(ctx, filterTags)
    if err != nil {
        resp.Diagnostics.AddError("Query Failed", err.Error())
        return
    }
    
    // Handle no results
    if len(dbs) == 0 {
        resp.Diagnostics.AddError(
            "No Database Found",
            fmt.Sprintf(
                "No database found matching tags:\n%s\n\n"+
                "Ensure:\n"+
                "  • Tag keys and values are correct\n"+
                "  • Database exists in the configured region\n"+
                "  • All specified tags are present on the database",
                formatTags(filterTags),
            ),
        )
        return
    }
    
    // Handle multiple results
    if len(dbs) > 1 {
        resp.Diagnostics.AddError(
            "Multiple Databases Found",
            fmt.Sprintf(
                "Found %d databases matching the specified tags.\n\n"+
                "Add more specific tags to narrow the search.\n\n"+
                "Matching databases:\n%s",
                len(dbs), formatDatabaseList(dbs),
            ),
        )
        return
    }
    
    // Exactly one result
    db := dbs[0]
    config.ID = types.StringValue(db.ID)
    config.Name = types.StringValue(db.Name)
    config.Endpoint = types.StringValue(db.Endpoint)
    
    resp.State.Set(ctx, config)
}

Setting Computed Attributes

Basic Types

// String
config.ID = types.StringValue(db.ID)
config.Name = types.StringValue(db.Name)
config.Status = types.StringValue(db.Status)

// Int64
config.Port = types.Int64Value(int64(db.Port))
config.StorageGB = types.Int64Value(int64(db.StorageGB))

// Bool
config.MultiAZ = types.BoolValue(db.MultiAZ)
config.PubliclyAccessible = types.BoolValue(db.PubliclyAccessible)

// Handle nil/empty strings
if db.Description == "" {
    config.Description = types.StringNull()
} else {
    config.Description = types.StringValue(db.Description)
}

Map Attributes

import (
    "github.com/hashicorp/terraform-plugin-framework/attr"
)

// Convert map[string]string to types.Map
if len(db.Tags) > 0 {
    tagsMap := make(map[string]attr.Value)
    for k, v := range db.Tags {
        tagsMap[k] = types.StringValue(v)
    }
    mapValue, diags := types.MapValue(types.StringType, tagsMap)
    resp.Diagnostics.Append(diags...)
    config.Tags = mapValue
} else {
    config.Tags = types.MapNull(types.StringType)
}

List Attributes

// Convert []string to types.List
if len(db.SecurityGroupIDs) > 0 {
    elements := make([]attr.Value, len(db.SecurityGroupIDs))
    for i, id := range db.SecurityGroupIDs {
        elements[i] = types.StringValue(id)
    }
    listValue, diags := types.ListValue(types.StringType, elements)
    resp.Diagnostics.Append(diags...)
    config.SecurityGroupIDs = listValue
} else {
    config.SecurityGroupIDs = types.ListNull(types.StringType)
}

Nested Objects

type DatabaseDataSourceModel struct {
    ID             types.String `tfsdk:"id"`
    ConnectionInfo types.Object `tfsdk:"connection_info"`
}

// Define nested object type
connectionInfoType := types.ObjectType{
    AttrTypes: map[string]attr.Type{
        "endpoint": types.StringType,
        "port":     types.Int64Type,
        "protocol": types.StringType,
    },
}

// Create nested object
connectionInfoValue, diags := types.ObjectValue(
    connectionInfoType.AttrTypes,
    map[string]attr.Value{
        "endpoint": types.StringValue(db.Endpoint),
        "port":     types.Int64Value(int64(db.Port)),
        "protocol": types.StringValue("tcp"),
    },
)
resp.Diagnostics.Append(diags...)
config.ConnectionInfo = connectionInfoValue

List of Objects

type DatabaseDataSourceModel struct {
    ID       types.String `tfsdk:"id"`
    Replicas types.List   `tfsdk:"replicas"`
}

// Define object type
replicaType := types.ObjectType{
    AttrTypes: map[string]attr.Type{
        "id":       types.StringType,
        "region":   types.StringType,
        "status":   types.StringType,
        "endpoint": types.StringType,
    },
}

// Convert []Replica to types.List
if len(db.Replicas) > 0 {
    replicaObjects := make([]attr.Value, len(db.Replicas))
    
    for i, replica := range db.Replicas {
        objValue, diags := types.ObjectValue(
            replicaType.AttrTypes,
            map[string]attr.Value{
                "id":       types.StringValue(replica.ID),
                "region":   types.StringValue(replica.Region),
                "status":   types.StringValue(replica.Status),
                "endpoint": types.StringValue(replica.Endpoint),
            },
        )
        resp.Diagnostics.Append(diags...)
        replicaObjects[i] = objValue
    }
    
    listValue, diags := types.ListValue(replicaType, replicaObjects)
    resp.Diagnostics.Append(diags...)
    config.Replicas = listValue
} else {
    config.Replicas = types.ListNull(replicaType)
}

Error Handling

Not Found Errors

db, err := d.client.GetDatabase(ctx, id)
if err != nil {
    if client.IsNotFoundError(err) {
        resp.Diagnostics.AddError(
            "Database Not Found",
            fmt.Sprintf(
                "No database found with ID '%s'.\n\n"+
                "Possible reasons:\n"+
                "  • Database doesn't exist\n"+
                "  • Database is in a different region\n"+
                "  • Insufficient permissions\n\n"+
                "Check the database ID and region configuration.",
                id,
            ),
        )
        return
    }
    
    // Other API errors
    resp.Diagnostics.AddError(
        "Failed to Query Database",
        fmt.Sprintf("API error: %s", err.Error()),
    )
    return
}

Multiple Results

if len(dbs) > 1 {
    // Format list of matching databases
    var dbList strings.Builder
    for _, db := range dbs {
        fmt.Fprintf(&dbList, "  • %s (ID: %s)\n", db.Name, db.ID)
    }
    
    resp.Diagnostics.AddError(
        "Multiple Databases Found",
        fmt.Sprintf(
            "Found %d databases matching the criteria. Expected exactly one.\n\n"+
            "Matching databases:\n%s\n"+
            "Refine your search by:\n"+
            "  • Using database ID instead of name\n"+
            "  • Adding more specific tag filters\n"+
            "  • Checking for duplicate names",
            len(dbs), dbList.String(),
        ),
    )
    return
}

Permission Errors

if client.IsPermissionError(err) {
    resp.Diagnostics.AddError(
        "Permission Denied",
        fmt.Sprintf(
            "Insufficient permissions to query database.\n\n"+
            "Required permissions:\n"+
            "  • rds:DescribeDBInstances\n"+
            "  • rds:ListTagsForResource\n\n"+
            "Error: %s",
            err.Error(),
        ),
    )
    return
}

Network Errors

if client.IsNetworkError(err) {
    resp.Diagnostics.AddError(
        "Network Error",
        fmt.Sprintf(
            "Failed to connect to API endpoint.\n\n"+
            "Check:\n"+
            "  • Network connectivity\n"+
            "  • API endpoint configuration\n"+
            "  • Firewall rules\n\n"+
            "Error: %s",
            err.Error(),
        ),
    )
    return
}

Complete Read Example

func (d *DatabaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
    // 1. Get configuration
    var config DatabaseDataSourceModel
    diags := req.Config.Get(ctx, &config)
    resp.Diagnostics.Append(diags...)
    if resp.Diagnostics.HasError() {
        return
    }
    
    // 2. Determine query method
    var db *client.Database
    var err error
    
    if !config.ID.IsNull() {
        // Query by ID
        id := config.ID.ValueString()
        tflog.Debug(ctx, "Looking up database by ID", map[string]any{"id": id})
        
        db, err = d.client.GetDatabase(ctx, id)
        
    } else if !config.Name.IsNull() {
        // Query by name
        name := config.Name.ValueString()
        tflog.Debug(ctx, "Looking up database by name", map[string]any{"name": name})
        
        dbs, listErr := d.client.ListDatabases(ctx, &client.ListOptions{
            Filters: map[string]string{"name": name},
        })
        
        if listErr != nil {
            err = listErr
        } else if len(dbs) == 0 {
            resp.Diagnostics.AddError(
                "Database Not Found",
                fmt.Sprintf("No database found with name: %s", name),
            )
            return
        } else if len(dbs) > 1 {
            resp.Diagnostics.AddError(
                "Multiple Databases Found",
                fmt.Sprintf("Found %d databases with name '%s'", len(dbs), name),
            )
            return
        } else {
            db = &dbs[0]
        }
        
    } else {
        // Query by tags
        var tags map[string]string
        config.Tags.ElementsAs(ctx, &tags, false)
        
        tflog.Debug(ctx, "Looking up database by tags", map[string]any{"tags": tags})
        
        dbs, listErr := d.client.ListDatabasesByTags(ctx, tags)
        
        if listErr != nil {
            err = listErr
        } else if len(dbs) == 0 {
            resp.Diagnostics.AddError(
                "No Database Found",
                fmt.Sprintf("No database found matching tags: %v", tags),
            )
            return
        } else if len(dbs) > 1 {
            resp.Diagnostics.AddError(
                "Multiple Databases Found",
                fmt.Sprintf("Found %d databases matching tags", len(dbs)),
            )
            return
        } else {
            db = &dbs[0]
        }
    }
    
    // 3. Handle query errors
    if err != nil {
        if client.IsNotFoundError(err) {
            resp.Diagnostics.AddError(
                "Database Not Found",
                "The specified database does not exist",
            )
            return
        }
        
        resp.Diagnostics.AddError(
            "Failed to Query Database",
            fmt.Sprintf("API error: %s", err.Error()),
        )
        return
    }
    
    // 4. Populate all computed attributes
    config.ID = types.StringValue(db.ID)
    config.Name = types.StringValue(db.Name)
    config.ARN = types.StringValue(db.ARN)
    config.Engine = types.StringValue(db.Engine)
    config.EngineVersion = types.StringValue(db.EngineVersion)
    config.Endpoint = types.StringValue(db.Endpoint)
    config.Port = types.Int64Value(int64(db.Port))
    config.Status = types.StringValue(db.Status)
    config.InstanceClass = types.StringValue(db.InstanceClass)
    config.StorageGB = types.Int64Value(int64(db.StorageGB))
    config.MultiAZ = types.BoolValue(db.MultiAZ)
    config.CreatedAt = types.StringValue(db.CreatedAt)
    
    // Convert tags
    if len(db.Tags) > 0 {
        tagsMap := make(map[string]attr.Value)
        for k, v := range db.Tags {
            tagsMap[k] = types.StringValue(v)
        }
        mapValue, diags := types.MapValue(types.StringType, tagsMap)
        resp.Diagnostics.Append(diags...)
        config.Tags = mapValue
    } else {
        config.Tags = types.MapNull(types.StringType)
    }
    
    // Convert security group IDs
    if len(db.SecurityGroupIDs) > 0 {
        elements := make([]attr.Value, len(db.SecurityGroupIDs))
        for i, id := range db.SecurityGroupIDs {
            elements[i] = types.StringValue(id)
        }
        listValue, diags := types.ListValue(types.StringType, elements)
        resp.Diagnostics.Append(diags...)
        config.SecurityGroupIDs = listValue
    } else {
        config.SecurityGroupIDs = types.ListNull(types.StringType)
    }
    
    // 5. Save to state
    diags = resp.State.Set(ctx, &config)
    resp.Diagnostics.Append(diags...)
    
    // 6. Log success
    tflog.Info(ctx, "Successfully read database", map[string]any{
        "id":   db.ID,
        "name": db.Name,
    })
}

Logging Best Practices

// Debug: Detailed information
tflog.Debug(ctx, "Querying database", map[string]any{
    "name":   name,
    "region": region,
})

// Info: Key milestones
tflog.Info(ctx, "Database found", map[string]any{
    "id":   db.ID,
    "name": db.Name,
})

// Trace: Very detailed information
tflog.Trace(ctx, "API response", map[string]any{
    "response": fmt.Sprintf("%+v", db),
})

// Warn: Unexpected but handled situations
tflog.Warn(ctx, "Multiple databases found, using first", map[string]any{
    "count": len(dbs),
})

Performance Considerations

Minimize API Calls

// ✅ Good: Single API call
db, err := d.client.GetDatabase(ctx, id)

// ❌ Bad: Multiple API calls
db, err := d.client.GetDatabase(ctx, id)
tags, _ := d.client.GetTags(ctx, id)
replicas, _ := d.client.GetReplicas(ctx, id)

Use Efficient Queries

// ✅ Good: Filter on server side
dbs, err := d.client.ListDatabases(ctx, &client.ListOptions{
    Filters: map[string]string{
        "status": "available",
        "engine": "postgres",
    },
})

// ❌ Bad: Fetch all, filter client-side
allDbs, err := d.client.ListAllDatabases(ctx)
filtered := filterDatabases(allDbs, status, engine)

Key Takeaways

  • ✅ Read is the only data source operation
  • ✅ Parse configuration to get search criteria
  • ✅ Query API with appropriate filters
  • ✅ Handle not-found and multiple result scenarios
  • ✅ Populate ALL computed attributes
  • ✅ Convert API responses to Framework types
  • ✅ Provide helpful error messages
  • ✅ Log query operations for debugging
  • ✅ Optimize API calls for performance
  • ✅ Save complete state at the end

What’s Next?

Learn how to implement flexible filtering for complex queries!

Next: Data Source Filtering →


Previous: ← Data Source Schema

Last updated: June 30, 2026

Updated: