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 53f428c  Plan no changes after importing row filters and column masks 
(#35)
53f428c is described below

commit 53f428c8aad43ded50283c3144ed8d2bc9c41775
Author: Jiajia Li <[email protected]>
AuthorDate: Sat Oct 10 15:11:13 2026 +0800

    Plan no changes after importing row filters and column masks (#35)
---
 docs/resources/column_mask.md                   |  5 ++
 docs/resources/row_filter.md                    |  5 ++
 internal/provider/helpers.go                    | 16 ++++++
 internal/provider/helpers_test.go               | 14 +++++
 internal/provider/management_acceptance_test.go | 76 +++++++++++++++++++++++++
 internal/provider/resource_policy.go            |  7 ++-
 6 files changed, 121 insertions(+), 2 deletions(-)

diff --git a/docs/resources/column_mask.md b/docs/resources/column_mask.md
index 646a008..e73f088 100644
--- a/docs/resources/column_mask.md
+++ b/docs/resources/column_mask.md
@@ -105,6 +105,11 @@ terraform import paimon_column_mask.analyst_email \
   'database=analytics&table=events&principal=role%3Aanalyst&column=email'
 ```
 
+Import records `allow_non_atomic_update` as its default `false` and stores the
+server's `transform` in `jsonencode` form (sorted keys, no whitespace), so
+adopting a policy whose configuration omits the flag plans no changes. When the
+configuration sets the flag to `true`, the next apply only records it in state.
+
 The provider compares known Java policy AST semantics across create, refresh,
 and update: field references resolve by name against the current table schema,
 and one-child AND/OR predicates simplify to their child. Equivalent configured
diff --git a/docs/resources/row_filter.md b/docs/resources/row_filter.md
index 009dafd..85e30c7 100644
--- a/docs/resources/row_filter.md
+++ b/docs/resources/row_filter.md
@@ -109,6 +109,11 @@ terraform import paimon_row_filter.analyst_region \
   'database=analytics&table=events&principal=role%3Aanalyst'
 ```
 
+Import records `allow_non_atomic_update` as its default `false` and stores the
+server's `predicate` in `jsonencode` form (sorted keys, no whitespace), so
+adopting a policy whose configuration omits the flag plans no changes. When the
+configuration sets the flag to `true`, the next apply only records it in state.
+
 The provider compares known Java policy AST semantics across create, refresh,
 and update: field references resolve by name against the current table schema,
 and one-child AND/OR predicates simplify to their child. Equivalent configured
diff --git a/internal/provider/helpers.go b/internal/provider/helpers.go
index 405f99b..0141d3f 100644
--- a/internal/provider/helpers.go
+++ b/internal/provider/helpers.go
@@ -123,6 +123,22 @@ func equivalentJSON(left, right string) bool {
        return reflect.DeepEqual(leftValue, rightValue)
 }
 
+// compactJSON formats JSON like jsonencode, avoiding whitespace-only diffs.
+func compactJSON(value string) string {
+       var decoded any
+       decoder := json.NewDecoder(strings.NewReader(value))
+       decoder.UseNumber()
+       if decoder.Decode(&decoded) != nil || !json.Valid([]byte(value)) {
+               return value
+       }
+       encoded, err := json.Marshal(decoded)
+       if err != nil {
+               return value
+       }
+
+       return string(encoded)
+}
+
 func mutationRecoveryContext(ctx context.Context, api *client.Client) 
(context.Context, context.CancelFunc) {
        // A canceled apply context can still follow a mutation that committed 
remotely.
        // Keep request-scoped values, but give reconciliation and repair a 
fresh bound.
diff --git a/internal/provider/helpers_test.go 
b/internal/provider/helpers_test.go
index 182aa7d..d3c285d 100644
--- a/internal/provider/helpers_test.go
+++ b/internal/provider/helpers_test.go
@@ -62,3 +62,17 @@ func TestRetryLookupUntilReturnsLastStaleStateAtDeadline(t 
*testing.T) {
        assert.False(t, converged)
        assert.Equal(t, "stale", value)
 }
+
+func TestCompactJSONMatchesJSONEncode(t *testing.T) {
+       for name, tc := range map[string]struct{ in, want string }{
+               "whitespace":    {"{\n  \"name\": \"NULL\"\n}", 
`{"name":"NULL"}`},
+               "key order":     {`{"name":"CONCAT","inputs":["***"]}`, 
`{"inputs":["***"],"name":"CONCAT"}`},
+               "large integer": {`{"literals":[9007199254740993]}`, 
`{"literals":[9007199254740993]}`},
+               "html escaping": {`{"literals":["a<b&c>d"]}`, 
`{"literals":["a\u003cb\u0026c\u003ed"]}`},
+               "invalid":       {`{"name":`, `{"name":`},
+       } {
+               t.Run(name, func(t *testing.T) {
+                       assert.Equal(t, tc.want, compactJSON(tc.in))
+               })
+       }
+}
diff --git a/internal/provider/management_acceptance_test.go 
b/internal/provider/management_acceptance_test.go
index 41ad155..9053ef9 100644
--- a/internal/provider/management_acceptance_test.go
+++ b/internal/provider/management_acceptance_test.go
@@ -16,6 +16,7 @@
 package provider
 
 import (
+       "bytes"
        "encoding/json"
        "fmt"
        "net/http"
@@ -198,3 +199,78 @@ func TestAccManagementResourceLifecycle(t *testing.T) {
        require.Empty(t, catalog.policies)
        require.Empty(t, catalog.permissions)
 }
+
+func TestAccPolicyImportPlansNoChanges(t *testing.T) {
+       if os.Getenv("TF_ACC") == "" {
+               t.Skip("set TF_ACC=1 to run Terraform acceptance tests")
+       }
+       // Some servers return indented policy JSON.
+       indent := func(value string) string {
+               var out bytes.Buffer
+               require.NoError(t, json.Indent(&out, []byte(value), "", "  "))
+
+               return out.String()
+       }
+       catalog := &managementAcceptanceCatalog{policies: 
make(map[string]client.DataPolicy), canonicalize: func(p client.PolicyRequest) 
client.PolicyRequest {
+               if p.RowFilter != nil {
+                       p.RowFilter = &client.RowFilter{Predicate: 
indent(p.RowFilter.Predicate)}
+               }
+               if p.ColumnMask != nil {
+                       p.ColumnMask = &client.ColumnMask{OnColumn: 
p.ColumnMask.OnColumn, Transform: indent(p.ColumnMask.Transform)}
+               }
+
+               return p
+       }}
+       server := httptest.NewServer(catalog)
+       defer server.Close()
+       config := fmt.Sprintf("provider \"paimon\" { uri = %q }\n", server.URL) 
+ managementTableConfig("analytics") + `
+resource "paimon_row_filter" "region" {
+  database = paimon_table.events.database
+  table = paimon_table.events.name
+  principal = "role:analyst"
+  predicate = jsonencode({
+    kind = "LEAF"
+    transform = { name = "FIELD_REF", fieldRef = { index = 1, name = "tenant", 
type = "STRING" } }
+    function = "EQUAL"
+    literals = ["APAC"]
+  })
+}
+resource "paimon_column_mask" "secret" {
+  database = paimon_table.events.database
+  table = paimon_table.events.name
+  principal = "role:analyst"
+  column = "secret"
+  transform = jsonencode({ name = "NULL" })
+}
+`
+       // Forget the policies without dropping them.
+       forget := fmt.Sprintf("provider \"paimon\" { uri = %q }\n", server.URL) 
+ managementTableConfig("analytics") + `
+removed {
+  from = paimon_row_filter.region
+  lifecycle { destroy = false }
+}
+removed {
+  from = paimon_column_mask.secret
+  lifecycle { destroy = false }
+}
+`
+       resource.Test(t, resource.TestCase{
+               ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
+               Steps: []resource.TestStep{
+                       {Config: config},
+                       {Config: forget},
+                       {
+                               Config: config, ResourceName: 
"paimon_row_filter.region", ImportState: true, ImportStatePersist: true,
+                               ImportStateId: 
"database=analytics&table=events&principal=role%3Aanalyst",
+                       },
+                       {
+                               Config: config, ResourceName: 
"paimon_column_mask.secret", ImportState: true, ImportStatePersist: true,
+                               ImportStateId: 
"database=analytics&table=events&principal=role%3Aanalyst&column=secret",
+                       },
+                       {Config: config, PlanOnly: true},
+               },
+       })
+       catalog.mu.Lock()
+       defer catalog.mu.Unlock()
+       require.Empty(t, catalog.policies)
+}
diff --git a/internal/provider/resource_policy.go 
b/internal/provider/resource_policy.go
index cf21f3b..9969608 100644
--- a/internal/provider/resource_policy.go
+++ b/internal/provider/resource_policy.go
@@ -313,6 +313,8 @@ func (r *rowFilterResource) ImportState(ctx 
context.Context, req resource.Import
        resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("database"), model.Database)...)
        resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("table"), model.Table)...)
        resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("principal"), model.Principal)...)
