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 bcde0d5  Add `iceberg_tables` data source (#71)
bcde0d5 is described below

commit bcde0d59587de510e3293af0629b6e459b458c2f
Author: Dao Thanh Tung <[email protected]>
AuthorDate: Thu Oct 15 12:22:31 2026 +0100

    Add `iceberg_tables` data source (#71)
    
    Add iceberg_tables data source
    
    Signed-off-by: dttung2905 <[email protected]>
---
 README.md                                          |   1 +
 docs/data-sources/tables.md                        | 141 ++++++
 docs/index.md                                      |   1 +
 .../data-sources/iceberg_tables/data-source.tf     |  69 +++
 internal/provider/data_source_tables.go            | 303 ++++++++++++
 internal/provider/data_source_tables_test.go       | 520 +++++++++++++++++++++
 internal/provider/provider.go                      |   1 +
 7 files changed, 1036 insertions(+)

diff --git a/README.md b/README.md
index bdd5509..aeef6a1 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ This [Terraform](https://terraform.io) and 
[OpenTofu](https://www.opentofu.org/)
 
 - `iceberg_namespace`: Read metadata for an existing namespace (catalog 
properties).
 - `iceberg_table`: Read metadata for an existing table (schema, partition 
spec, sort order, and catalog properties).
+- `iceberg_tables`: List table names in a namespace (catalog discovery).
 
 ## Supported Resources
 
diff --git a/docs/data-sources/tables.md b/docs/data-sources/tables.md
new file mode 100644
index 0000000..9637108
--- /dev/null
+++ b/docs/data-sources/tables.md
@@ -0,0 +1,141 @@
+---
+page_title: "iceberg_tables Data Source - Iceberg"
+subcategory: ""
+description: |-
+  Lists table names in an Iceberg namespace 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_tables (Data Source)
+
+Lists table names in an Iceberg namespace from the catalog.
+
+Use this data source to discover existing tables in a namespace—for example, 
to validate namespace contents, wire downstream jobs, or drive `for_each` over 
tables created outside Terraform.
+
+- `tables` returns table names only (e.g. `events`, `orders`).
+- `identifiers` returns dot-separated full table identifiers (e.g. 
`analytics.raw.events`), matching the `id` format used by 
[`iceberg_table`](table.md).
+
+Listing is non-recursive: only tables directly in the given namespace are 
returned, not tables in child namespaces.
+
+## Example Usage
+
+### List tables in a namespace
+
+```terraform
+data "iceberg_tables" "analytics" {
+  namespace = ["analytics", "raw"]
+}
+
+output "table_names" {
+  value = data.iceberg_tables.analytics.tables
+}
+
+output "table_identifiers" {
+  value = data.iceberg_tables.analytics.identifiers
+}
+```
+
+### Drive resources with `for_each`
+
+Use `tables` when the namespace is already known (short names). Use 
`identifiers` when downstream systems need the full catalog path.
+
+```terraform
+data "iceberg_tables" "in_ns" {
+  namespace = ["analytics", "raw"]
+}
+
+resource "example_downstream" "per_table" {
+  for_each = toset(data.iceberg_tables.in_ns.tables)
+
+  table_name = each.value
+}
+
+output "full_table_paths" {
+  value = data.iceberg_tables.in_ns.identifiers
+}
+```
+
+### List tables after creating them in the same configuration
+
+When tables are managed in the same Terraform configuration, add `depends_on` 
so the data source is read after those tables exist. Referencing only 
`namespace` creates an implicit dependency on the namespace resource, not on 
individual tables.
+
+```terraform
+resource "iceberg_namespace" "example" {
+  name = ["example_namespace"]
+}
+
+resource "iceberg_table" "events" {
+  namespace = iceberg_namespace.example.name
+  name      = "events"
+
+  schema = {
+    fields = [
+      {
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+resource "iceberg_table" "orders" {
+  namespace = iceberg_namespace.example.name
+  name      = "orders"
+
+  schema = {
+    fields = [
+      {
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+data "iceberg_tables" "example" {
+  namespace = iceberg_namespace.example.name
+
+  depends_on = [
+    iceberg_table.events,
+    iceberg_table.orders,
+  ]
+}
+```
+
+## Schema
+
+### Required
+
+- `namespace` (List of String) The namespace to list tables in.
+
+### Read-Only
+
+- `id` (String) Dot-separated full namespace identifier.
+- `tables` (List of String) Table names in the namespace, without namespace 
segments. Sorted alphabetically.
+- `identifiers` (List of String) Dot-separated full table identifiers 
(namespace + table name), e.g. `analytics.raw.events`. Sorted alphabetically. 
Matches the `id` format of [`iceberg_table`](table.md).
+
+## Error handling
+
+- If the namespace does not exist, apply fails with a **Namespace not found** 
error.
+- If listing fails for another reason (including a mid-pagination catalog 
error), apply fails with **failed to list tables** and the underlying catalog 
message.
+- 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 65120e2..c7239eb 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -197,6 +197,7 @@ carry duplicate credentials.
 
 - [iceberg_namespace](data-sources/namespace.md) — Read metadata for an 
existing namespace from the catalog.
 - [iceberg_table](data-sources/table.md) — Read metadata for an existing table 
from the catalog.
+- [iceberg_tables](data-sources/tables.md) — List table names in a namespace 
from the catalog.
 
 ## Resources
 
diff --git a/examples/data-sources/iceberg_tables/data-source.tf 
b/examples/data-sources/iceberg_tables/data-source.tf
new file mode 100644
index 0000000..145a719
--- /dev/null
+++ b/examples/data-sources/iceberg_tables/data-source.tf
@@ -0,0 +1,69 @@
+// 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" "events" {
+  namespace = iceberg_namespace.example.name
+  name      = "events"
+
+  schema = {
+    fields = [
+      {
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+resource "iceberg_table" "orders" {
+  namespace = iceberg_namespace.example.name
+  name      = "orders"
+
+  schema = {
+    fields = [
+      {
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+data "iceberg_tables" "example" {
+  namespace = iceberg_namespace.example.name
+
+  depends_on = [
+    iceberg_table.events,
+    iceberg_table.orders,
+  ]
+}
+
+output "table_names" {
+  value = data.iceberg_tables.example.tables
+}
+
+output "table_identifiers" {
+  value = data.iceberg_tables.example.identifiers
+}
+
+output "namespace_id" {
+  value = data.iceberg_tables.example.id
+}
diff --git a/internal/provider/data_source_tables.go 
b/internal/provider/data_source_tables.go
new file mode 100644
index 0000000..1c24ca3
--- /dev/null
+++ b/internal/provider/data_source_tables.go
@@ -0,0 +1,303 @@
+// 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"
+       "iter"
+       "slices"
+       "strings"
+
+       "github.com/apache/iceberg-go/catalog"
+       "github.com/apache/iceberg-go/table"
+       "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 = &icebergTablesDataSource{}
+
+func NewTablesDataSource() datasource.DataSource {
+       return &icebergTablesDataSource{}
+}
+
+type icebergTablesDataSourceModel struct {
+       ID          types.String `tfsdk:"id"`
+       Namespace   types.List   `tfsdk:"namespace"`
+       Tables      types.List   `tfsdk:"tables"`
+       Identifiers types.List   `tfsdk:"identifiers"`
+}
+
+type icebergTablesDataSource struct {
+       catalog  catalog.Catalog
+       provider *icebergProvider
+}
+
+// errNamespaceNotFound is returned by collectListedTables only when ListTables
+// fails with catalog.ErrNoSuchNamespace and CheckNamespaceExists confirms the
+// namespace is absent. REST page-level HTTP 404s wrap the same catalog 
sentinel,
+// so a listing failure against an existing namespace must not use this error.
+var errNamespaceNotFound = errors.New("namespace not found")
+
+// namespaceExister is the CheckNamespaceExists subset of catalog.Catalog used
+// to disambiguate ErrNoSuchNamespace on the ListTables error path.
+type namespaceExister interface {
+       CheckNamespaceExists(ctx context.Context, namespace table.Identifier) 
(bool, error)
+}
+
+func (d *icebergTablesDataSource) Metadata(_ context.Context, req 
datasource.MetadataRequest, resp *datasource.MetadataResponse) {
+       resp.TypeName = req.ProviderTypeName + "_tables"
+}
+
+func (d *icebergTablesDataSource) Schema(_ context.Context, _ 
datasource.SchemaRequest, resp *datasource.SchemaResponse) {
+       resp.Schema = dschema.Schema{
+               Description: "Lists table names in an Iceberg namespace from 
the catalog.",
+               Attributes: map[string]dschema.Attribute{
+                       "id": dschema.StringAttribute{
+                               Description: "Dot-separated full namespace 
identifier.",
+                               Computed:    true,
+                       },
+                       "namespace": dschema.ListAttribute{
+                               Description: "The namespace to list tables in.",
+                               Required:    true,
+                               ElementType: types.StringType,
+                       },
+                       "tables": dschema.ListAttribute{
+                               Description: "Table names in the namespace, 
without namespace segments. Sorted alphabetically.",
+                               Computed:    true,
+                               ElementType: types.StringType,
+                       },
+                       "identifiers": dschema.ListAttribute{
+                               Description: "Dot-separated full table 
identifiers (namespace segments + table name), matching iceberg_table id 
format. Sorted alphabetically.",
+                               Computed:    true,
+                               ElementType: types.StringType,
+                       },
+               },
+       }
+}
+
+func (d *icebergTablesDataSource) 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 *icebergTablesDataSource) 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 access catalog",
+                       "Failed to access catalog: "+err.Error(),
+               )
+
+               return
+       }
+       d.catalog = cat
+}
+
+// identifierString formats a table identifier as a dot-separated string.
+// table.Identifier is currently a []string alias with no String() method;
+// keep this helper so formatting stays in one place if that changes before 
v1.0.
+func identifierString(ident table.Identifier) string {
+       return strings.Join(ident, ".")
+}
+
+func sortTableIdentifiers(identifiers []table.Identifier) {
+       // Stable sort keeps list order deterministic across refreshes so 
Terraform
+       // does not report spurious diffs when the catalog returns tables 
unordered.
+       slices.SortStableFunc(identifiers, func(a, b table.Identifier) int {
+               return strings.Compare(identifierString(a), identifierString(b))
+       })
+}
+
+func tableNamesFromIdentifiers(identifiers []table.Identifier) []string {
+       names := make([]string, 0, len(identifiers))
+       for _, ident := range identifiers {
+               names = append(names, catalog.TableNameFromIdent(ident))
+       }
+
+       return names
+}
+
+func tableIdentifierStrings(identifiers []table.Identifier) []string {
+       out := make([]string, 0, len(identifiers))
+       for _, ident := range identifiers {
+               out = append(out, identifierString(ident))
+       }
+
+       return out
+}
+
+// collectListedTables consumes a ListTables iterator, enforcing that every
+// yielded identifier belongs to namespaceIdent (non-recursive listing).
+//
+// Yield count cannot distinguish a missing namespace from a page-level HTTP 
404:
+// iceberg-go's REST catalog wraps every 404 with ErrNoSuchNamespace, and an
+// empty non-final page yields nothing before the error. On that sentinel, 
probe
+// CheckNamespaceExists and only then report errNamespaceNotFound.
+func collectListedTables(
+       ctx context.Context,
+       seq iter.Seq2[table.Identifier, error],
+       namespaceIdent table.Identifier,
+       checkExists namespaceExister,
+) ([]table.Identifier, error) {
+       var tableIdents []table.Identifier
+       for ident, err := range seq {
+               if err != nil {
+                       if errors.Is(err, catalog.ErrNoSuchNamespace) {
+                               return nil, classifyNoSuchNamespace(ctx, err, 
namespaceIdent, checkExists)
+                       }
+
+                       return nil, err
+               }
+
+               // NamespaceFromIdent is ident[:len(ident)-1] with no bounds 
check and
+               // panics on an empty identifier; keep this guard first.
+               if len(ident) == 0 {
+                       return nil, errors.New("catalog returned empty table 
identifier")
+               }
+               if !slices.Equal(catalog.NamespaceFromIdent(ident), 
namespaceIdent) {
+                       return nil, fmt.Errorf(
+                               "catalog returned table %q outside requested 
namespace %q",
+                               identifierString(ident),
+                               identifierString(namespaceIdent),
+                       )
+               }
+
+               tableIdents = append(tableIdents, ident)
+       }
+
+       return tableIdents, nil
+}
+
+func classifyNoSuchNamespace(ctx context.Context, listErr error, 
namespaceIdent table.Identifier, checkExists namespaceExister) error {
+       exists, checkErr := checkExists.CheckNamespaceExists(ctx, 
namespaceIdent)
+       if checkErr == nil && !exists {
+               return fmt.Errorf("%w: %s", errNamespaceNotFound, 
identifierString(namespaceIdent))
+       }
+
+       return listErr
+}
+
+func (d *icebergTablesDataSource) Read(ctx context.Context, req 
datasource.ReadRequest, resp *datasource.ReadResponse) {
+       tflog.Info(ctx, "Reading iceberg_tables data source")
+       d.configureCatalog(ctx, &resp.Diagnostics)
+       if resp.Diagnostics.HasError() {
+               return
+       }
+
+       var data icebergTablesDataSourceModel
+       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
+       }
+
+       var namespaceName []string
+       resp.Diagnostics.Append(data.Namespace.ElementsAs(ctx, &namespaceName, 
false)...)
+       if resp.Diagnostics.HasError() {
+               return
+       }
+
+       if len(namespaceName) == 0 {
+               resp.Diagnostics.AddError(
+                       "Invalid namespace",
+                       "The namespace attribute must contain at least one 
namespace segment.",
+               )
+
+               return
+       }
+
+       namespaceIdent := catalog.ToIdentifier(namespaceName...)
+
+       tableIdents, err := collectListedTables(ctx, d.catalog.ListTables(ctx, 
namespaceIdent), namespaceIdent, d.catalog)
+       if err != nil {
+               if errors.Is(err, errNamespaceNotFound) {
+                       resp.Diagnostics.AddError(
+                               "Namespace not found",
+                               "No such namespace: 
"+identifierString(namespaceIdent),
+                       )
+
+                       return
+               }
+               resp.Diagnostics.AddError("failed to list tables", err.Error())
+
+               return
+       }
+
+       sortTableIdentifiers(tableIdents)
+       tableNames := tableNamesFromIdentifiers(tableIdents)
+       identifierStrings := tableIdentifierStrings(tableIdents)
+
+       tables, diags := types.ListValueFrom(ctx, types.StringType, tableNames)
+       resp.Diagnostics.Append(diags...)
+       if resp.Diagnostics.HasError() {
+               return
+       }
+
+       identifiers, diags := types.ListValueFrom(ctx, types.StringType, 
identifierStrings)
+       resp.Diagnostics.Append(diags...)
+       if resp.Diagnostics.HasError() {
+               return
+       }
+
+       data.ID = types.StringValue(identifierString(namespaceIdent))
+       data.Tables = tables
+       data.Identifiers = identifiers
+
+       resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
+}
diff --git a/internal/provider/data_source_tables_test.go 
b/internal/provider/data_source_tables_test.go
new file mode 100644
index 0000000..7942a68
--- /dev/null
+++ b/internal/provider/data_source_tables_test.go
@@ -0,0 +1,520 @@
+// 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"
+       "iter"
+       "os"
+       "regexp"
+       "testing"
+
+       "github.com/apache/iceberg-go/catalog"
+       "github.com/apache/iceberg-go/table"
+       "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
+       "github.com/hashicorp/terraform-plugin-testing/helper/resource"
+       "github.com/stretchr/testify/assert"
+)
+
+func TestIdentifierString(t *testing.T) {
+       t.Parallel()
+
+       assert.Equal(t, "db.events", identifierString(table.Identifier{"db", 
"events"}))
+       assert.Equal(t, "analytics.raw.orders", 
identifierString(table.Identifier{"analytics", "raw", "orders"}))
+       assert.Equal(t, "", identifierString(nil))
+}
+
+func TestSortTableIdentifiers(t *testing.T) {
+       t.Parallel()
+
+       ids := []table.Identifier{
+               {"analytics", "raw", "orders"},
+               {"analytics", "raw", "events"},
+               {"ns", "b"},
+               {"ns", "a"},
+               {"ns", "a"},
+       }
+       sortTableIdentifiers(ids)
+
+       assert.Equal(t, []table.Identifier{
+               {"analytics", "raw", "events"},
+               {"analytics", "raw", "orders"},
+               {"ns", "a"},
+               {"ns", "a"},
+               {"ns", "b"},
+       }, ids)
+}
+
+func TestTableNamesFromIdentifiers(t *testing.T) {
+       t.Parallel()
+
+       tests := []struct {
+               name string
+               ids  []table.Identifier
+               want []string
+       }{
+               {
+                       name: "empty",
+                       ids:  nil,
+                       want: []string{},
+               },
+               {
+                       name: "single table in flat namespace",
+                       ids:  []table.Identifier{{"db", "events"}},
+                       want: []string{"events"},
+               },
+               {
+                       name: "preserves caller order",
+                       ids: []table.Identifier{
+                               {"analytics", "raw", "orders"},
+                               {"analytics", "raw", "events"},
+                       },
+                       want: []string{"orders", "events"},
+               },
+               {
+                       name: "nested namespace",
+                       ids:  []table.Identifier{{"analytics", "prod", 
"metrics"}},
+                       want: []string{"metrics"},
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+                       assert.Equal(t, tt.want, 
tableNamesFromIdentifiers(tt.ids))
+               })
+       }
+}
+
+func TestTableIdentifierStrings(t *testing.T) {
+       t.Parallel()
+
+       tests := []struct {
+               name string
+               ids  []table.Identifier
+               want []string
+       }{
+               {
+                       name: "empty",
+                       ids:  nil,
+                       want: []string{},
+               },
+               {
+                       name: "single table in flat namespace",
+                       ids:  []table.Identifier{{"db", "events"}},
+                       want: []string{"db.events"},
+               },
+               {
+                       name: "preserves caller order",
+                       ids: []table.Identifier{
+                               {"analytics", "raw", "orders"},
+                               {"analytics", "raw", "events"},
+                       },
+                       want: []string{"analytics.raw.orders", 
"analytics.raw.events"},
+               },
+               {
+                       name: "nested namespace",
+                       ids:  []table.Identifier{{"analytics", "prod", 
"metrics"}},
+                       want: []string{"analytics.prod.metrics"},
+               },
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       t.Parallel()
+                       assert.Equal(t, tt.want, tableIdentifierStrings(tt.ids))
+               })
+       }
+}
+
+func TestSortedTableListOutputs(t *testing.T) {
+       t.Parallel()
+
+       ids := []table.Identifier{
+               {"analytics", "raw", "orders"},
+               {"analytics", "raw", "events"},
+       }
+       sortTableIdentifiers(ids)
+
+       assert.Equal(t, []string{"events", "orders"}, 
tableNamesFromIdentifiers(ids))
+       assert.Equal(t, []string{"analytics.raw.events", 
"analytics.raw.orders"}, tableIdentifierStrings(ids))
+}
+
+func listTablesSeq(yields []table.Identifier, finalErr error) 
iter.Seq2[table.Identifier, error] {
+       return func(yield func(table.Identifier, error) bool) {
+               for _, ident := range yields {
+                       if !yield(ident, nil) {
+                               return
+                       }
+               }
+               if finalErr != nil {
+                       yield(table.Identifier{}, finalErr)
+               }
+       }
+}
+
+type stubNamespaceExister struct {
+       exists bool
+       err    error
+}
+
+func (s stubNamespaceExister) CheckNamespaceExists(context.Context, 
table.Identifier) (bool, error) {
+       return s.exists, s.err
+}
+
+type unexpectedNamespaceExister struct {
+       t *testing.T
+}
+
+func (s unexpectedNamespaceExister) CheckNamespaceExists(context.Context, 
table.Identifier) (bool, error) {
+       s.t.Helper()
+       s.t.Fatal("CheckNamespaceExists should not be called")
+
+       return false, nil
+}
+
+func TestCollectListedTables(t *testing.T) {
+       t.Parallel()
+
+       ctx := context.Background()
+       ns := table.Identifier{"analytics", "raw"}
+
+       t.Run("success", func(t *testing.T) {
+               t.Parallel()
+
+               got, err := collectListedTables(ctx, 
listTablesSeq([]table.Identifier{
+                       {"analytics", "raw", "orders"},
+                       {"analytics", "raw", "events"},
+               }, nil), ns, unexpectedNamespaceExister{t})
+               if !assert.NoError(t, err) {
+                       return
+               }
+               assert.Equal(t, []table.Identifier{
+                       {"analytics", "raw", "orders"},
+                       {"analytics", "raw", "events"},
+               }, got)
+       })
+
+       t.Run("empty results", func(t *testing.T) {
+               t.Parallel()
+
+               got, err := collectListedTables(ctx, listTablesSeq(nil, nil), 
ns, unexpectedNamespaceExister{t})
+               if !assert.NoError(t, err) {
+                       return
+               }
+               assert.Empty(t, got)
+       })
+
+       t.Run("first yield ErrNoSuchNamespace", func(t *testing.T) {
+               t.Parallel()
+
+               got, err := collectListedTables(ctx, listTablesSeq(nil, 
catalog.ErrNoSuchNamespace), ns, stubNamespaceExister{})
+               assert.Nil(t, got)
+               if !assert.Error(t, err) {
+                       return
+               }
+               assert.ErrorIs(t, err, errNamespaceNotFound)
+               assert.Contains(t, err.Error(), "analytics.raw")
+               // Catalog sentinel is intentionally not wrapped here; Read 
keys off
+               // errNamespaceNotFound so mid-pagination ErrNoSuchNamespace 
stays distinct.
+               assert.False(t, errors.Is(err, catalog.ErrNoSuchNamespace))
+       })
+
+       t.Run("generic error", func(t *testing.T) {
+               t.Parallel()
+
+               wantErr := errors.New("catalog unavailable")
+               got, err := collectListedTables(ctx, listTablesSeq(nil, 
wantErr), ns, unexpectedNamespaceExister{t})
+               assert.Nil(t, got)
+               assert.ErrorIs(t, err, wantErr)
+       })
+
+       t.Run("error after partial results preserves cause", func(t *testing.T) 
{
+               t.Parallel()
+
+               // Mimics iceberg-go REST pagination: identifiers from page 1, 
then a
+               // page-level HTTP 404 wrapped as ErrNoSuchNamespace (e.g. bad 
page token).
+               pageErr := fmt.Errorf("NoSuchPageTokenException: %w", 
catalog.ErrNoSuchNamespace)
+               got, err := collectListedTables(ctx, 
listTablesSeq([]table.Identifier{
+                       {"analytics", "raw", "events"},
+                       {"analytics", "raw", "orders"},
+               }, pageErr), ns, stubNamespaceExister{exists: true})
+               assert.Nil(t, got)
+               if !assert.Error(t, err) {
+                       return
+               }
+               assert.ErrorIs(t, err, catalog.ErrNoSuchNamespace)
+               assert.False(t, errors.Is(err, errNamespaceNotFound))
+               assert.Contains(t, err.Error(), "NoSuchPageTokenException")
+       })
+
+       t.Run("empty first page then pagination 404 is not missing namespace", 
func(t *testing.T) {
+               t.Parallel()
+
+               // Empty non-final page yields nothing, then a later page 404. 
Yield
+               // count is still 0, but CheckNamespaceExists reports the 
namespace
+               // is present — must not become errNamespaceNotFound.
+               pageErr := fmt.Errorf("NoSuchPageTokenException: %w", 
catalog.ErrNoSuchNamespace)
+               got, err := collectListedTables(ctx, listTablesSeq(nil, 
pageErr), ns, stubNamespaceExister{exists: true})
+               assert.Nil(t, got)
+               if !assert.Error(t, err) {
+                       return
+               }
+               assert.ErrorIs(t, err, catalog.ErrNoSuchNamespace)
+               assert.False(t, errors.Is(err, errNamespaceNotFound))
+               assert.Contains(t, err.Error(), "NoSuchPageTokenException")
+       })
+
+       t.Run("existence probe error preserves list error", func(t *testing.T) {
+               t.Parallel()
+
+               pageErr := fmt.Errorf("NoSuchPageTokenException: %w", 
catalog.ErrNoSuchNamespace)
+               got, err := collectListedTables(ctx, listTablesSeq(nil, 
pageErr), ns, stubNamespaceExister{
+                       err: errors.New("HEAD failed"),
+               })
+               assert.Nil(t, got)
+               if !assert.Error(t, err) {
+                       return
+               }
+               assert.ErrorIs(t, err, catalog.ErrNoSuchNamespace)
+               assert.False(t, errors.Is(err, errNamespaceNotFound))
+               assert.Contains(t, err.Error(), "NoSuchPageTokenException")
+       })
+
+       t.Run("rejects empty identifier", func(t *testing.T) {
+               t.Parallel()
+
+               got, err := collectListedTables(ctx, 
listTablesSeq([]table.Identifier{{}}, nil), ns, unexpectedNamespaceExister{t})
+               assert.Nil(t, got)
+               if !assert.Error(t, err) {
+                       return
+               }
+               assert.Contains(t, err.Error(), "empty table identifier")
+       })
+
+       t.Run("rejects identifier outside namespace", func(t *testing.T) {
+               t.Parallel()
+
+               got, err := collectListedTables(ctx, 
listTablesSeq([]table.Identifier{
+                       {"other", "ns", "events"},
+               }, nil), ns, unexpectedNamespaceExister{t})
+               assert.Nil(t, got)
+               if !assert.Error(t, err) {
+                       return
+               }
+               assert.Contains(t, err.Error(), `outside requested namespace 
"analytics.raw"`)
+       })
+}
+
+func TestAccIcebergTablesDataSource_Full(t *testing.T) {
+       catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
+       if catalogURI == "" {
+               t.Skip("ICEBERG_CATALOG_URI not set, skipping tables data 
source E2E test")
+       }
+
+       providerCfg := fmt.Sprintf(providerConfig, catalogURI)
+       suffix := acctest.RandString(8)
+       basicNS := "ns_tables_ds_basic_" + suffix
+       nestedParent := "analytics_" + suffix
+       emptyNS := "ns_tables_ds_empty_" + suffix
+
+       resource.Test(t, resource.TestCase{
+               PreCheck:                 func() { testAccPreCheck(t) },
+               ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+               Steps: []resource.TestStep{
+                       {
+                               Config: 
testAccIcebergTablesDataSourceBasicConfig(providerCfg, basicNS),
+                               Check: resource.ComposeTestCheckFunc(
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "namespace.0", 
basicNS),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "id", basicNS),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "tables.#", "2"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "tables.0", 
"alpha_table"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "tables.1", 
"beta_table"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "identifiers.#", 
"2"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "identifiers.0", 
basicNS+".alpha_table"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "identifiers.1", 
basicNS+".beta_table"),
+                               ),
+                       },
+                       {
+                               Config: 
testAccIcebergTablesDataSourceNestedConfig(providerCfg, nestedParent),
+                               Check: resource.ComposeTestCheckFunc(
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "namespace.0", 
nestedParent),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "namespace.1", 
"raw"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "id", 
nestedParent+".raw"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "tables.#", "1"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "tables.0", 
"events"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "identifiers.#", 
"1"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "identifiers.0", 
nestedParent+".raw.events"),
+                               ),
+                       },
+                       {
+                               Config: 
testAccIcebergTablesDataSourceEmptyNamespaceConfig(providerCfg, emptyNS),
+                               Check: resource.ComposeTestCheckFunc(
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "namespace.0", 
emptyNS),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "tables.#", "0"),
+                                       
resource.TestCheckResourceAttr("data.iceberg_tables.read", "identifiers.#", 
"0"),
+                               ),
+                       },
+               },
+       })
+}
+
+func TestAccIcebergTablesDataSource_NotFound(t *testing.T) {
+       catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
+       if catalogURI == "" {
+               t.Skip("ICEBERG_CATALOG_URI not set, skipping tables data 
source E2E test")
+       }
+
+       providerCfg := fmt.Sprintf(providerConfig, catalogURI)
+       missingNS := "ns_tables_ds_missing_" + acctest.RandString(8)
+
+       resource.Test(t, resource.TestCase{
+               PreCheck:                 func() { testAccPreCheck(t) },
+               ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+               Steps: []resource.TestStep{
+                       {
+                               Config:      
testAccIcebergTablesDataSourceMissingConfig(providerCfg, missingNS),
+                               ExpectError: regexp.MustCompile(`Namespace not 
found`),
+                       },
+               },
+       })
+}
+
+func TestAccIcebergTablesDataSource_EmptyNamespaceAttribute(t *testing.T) {
+       catalogURI := os.Getenv("ICEBERG_CATALOG_URI")
+       if catalogURI == "" {
+               t.Skip("ICEBERG_CATALOG_URI not set, skipping tables data 
source E2E test")
+       }
+
+       providerCfg := fmt.Sprintf(providerConfig, catalogURI)
+
+       resource.Test(t, resource.TestCase{
+               PreCheck:                 func() { testAccPreCheck(t) },
+               ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+               Steps: []resource.TestStep{
+                       {
+                               Config:      
testAccIcebergTablesDataSourceEmptyNamespaceAttributeConfig(providerCfg),
+                               ExpectError: regexp.MustCompile(`(?s)Invalid 
namespace.*at least one namespace segment`),
+                       },
+               },
+       })
+}
+
+func testAccIcebergTablesDataSourceBasicConfig(providerCfg, namespace string) 
string {
+       return providerCfg + fmt.Sprintf(`
+resource "iceberg_namespace" "db" {
+  name = [%q]
+}
+
+resource "iceberg_table" "alpha" {
+  namespace = iceberg_namespace.db.name
+  name      = "alpha_table"
+  schema = {
+    fields = [
+      {
+        id       = 1
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+resource "iceberg_table" "beta" {
+  namespace = iceberg_namespace.db.name
+  name      = "beta_table"
+  schema = {
+    fields = [
+      {
+        id       = 1
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+data "iceberg_tables" "read" {
+  namespace = iceberg_namespace.db.name
+
+  depends_on = [
+    iceberg_table.alpha,
+    iceberg_table.beta,
+  ]
+}
+`, namespace)
+}
+
+func testAccIcebergTablesDataSourceNestedConfig(providerCfg, parentNS string) 
string {
+       return providerCfg + fmt.Sprintf(`
+resource "iceberg_namespace" "db" {
+  name = [%q, "raw"]
+}
+
+resource "iceberg_table" "events" {
+  namespace = iceberg_namespace.db.name
+  name      = "events"
+  schema = {
+    fields = [
+      {
+        id       = 1
+        name     = "id"
+        type     = "long"
+        required = true
+      }
+    ]
+  }
+}
+
+data "iceberg_tables" "read" {
+  namespace = iceberg_namespace.db.name
+
+  depends_on = [iceberg_table.events]
+}
+`, parentNS)
+}
+
+func testAccIcebergTablesDataSourceEmptyNamespaceConfig(providerCfg, namespace 
string) string {
+       return providerCfg + fmt.Sprintf(`
+resource "iceberg_namespace" "db" {
+  name = [%q]
+}
+
+data "iceberg_tables" "read" {
+  namespace = iceberg_namespace.db.name
+}
+`, namespace)
+}
+
+func testAccIcebergTablesDataSourceEmptyNamespaceAttributeConfig(providerCfg 
string) string {
+       return providerCfg + `
+data "iceberg_tables" "empty_attr" {
+  namespace = []
+}
+`
+}
+
+func testAccIcebergTablesDataSourceMissingConfig(providerCfg, namespace 
string) string {
+       return providerCfg + fmt.Sprintf(`
+data "iceberg_tables" "missing" {
+  namespace = [%q]
+}
+`, namespace)
+}
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 9bd8dee..ff8d555 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -394,6 +394,7 @@ func (p *icebergProvider) DataSources(_ context.Context) 
[]func() datasource.Dat
        return []func() datasource.DataSource{
                NewNamespaceDataSource,
                NewTableDataSource,
+               NewTablesDataSource,
        }
 }
 

Reply via email to