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

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


The following commit(s) were added to refs/heads/main by this push:
     new 9ec965c  feat(client): name the missing resource in REST error 
diagnostics (#23)
9ec965c is described below

commit 9ec965cf1ef3f5faa60acf995188989aebdda689
Author: Jiajia Li <[email protected]>
AuthorDate: Fri Sep 18 11:04:52 2026 +0800

    feat(client): name the missing resource in REST error diagnostics (#23)
---
 internal/client/client.go                      | 148 ++++++
 internal/client/client_test.go                 | 396 +++++++++++++++
 internal/provider/data_source_database.go      |   4 +-
 internal/provider/data_source_table.go         |  13 +-
 internal/provider/production_readiness_test.go |  14 +-
 internal/provider/provider_management_test.go  | 666 +++++++++++++++++++++++++
 internal/provider/resource_database.go         |   4 +-
 internal/provider/resource_permission.go       |   4 +-
 internal/provider/resource_policy.go           |  41 +-
 internal/provider/resource_table.go            |  13 +-
 internal/provider/write_diagnostics.go         |  45 ++
 11 files changed, 1299 insertions(+), 49 deletions(-)

diff --git a/internal/client/client.go b/internal/client/client.go
index 6442c0a..6b11010 100644
--- a/internal/client/client.go
+++ b/internal/client/client.go
@@ -78,6 +78,8 @@ type APIError struct {
        ResourceType *string `json:"resourceType"`
        ResourceName *string `json:"resourceName"`
        RequestID    string  `json:"-"`
+       // LoadingConfig: the catalog config request failed, not the operation.
+       LoadingConfig bool `json:"-"`
 }
 
 func (e *APIError) Error() string {
@@ -88,10 +90,142 @@ func (e *APIError) Error() string {
        if e.RequestID != "" {
                detail += " (request ID " + e.RequestID + ")"
        }
+       if subject := e.resourceSubject(); subject != "" {
+               if e.StatusCode == http.StatusNotFound {
+                       detail += ": " + subject + " not found"
+               } else {
+                       detail += " for " + subject
+               }
+       }
 
        return detail
 }
 
+// resourceSubject renders e.g. `USER "analyst"`; never the free-text message.
+func (e *APIError) resourceSubject() string {
+       kind := "resource"
+       if e.ResourceType != nil {
+               kind = *e.ResourceType
+       }
+       switch {
+       case e.ResourceName != nil:
+               return kind + " " + strconv.Quote(*e.ResourceName)
+       case e.ResourceType != nil:
+               return kind
+       default:
+               return ""
+       }
+}
+
+// resourceTypes is the allowlist of kinds worth naming: Paimon's
+// ErrorResponse kinds plus the principal kinds access-controlled servers use.
+var resourceTypes = map[string]struct{}{
+       "catalog": {}, "database": {}, "table": {}, "view": {}, "function": {}, 
"column": {},
+       "partition": {}, "snapshot": {}, "branch": {}, "tag": {}, "dialect": 
{}, "definition": {},
+       "policy": {}, "permission": {}, "user": {}, "role": {}, "principal": {},
+}
+
+func safeResourceType(value *string) *string {
+       if value == nil {
+               return nil
+       }
+       if _, ok := resourceTypes[strings.ToLower(*value)]; !ok {
+               return nil
+       }
+
+       return value
+}
+
+// safeResourceName keeps a resourceName only if the client sent it as an
+// identifier (or two joined by a dot), so a server cannot echo arbitrary text.
+func safeResourceName(sent map[string]struct{}, value *string) *string {
+       if value == nil {
+               return nil
+       }
+       longest := 0
+       for identifier := range sent {
+               longest = max(longest, len(identifier))
+       }
+       name := *value
+       if len(name) > 2*longest+1 {
+               return nil
+       }
+       if _, ok := sent[name]; ok {
+               return value
+       }
+       for head := range sent {
+               if len(name) > len(head)+1 && name[len(head)] == '.' && 
name[:len(head)] == head {
+                       if _, ok := sent[name[len(head)+1:]]; ok {
+                               return value
+                       }
+               }
+       }
+
+       return nil
+}
+
+// identityKeys name catalog objects and principals, which configuration
+// pins; column names are left out because they can come from the server via
+// state or data sources. dataKeys are subtrees that are never entered.
+var (
+       identityKeys = map[string]struct{}{
+               "warehouse": {}, "name": {}, "database": {}, "table": {}, 
"object": {}, "function": {},
+               "view": {}, "principal": {},
+       }
+       dataKeys = map[string]struct{}{
+               "schema": {}, "changes": {}, "columns": {}, "rowFilter": {}, 
"columnMask": {},
+               "options": {}, "updates": {}, "removals": {}, "comment": {}, 
"description": {},
+       }
+)
+
+// requestIdentifiers collects what the request named: path segments after the
+// server-provided prefix, plus query and body values under identityKeys.
+func requestIdentifiers(segments []string, query url.Values, body []byte) 
map[string]struct{} {
+       sent := make(map[string]struct{})
+       if len(segments) > 2 {
+               for _, segment := range segments[2:] {
+                       sent[segment] = struct{}{}
+               }
+       }
+       for key, values := range query {
+               if _, ok := identityKeys[key]; !ok {
+                       continue
+               }
+               for _, value := range values {
+                       sent[value] = struct{}{}
+               }
+       }
+       if len(body) > 0 {
+               var decoded any
+               if err := json.Unmarshal(body, &decoded); err == nil {
+                       collectIdentifiers(decoded, "", sent)
+               }
+       }
+       delete(sent, "")
+
+       return sent
+}
+
+func collectIdentifiers(value any, key string, into map[string]struct{}) {
+       if _, data := dataKeys[key]; data {
+               return
+       }
+       switch typed := value.(type) {
+       case string:
+               if _, ok := identityKeys[key]; ok {
+                       into[typed] = struct{}{}
+               }
+       case []any:
+               for _, element := range typed {
+                       collectIdentifiers(element, key, into)
+               }
+       case map[string]any:
+               for childKey, element := range typed {
+                       collectIdentifiers(element, childKey, into)
+               }
+       }
+}
+
 var requestIDPattern = 
regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
 
 // Only expose conventional UUID correlation IDs, never arbitrary response text
@@ -124,6 +258,13 @@ func IsNotFound(err error) bool {
        return errors.As(err, &apiErr) && apiErr.StatusCode == 
http.StatusNotFound
 }
 
+// IsConfigurationFailure reports whether err came from loading the config.
+func IsConfigurationFailure(err error) bool {
+       var apiErr *APIError
+
+       return errors.As(err, &apiErr) && apiErr.LoadingConfig
+}
+
 // IsMutationOutcomeUncertain reports whether a failed mutation may have been
 // accepted remotely despite the client receiving an error.
 func IsMutationOutcomeUncertain(err error) bool {
@@ -299,6 +440,11 @@ func (c *Client) ensureConfigured(ctx context.Context) 
error {
 
        var response ConfigResponse
        if err := c.doRaw(ctx, http.MethodGet, []string{"v1", "config"}, query, 
nil, &response, c.headers); err != nil {
+               var apiErr *APIError
+               if errors.As(err, &apiErr) {
+                       apiErr.LoadingConfig = true
+               }
+
                return fmt.Errorf("load Paimon REST catalog config: %w", err)
        }
 
@@ -404,6 +550,8 @@ func (c *Client) doRaw(ctx context.Context, method string, 
segments []string, qu
                if err := json.NewDecoder(limited).Decode(apiErr); err != nil 
&& err != io.EOF {
                        apiErr.Message = http.StatusText(response.StatusCode)
                }
+               apiErr.ResourceType = safeResourceType(apiErr.ResourceType)
+               apiErr.ResourceName = 
safeResourceName(requestIdentifiers(segments, query, requestBody), 
apiErr.ResourceName)
 
                return apiErr
        }
diff --git a/internal/client/client_test.go b/internal/client/client_test.go
index 81b9e22..cda0223 100644
--- a/internal/client/client_test.go
+++ b/internal/client/client_test.go
@@ -121,6 +121,402 @@ func TestClientDoesNotExposeRemoteErrorMessage(t 
*testing.T) {
        assert.NotContains(t, err.Error(), "secret-token")
 }
 
+// errorServer answers every non-config request with the given error.
+func errorServer(t *testing.T, status int, body map[string]any) 
*httptest.Server {
+       t.Helper()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               if r.URL.Path == "/v1/config" {
+                       writeJSON(t, w, ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}})
+
+                       return
+               }
+               w.WriteHeader(status)
+               writeJSON(t, w, body)
+       }))
+       t.Cleanup(server.Close)
+
+       return server
+}
+
+func TestClientNamesMissingResourceFromStructuredErrorFields(t *testing.T) {
+       server := errorServer(t, http.StatusNotFound, map[string]any{
+               "resourceType": "USER",
+               "resourceName": "user:analyst",
+               "message":      "User user:analyst does not exist.",
+               "code":         404,
+       })
+       defer server.Close()
+
+       api, err := New(Config{URI: server.URL, Token: "secret-token"})
+       require.NoError(t, err)
+       err = api.GrantPermission(context.Background(), PermissionAssignment{
+               Resource:  PermissionResource{Type: ResourceTypeTable, 
Database: "analytics", Table: "events"},
+               Access:    PermissionAccessSelect,
+               Principal: "user:analyst",
+       })
+       require.Error(t, err)
+       assert.EqualError(t, err, `Paimon REST API returned HTTP 404 with code 
404: USER "user:analyst" not found`)
+       assert.NotContains(t, err.Error(), "does not exist")
+}
+
+func TestClientNamesResourceNeutrallyOnOtherStatuses(t *testing.T) {
+       server := errorServer(t, http.StatusConflict, map[string]any{
+               "resourceType": "TABLE",
+               "resourceName": "analytics.events",
+               "code":         409,
+       })
+       defer server.Close()
+
+       api, err := New(Config{URI: server.URL})
+       require.NoError(t, err)
+       _, err = api.GetTable(context.Background(), "analytics", "events")
+       require.Error(t, err)
+       assert.EqualError(t, err, `Paimon REST API returned HTTP 409 with code 
409 for TABLE "analytics.events"`)
+}
+
+func TestClientNamesOnlyIdentifiersItSentItself(t *testing.T) {
+       for name, resourceName := range map[string]string{
+               "bare token":                 "secret-token",
+               "whole header":               "Bearer secret-token",
+               "token behind a prefix":      "principal:secret-token",
+               "token split by a mark":      "Bearer\u034f secret-token",
+               "short custom header value":  "abc",
+               "unrelated identifier":       "other",
+               "identifier plus extra text": "analytics extra",
+               "substring of an identifier": "analytic",
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "DATABASE",
+                               "resourceName": resourceName,
+                               "code":         404,
+                       })
+                       defer server.Close()
+
+                       api, err := New(Config{URI: server.URL, Token: 
"secret-token", Headers: map[string]string{"X-Custom-Secret": "abc"}})
+                       require.NoError(t, err)
+                       _, err = api.GetDatabase(context.Background(), 
"analytics")
+                       require.Error(t, err)
+                       assert.EqualError(t, err, "Paimon REST API returned 
HTTP 404 with code 404: DATABASE not found")
+               })
+       }
+}
+
+func TestClientNamesIdentifiersThatOverlapProtocolHeaders(t *testing.T) {
+       for _, database := range []string{"app", "terraform"} {
+               t.Run(database, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "DATABASE",
+                               "resourceName": database,
+                               "code":         404,
+                       })
+                       defer server.Close()
+
+                       api, err := New(Config{URI: server.URL})
+                       require.NoError(t, err)
+                       _, err = api.GetDatabase(context.Background(), database)
+                       require.Error(t, err)
+                       assert.EqualError(t, err, `Paimon REST API returned 
HTTP 404 with code 404: DATABASE "`+database+`" not found`)
+               })
+       }
+}
+
+func TestClientDoesNotTreatTheServerPrefixAsAnIdentifier(t *testing.T) {
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               if r.URL.Path == "/v1/config" {
+                       writeJSON(t, w, ConfigResponse{Overrides: 
map[string]string{"prefix": "Bearer secret-token"}})
+
+                       return
+               }
+               w.WriteHeader(http.StatusNotFound)
+               writeJSON(t, w, map[string]any{"resourceType": "CATALOG", 
"resourceName": "Bearer secret-token", "code": 404})
+       }))
+       defer server.Close()
+
+       api, err := New(Config{URI: server.URL, Token: "secret-token"})
+       require.NoError(t, err)
+       _, err = api.GetDatabase(context.Background(), "analytics")
+       require.Error(t, err)
+       assert.EqualError(t, err, "Paimon REST API returned HTTP 404 with code 
404: CATALOG not found")
+}
+
+func TestClientDoesNotTreatDataValuesAsIdentifiers(t *testing.T) {
+       for name, tc := range map[string]struct {
+               reflected string
+               call      func(*Client) error
+       }{
+               "database option value": {"opt-secret", func(api *Client) error 
{
+                       return api.CreateDatabase(context.Background(), 
"analytics", map[string]string{"s3.secret-key": "opt-secret"})
+               }},
+               "database option under an identifier-looking key": 
{"secret-option-value", func(api *Client) error {
+                       return api.CreateDatabase(context.Background(), 
"analytics", map[string]string{"name": "secret-option-value"})
+               }},
+               "database option update": {"updated-secret", func(api *Client) 
error {
+                       return api.AlterDatabase(context.Background(), 
"analytics", nil, map[string]string{"table": "updated-secret"})
+               }},
+               "table option value": {"tbl-secret", func(api *Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{Options: map[string]string{"principal": 
"tbl-secret"}})
+               }},
+               "table option change value": {"changed-secret", func(api 
*Client) error {
+                       return api.AlterTable(context.Background(), 
"analytics", "events", []SchemaChange{{"action": "setOption", "key": "name", 
"value": "changed-secret"}})
+               }},
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "DATABASE",
+                               "resourceName": tc.reflected,
+                               "code":         404,
+                       })
+                       defer server.Close()
+
+                       api, err := New(Config{URI: server.URL})
+                       require.NoError(t, err)
+                       err = tc.call(api)
+                       require.Error(t, err)
+                       assert.EqualError(t, err, "Paimon REST API returned 
HTTP 404 with code 404: DATABASE not found")
+               })
+       }
+}
+
+func TestClientNamesIdentifiersFromTableBodies(t *testing.T) {
+       for name, tc := range map[string]struct {
+               reflected string
+               call      func(*Client) error
+       }{
+               "table object": {"events", func(api *Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{})
+               }},
+               "qualified table object": {"analytics.events", func(api 
*Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{})
+               }},
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "TABLE",
+                               "resourceName": tc.reflected,
+                               "code":         404,
+                       })
+
+                       api, err := New(Config{URI: server.URL})
+                       require.NoError(t, err)
+                       err = tc.call(api)
+                       require.Error(t, err)
+                       assert.EqualError(t, err, `Paimon REST API returned 
HTTP 404 with code 404: TABLE "`+tc.reflected+`" not found`)
+               })
+       }
+}
+
+func TestClientDoesNotPrintColumnNamesInAnyShape(t *testing.T) {
+       // Column names can come from the server, so none of them are printed.
+       for name, call := range map[string]func(*Client) error{
+               "schema field": func(api *Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{Fields: []Field{{Name: "secret-token"}}})
+               },
+               "field nested in a map value type": func(api *Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{Fields: []Field{{Name: "attrs", Type: 
DataType("MAP<STRING, ROW<`secret-token` STRING>>")}}})
+               },
+               "dropped field": func(api *Client) error {
+                       return api.AlterTable(context.Background(), 
"analytics", "events", []SchemaChange{{"action": "dropColumn", "fieldNames": 
[]string{"secret-token"}}})
+               },
+               "renamed field": func(api *Client) error {
+                       return api.AlterTable(context.Background(), 
"analytics", "events", []SchemaChange{{"action": "renameColumn", "fieldNames": 
[]string{"email"}, "newName": "secret-token"}})
+               },
+               "moved field": func(api *Client) error {
+                       return api.AlterTable(context.Background(), 
"analytics", "events", []SchemaChange{{"action": "updateColumnPosition", 
"move": map[string]any{"fieldName": "secret-token", "referenceFieldName": 
"secret-token", "type": "AFTER"}}})
+               },
+               "masked column": func(api *Client) error {
+                       return api.CreatePolicy(context.Background(), 
"analytics", "events", PolicyRequest{Principal: "user:analyst", ColumnMask: 
&ColumnMask{OnColumn: "secret-token", Transform: `{"type":"null"}`}})
+               },
+               "dropped policy column": func(api *Client) error {
+                       return api.DropPolicy(context.Background(), 
"analytics", "events", PolicyTypeColumnMasking, "user:analyst", "secret-token")
+               },
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "COLUMN",
+                               "resourceName": "secret-token",
+                               "code":         404,
+                       })
+
+                       api, err := New(Config{URI: server.URL, Token: 
"secret-token"})
+                       require.NoError(t, err)
+                       err = call(api)
+                       require.Error(t, err)
+                       assert.EqualError(t, err, "Paimon REST API returned 
HTTP 404 with code 404: COLUMN not found")
+               })
+       }
+}
+
+func TestClientDoesNotPrintColumnListsThatMayComeFromTheServer(t *testing.T) {
+       // Column and key lists can come back from state unchanged.
+       for name, call := range map[string]func(*Client) error{
+               "permission column": func(api *Client) error {
+                       return api.GrantPermission(context.Background(), 
PermissionAssignment{
+                               Resource:  PermissionResource{Type: 
ResourceTypeTable, Database: "analytics", Table: "events"},
+                               Access:    PermissionAccessSelect,
+                               Principal: "user:analyst",
+                               Columns:   &PermissionColumns{ColumnNames: 
[]string{"secret-token"}},
+                       })
+               },
+               "excluded permission column": func(api *Client) error {
+                       return api.GrantPermission(context.Background(), 
PermissionAssignment{
+                               Resource:  PermissionResource{Type: 
ResourceTypeTable, Database: "analytics", Table: "events"},
+                               Access:    PermissionAccessSelect,
+                               Principal: "user:analyst",
+                               Columns:   
&PermissionColumns{ExcludedColumnNames: []string{"secret-token"}},
+                       })
+               },
+               "partition key": func(api *Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{PartitionKeys: []string{"secret-token"}})
+               },
+               "primary key": func(api *Client) error {
+                       return api.CreateTable(context.Background(), 
"analytics", "events", Schema{PrimaryKeys: []string{"secret-token"}})
+               },
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "COLUMN",
+                               "resourceName": "secret-token",
+                               "code":         404,
+                       })
+
+                       api, err := New(Config{URI: server.URL, Token: 
"secret-token"})
+                       require.NoError(t, err)
+                       err = call(api)
+                       require.Error(t, err)
+                       assert.EqualError(t, err, "Paimon REST API returned 
HTTP 404 with code 404: COLUMN not found")
+               })
+       }
+}
+
+func TestClientRejectsQualifiedNamesWithAnUnsentComponent(t *testing.T) {
+       for name, resourceName := range map[string]string{
+               "unsent suffix":  "analytics.abc",
+               "unsent prefix":  "abc.analytics",
+               "header in both": "abc.abc",
+               "three parts":    "analytics.events.abc",
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "TABLE",
+                               "resourceName": resourceName,
+                               "code":         404,
+                       })
+
+                       api, err := New(Config{URI: server.URL, Headers: 
map[string]string{"X-Custom-Secret": "abc"}})
+                       require.NoError(t, err)
+                       _, err = api.GetTable(context.Background(), 
"analytics", "events")
+                       require.Error(t, err)
+                       assert.EqualError(t, err, "Paimon REST API returned 
HTTP 404 with code 404: TABLE not found")
+               })
+       }
+}
+
+func TestClientRejectsOversizedResourceNamesBeforeScanning(t *testing.T) {
+       huge := strings.Repeat("analytics.", 25000)
+       server := errorServer(t, http.StatusNotFound, map[string]any{
+               "resourceType": "TABLE",
+               "resourceName": huge,
+               "code":         404,
+       })
+       defer server.Close()
+
+       api, err := New(Config{URI: server.URL})
+       require.NoError(t, err)
+       err = api.GrantPermission(context.Background(), PermissionAssignment{
+               Resource: PermissionResource{Type: ResourceTypeTable, Database: 
"analytics", Table: "events"},
+               Columns:  &PermissionColumns{ColumnNames: []string{"a", "b", 
"c", "d", "e", "f", "g", "h", "i", "j"}},
+       })
+       require.Error(t, err)
+       assert.EqualError(t, err, "Paimon REST API returned HTTP 404 with code 
404: TABLE not found")
+}
+
+func TestClientNamesQualifiedTargetsSentInTheBody(t *testing.T) {
+       for _, tc := range []struct{ database, table, qualified string }{
+               {"analytics", "events", "analytics.events"},
+               {"analytics", "events.v2", "analytics.events.v2"},
+               {"analytics.v2", "events", "analytics.v2.events"},
+       } {
+               t.Run(tc.qualified, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, 
map[string]any{
+                               "resourceType": "TABLE",
+                               "resourceName": tc.qualified,
+                               "code":         404,
+                       })
+                       defer server.Close()
+
+                       api, err := New(Config{URI: server.URL})
+                       require.NoError(t, err)
+                       err = api.GrantPermission(context.Background(), 
PermissionAssignment{
+                               Resource:  PermissionResource{Type: 
ResourceTypeTable, Database: tc.database, Table: tc.table},
+                               Access:    PermissionAccessSelect,
+                               Principal: "user:analyst",
+                       })
+                       require.Error(t, err)
+                       assert.EqualError(t, err, `Paimon REST API returned 
HTTP 404 with code 404: TABLE "`+tc.qualified+`" not found`)
+               })
+       }
+}
+
+func TestClientMarksConfigFailuresSeparatelyFromOperations(t *testing.T) {
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
+               w.WriteHeader(http.StatusNotFound)
+               writeJSON(t, w, map[string]any{"resourceType": "CATALOG", 
"resourceName": "analytics", "code": 404})
+       }))
+       defer server.Close()
+
+       api, err := New(Config{URI: server.URL, Warehouse: "analytics"})
+       require.NoError(t, err)
+       err = api.GrantPermission(context.Background(), 
PermissionAssignment{Principal: "user:analyst"})
+       require.Error(t, err)
+       assert.True(t, IsNotFound(err))
+       assert.True(t, IsConfigurationFailure(err))
+       assert.ErrorContains(t, err, `CATALOG "analytics" not found`)
+
+       operationServer := errorServer(t, http.StatusNotFound, 
map[string]any{"code": 404})
+       api, err = New(Config{URI: operationServer.URL})
+       require.NoError(t, err)
+       err = api.GrantPermission(context.Background(), 
PermissionAssignment{Principal: "user:analyst"})
+       require.Error(t, err)
+       assert.True(t, IsNotFound(err))
+       assert.False(t, IsConfigurationFailure(err))
+}
+
+func TestClientDropsResourceTypeOutsideTheContract(t *testing.T) {
+       server := errorServer(t, http.StatusNotFound, map[string]any{
+               "resourceType": "Bearer secret-token",
+               "resourceName": "analytics",
+               "code":         404,
+       })
+       defer server.Close()
+
+       api, err := New(Config{URI: server.URL, Token: "secret-token"})
+       require.NoError(t, err)
+       _, err = api.GetDatabase(context.Background(), "analytics")
+       require.Error(t, err)
+       assert.EqualError(t, err, `Paimon REST API returned HTTP 404 with code 
404: resource "analytics" not found`)
+}
+
+func TestClientOmitsResourceIdentityWithoutUsableFields(t *testing.T) {
+       for name, body := range map[string]map[string]any{
+               "null fields":    {"resourceType": nil, "resourceName": nil, 
"code": 404},
+               "empty fields":   {"resourceType": "", "resourceName": "", 
"code": 404},
+               "unknown type":   {"resourceType": "Bearer secret-token", 
"resourceName": "secret-token", "code": 404},
+               "no such fields": {"code": 404},
+       } {
+               t.Run(name, func(t *testing.T) {
+                       server := errorServer(t, http.StatusNotFound, body)
+                       defer server.Close()
+
+                       api, err := New(Config{URI: server.URL, Token: 
"secret-token"})
+                       require.NoError(t, err)
+                       _, err = api.GetDatabase(context.Background(), 
"analytics")
+                       require.Error(t, err)
+                       assert.EqualError(t, err, "Paimon REST API returned 
HTTP 404 with code 404")
+               })
+       }
+}
+
 func TestClientRetriesRetryableReads(t *testing.T) {
        var configCalls atomic.Int32
        server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, r *http.Request) {
diff --git a/internal/provider/data_source_database.go 
b/internal/provider/data_source_database.go
index 1b2b2d0..7a81c6d 100644
--- a/internal/provider/data_source_database.go
+++ b/internal/provider/data_source_database.go
@@ -91,9 +91,9 @@ func (d *databaseDataSource) Read(ctx context.Context, req 
datasource.ReadReques
 
                return
        }
-       data.ID = types.StringValue(database.Name)
+       // Identity comes from the caller, never from the response.
+       data.ID = types.StringValue(data.Name.ValueString())
        data.ServerID = types.StringValue(database.ID)
-       data.Name = types.StringValue(database.Name)
        data.Options = stringMapValue(ctx, database.Options, &resp.Diagnostics)
        data.Location = types.StringValue(database.Location)
        data.Owner = types.StringValue(database.Owner)
diff --git a/internal/provider/data_source_table.go 
b/internal/provider/data_source_table.go
index c584758..fdf66dd 100644
--- a/internal/provider/data_source_table.go
+++ b/internal/provider/data_source_table.go
@@ -83,18 +83,9 @@ func (d *tableDataSource) Read(ctx context.Context, req 
datasource.ReadRequest,
 
                return
        }
-       database := table.Database
-       if database == "" {
-               database = data.Database.ValueString()
-       }
-       name := table.Name
-       if name == "" {
-               name = data.Name.ValueString()
-       }
-       data.ID = types.StringValue(tableID(database, name))
+       // Identity comes from the caller, never from the response.
+       data.ID = types.StringValue(tableID(data.Database.ValueString(), 
data.Name.ValueString()))
        data.ServerID = types.StringValue(table.ID)
-       data.Database = types.StringValue(database)
-       data.Name = types.StringValue(name)
        data.Fields = fieldsValueFromRemote(ctx, table.Schema.Fields, 
&resp.Diagnostics)
        data.PartitionKeys = stringListValue(ctx, table.Schema.PartitionKeys, 
&resp.Diagnostics)
        data.PrimaryKeys = stringListValue(ctx, table.Schema.PrimaryKeys, 
&resp.Diagnostics)
diff --git a/internal/provider/production_readiness_test.go 
b/internal/provider/production_readiness_test.go
index 9081ff3..16d5ec3 100644
--- a/internal/provider/production_readiness_test.go
+++ b/internal/provider/production_readiness_test.go
@@ -57,7 +57,7 @@ func TestDatabaseFailedOptionRemovalRetainsManagement(t 
*testing.T) {
        var sr resource.SchemaResponse
        res.Schema(ctx, resource.SchemaRequest{}, &sr)
        var ds diag.Diagnostics
-       model := databaseResourceModel{Options: 
types.MapValueMust(types.StringType, map[string]attr.Value{"owner": 
types.StringValue("old")})}
+       model := databaseResourceModel{Name: types.StringValue("analytics"), 
Options: types.MapValueMust(types.StringType, map[string]attr.Value{"owner": 
types.StringValue("old")})}
        setDatabaseResourceModel(ctx, &model, remote, &ds)
        require.False(t, ds.HasError(), ds)
        state := tfsdk.State{Schema: sr.Schema}
@@ -98,7 +98,7 @@ func TestTableFailedOptionRemovalRetainsManagement(t 
*testing.T) {
        var sr resource.SchemaResponse
        res.Schema(ctx, resource.SchemaRequest{}, &sr)
        var ds diag.Diagnostics
-       model := tableResourceModel{Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapValueMust(types.StringType, map[string]attr.Value{"retention": 
types.StringValue("old")})}
+       model := tableResourceModel{Database: types.StringValue("analytics"), 
Name: types.StringValue("events"), Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapValueMust(types.StringType, map[string]attr.Value{"retention": 
types.StringValue("old")})}
        setTableResourceModel(ctx, &model, remote, &ds)
        require.False(t, ds.HasError(), ds)
        state := tfsdk.State{Schema: sr.Schema}
@@ -134,7 +134,7 @@ func TestImportedImmutableOptionCanBecomeManaged(t 
*testing.T) {
        ctx := context.Background()
        remote := &client.Table{Database: "analytics", Name: "events", Schema: 
client.Schema{Fields: []client.Field{{ID: 0, Name: "id", Type: "BIGINT"}}, 
Options: map[string]string{"merge-engine": "partial-update"}}}
        var ds diag.Diagnostics
-       imported := tableResourceModel{Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapNull(types.StringType)}
+       imported := tableResourceModel{Database: 
types.StringValue("analytics"), Name: types.StringValue("events"), Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapNull(types.StringType)}
        setTableResourceModel(ctx, &imported, remote, &ds)
        require.False(t, ds.HasError(), ds)
        require.True(t, imported.Options.IsNull())
@@ -261,7 +261,7 @@ func 
TestTableUpdateRejectsResolvedNewFieldIDBeforeMutation(t *testing.T) {
        ctx := context.Background()
        var diags diag.Diagnostics
        remote := &client.Table{Database: "analytics", Name: "events", Schema: 
client.Schema{Fields: []client.Field{{ID: 0, Name: "id", Type: "BIGINT"}}}}
-       model := tableResourceModel{Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapNull(types.StringType)}
+       model := tableResourceModel{Database: types.StringValue("analytics"), 
Name: types.StringValue("events"), Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapNull(types.StringType)}
        setTableResourceModel(ctx, &model, remote, &diags)
        require.False(t, diags.HasError(), diags)
        res := &tableResource{} // No client: validation must finish before any 
REST call.
@@ -297,7 +297,7 @@ func TestTableReplacementRequiresOptIn(t *testing.T) {
                                var sr resource.SchemaResponse
                                res.Schema(ctx, resource.SchemaRequest{}, &sr)
                                var diags diag.Diagnostics
-                               before := tableResourceModel{Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapNull(types.StringType)}
+                               before := tableResourceModel{Database: 
types.StringValue("analytics"), Name: types.StringValue("events"), Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapNull(types.StringType)}
                                remote := &client.Table{Database: "analytics", 
Name: "events", Schema: client.Schema{Fields: []client.Field{{ID: 0, Name: 
"id", Type: "BIGINT NOT NULL"}}}}
                                setTableResourceModel(ctx, &before, remote, 
&diags)
                                require.False(t, diags.HasError(), diags)
@@ -360,7 +360,7 @@ func 
TestDatabaseSuccessfulButStaleRemovalRetainsManagement(t *testing.T) {
        var sr resource.SchemaResponse
        res.Schema(ctx, resource.SchemaRequest{}, &sr)
        var ds diag.Diagnostics
-       model := databaseResourceModel{Options: 
types.MapValueMust(types.StringType, map[string]attr.Value{"owner": 
types.StringValue("old")})}
+       model := databaseResourceModel{Name: types.StringValue("analytics"), 
Options: types.MapValueMust(types.StringType, map[string]attr.Value{"owner": 
types.StringValue("old")})}
        setDatabaseResourceModel(ctx, &model, remote, &ds)
        require.False(t, ds.HasError(), ds)
        state := tfsdk.State{Schema: sr.Schema}
@@ -414,7 +414,7 @@ func TestTableSuccessfulButStaleRemovalRetainsManagement(t 
*testing.T) {
        var sr resource.SchemaResponse
        res.Schema(ctx, resource.SchemaRequest{}, &sr)
        var ds diag.Diagnostics
-       model := tableResourceModel{Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapValueMust(types.StringType, map[string]attr.Value{"retention": 
types.StringValue("old")})}
+       model := tableResourceModel{Database: types.StringValue("analytics"), 
Name: types.StringValue("events"), Fields: 
types.ListNull(types.ObjectType{AttrTypes: tableFieldAttrTypes()}), Options: 
types.MapValueMust(types.StringType, map[string]attr.Value{"retention": 
types.StringValue("old")})}
        setTableResourceModel(ctx, &model, remote, &ds)
        require.False(t, ds.HasError(), ds)
        state := tfsdk.State{Schema: sr.Schema}
diff --git a/internal/provider/provider_management_test.go 
b/internal/provider/provider_management_test.go
index a5fa835..e1262d6 100644
--- a/internal/provider/provider_management_test.go
+++ b/internal/provider/provider_management_test.go
@@ -30,6 +30,7 @@ import (
 
        "github.com/apache/terraform-provider-paimon/internal/client"
        "github.com/hashicorp/terraform-plugin-framework/attr"
+       "github.com/hashicorp/terraform-plugin-framework/datasource"
        "github.com/hashicorp/terraform-plugin-framework/diag"
        "github.com/hashicorp/terraform-plugin-framework/resource"
        resourceschema 
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
@@ -636,6 +637,671 @@ func 
TestPermissionCreateRetainsStateWhenReconciliationFails(t *testing.T) {
        assert.GreaterOrEqual(t, listCalls, 3)
 }
 
+// unknownPrincipalServer refuses every write with an unknown-principal 404.
+func unknownPrincipalServer(t *testing.T) *httptest.Server {
+       t.Helper()
+
+       return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, 
request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodPost:
+                       w.WriteHeader(http.StatusNotFound)
+                       require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{
+                               "resourceType": "USER",
+                               "resourceName": "role:analyst",
+                               "message":      "User role:analyst does not 
exist.",
+                               "code":         404,
+                       }))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/permissions":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ListPermissionsResponse{}))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ListPoliciesResponse{}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+}
+
+func TestPermissionCreateExplainsUnknownPrincipal(t *testing.T) {
+       ctx := context.Background()
+       server := unknownPrincipalServer(t)
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &permissionResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       planModel := permissionResourceModel{
+               ID:                  types.StringUnknown(),
+               ResourceType:        
types.StringValue(client.ResourceTypeTable),
+               Database:            types.StringValue("analytics"),
+               Table:               types.StringValue("events"),
+               Function:            types.StringNull(),
+               View:                types.StringNull(),
+               Access:              
types.StringValue(client.PermissionAccessSelect),
+               Principal:           types.StringValue("role:analyst"),
+               ColumnNames:         types.SetNull(types.StringType),
+               ExcludedColumnNames: types.SetNull(types.StringType),
+               ExpireTime:          types.StringNull(),
+       }
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.CreateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Create(ctx, resource.CreateRequest{Plan: plan}, &response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the 
catalog, database, table, function, view, or columns the permission targets")
+       assert.NotContains(t, detail, "does not exist")
+}
+
+func TestRowFilterCreateExplainsUnknownPrincipal(t *testing.T) {
+       ctx := context.Background()
+       server := unknownPrincipalServer(t)
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &rowFilterResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       planModel := rowFilterResourceModel{
+               ID:        types.StringUnknown(),
+               Database:  types.StringValue("analytics"),
+               Table:     types.StringValue("events"),
+               Principal: types.StringValue("role:analyst"),
+               Predicate: types.StringValue(`{"field":"tenant_id"}`),
+       }
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.CreateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Create(ctx, resource.CreateRequest{Plan: plan}, &response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the table 
the row filter targets")
+}
+
+func TestColumnMaskCreateExplainsUnknownPrincipal(t *testing.T) {
+       ctx := context.Background()
+       server := unknownPrincipalServer(t)
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &columnMaskResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       planModel := columnMaskResourceModel{
+               AllowNonAtomicUpdate: types.BoolValue(true),
+               ID:                   types.StringUnknown(),
+               Database:             types.StringValue("analytics"),
+               Table:                types.StringValue("events"),
+               Principal:            types.StringValue("role:analyst"),
+               Column:               types.StringValue("email"),
+               Transform:            types.StringValue(`{"type":"null"}`),
+       }
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.CreateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Create(ctx, resource.CreateRequest{Plan: plan}, &response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal, the table, or 
the column the column mask targets")
+}
+
+func TestRowFilterUpdateExplainsUnknownPrincipalWhenRestoreSucceeds(t 
*testing.T) {
+       ctx := context.Background()
+       var mu sync.Mutex
+       remote := []client.DataPolicy{}
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               mu.Lock()
+               defer mu.Unlock()
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies/drop":
+                       remote = nil
+                       w.WriteHeader(http.StatusOK)
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       var body client.PolicyRequest
+                       require.NoError(t, 
json.NewDecoder(request.Body).Decode(&body))
+                       if body.RowFilter.Predicate == `{"version":"new"}` {
+                               w.WriteHeader(http.StatusNotFound)
+                               require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{
+                                       "resourceType": "USER", "resourceName": 
"role:analyst", "code": 404,
+                               }))
+
+                               return
+                       }
+                       remote = []client.DataPolicy{{
+                               Resource:  client.PermissionResource{Type: 
client.ResourceTypeTable, Database: "analytics", Table: "events"},
+                               RowFilter: body.RowFilter,
+                               Principal: body.Principal,
+                       }}
+                       w.WriteHeader(http.StatusOK)
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ListPoliciesResponse{Policies: remote}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       detail := updateRowFilterPredicate(ctx, t, server.URL)
+       assert.Contains(t, detail, "previous row filter was restored")
+       assert.NotContains(t, detail, "verification of the restored policy also 
failed")
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the table 
the row filter targets")
+}
+
+func TestRowFilterUpdateExplainsUnknownPrincipalWhenRestoreFails(t *testing.T) 
{
+       ctx := context.Background()
+       server := unknownPrincipalServer(t)
+       defer server.Close()
+
+       detail := updateRowFilterPredicate(ctx, t, server.URL)
+       assert.Contains(t, detail, "restoring the previous row filter also 
failed")
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the table 
the row filter targets")
+}
+
+func TestRowFilterUpdateExplainsUnknownPrincipalWhenOnlyRestoreIs404(t 
*testing.T) {
+       ctx := context.Background()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies/drop":
+                       w.WriteHeader(http.StatusOK)
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       var body client.PolicyRequest
+                       require.NoError(t, 
json.NewDecoder(request.Body).Decode(&body))
+                       if body.RowFilter.Predicate == `{"version":"new"}` {
+                               w.WriteHeader(http.StatusBadRequest)
+                               require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{"code": 400}))
+
+                               return
+                       }
+                       w.WriteHeader(http.StatusNotFound)
+                       require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{
+                               "resourceType": "USER", "resourceName": 
"role:analyst", "code": 404,
+                       }))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ListPoliciesResponse{}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       detail := updateRowFilterPredicate(ctx, t, server.URL)
+       assert.Contains(t, detail, "restoring the previous row filter also 
failed")
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the table 
the row filter targets")
+}
+
+func TestRowFilterUpdateExplainsUnknownPrincipalAfterConfigOutage(t 
*testing.T) {
+       // Config fails during the replacement and recovers before the restore.
+       ctx := context.Background()
+       var mu sync.Mutex
+       configCalls := 0
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               mu.Lock()
+               defer mu.Unlock()
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       configCalls++
+                       if configCalls <= 2 {
+                               w.WriteHeader(http.StatusNotFound)
+                               require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{"code": 404}))
+
+                               return
+                       }
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       w.WriteHeader(http.StatusNotFound)
+                       require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{
+                               "resourceType": "USER", "resourceName": 
"role:analyst", "code": 404,
+                       }))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ListPoliciesResponse{}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       detail := updateRowFilterPredicate(ctx, t, server.URL)
+       assert.Contains(t, detail, "restoring the previous row filter also 
failed")
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the table 
the row filter targets")
+}
+
+func TestColumnMaskUpdateExplainsUnknownPrincipal(t *testing.T) {
+       ctx := context.Background()
+       server := unknownPrincipalServer(t)
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &columnMaskResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       stateModel := columnMaskResourceModel{
+               AllowNonAtomicUpdate: types.BoolValue(true),
+               ID:                   
types.StringValue("column=email&database=analytics&principal=role%3Aanalyst&table=events"),
+               Database:             types.StringValue("analytics"),
+               Table:                types.StringValue("events"),
+               Principal:            types.StringValue("role:analyst"),
+               Column:               types.StringValue("email"),
+               Transform:            types.StringValue(`{"type":"null"}`),
+       }
+       planModel := stateModel
+       planModel.Transform = types.StringValue(`{"type":"hash"}`)
+       state := tfsdk.State{Schema: schemaResponse.Schema}
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, state.Set(ctx, &stateModel).HasError())
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.UpdateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Update(ctx, resource.UpdateRequest{State: state, Plan: plan}, 
&response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal, the table, or 
the column the column mask targets")
+}
+
+// updateRowFilterPredicate runs a row-filter Update and returns the error 
detail.
+func updateRowFilterPredicate(ctx context.Context, t *testing.T, uri string) 
string {
+       t.Helper()
+       api, err := client.New(client.Config{URI: uri, RecoveryTimeout: 250 * 
time.Millisecond})
+       require.NoError(t, err)
+       managed := &rowFilterResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       stateModel := rowFilterResourceModel{
+               AllowNonAtomicUpdate: types.BoolValue(true),
+               ID:                   
types.StringValue("database=analytics&principal=role%3Aanalyst&table=events"),
+               Database:             types.StringValue("analytics"),
+               Table:                types.StringValue("events"),
+               Principal:            types.StringValue("role:analyst"),
+               Predicate:            types.StringValue(`{"version":"old"}`),
+       }
+       planModel := stateModel
+       planModel.Predicate = types.StringValue(`{"version":"new"}`)
+       state := tfsdk.State{Schema: schemaResponse.Schema}
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, state.Set(ctx, &stateModel).HasError())
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.UpdateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Update(ctx, resource.UpdateRequest{State: state, Plan: plan}, 
&response)
+       require.True(t, response.Diagnostics.HasError())
+
+       return response.Diagnostics.Errors()[0].Detail()
+}
+
+func TestRowFilterCreateDoesNotBlameThePrincipalWhenConfigFails(t *testing.T) {
+       ctx := context.Background()
+       posts := 0
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               if request.Method == http.MethodPost {
+                       posts++
+               }
+               w.WriteHeader(http.StatusNotFound)
+               require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{"resourceType": "CATALOG", 
"resourceName": "analytics", "code": 404}))
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, Warehouse: 
"analytics", RecoveryTimeout: 250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &rowFilterResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       planModel := rowFilterResourceModel{
+               ID:        types.StringUnknown(),
+               Database:  types.StringValue("analytics"),
+               Table:     types.StringValue("events"),
+               Principal: types.StringValue("role:analyst"),
+               Predicate: types.StringValue(`{"field":"tenant_id"}`),
+       }
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.CreateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Create(ctx, resource.CreateRequest{Plan: plan}, &response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, `CATALOG "analytics" not found`)
+       assert.NotContains(t, detail, "could not find the principal")
+       assert.Equal(t, 0, posts)
+}
+
+func TestRowFilterCreateHintMentionsUnsupportedManagementAPI(t *testing.T) {
+       ctx := context.Background()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               if request.Method == http.MethodGet && request.URL.Path == 
"/v1/config" {
+                       w.Header().Set("Content-Type", "application/json")
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+
+                       return
+               }
+               http.NotFound(w, request)
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &rowFilterResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       planModel := rowFilterResourceModel{
+               ID:        types.StringUnknown(),
+               Database:  types.StringValue("analytics"),
+               Table:     types.StringValue("events"),
+               Principal: types.StringValue("role:analyst"),
+               Predicate: types.StringValue(`{"field":"tenant_id"}`),
+       }
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.CreateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Create(ctx, resource.CreateRequest{Plan: plan}, &response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, "usually means it could not find the 
principal or the table the row filter targets")
+       assert.Contains(t, detail, "does not serve this management API")
+}
+
+func TestDatabaseReadKeepsTheRequestedName(t *testing.T) {
+       ctx := context.Background()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.Database{ID: "db-1", Name: "secret-token", 
Location: "oss://bucket/analytics"}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL})
+       require.NoError(t, err)
+       managed := &databaseResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       stateModel := databaseResourceModel{
+               ID:            types.StringValue("analytics"),
+               ServerID:      types.StringValue("db-1"),
+               Name:          types.StringValue("analytics"),
+               Options:       types.MapNull(types.StringType),
+               ServerOptions: types.MapNull(types.StringType),
+               Location:      types.StringValue("oss://bucket/analytics"),
+               Owner:         types.StringNull(),
+               CreatedAt:     types.Int64Null(),
+               CreatedBy:     types.StringNull(),
+               UpdatedAt:     types.Int64Null(),
+               UpdatedBy:     types.StringNull(),
+       }
+       state := tfsdk.State{Schema: schemaResponse.Schema}
+       require.False(t, state.Set(ctx, &stateModel).HasError())
+       response := resource.ReadResponse{State: state}
+       managed.Read(ctx, resource.ReadRequest{State: state}, &response)
+
+       require.False(t, response.Diagnostics.HasError(), 
response.Diagnostics.Errors())
+       var refreshed databaseResourceModel
+       require.False(t, response.State.Get(ctx, &refreshed).HasError())
+       assert.Equal(t, "analytics", refreshed.Name.ValueString())
+       assert.Equal(t, "analytics", refreshed.ID.ValueString())
+}
+
+func TestRowFilterUpdateExplainsUnknownPrincipalWhenComparisonFails(t 
*testing.T) {
+       // Comparing the leftover policy needs the schema, which stops being 
served.
+       ctx := context.Background()
+       var mu sync.Mutex
+       tableCalls := 0
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               mu.Lock()
+               defer mu.Unlock()
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies/drop":
+                       w.WriteHeader(http.StatusOK)
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       w.WriteHeader(http.StatusNotFound)
+                       require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{
+                               "resourceType": "USER", "resourceName": 
"role:analyst", "code": 404,
+                       }))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ListPoliciesResponse{Policies: 
[]client.DataPolicy{{
+                               Resource:  client.PermissionResource{Type: 
client.ResourceTypeTable, Database: "analytics", Table: "events"},
+                               RowFilter: &client.RowFilter{Predicate: 
`{"version":"other"}`},
+                               Principal: "role:analyst",
+                       }}}))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events":
+                       tableCalls++
+                       if tableCalls > 1 {
+                               w.WriteHeader(http.StatusInternalServerError)
+                               require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{"code": 500}))
+
+                               return
+                       }
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.Table{Database: "analytics", Name: "events", 
Schema: client.Schema{Fields: []client.Field{{ID: 0, Name: "region", Type: 
client.DataType("STRING")}}}}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &rowFilterResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       stateModel := rowFilterResourceModel{
+               AllowNonAtomicUpdate: types.BoolValue(true),
+               ID:                   
types.StringValue("database=analytics&principal=role%3Aanalyst&table=events"),
+               Database:             types.StringValue("analytics"),
+               Table:                types.StringValue("events"),
+               Principal:            types.StringValue("role:analyst"),
+               Predicate:            
types.StringValue(`{"kind":"LEAF","transform":{"name":"FIELD_REF","fieldRef":{"name":"region"}}}`),
+       }
+       planModel := stateModel
+       planModel.Predicate = types.StringValue(`{"version":"new"}`)
+       state := tfsdk.State{Schema: schemaResponse.Schema}
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, state.Set(ctx, &stateModel).HasError())
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.UpdateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Update(ctx, resource.UpdateRequest{State: state, Plan: plan}, 
&response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, "comparing the remaining policy failed")
+       assert.Contains(t, detail, `USER "role:analyst" not found`)
+       assert.Contains(t, detail, "could not find the principal or the table 
the row filter targets")
+}
+
+func TestTableReadKeepsTheRequestedIdentity(t *testing.T) {
+       ctx := context.Background()
+       legit := &client.Table{ID: "tbl-1", Database: "analytics", Name: 
"events", Schema: client.Schema{Fields: []client.Field{{ID: 0, Name: "id", 
Type: client.DataType("BIGINT")}}}}
+       poisoned := *legit
+       poisoned.Database, poisoned.Name = "secret-token", "secret-token"
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       writeProviderJSON(t, w, client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}})
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events":
+                       writeProviderJSON(t, w, poisoned)
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL})
+       require.NoError(t, err)
+       managed := &tableResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       var ds diag.Diagnostics
+       model := tableResourceModel{
+               Database: types.StringValue("analytics"),
+               Name:     types.StringValue("events"),
+               Fields:   types.ListNull(types.ObjectType{AttrTypes: 
tableFieldAttrTypes()}),
+               Options:  types.MapValueMust(types.StringType, 
map[string]attr.Value{}),
+       }
+       setTableResourceModel(ctx, &model, legit, &ds)
+       require.False(t, ds.HasError(), ds)
+       state := tfsdk.State{Schema: schemaResponse.Schema}
+       require.False(t, state.Set(ctx, &model).HasError())
+       read := resource.ReadResponse{State: state}
+       managed.Read(ctx, resource.ReadRequest{State: state}, &read)
+
+       require.False(t, read.Diagnostics.HasError(), read.Diagnostics)
+       var after tableResourceModel
+       require.False(t, read.State.Get(ctx, &after).HasError())
+       assert.Equal(t, "analytics", after.Database.ValueString())
+       assert.Equal(t, "events", after.Name.ValueString())
+       assert.Equal(t, tableID("analytics", "events"), after.ID.ValueString())
+}
+
+func TestDatabaseDataSourceKeepsTheConfiguredName(t *testing.T) {
+       ctx := context.Background()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       writeProviderJSON(t, w, client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}})
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics":
+                       writeProviderJSON(t, w, client.Database{ID: "db-1", 
Name: "secret-token"})
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL})
+       require.NoError(t, err)
+       source := &databaseDataSource{client: api}
+       var schemaResponse datasource.SchemaResponse
+       source.Schema(ctx, datasource.SchemaRequest{}, &schemaResponse)
+       config := tfsdk.State{Schema: schemaResponse.Schema}
+       require.False(t, config.Set(ctx, &databaseDataSourceModel{
+               ID: types.StringUnknown(), ServerID: types.StringUnknown(), 
Name: types.StringValue("analytics"),
+               Options: types.MapUnknown(types.StringType), Location: 
types.StringUnknown(), Owner: types.StringUnknown(),
+               CreatedAt: types.Int64Unknown(), CreatedBy: 
types.StringUnknown(), UpdatedAt: types.Int64Unknown(), UpdatedBy: 
types.StringUnknown(),
+       }).HasError())
+       response := datasource.ReadResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       source.Read(ctx, datasource.ReadRequest{Config: tfsdk.Config{Raw: 
config.Raw, Schema: schemaResponse.Schema}}, &response)
+
+       require.False(t, response.Diagnostics.HasError(), response.Diagnostics)
+       var data databaseDataSourceModel
+       require.False(t, response.State.Get(ctx, &data).HasError())
+       assert.Equal(t, "analytics", data.Name.ValueString())
+       assert.Equal(t, "analytics", data.ID.ValueString())
+}
+
+func TestTableDataSourceKeepsTheConfiguredIdentity(t *testing.T) {
+       ctx := context.Background()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       writeProviderJSON(t, w, client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}})
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events":
+                       writeProviderJSON(t, w, client.Table{ID: "tbl-1", 
Database: "secret-token", Name: "secret-token", Schema: client.Schema{Fields: 
[]client.Field{{ID: 0, Name: "id", Type: client.DataType("BIGINT")}}}})
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL})
+       require.NoError(t, err)
+       source := &tableDataSource{client: api}
+       var schemaResponse datasource.SchemaResponse
+       source.Schema(ctx, datasource.SchemaRequest{}, &schemaResponse)
+       config := tfsdk.State{Schema: schemaResponse.Schema}
+       require.False(t, config.Set(ctx, &tableDataSourceModel{
+               ID: types.StringUnknown(), ServerID: types.StringUnknown(), 
Database: types.StringValue("analytics"), Name: types.StringValue("events"),
+               Fields: types.ListUnknown(types.ObjectType{AttrTypes: 
tableFieldAttrTypes()}), PartitionKeys: types.ListUnknown(types.StringType), 
PrimaryKeys: types.ListUnknown(types.StringType),
+               Options: types.MapUnknown(types.StringType), Comment: 
types.StringUnknown(), SchemaID: types.Int64Unknown(), Path: 
types.StringUnknown(), IsExternal: types.BoolUnknown(),
+               Owner: types.StringUnknown(), CreatedAt: types.Int64Unknown(), 
CreatedBy: types.StringUnknown(), UpdatedAt: types.Int64Unknown(), UpdatedBy: 
types.StringUnknown(),
+       }).HasError())
+       response := datasource.ReadResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       source.Read(ctx, datasource.ReadRequest{Config: tfsdk.Config{Raw: 
config.Raw, Schema: schemaResponse.Schema}}, &response)
+
+       require.False(t, response.Diagnostics.HasError(), response.Diagnostics)
+       var data tableDataSourceModel
+       require.False(t, response.State.Get(ctx, &data).HasError())
+       assert.Equal(t, "analytics", data.Database.ValueString())
+       assert.Equal(t, "events", data.Name.ValueString())
+       assert.Equal(t, tableID("analytics", "events"), data.ID.ValueString())
+}
+
+func TestRowFilterCreateDoesNotBlameThePrincipalWhenOnlyTheLookupIs404(t 
*testing.T) {
+       // Only the lookup answers 404; the write itself failed with 500.
+       ctx := context.Background()
+       server := httptest.NewServer(http.HandlerFunc(func(w 
http.ResponseWriter, request *http.Request) {
+               w.Header().Set("Content-Type", "application/json")
+               switch {
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/config":
+                       require.NoError(t, 
json.NewEncoder(w).Encode(client.ConfigResponse{Defaults: 
map[string]string{"prefix": "catalog"}}))
+               case request.Method == http.MethodPost && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       w.WriteHeader(http.StatusInternalServerError)
+                       require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{"code": 500}))
+               case request.Method == http.MethodGet && request.URL.Path == 
"/v1/catalog/databases/analytics/tables/events/policies":
+                       w.WriteHeader(http.StatusNotFound)
+                       require.NoError(t, 
json.NewEncoder(w).Encode(map[string]any{"resourceType": "TABLE", 
"resourceName": "events", "code": 404}))
+               default:
+                       http.NotFound(w, request)
+               }
+       }))
+       defer server.Close()
+
+       api, err := client.New(client.Config{URI: server.URL, RecoveryTimeout: 
250 * time.Millisecond})
+       require.NoError(t, err)
+       managed := &rowFilterResource{client: api}
+       var schemaResponse resource.SchemaResponse
+       managed.Schema(ctx, resource.SchemaRequest{}, &schemaResponse)
+       planModel := rowFilterResourceModel{
+               ID:        types.StringUnknown(),
+               Database:  types.StringValue("analytics"),
+               Table:     types.StringValue("events"),
+               Principal: types.StringValue("role:analyst"),
+               Predicate: types.StringValue(`{"field":"tenant_id"}`),
+       }
+       plan := tfsdk.Plan{Schema: schemaResponse.Schema}
+       require.False(t, plan.Set(ctx, &planModel).HasError())
+       response := resource.CreateResponse{State: tfsdk.State{Schema: 
schemaResponse.Schema}}
+       managed.Create(ctx, resource.CreateRequest{Plan: plan}, &response)
+
+       require.True(t, response.Diagnostics.HasError())
+       detail := response.Diagnostics.Errors()[0].Detail()
+       assert.Contains(t, detail, "HTTP 500")
+       assert.NotContains(t, detail, "HTTP 404 to this write")
+}
+
+func TestExplainNotFoundLeavesOtherFailuresAlone(t *testing.T) {
+       detail := explainNotFound("boom", &client.APIError{StatusCode: 
http.StatusInternalServerError}, "the principal")
+       assert.Equal(t, "boom", detail)
+}
+
 func TestPermissionCreateReconcilesLostResponse(t *testing.T) {
        ctx := context.Background()
        var mu sync.Mutex
diff --git a/internal/provider/resource_database.go 
b/internal/provider/resource_database.go
index 676b4c3..2ef9082 100644
--- a/internal/provider/resource_database.go
+++ b/internal/provider/resource_database.go
@@ -270,9 +270,9 @@ func databaseMatchesOptions(database *client.Database, name 
string, options map[
 }
 
 func setDatabaseResourceModel(ctx context.Context, model 
*databaseResourceModel, database *client.Database, diags *diag.Diagnostics) {
-       model.ID = types.StringValue(database.Name)
+       // Identity comes from the caller, never from the response.
+       model.ID = types.StringValue(model.Name.ValueString())
        model.ServerID = types.StringValue(database.ID)
-       model.Name = types.StringValue(database.Name)
        model.Options = syncManagedOptions(ctx, model.Options, 
database.Options, diags)
        model.ServerOptions = stringMapValue(ctx, database.Options, diags)
        model.Location = types.StringValue(database.Location)
diff --git a/internal/provider/resource_permission.go 
b/internal/provider/resource_permission.go
index eff1f79..55c7e3d 100644
--- a/internal/provider/resource_permission.go
+++ b/internal/provider/resource_permission.go
@@ -218,7 +218,7 @@ func (r *permissionResource) Create(ctx context.Context, 
req resource.CreateRequ
        if err := r.client.GrantPermission(ctx, assignment); err != nil {
                observed, recovered, recoveryErr := r.reconcileFailedGrant(ctx, 
plan, assignment, err)
                if !recovered {
-                       resp.Diagnostics.AddError("Unable to grant Paimon 
permission", recoveryErr.Error())
+                       resp.Diagnostics.AddError("Unable to grant Paimon 
permission", explainNotFound(recoveryErr.Error(), err, permissionReferences))
 
                        return
                }
@@ -281,7 +281,7 @@ func (r *permissionResource) Update(ctx context.Context, 
req resource.UpdateRequ
        if err := r.client.GrantPermission(ctx, assignment); err != nil {
                observed, recovered, recoveryErr := r.reconcileFailedGrant(ctx, 
plan, assignment, err)
                if !recovered {
-                       resp.Diagnostics.AddError("Unable to update Paimon 
permission", recoveryErr.Error())
+                       resp.Diagnostics.AddError("Unable to update Paimon 
permission", explainNotFound(recoveryErr.Error(), err, permissionReferences))
 
                        return
                }
diff --git a/internal/provider/resource_policy.go 
b/internal/provider/resource_policy.go
index d82b54c..cf21f3b 100644
--- a/internal/provider/resource_policy.go
+++ b/internal/provider/resource_policy.go
@@ -20,6 +20,7 @@ package provider
 import (
        "context"
        "encoding/json"
+       "errors"
        "fmt"
        "net/url"
 
@@ -202,7 +203,7 @@ func (r *rowFilterResource) Create(ctx context.Context, req 
resource.CreateReque
        }
        result := createPolicyWithReconciliation(ctx, r.client, 
rowFilterSpec(plan), false)
        if !result.accepted {
-               resp.Diagnostics.AddError("Unable to create Paimon row filter", 
result.err.Error())
+               resp.Diagnostics.AddError("Unable to create Paimon row filter", 
explainNotFound(result.err.Error(), result.cause, rowFilterReferences))
 
                return
        }
@@ -281,7 +282,7 @@ func (r *rowFilterResource) Update(ctx context.Context, req 
resource.UpdateReque
                resp.Diagnostics.AddWarning("Recovered Paimon row-filter 
replacement", result.warning)
        }
        if result.err != nil {
-               resp.Diagnostics.AddError("Unable to replace Paimon row 
filter", result.err.Error())
+               resp.Diagnostics.AddError("Unable to replace Paimon row 
filter", explainNotFound(result.err.Error(), result.cause, rowFilterReferences))
        }
 }
 
@@ -392,7 +393,7 @@ func (r *columnMaskResource) Create(ctx context.Context, 
req resource.CreateRequ
        }
        result := createPolicyWithReconciliation(ctx, r.client, 
columnMaskSpec(plan), false)
        if !result.accepted {
-               resp.Diagnostics.AddError("Unable to create Paimon column 
mask", result.err.Error())
+               resp.Diagnostics.AddError("Unable to create Paimon column 
mask", explainNotFound(result.err.Error(), result.cause, columnMaskReferences))
 
                return
        }
@@ -471,7 +472,7 @@ func (r *columnMaskResource) Update(ctx context.Context, 
req resource.UpdateRequ
                resp.Diagnostics.AddWarning("Recovered Paimon column-mask 
replacement", result.warning)
        }
        if result.err != nil {
-               resp.Diagnostics.AddError("Unable to replace Paimon column 
mask", result.err.Error())
+               resp.Diagnostics.AddError("Unable to replace Paimon column 
mask", explainNotFound(result.err.Error(), result.cause, columnMaskReferences))
        }
 }
 
@@ -623,12 +624,14 @@ type policyCreateResult struct {
        observed *client.DataPolicy
        warning  string
        err      error
+       // cause is the raw REST error behind err.
+       cause error
 }
 
 func createPolicyWithReconciliation(ctx context.Context, api *client.Client, 
spec policySpec, alreadyManaged bool) policyCreateResult {
        createErr := api.CreatePolicy(ctx, spec.database, spec.table, 
spec.request)
        if createErr != nil && !alreadyManaged && 
!client.IsMutationOutcomeUncertain(createErr) {
-               return policyCreateResult{err: fmt.Errorf("creating the %s was 
rejected: %w. Import an existing policy explicitly; it has not been adopted 
into state", spec.label, createErr)}
+               return policyCreateResult{cause: createErr, err: 
fmt.Errorf("creating the %s was rejected: %w. Import an existing policy 
explicitly; it has not been adopted into state", spec.label, createErr)}
        }
        recoveryCtx, cancel := mutationRecoveryContext(ctx, api)
        defer cancel()
@@ -658,13 +661,13 @@ func createPolicyWithReconciliation(ctx context.Context, 
api *client.Client, spe
                return result
        }
        if reconcileErr != nil {
-               return policyCreateResult{err: fmt.Errorf("creating the %s 
failed (%s), and bounded reconciliation could not establish the remote state: 
%w", spec.label, createErr, reconcileErr)}
+               return policyCreateResult{cause: createErr, err: 
fmt.Errorf("creating the %s failed (%s), and bounded reconciliation could not 
establish the remote state: %w", spec.label, createErr, reconcileErr)}
        }
        if !found {
-               return policyCreateResult{err: fmt.Errorf("creating the %s 
failed, and bounded reconciliation confirmed that the policy is absent: %w", 
spec.label, createErr)}
+               return policyCreateResult{cause: createErr, err: 
fmt.Errorf("creating the %s failed, and bounded reconciliation confirmed that 
the policy is absent: %w", spec.label, createErr)}
        }
        if !converged {
-               return policyCreateResult{observed: &observed, err: 
fmt.Errorf("creating the %s failed (%s), and the same identity exists with 
different policy content", spec.label, createErr)}
+               return policyCreateResult{cause: createErr, observed: 
&observed, err: fmt.Errorf("creating the %s failed (%s), and the same identity 
exists with different policy content", spec.label, createErr)}
        }
 
        return policyCreateResult{
@@ -679,6 +682,7 @@ type policyReplacementResult struct {
        observed *client.DataPolicy
        warning  string
        err      error
+       cause    error
 }
 
 type policyLookupObservation struct {
@@ -732,13 +736,13 @@ func replacePolicyWithReconciliation(ctx context.Context, 
api *client.Client, pr
        if created.observed != nil {
                matches, err := previous.matchesWithSchema(ctx, api, 
*created.observed)
                if err != nil {
-                       return policyReplacementResult{observed: 
created.observed, err: err}
+                       return policyReplacementResult{cause: created.cause, 
observed: created.observed, err: fmt.Errorf("creating the replacement failed 
(%s), and comparing the remaining policy failed: %w", created.err, err)}
                }
                if matches {
-                       return policyReplacementResult{observed: 
created.observed, err: fmt.Errorf("creating the replacement failed, but 
reconciliation confirmed that the previous %s remains attached: %w", 
previous.label, created.err)}
+                       return policyReplacementResult{cause: created.cause, 
observed: created.observed, err: fmt.Errorf("creating the replacement failed, 
but reconciliation confirmed that the previous %s remains attached: %w", 
previous.label, created.err)}
                }
 
-               return policyReplacementResult{observed: created.observed, err: 
fmt.Errorf("creating the replacement failed, and reconciliation found 
unexpected policy content for the same identity: %w", created.err)}
+               return policyReplacementResult{cause: created.cause, observed: 
created.observed, err: fmt.Errorf("creating the replacement failed, and 
reconciliation found unexpected policy content for the same identity: %w", 
created.err)}
        }
 
        recoveryCtx, cancel := mutationRecoveryContext(ctx, api)
@@ -750,14 +754,14 @@ func replacePolicyWithReconciliation(ctx context.Context, 
api *client.Client, pr
                        detail += fmt.Sprintf(", but verification of the 
restored policy also failed (%s)", restored.err)
                }
 
-               return policyReplacementResult{observed: restored.observed, 
err: fmt.Errorf("%s", detail)}
+               return policyReplacementResult{cause: created.cause, observed: 
restored.observed, err: errors.New(detail)}
        }
        restoredDesired := false
        if restored.observed != nil {
                var err error
                restoredDesired, err = desired.matchesWithSchema(recoveryCtx, 
api, *restored.observed)
                if err != nil {
-                       return policyReplacementResult{observed: 
restored.observed, err: err}
+                       return policyReplacementResult{cause: 
notFoundCause(created.cause, restored.cause), observed: restored.observed, err: 
fmt.Errorf("creating the replacement failed (%s), restoring the previous %s 
failed (%s), and comparing the remaining policy failed: %w", created.err, 
previous.label, restored.err, err)}
                }
        }
        if restoredDesired {
@@ -768,7 +772,16 @@ func replacePolicyWithReconciliation(ctx context.Context, 
api *client.Client, pr
                }
        }
 
-       return policyReplacementResult{err: fmt.Errorf("creating the 
replacement failed (%s), and restoring the previous %s also failed (%s). The 
principal may currently have no %s", created.err, previous.label, restored.err, 
previous.label)}
+       return policyReplacementResult{cause: notFoundCause(created.cause, 
restored.cause), err: fmt.Errorf("creating the replacement failed (%s), and 
restoring the previous %s also failed (%s). The principal may currently have no 
%s", created.err, previous.label, restored.err, previous.label)}
+}
+
+// notFoundCause picks the write that was refused with HTTP 404, if either was.
+func notFoundCause(first, second error) error {
+       if !explainableNotFound(first) && explainableNotFound(second) {
+               return second
+       }
+
+       return first
 }
 
 func validateSerializedPolicy(name, value string, diags *diag.Diagnostics) {
diff --git a/internal/provider/resource_table.go 
b/internal/provider/resource_table.go
index 6a3129a..f02928a 100644
--- a/internal/provider/resource_table.go
+++ b/internal/provider/resource_table.go
@@ -467,18 +467,9 @@ func tableMatchesPlannedSchema(table *client.Table, 
expected client.Schema, comp
 }
 
 func setTableResourceModel(ctx context.Context, model *tableResourceModel, 
table *client.Table, diags *diag.Diagnostics) {
-       database := table.Database
-       if database == "" {
-               database = model.Database.ValueString()
-       }
-       name := table.Name
-       if name == "" {
-               name = model.Name.ValueString()
-       }
-       model.ID = types.StringValue(tableID(database, name))
+       // Identity comes from the caller, never from the response.
+       model.ID = types.StringValue(tableID(model.Database.ValueString(), 
model.Name.ValueString()))
        model.ServerID = types.StringValue(table.ID)
-       model.Database = types.StringValue(database)
-       model.Name = types.StringValue(name)
        model.Fields = resourceFieldsValueFromRemote(ctx, model.Fields, 
table.Schema.Fields, diags)
        model.PartitionKeys = stringListValue(ctx, table.Schema.PartitionKeys, 
diags)
        model.PrimaryKeys = stringListValue(ctx, table.Schema.PrimaryKeys, 
diags)
diff --git a/internal/provider/write_diagnostics.go 
b/internal/provider/write_diagnostics.go
new file mode 100644
index 0000000..f257bb0
--- /dev/null
+++ b/internal/provider/write_diagnostics.go
@@ -0,0 +1,45 @@
+// 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/apache/terraform-provider-paimon/internal/client"
+)
+
+const (
+       permissionReferences = "the principal or the catalog, database, table, 
function, view, or columns the permission targets"
+       rowFilterReferences  = "the principal or the table the row filter 
targets"
+       columnMaskReferences = "the principal, the table, or the column the 
column mask targets"
+)
+
+// explainNotFound adds a hint when a write was refused with HTTP 404; a 404
+// from loading the config is not about the write and gets none.
+func explainNotFound(detail string, cause error, referenced string) string {
+       if !explainableNotFound(cause) {
+               return detail
+       }
+
+       return detail + "\n\nThe REST Catalog answered HTTP 404 to this write. 
That usually means it could not find " + referenced +
+               ": check that each of them exists on the catalog exactly as 
written, and that the principal is already known to the catalog's identity 
provider." +
+               " A catalog that does not serve this management API answers the 
same way."
+}
+
+// explainableNotFound: a 404 from the operation, not from loading the config.
+func explainableNotFound(err error) bool {
+       return client.IsNotFound(err) && !client.IsConfigurationFailure(err)
+}

Reply via email to