You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by ze...@apache.org on 2026/06/12 18:48:50 UTC

(terraform-provider-iceberg) branch main updated: Add data source table (#41)

This is an automated email from the ASF dual-hosted git repository.

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/terraform-provider-iceberg.git


The following commit(s) were added to refs/heads/main by this push:
     new e95ff7c  Add data source table (#41)
e95ff7c is described below

commit e95ff7ce0c7cb9b51ba5c8dcf4217e7fda8d469a
Author: Dao Thanh Tung <tt...@accountancy.smu.edu.sg>
AuthorDate: Fri Jun 12 19:48:45 2026 +0100

    Add data source table (#41)
    
    * Add data source table
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
    
    * Add docs
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
    
    * fixes comments from code review
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
    
    * Fix docs
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
    
    * rename sync to convert
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
    
    ---------
    
    Signed-off-by: dttung2905 <tt...@accountancy.smu.edu.sg>
---
 Makefile                                           |   4 +
 README.md                                          |   9 +-
 docs/data-sources/table.md                         | 100 +++++
 docs/index.md                                      |   8 +
 examples/data-sources/iceberg_table/data-source.tf |  57 +++
 internal/provider/data_source_table.go             | 214 +++++++++++
 internal/provider/data_source_table_test.go        | 191 ++++++++++
 internal/provider/provider.go                      |   4 +-
 internal/provider/resource_table.go                | 246 +-----------
 internal/provider/table_metadata_convert.go        |  86 +++++
 internal/provider/table_tfschema.go                | 424 +++++++++++++++++++++
 11 files changed, 1103 insertions(+), 240 deletions(-)

diff --git a/Makefile b/Makefile
index c131a6d..12ae734 100644
--- a/Makefile
+++ b/Makefile
@@ -15,6 +15,10 @@
 
 .PHONY: test-integration test-integration-setup test-integration-exec test-integration-cleanup
 
+test-integration: ## Start Docker services, run integration tests, then tear down
+	@$(MAKE) test-integration-setup
+	@EXIT=0; $(MAKE) test-integration-exec || EXIT=$$?; $(MAKE) test-integration-cleanup; exit $$EXIT
+
 test-integration-setup: ## Start Docker services for integration tests
 	docker compose -f dev/docker-compose.yml kill
 	docker compose -f dev/docker-compose.yml rm -f
diff --git a/README.md b/README.md
index de88efd..e4d0e8d 100644
--- a/README.md
+++ b/README.md
@@ -19,12 +19,17 @@
 
 This [Terraform](https://terraform.io) and [OpenTofu](https://www.opentofu.org/) provider allows you to manage Iceberg resources, such as namespaces and tables.
 
-## Supported Resources
+## Supported Data Sources
+
+- `iceberg_table`: Read metadata for an existing table (schema, partition spec, sort order, and catalog properties).
 
-The provider currently supports the following resources:
+## Supported Resources
 
 - `iceberg_namespace`: Manage Iceberg namespaces and their properties.
 - `iceberg_table`: Manage Iceberg tables, including schema definitions and properties.
+- `iceberg_polaris_principal`: Manage Polaris principals and client credentials (when using a Polaris catalog).
+
+See the [provider documentation](docs/index.md) for examples and attribute reference.
 
 ## Local Development
 
diff --git a/docs/data-sources/table.md b/docs/data-sources/table.md
new file mode 100644
index 0000000..de040d4
--- /dev/null
+++ b/docs/data-sources/table.md
@@ -0,0 +1,100 @@
+---
+page_title: "iceberg_table Data Source - Iceberg"
+subcategory: ""
+description: |-
+  Reads metadata for an existing Iceberg table from the catalog.
+---
+
+<!--
+  - Licensed to the Apache Software Foundation (ASF) under one
+  - or more contributor license agreements.  See the NOTICE file
+  - distributed with this work for additional information
+  - regarding copyright ownership.  The ASF licenses this file
+  - to you under the Apache License, Version 2.0 (the
+  - "License"); you may not use this file except in compliance
+  - with the License.  You may obtain a copy of the License at
+  -
+  -   http://www.apache.org/licenses/LICENSE-2.0
+  -
+  - Unless required by applicable law or agreed to in writing,
+  - software distributed under the License is distributed on an
+  - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+  - KIND, either express or implied.  See the License for the
+  - specific language governing permissions and limitations
+  - under the License.
+  -->
+
+# iceberg_table (Data Source)
+
+Reads metadata for an existing Iceberg table from the catalog.
+
+Use this data source to inspect tables created outside Terraform, to reference tables managed in another workspace, or to read the current catalog state of a table also managed by an [`iceberg_table` resource](../resources/table.md).
+
+## Example Usage
+
+### Read a table by namespace and name
+
+```terraform
+data "iceberg_table" "events" {
+  namespace = ["analytics", "prod"]
+  name      = "events"
+}
+
+output "events_table_id" {
+  value = data.iceberg_table.events.id
+}
+
+output "events_id_column" {
+  value = data.iceberg_table.events.schema.fields[0].name
+}
+```
+
+### Read a table managed by this provider
+
+```terraform
+resource "iceberg_namespace" "example" {
+  name = ["example_namespace"]
+}
+
+resource "iceberg_table" "example" {
+  namespace = iceberg_namespace.example.name
+  name      = "example_table"
+
+  schema = {
+    fields = [
+      {
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+data "iceberg_table" "example" {
+  depends_on = [iceberg_table.example]
+
+  namespace = iceberg_namespace.example.name
+  name      = iceberg_table.example.name
+}
+```
+
+## Schema
+
+### Required
+
+- `name` (String) The name of the table.
+- `namespace` (List of String) The namespace of the table.
+
+### Read-Only
+
+- `id` (String) Dot-separated full table identifier (namespace segments and table name).
+- `partition_spec` (Attributes) The partition spec of the table. Empty when the table is not partitioned. Nested field shapes match the [`iceberg_table` resource](../resources/table.md#nestedatt--partition_spec).
+- `schema` (Attributes) The current schema of the table. Nested field shapes match the [`iceberg_table` resource](../resources/table.md#nestedatt--schema); all nested attributes are computed on read.
+- `server_properties` (Map of String) Properties from table metadata as returned by the catalog (the same values surfaced as `server_properties` on the table resource after apply).
+- `sort_order` (Attributes) The sort order of the table. Empty when the table is unsorted. Nested field shapes match the [`iceberg_table` resource](../resources/table.md#nestedatt--sort_order).
+
+## Error handling
+
+- If the table does not exist, apply fails with a **Table not found** error.
+- If `catalog_uri` is not configured on the provider, apply fails with **Catalog not available**.
diff --git a/docs/index.md b/docs/index.md
index 97c104d..ca107f8 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -27,7 +27,15 @@ description: |-
 
 Use Terraform to interact with Iceberg REST Catalog instances.
 
+## Data Sources
 
+- [iceberg_table](data-sources/table.md) — Read metadata for an existing table from the catalog.
+
+## Resources
+
+- [iceberg_namespace](resources/namespace.md) — Manage a catalog namespace.
+- [iceberg_table](resources/table.md) — Manage an Iceberg table.
+- [iceberg_polaris_principal](resources/polaris_principal.md) — Manage a Polaris principal (Polaris deployments).
 
 ## Schema
 
diff --git a/examples/data-sources/iceberg_table/data-source.tf b/examples/data-sources/iceberg_table/data-source.tf
new file mode 100644
index 0000000..bd03d79
--- /dev/null
+++ b/examples/data-sources/iceberg_table/data-source.tf
@@ -0,0 +1,57 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//    http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+resource "iceberg_namespace" "example" {
+  name = ["example_namespace"]
+}
+
+resource "iceberg_table" "example" {
+  namespace = iceberg_namespace.example.name
+  name      = "example_table"
+
+  schema = {
+    fields = [
+      {
+        name     = "id"
+        type     = "long"
+        required = true
+      },
+      {
+        name     = "data"
+        type     = "string"
+        required = false
+      }
+    ]
+  }
+}
+
+data "iceberg_table" "example" {
+  depends_on = [iceberg_table.example]
+
+  namespace = iceberg_namespace.example.name
+  name      = iceberg_table.example.name
+}
+
+output "table_id" {
+  value = data.iceberg_table.example.id
+}
+
+output "table_schema_field_names" {
+  value = [for f in data.iceberg_table.example.schema.fields : f.name]
+}
+
+output "table_server_properties" {
+  value = data.iceberg_table.example.server_properties
+}
diff --git a/internal/provider/data_source_table.go b/internal/provider/data_source_table.go
new file mode 100644
index 0000000..a26b38e
--- /dev/null
+++ b/internal/provider/data_source_table.go
@@ -0,0 +1,214 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/apache/iceberg-go/catalog"
+	"github.com/hashicorp/terraform-plugin-framework/datasource"
+	dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+	"github.com/hashicorp/terraform-plugin-log/tflog"
+)
+
+var _ datasource.DataSource = &icebergTableDataSource{}
+
+func NewTableDataSource() datasource.DataSource {
+	return &icebergTableDataSource{}
+}
+
+type icebergTableDataSourceModel struct {
+	ID               types.String `tfsdk:"id"`
+	Namespace        types.List   `tfsdk:"namespace"`
+	Name             types.String `tfsdk:"name"`
+	Schema           types.Object `tfsdk:"schema"`
+	PartitionSpec    types.Object `tfsdk:"partition_spec"`
+	SortOrder        types.Object `tfsdk:"sort_order"`
+	ServerProperties types.Map    `tfsdk:"server_properties"`
+}
+
+type icebergTableDataSource struct {
+	catalog  catalog.Catalog
+	provider *icebergProvider
+}
+
+func (d *icebergTableDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_table"
+}
+
+func (d *icebergTableDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+	resp.Schema = dschema.Schema{
+		Description: "Reads metadata for an existing Iceberg table from the catalog.",
+		Attributes: map[string]dschema.Attribute{
+			"id": dschema.StringAttribute{
+				Description: "Dot-separated full table identifier (namespace segments + name).",
+				Computed:    true,
+			},
+			"namespace": dschema.ListAttribute{
+				Description: "The namespace of the table.",
+				Required:    true,
+				ElementType: types.StringType,
+			},
+			"name": dschema.StringAttribute{
+				Description: "The name of the table.",
+				Required:    true,
+			},
+			"schema": dschema.SingleNestedAttribute{
+				Description: "The current schema of the table.",
+				Computed:    true,
+				Attributes:  tableSchemaDataSourceAttributes(),
+			},
+			"partition_spec": dschema.SingleNestedAttribute{
+				Description: "The partition spec of the table; fields is empty when unpartitioned.",
+				Computed:    true,
+				Attributes:  tablePartitionSpecDataSourceAttributes(),
+			},
+			"sort_order": dschema.SingleNestedAttribute{
+				Description: "The sort order of the table; fields is empty when unsorted.",
+				Computed:    true,
+				Attributes:  tableSortOrderDataSourceAttributes(),
+			},
+			"server_properties": dschema.MapAttribute{
+				Description: "Properties from table metadata as returned by the catalog.",
+				Computed:    true,
+				ElementType: types.StringType,
+			},
+		},
+	}
+}
+
+func (d *icebergTableDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	if req.ProviderData == nil {
+		return
+	}
+
+	provider, ok := req.ProviderData.(*icebergProvider)
+	if !ok {
+		resp.Diagnostics.AddError(
+			"Unexpected Data Source Configure Type",
+			fmt.Sprintf("Expected *icebergProvider, got: %T. Please report this issue to the provider developers.", req.ProviderData),
+		)
+
+		return
+	}
+
+	d.provider = provider
+}
+
+func (d *icebergTableDataSource) configureCatalog(ctx context.Context, diags *diag.Diagnostics) {
+	if d.catalog != nil {
+		return
+	}
+
+	if d.provider == nil {
+		diags.AddError(
+			"Provider not configured",
+			"The provider hasn't been configured before this operation",
+		)
+
+		return
+	}
+
+	if d.provider.catalogURI == "" {
+		return
+	}
+
+	cat, err := d.provider.NewCatalog(ctx)
+	if err != nil {
+		diags.AddError(
+			"Failed to create catalog",
+			"Failed to create catalog: "+err.Error(),
+		)
+
+		return
+	}
+	d.catalog = cat
+}
+
+func (d *icebergTableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
+	tflog.Info(ctx, "Reading iceberg_table data source")
+	d.configureCatalog(ctx, &resp.Diagnostics)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	var data icebergTableDataSourceModel
+	resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	if d.catalog == nil {
+		resp.Diagnostics.AddError(
+			"Catalog not available",
+			"The catalog could not be created (is catalog_uri set?).",
+		)
+
+		return
+	}
+
+	if data.Name.IsNull() || data.Name.IsUnknown() {
+		resp.Diagnostics.AddError(
+			"Invalid table name",
+			"The name attribute must be set to a non-null, known value.",
+		)
+
+		return
+	}
+
+	var namespaceName []string
+	resp.Diagnostics.Append(data.Namespace.ElementsAs(ctx, &namespaceName, false)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	tableName := data.Name.ValueString()
+	tableIdent := append(namespaceName, tableName)
+
+	tbl, err := d.catalog.LoadTable(ctx, tableIdent)
+	if err != nil {
+		if errors.Is(err, catalog.ErrNoSuchTable) {
+			resp.Diagnostics.AddError(
+				"Table not found",
+				"No such table: "+strings.Join(tableIdent, "."),
+			)
+
+			return
+		}
+		resp.Diagnostics.AddError("failed to load table", err.Error())
+
+		return
+	}
+
+	data.ID = types.StringValue(strings.Join(tableIdent, "."))
+
+	meta := icebergTableMetadataFields{}
+	convertIcebergTableMetadataToModel(ctx, tbl, &meta, &resp.Diagnostics)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+	data.Schema = meta.Schema
+	data.PartitionSpec = meta.PartitionSpec
+	data.SortOrder = meta.SortOrder
+	data.ServerProperties = meta.ServerProperties
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_table_test.go b/internal/provider/data_source_table_test.go
new file mode 100644
index 0000000..917d18f
--- /dev/null
+++ b/internal/provider/data_source_table_test.go
@@ -0,0 +1,191 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+	"fmt"
+	"os"
+	"regexp"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+)
+
+// TestAccIcebergTableDataSource_Full runs end-to-end against a real REST catalog when ICEBERG_CATALOG_URI is set
+// (same pattern as TestAccPolarisPrincipal_Full and the integration Makefile targets).
+func TestAccIcebergTableDataSource_Full(t *testing.T) {
+	catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
+	if catalogURI == "" {
+		t.Skip("ICEBERG_CATALOG_URI not set, skipping table data source E2E test")
+	}
+
+	providerCfg := fmt.Sprintf(providerConfig, catalogURI)
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config: testAccIcebergTableDataSourceBasicConfig(providerCfg, "ds_e2e_basic"),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "namespace.0", "db_table_ds_basic"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "name", "ds_e2e_basic"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "schema.fields.0.name", "id"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "schema.fields.0.type", "long"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "schema.fields.1.name", "data"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "schema.fields.1.type", "string"),
+					resource.TestCheckResourceAttrPair("data.iceberg_table.read", "id", "iceberg_table.subject", "id"),
+					resource.TestCheckResourceAttrSet("data.iceberg_table.read", "schema.id"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "partition_spec.fields.#", "0"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "sort_order.fields.#", "0"),
+				),
+			},
+			{
+				Config: testAccIcebergTableDataSourcePartitionSortConfig(providerCfg, "ds_e2e_partition_sort", "bucket[16]", "asc"),
+				Check: resource.ComposeTestCheckFunc(
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "name", "ds_e2e_partition_sort"),
+					resource.TestCheckResourceAttrPair("data.iceberg_table.read", "id", "iceberg_table.subject", "id"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "partition_spec.fields.#", "1"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "partition_spec.fields.0.source_ids.0", "1"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "partition_spec.fields.0.field_id", "1000"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "partition_spec.fields.0.name", "id_bucket"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "partition_spec.fields.0.transform", "bucket[16]"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "sort_order.fields.#", "1"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "sort_order.fields.0.source_id", "1"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "sort_order.fields.0.transform", "identity"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "sort_order.fields.0.direction", "asc"),
+					resource.TestCheckResourceAttr("data.iceberg_table.read", "sort_order.fields.0.null_order", "nulls-first"),
+				),
+			},
+		},
+	})
+}
+
+// TestAccIcebergTableDataSource_TableNotFound expects a clear error when the table does not exist.
+func TestAccIcebergTableDataSource_TableNotFound(t *testing.T) {
+	catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
+	if catalogURI == "" {
+		t.Skip("ICEBERG_CATALOG_URI not set, skipping table data source E2E test")
+	}
+
+	providerCfg := fmt.Sprintf(providerConfig, catalogURI)
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 func() { testAccPreCheck(t) },
+		ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+		Steps: []resource.TestStep{
+			{
+				Config:      testAccIcebergTableDataSourceMissingTableConfig(providerCfg),
+				ExpectError: regexp.MustCompile(`Table not found`),
+			},
+		},
+	})
+}
+
+func testAccIcebergTableDataSourceBasicConfig(providerCfg string, tableName string) string {
+	return providerCfg + fmt.Sprintf(`
+resource "iceberg_namespace" "db" {
+  name = ["db_table_ds_basic"]
+}
+
+resource "iceberg_table" "subject" {
+  namespace = iceberg_namespace.db.name
+  name      = "%s"
+  schema = {
+    fields = [
+      {
+        id       = 1
+        name     = "id"
+        type     = "long"
+        required = true
+      },
+      {
+        id       = 2
+        name     = "data"
+        type     = "string"
+        required = false
+      }
+    ]
+  }
+}
+
+data "iceberg_table" "read" {
+  namespace = iceberg_namespace.db.name
+  name      = iceberg_table.subject.name
+}
+`, tableName)
+}
+
+func testAccIcebergTableDataSourcePartitionSortConfig(providerCfg string, tableName string, partitionTransform string, sortDirection string) string {
+	return providerCfg + fmt.Sprintf(`
+resource "iceberg_namespace" "db" {
+  name = ["db_table_ds_part"]
+}
+
+resource "iceberg_table" "subject" {
+  namespace = iceberg_namespace.db.name
+  name      = "%s"
+  schema = {
+    fields = [
+      {
+        id       = 1
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+  partition_spec = {
+    fields = [
+      {
+        source_ids = [1]
+        field_id   = 1000
+        name       = "id_bucket"
+        transform  = "%s"
+      }
+    ]
+  }
+  sort_order = {
+    fields = [
+      {
+        source_id  = 1
+        transform  = "identity"
+        direction  = "%s"
+        null_order = "nulls-first"
+      }
+    ]
+  }
+}
+
+data "iceberg_table" "read" {
+  namespace = iceberg_namespace.db.name
+  name      = iceberg_table.subject.name
+}
+`, tableName, partitionTransform, sortDirection)
+}
+
+func testAccIcebergTableDataSourceMissingTableConfig(providerCfg string) string {
+	return providerCfg + `
+resource "iceberg_namespace" "empty" {
+  name = ["db_table_ds_missing"]
+}
+
+data "iceberg_table" "missing" {
+  namespace = iceberg_namespace.empty.name
+  name      = "definitely_no_such_table"
+}
+`
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index d165ce4..be73730 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -233,7 +233,9 @@ func (h *headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error
 
 // DataSources defines the data sources implemented in the provider.
 func (p *icebergProvider) DataSources(_ context.Context) []func() datasource.DataSource {
-	return nil
+	return []func() datasource.DataSource{
+		NewTableDataSource,
+	}
 }
 
 // Resources defines the resources implemented in the provider.
diff --git a/internal/provider/resource_table.go b/internal/provider/resource_table.go
index 80171b8..de8c13e 100644
--- a/internal/provider/resource_table.go
+++ b/internal/provider/resource_table.go
@@ -24,7 +24,6 @@ import (
 	"github.com/apache/iceberg-go"
 	"github.com/apache/iceberg-go/catalog"
 	"github.com/apache/iceberg-go/table"
-	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
 	"github.com/hashicorp/terraform-plugin-framework/diag"
 	"github.com/hashicorp/terraform-plugin-framework/path"
 	"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -32,7 +31,6 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
-	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
 	"github.com/hashicorp/terraform-plugin-framework/types"
 	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
 	"github.com/hashicorp/terraform-plugin-log/tflog"
@@ -92,98 +90,19 @@ func (r *icebergTableResource) Schema(_ context.Context, _ resource.SchemaReques
 			"schema": rscschema.SingleNestedAttribute{
 				Description: "The schema of the table.",
 				Required:    true,
-				Attributes: map[string]rscschema.Attribute{
-					"id": rscschema.Int64Attribute{
-						Description: "The schema ID.",
-						Optional:    true,
-						Computed:    true,
-					},
-					"fields": rscschema.ListNestedAttribute{
-						Description: "The fields of the schema",
-						Required:    true,
-						NestedObject: rscschema.NestedAttributeObject{
-							Attributes: schemaFieldAttributes(4),
-						},
-					},
-				},
+				Attributes:  tableSchemaResourceAttributes(),
 			},
 			"partition_spec": rscschema.SingleNestedAttribute{
 				Description: "The partition spec of the table.",
 				Optional:    true,
 				Computed:    true,
-				Attributes: map[string]rscschema.Attribute{
-					"spec_id": rscschema.Int64Attribute{
-						Description: "The partition spec ID.",
-						Computed:    true,
-					},
-					"fields": rscschema.ListNestedAttribute{
-						Description: "The fields of the partition spec.",
-						Required:    true,
-						NestedObject: rscschema.NestedAttributeObject{
-							Attributes: map[string]rscschema.Attribute{
-								"source_ids": rscschema.ListAttribute{
-									Description: "The source field IDs.",
-									Required:    true,
-									ElementType: types.Int64Type,
-								},
-								"field_id": rscschema.Int64Attribute{
-									Description: "The partition field ID.",
-									Optional:    true,
-									Computed:    true,
-								},
-								"name": rscschema.StringAttribute{
-									Description: "The partition field name.",
-									Required:    true,
-								},
-								"transform": rscschema.StringAttribute{
-									Description: "The partition transform.",
-									Required:    true,
-								},
-							},
-						},
-					},
-				},
+				Attributes:  tablePartitionSpecResourceAttributes(),
 			},
 			"sort_order": rscschema.SingleNestedAttribute{
 				Description: "The sort order of the table.",
 				Optional:    true,
 				Computed:    true,
-				Attributes: map[string]rscschema.Attribute{
-					"order_id": rscschema.Int64Attribute{
-						Description: "The sort order ID.",
-						Computed:    true,
-					},
-					"fields": rscschema.ListNestedAttribute{
-						Description: "The fields of the sort order.",
-						Required:    true,
-						NestedObject: rscschema.NestedAttributeObject{
-							Attributes: map[string]rscschema.Attribute{
-								"source_id": rscschema.Int64Attribute{
-									Description: "The source field ID.",
-									Required:    true,
-								},
-								"transform": rscschema.StringAttribute{
-									Description: "The sort transform.",
-									Required:    true,
-								},
-								"direction": rscschema.StringAttribute{
-									Description: "The sort direction (asc or desc).",
-									Required:    true,
-									Validators: []validator.String{
-										stringvalidator.OneOf("asc", "desc"),
-									},
-								},
-								"null_order": rscschema.StringAttribute{
-									Description: "The null order (nulls-first or nulls-last).",
-									Required:    true,
-									Validators: []validator.String{
-										stringvalidator.OneOf("nulls-first", "nulls-last"),
-									},
-								},
-							},
-						},
-					},
-				},
+				Attributes:  tableSortOrderResourceAttributes(),
 			},
 			"user_properties": rscschema.MapAttribute{
 				Description: "User-defined properties for the table.",
@@ -199,108 +118,6 @@ func (r *icebergTableResource) Schema(_ context.Context, _ resource.SchemaReques
 	}
 }
 
-func schemaFieldAttributes(depth int) map[string]rscschema.Attribute {
-	attrs := map[string]rscschema.Attribute{
-		"id": rscschema.Int64Attribute{
-			Description: "The field ID.",
-			Optional:    true,
-		},
-		"name": rscschema.StringAttribute{
-			Description: "The field name.",
-			Required:    true,
-		},
-		"type": rscschema.StringAttribute{
-			Description: "The field type (e.g., 'int', 'string', 'decimal(10,2)', 'struct'). For struct, use struct_properties.",
-			Required:    true,
-		},
-		"required": rscschema.BoolAttribute{
-			Description: "Whether the field is required.",
-			Required:    true,
-		},
-		"doc": rscschema.StringAttribute{
-			Description: "The field documentation.",
-			Optional:    true,
-		},
-		"list_properties": rscschema.SingleNestedAttribute{
-			Description: "Properties for list type.",
-			Optional:    true,
-			Attributes: map[string]rscschema.Attribute{
-				"element_id": rscschema.Int64Attribute{
-					Description: "The list element id.",
-					Required:    true,
-				},
-				"element_type": rscschema.StringAttribute{
-					Description: "The list element type.",
-					Required:    true,
-				},
-				"element_required": rscschema.BoolAttribute{
-					Description: "Whether the list element is required.",
-					Required:    true,
-				},
-			},
-		},
-		"map_properties": rscschema.SingleNestedAttribute{
-			Description: "Properties for map type.",
-			Optional:    true,
-			Attributes: map[string]rscschema.Attribute{
-				"key_id": rscschema.Int64Attribute{
-					Description: "The map key id.",
-					Required:    true,
-				},
-				"key_type": rscschema.StringAttribute{
-					Description: "The map key type.",
-					Required:    true,
-				},
-				"value_id": rscschema.Int64Attribute{
-					Description: "The map value id.",
-					Required:    true,
-				},
-				"value_type": rscschema.StringAttribute{
-					Description: "The map value type.",
-					Required:    true,
-				},
-				"value_required": rscschema.BoolAttribute{
-					Description: "Whether the map value is required.",
-					Required:    true,
-				},
-			},
-		},
-	}
-
-	if depth > 0 {
-		attrs["struct_properties"] = rscschema.SingleNestedAttribute{
-			Description: "Properties for struct type.",
-			Optional:    true,
-			Attributes: map[string]rscschema.Attribute{
-				"fields": rscschema.ListNestedAttribute{
-					Description: "The fields of the struct.",
-					Required:    true,
-					NestedObject: rscschema.NestedAttributeObject{
-						Attributes: schemaFieldAttributes(depth - 1),
-					},
-				},
-			},
-		}
-	} else {
-		// At max depth, we still need the attribute defined but it won't have fields
-		attrs["struct_properties"] = rscschema.SingleNestedAttribute{
-			Description: "Properties for struct type.",
-			Optional:    true,
-			Attributes: map[string]rscschema.Attribute{
-				"fields": rscschema.ListNestedAttribute{
-					Description: "The fields of the struct.",
-					Required:    true,
-					NestedObject: rscschema.NestedAttributeObject{
-						Attributes: map[string]rscschema.Attribute{},
-					},
-				},
-			},
-		}
-	}
-
-	return attrs
-}
-
 func (r *icebergTableResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
 	if req.ProviderData == nil {
 		return
@@ -772,60 +589,15 @@ func (r *icebergTableResource) calculateSortOrderUpdates(ctx context.Context, pl
 }
 
 func (r *icebergTableResource) syncTableToModel(ctx context.Context, tbl *table.Table, model *icebergTableResourceModel, diags *diag.Diagnostics) {
-	// Update ServerProperties
-	serverProperties, d := types.MapValueFrom(ctx, types.StringType, tbl.Properties())
-	diags.Append(d...)
+	meta := icebergTableMetadataFields{}
+	convertIcebergTableMetadataToModel(ctx, tbl, &meta, diags)
 	if diags.HasError() {
 		return
 	}
-	model.ServerProperties = serverProperties
-
-	// Update Schema from the table to capture any server-assigned IDs
-	icebergSchema := tbl.Schema()
-	var updatedSchema icebergTableSchema
-	if err := updatedSchema.FromIceberg(icebergSchema); err != nil {
-		diags.AddError("failed to convert iceberg schema to terraform schema", err.Error())
-
-		return
-	}
-	var d2 diag.Diagnostics
-	model.Schema, d2 = types.ObjectValueFrom(ctx, icebergTableSchema{}.AttrTypes(), updatedSchema)
-	diags.Append(d2...)
-	if diags.HasError() {
-		return
-	}
-
-	// Update PartitionSpec
-	icebergSpec := tbl.Spec()
-	if icebergSpec.NumFields() > 0 {
-		var updatedSpec icebergTablePartitionSpec
-		if err := updatedSpec.FromIceberg(icebergSpec); err != nil {
-			diags.AddError("failed to convert iceberg partition spec to terraform partition spec", err.Error())
-
-			return
-		}
-		var d3 diag.Diagnostics
-		model.PartitionSpec, d3 = types.ObjectValueFrom(ctx, icebergTablePartitionSpec{}.AttrTypes(), updatedSpec)
-		diags.Append(d3...)
-	} else {
-		model.PartitionSpec = types.ObjectNull(icebergTablePartitionSpec{}.AttrTypes())
-	}
-
-	// Update SortOrder
-	icebergOrder := tbl.SortOrder()
-	if icebergOrder.Len() > 0 {
-		var updatedOrder icebergTableSortOrder
-		if err := updatedOrder.FromIceberg(icebergOrder); err != nil {
-			diags.AddError("failed to convert iceberg sort order to terraform sort order", err.Error())
-
-			return
-		}
-		var d4 diag.Diagnostics
-		model.SortOrder, d4 = types.ObjectValueFrom(ctx, icebergTableSortOrder{}.AttrTypes(), updatedOrder)
-		diags.Append(d4...)
-	} else {
-		model.SortOrder = types.ObjectNull(icebergTableSortOrder{}.AttrTypes())
-	}
+	model.Schema = meta.Schema
+	model.PartitionSpec = meta.PartitionSpec
+	model.SortOrder = meta.SortOrder
+	model.ServerProperties = meta.ServerProperties
 
 	// Update UserProperties to match reality for tracked keys
 	if !model.UserProperties.IsNull() {
diff --git a/internal/provider/table_metadata_convert.go b/internal/provider/table_metadata_convert.go
new file mode 100644
index 0000000..2dd3a92
--- /dev/null
+++ b/internal/provider/table_metadata_convert.go
@@ -0,0 +1,86 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+	"context"
+
+	"github.com/apache/iceberg-go/table"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+// icebergTableMetadataFields holds Terraform values converted from an Iceberg table's metadata
+// (schema, partition spec, sort order, properties). Used by the table resource and table data source.
+type icebergTableMetadataFields struct {
+	Schema           types.Object
+	PartitionSpec    types.Object
+	SortOrder        types.Object
+	ServerProperties types.Map
+}
+
+func convertIcebergTableMetadataToModel(ctx context.Context, tbl *table.Table, out *icebergTableMetadataFields, diags *diag.Diagnostics) {
+	serverProperties, d := types.MapValueFrom(ctx, types.StringType, tbl.Properties())
+	diags.Append(d...)
+	if diags.HasError() {
+		return
+	}
+	out.ServerProperties = serverProperties
+
+	icebergSchema := tbl.Schema()
+	var updatedSchema icebergTableSchema
+	if err := updatedSchema.FromIceberg(icebergSchema); err != nil {
+		diags.AddError("failed to convert iceberg schema to terraform schema", err.Error())
+
+		return
+	}
+	var d2 diag.Diagnostics
+	out.Schema, d2 = types.ObjectValueFrom(ctx, icebergTableSchema{}.AttrTypes(), updatedSchema)
+	diags.Append(d2...)
+	if diags.HasError() {
+		return
+	}
+
+	icebergSpec := tbl.Spec()
+	if icebergSpec.NumFields() > 0 {
+		var updatedSpec icebergTablePartitionSpec
+		if err := updatedSpec.FromIceberg(icebergSpec); err != nil {
+			diags.AddError("failed to convert iceberg partition spec to terraform partition spec", err.Error())
+
+			return
+		}
+		var d3 diag.Diagnostics
+		out.PartitionSpec, d3 = types.ObjectValueFrom(ctx, icebergTablePartitionSpec{}.AttrTypes(), updatedSpec)
+		diags.Append(d3...)
+	} else {
+		out.PartitionSpec = types.ObjectNull(icebergTablePartitionSpec{}.AttrTypes())
+	}
+
+	icebergOrder := tbl.SortOrder()
+	if icebergOrder.Len() > 0 {
+		var updatedOrder icebergTableSortOrder
+		if err := updatedOrder.FromIceberg(icebergOrder); err != nil {
+			diags.AddError("failed to convert iceberg sort order to terraform sort order", err.Error())
+
+			return
+		}
+		var d4 diag.Diagnostics
+		out.SortOrder, d4 = types.ObjectValueFrom(ctx, icebergTableSortOrder{}.AttrTypes(), updatedOrder)
+		diags.Append(d4...)
+	} else {
+		out.SortOrder = types.ObjectNull(icebergTableSortOrder{}.AttrTypes())
+	}
+}
diff --git a/internal/provider/table_tfschema.go b/internal/provider/table_tfschema.go
new file mode 100644
index 0000000..8332194
--- /dev/null
+++ b/internal/provider/table_tfschema.go
@@ -0,0 +1,424 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements.  See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//	http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package provider
+
+import (
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	dschema "github.com/hashicorp/terraform-plugin-framework/datasource/schema"
+	rscschema "github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+const tableSchemaFieldMaxDepth = 4
+
+func tableSchemaResourceAttributes() map[string]rscschema.Attribute {
+	return map[string]rscschema.Attribute{
+		"id": rscschema.Int64Attribute{
+			Description: "The schema ID.",
+			Optional:    true,
+			Computed:    true,
+		},
+		"fields": rscschema.ListNestedAttribute{
+			Description: "The fields of the schema",
+			Required:    true,
+			NestedObject: rscschema.NestedAttributeObject{
+				Attributes: tableSchemaFieldResourceAttributes(tableSchemaFieldMaxDepth),
+			},
+		},
+	}
+}
+
+func tableSchemaDataSourceAttributes() map[string]dschema.Attribute {
+	return map[string]dschema.Attribute{
+		"id": dschema.Int64Attribute{
+			Description: "The schema ID.",
+			Computed:    true,
+		},
+		"fields": dschema.ListNestedAttribute{
+			Description: "The fields of the schema.",
+			Computed:    true,
+			NestedObject: dschema.NestedAttributeObject{
+				Attributes: tableSchemaFieldDataSourceAttributes(tableSchemaFieldMaxDepth),
+			},
+		},
+	}
+}
+
+func tablePartitionSpecResourceAttributes() map[string]rscschema.Attribute {
+	return map[string]rscschema.Attribute{
+		"spec_id": rscschema.Int64Attribute{
+			Description: "The partition spec ID.",
+			Computed:    true,
+		},
+		"fields": rscschema.ListNestedAttribute{
+			Description: "The fields of the partition spec.",
+			Required:    true,
+			NestedObject: rscschema.NestedAttributeObject{
+				Attributes: tablePartitionSpecFieldResourceAttributes(),
+			},
+		},
+	}
+}
+
+func tablePartitionSpecDataSourceAttributes() map[string]dschema.Attribute {
+	return map[string]dschema.Attribute{
+		"spec_id": dschema.Int64Attribute{
+			Description: "The partition spec ID.",
+			Computed:    true,
+		},
+		"fields": dschema.ListNestedAttribute{
+			Description: "The fields of the partition spec.",
+			Computed:    true,
+			NestedObject: dschema.NestedAttributeObject{
+				Attributes: tablePartitionSpecFieldDataSourceAttributes(),
+			},
+		},
+	}
+}
+
+func tableSortOrderResourceAttributes() map[string]rscschema.Attribute {
+	return map[string]rscschema.Attribute{
+		"order_id": rscschema.Int64Attribute{
+			Description: "The sort order ID.",
+			Computed:    true,
+		},
+		"fields": rscschema.ListNestedAttribute{
+			Description: "The fields of the sort order.",
+			Required:    true,
+			NestedObject: rscschema.NestedAttributeObject{
+				Attributes: tableSortOrderFieldResourceAttributes(),
+			},
+		},
+	}
+}
+
+func tableSortOrderDataSourceAttributes() map[string]dschema.Attribute {
+	return map[string]dschema.Attribute{
+		"order_id": dschema.Int64Attribute{
+			Description: "The sort order ID.",
+			Computed:    true,
+		},
+		"fields": dschema.ListNestedAttribute{
+			Description: "The fields of the sort order.",
+			Computed:    true,
+			NestedObject: dschema.NestedAttributeObject{
+				Attributes: tableSortOrderFieldDataSourceAttributes(),
+			},
+		},
+	}
+}
+
+func tableSchemaFieldResourceAttributes(depth int) map[string]rscschema.Attribute {
+	attrs := map[string]rscschema.Attribute{
+		"id": rscschema.Int64Attribute{
+			Description: "The field ID.",
+			Optional:    true,
+		},
+		"name": rscschema.StringAttribute{
+			Description: "The field name.",
+			Required:    true,
+		},
+		"type": rscschema.StringAttribute{
+			Description: "The field type (e.g., 'int', 'string', 'decimal(10,2)', 'struct'). For struct, use struct_properties.",
+			Required:    true,
+		},
+		"required": rscschema.BoolAttribute{
+			Description: "Whether the field is required.",
+			Required:    true,
+		},
+		"doc": rscschema.StringAttribute{
+			Description: "The field documentation.",
+			Optional:    true,
+		},
+		"list_properties": rscschema.SingleNestedAttribute{
+			Description: "Properties for list type.",
+			Optional:    true,
+			Attributes: map[string]rscschema.Attribute{
+				"element_id": rscschema.Int64Attribute{
+					Description: "The list element id.",
+					Required:    true,
+				},
+				"element_type": rscschema.StringAttribute{
+					Description: "The list element type.",
+					Required:    true,
+				},
+				"element_required": rscschema.BoolAttribute{
+					Description: "Whether the list element is required.",
+					Required:    true,
+				},
+			},
+		},
+		"map_properties": rscschema.SingleNestedAttribute{
+			Description: "Properties for map type.",
+			Optional:    true,
+			Attributes: map[string]rscschema.Attribute{
+				"key_id": rscschema.Int64Attribute{
+					Description: "The map key id.",
+					Required:    true,
+				},
+				"key_type": rscschema.StringAttribute{
+					Description: "The map key type.",
+					Required:    true,
+				},
+				"value_id": rscschema.Int64Attribute{
+					Description: "The map value id.",
+					Required:    true,
+				},
+				"value_type": rscschema.StringAttribute{
+					Description: "The map value type.",
+					Required:    true,
+				},
+				"value_required": rscschema.BoolAttribute{
+					Description: "Whether the map value is required.",
+					Required:    true,
+				},
+			},
+		},
+	}
+
+	if depth > 0 {
+		attrs["struct_properties"] = rscschema.SingleNestedAttribute{
+			Description: "Properties for struct type.",
+			Optional:    true,
+			Attributes: map[string]rscschema.Attribute{
+				"fields": rscschema.ListNestedAttribute{
+					Description: "The fields of the struct.",
+					Required:    true,
+					NestedObject: rscschema.NestedAttributeObject{
+						Attributes: tableSchemaFieldResourceAttributes(depth - 1),
+					},
+				},
+			},
+		}
+	} else {
+		attrs["struct_properties"] = rscschema.SingleNestedAttribute{
+			Description: "Properties for struct type.",
+			Optional:    true,
+			Attributes: map[string]rscschema.Attribute{
+				"fields": rscschema.ListNestedAttribute{
+					Description: "The fields of the struct.",
+					Required:    true,
+					NestedObject: rscschema.NestedAttributeObject{
+						Attributes: map[string]rscschema.Attribute{},
+					},
+				},
+			},
+		}
+	}
+
+	return attrs
+}
+
+func tableSchemaFieldDataSourceAttributes(depth int) map[string]dschema.Attribute {
+	attrs := map[string]dschema.Attribute{
+		"id": dschema.Int64Attribute{
+			Description: "The field ID.",
+			Computed:    true,
+		},
+		"name": dschema.StringAttribute{
+			Description: "The field name.",
+			Computed:    true,
+		},
+		"type": dschema.StringAttribute{
+			Description: "The field type (e.g., 'int', 'string', 'decimal(10,2)', 'struct'). For struct, use struct_properties.",
+			Computed:    true,
+		},
+		"required": dschema.BoolAttribute{
+			Description: "Whether the field is required.",
+			Computed:    true,
+		},
+		"doc": dschema.StringAttribute{
+			Description: "The field documentation.",
+			Computed:    true,
+		},
+		"list_properties": dschema.SingleNestedAttribute{
+			Description: "Properties for list type.",
+			Computed:    true,
+			Attributes: map[string]dschema.Attribute{
+				"element_id": dschema.Int64Attribute{
+					Description: "The list element id.",
+					Computed:    true,
+				},
+				"element_type": dschema.StringAttribute{
+					Description: "The list element type.",
+					Computed:    true,
+				},
+				"element_required": dschema.BoolAttribute{
+					Description: "Whether the list element is required.",
+					Computed:    true,
+				},
+			},
+		},
+		"map_properties": dschema.SingleNestedAttribute{
+			Description: "Properties for map type.",
+			Computed:    true,
+			Attributes: map[string]dschema.Attribute{
+				"key_id": dschema.Int64Attribute{
+					Description: "The map key id.",
+					Computed:    true,
+				},
+				"key_type": dschema.StringAttribute{
+					Description: "The map key type.",
+					Computed:    true,
+				},
+				"value_id": dschema.Int64Attribute{
+					Description: "The map value id.",
+					Computed:    true,
+				},
+				"value_type": dschema.StringAttribute{
+					Description: "The map value type.",
+					Computed:    true,
+				},
+				"value_required": dschema.BoolAttribute{
+					Description: "Whether the map value is required.",
+					Computed:    true,
+				},
+			},
+		},
+	}
+
+	if depth > 0 {
+		attrs["struct_properties"] = dschema.SingleNestedAttribute{
+			Description: "Properties for struct type.",
+			Computed:    true,
+			Attributes: map[string]dschema.Attribute{
+				"fields": dschema.ListNestedAttribute{
+					Description: "The fields of the struct.",
+					Computed:    true,
+					NestedObject: dschema.NestedAttributeObject{
+						Attributes: tableSchemaFieldDataSourceAttributes(depth - 1),
+					},
+				},
+			},
+		}
+	} else {
+		attrs["struct_properties"] = dschema.SingleNestedAttribute{
+			Description: "Properties for struct type.",
+			Computed:    true,
+			Attributes: map[string]dschema.Attribute{
+				"fields": dschema.ListNestedAttribute{
+					Description: "The fields of the struct.",
+					Computed:    true,
+					NestedObject: dschema.NestedAttributeObject{
+						Attributes: map[string]dschema.Attribute{},
+					},
+				},
+			},
+		}
+	}
+
+	return attrs
+}
+
+func tablePartitionSpecFieldResourceAttributes() map[string]rscschema.Attribute {
+	return map[string]rscschema.Attribute{
+		"source_ids": rscschema.ListAttribute{
+			Description: "The source field IDs.",
+			Required:    true,
+			ElementType: types.Int64Type,
+		},
+		"field_id": rscschema.Int64Attribute{
+			Description: "The partition field ID.",
+			Optional:    true,
+			Computed:    true,
+		},
+		"name": rscschema.StringAttribute{
+			Description: "The partition field name.",
+			Required:    true,
+		},
+		"transform": rscschema.StringAttribute{
+			Description: "The partition transform.",
+			Required:    true,
+		},
+	}
+}
+
+func tablePartitionSpecFieldDataSourceAttributes() map[string]dschema.Attribute {
+	return map[string]dschema.Attribute{
+		"source_ids": dschema.ListAttribute{
+			Description: "The source field IDs.",
+			Computed:    true,
+			ElementType: types.Int64Type,
+		},
+		"field_id": dschema.Int64Attribute{
+			Description: "The partition field ID.",
+			Computed:    true,
+		},
+		"name": dschema.StringAttribute{
+			Description: "The partition field name.",
+			Computed:    true,
+		},
+		"transform": dschema.StringAttribute{
+			Description: "The partition transform.",
+			Computed:    true,
+		},
+	}
+}
+
+func tableSortOrderFieldResourceAttributes() map[string]rscschema.Attribute {
+	return map[string]rscschema.Attribute{
+		"source_id": rscschema.Int64Attribute{
+			Description: "The source field ID.",
+			Required:    true,
+		},
+		"transform": rscschema.StringAttribute{
+			Description: "The sort transform.",
+			Required:    true,
+		},
+		"direction": rscschema.StringAttribute{
+			Description: "The sort direction (asc or desc).",
+			Required:    true,
+			Validators: []validator.String{
+				stringvalidator.OneOf("asc", "desc"),
+			},
+		},
+		"null_order": rscschema.StringAttribute{
+			Description: "The null order (nulls-first or nulls-last).",
+			Required:    true,
+			Validators: []validator.String{
+				stringvalidator.OneOf("nulls-first", "nulls-last"),
+			},
+		},
+	}
+}
+
+func tableSortOrderFieldDataSourceAttributes() map[string]dschema.Attribute {
+	return map[string]dschema.Attribute{
+		"source_id": dschema.Int64Attribute{
+			Description: "The source field ID.",
+			Computed:    true,
+		},
+		"transform": dschema.StringAttribute{
+			Description: "The sort transform.",
+			Computed:    true,
+		},
+		"direction": dschema.StringAttribute{
+			Description: "The sort direction (asc or desc).",
+			Computed:    true,
+			Validators: []validator.String{
+				stringvalidator.OneOf("asc", "desc"),
+			},
+		},
+		"null_order": dschema.StringAttribute{
+			Description: "The null order (nulls-first or nulls-last).",
+			Computed:    true,
+			Validators: []validator.String{
+				stringvalidator.OneOf("nulls-first", "nulls-last"),
+			},
+		},
+	}
+}