laskoviymishka commented on code in PR #1406:
URL: https://github.com/apache/iceberg-go/pull/1406#discussion_r3570254056


##########
table/encryption.go:
##########
@@ -27,6 +31,18 @@ type EncryptionKey struct {
        Properties           map[string]string `json:"properties,omitempty"`
 }
 
+func (e EncryptionKey) Validate() error {
+       if strings.TrimSpace(e.KeyID) == "" {
+               return errors.New("encryption key-id must be non-empty")
+       }
+
+       if strings.TrimSpace(e.EncryptedKeyMetadata) == "" {

Review Comment:
   `TrimSpace` on `EncryptedKeyMetadata` doesn't really buy us anything — this 
field is a base64 blob, and whitespace is never valid base64, so the check 
collapses to `== ""` in practice.
   
   If we actually want to validate the field the way the spec describes it 
(base64-encoded), I'd do a real decode check — 
`base64.StdEncoding.DecodeString(...)` and reject on error. That catches 
genuinely malformed values, and it matches iceberg-rust, which hard-errors 
decoding this field on read — so a table we write with a non-base64 value would 
be unreadable there.
   
   Separately on `KeyID`: `TrimSpace` there is defensible, but the dedup loop 
just below compares raw `k.KeyID == key.KeyID`, so `"  key  "` and `" key "` 
land as distinct entries and you can't `RemoveEncryptionKey` with the trimmed 
form. If we're going to trim for validation, I'd store the trimmed value too 
(or just reject padded ids). Whichever we pick, worth a test pinning it. wdyt?



##########
table/encryption.go:
##########
@@ -27,6 +31,18 @@ type EncryptionKey struct {
        Properties           map[string]string `json:"properties,omitempty"`
 }
 
+func (e EncryptionKey) Validate() error {

Review Comment:
   `Validate()` is exported but nothing outside the package calls it — callers 
hand keys to `AddEncryptionKey` and let it validate. Every other validation 
surface in here (`commonMetadata.validate`, `metadataV3.validate`) is 
unexported, so this widens the public API for something internal, and the rules 
become a breaking change to touch later.
   
   The errors it returns are also plain `errors.New`, which makes the 
`fmt.Errorf("%w: %w", iceberg.ErrInvalidArgument, err)` at the call site a bit 
of a dead end — the second `%w` promises `errors.Is` matchability on a value no 
caller can reference.
   
   I'd either unexport it to `validate()`, or if there's a reason to keep it 
public, have it wrap with `ErrInvalidArgument` itself and drop the re-wrap. 
wdyt?



##########
table/updates_test.go:
##########
@@ -884,6 +884,37 @@ func TestAddEncryptionKeyUpdate_Apply_RejectsV2(t 
*testing.T) {
        assert.Contains(t, err.Error(), "format version 3")
 }
 
+func TestAddEncryptionKeyUpdate_Apply_RejectsMissingKeyID(t *testing.T) {
+       b := buildFromBaseV3(t)
+       key := EncryptionKey{KeyID: "", EncryptedKeyMetadata: "dGVzdA=="}
+
+       err := NewAddEncryptionKeyUpdate(key).Apply(b)
+       require.Error(t, err)
+       assert.ErrorIs(t, err, iceberg.ErrInvalidArgument)
+       assert.Contains(t, err.Error(), "key-id")
+}
+
+func TestAddEncryptionKeyUpdate_Apply_RejectsMissingEncryptedKeyMetadata(t 
*testing.T) {
+       b := buildFromBaseV3(t)
+       key := EncryptionKey{KeyID: "my-key", EncryptedKeyMetadata: ""}
+
+       err := NewAddEncryptionKeyUpdate(key).Apply(b)
+       require.Error(t, err)
+       assert.ErrorIs(t, err, iceberg.ErrInvalidArgument)
+       assert.Contains(t, err.Error(), "metadata")
+}
+
+func TestAddEncryptionKeyUpdate_UnmarshalMissingFields_ApplyRejects(t 
*testing.T) {
+       data := 
[]byte(`[{"action":"add-encryption-key","encryption-key":{"key-id":"my-key"}}]`)
+
+       var updates Updates
+       require.NoError(t, json.Unmarshal(data, &updates))
+       require.Len(t, updates, 1)
+
+       err := updates[0].Apply(buildFromBaseV3(t))
+       require.ErrorIs(t, err, iceberg.ErrInvalidArgument)

Review Comment:
   The other two tests assert both `ErrorIs` and a `Contains` on the field name 
— this one only checks `ErrorIs`. If the error ever fires for a different 
reason (a future version guard, say), this still passes while testing the wrong 
thing. I'd add `assert.Contains(t, err.Error(), "key-id")` to pin that it's the 
missing field being rejected.
   
   Minor while we're here: the three new tests use `assert.ErrorIs` where the 
rest of the file uses `require.ErrorIs` — worth matching.



##########
table/updates_test.go:
##########
@@ -884,6 +884,37 @@ func TestAddEncryptionKeyUpdate_Apply_RejectsV2(t 
*testing.T) {
        assert.Contains(t, err.Error(), "format version 3")
 }
 
+func TestAddEncryptionKeyUpdate_Apply_RejectsMissingKeyID(t *testing.T) {

Review Comment:
   All three cases go through `NewAddEncryptionKeyUpdate(key).Apply` — none 
calls `b.AddEncryptionKey` directly, which is the exported, documented entry 
point the validation actually lives on. If `Apply` ever gets refactored to not 
route through `AddEncryptionKey`, these still pass while the builder contract 
silently breaks. I'd add one case hitting `b.AddEncryptionKey(key)` directly.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to