+       // The server does not store this flag; import its default.
+       resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("allow_non_atomic_update"), false)...)
 }
 
 func (r *rowFilterResource) readIntoState(ctx context.Context, model 
*rowFilterResourceModel, diags *diag.Diagnostics) bool {
@@ -333,7 +335,7 @@ func (r *rowFilterResource) readIntoState(ctx 
context.Context, model *rowFilterR
                return false
        }
        if model.Predicate.IsNull() || model.Predicate.IsUnknown() || !equal {
-               model.Predicate = types.StringValue(remote)
+               model.Predicate = types.StringValue(compactJSON(remote))
        }
        model.ID = types.StringValue(rowFilterID(*model))
 
@@ -505,6 +507,7 @@ func (r *columnMaskResource) ImportState(ctx 
context.Context, req resource.Impor
        resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("table"), model.Table)...)
        resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("principal"), model.Principal)...)
        resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("column"), model.Column)...)
+       resp.Diagnostics.Append(resp.State.SetAttribute(ctx, 
path.Root("allow_non_atomic_update"), false)...)
 }
 
 func (r *columnMaskResource) readIntoState(ctx context.Context, model 
*columnMaskResourceModel, diags *diag.Diagnostics) bool {
@@ -525,7 +528,7 @@ func (r *columnMaskResource) readIntoState(ctx 
context.Context, model *columnMas
                return false
        }
        if model.Transform.IsNull() || model.Transform.IsUnknown() || !equal {
-               model.Transform = types.StringValue(remote)
+               model.Transform = types.StringValue(compactJSON(remote))
        }
        model.ID = types.StringValue(columnMaskID(*model))
 

Reply via email to