Data Source Read Operation
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!
Previous: ← Data Source Schema
Last updated: June 30, 2026