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

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


The following commit(s) were added to refs/heads/main by this push:
     new bf1536b7a refactor(puffin): centralize writer state checks (#2365)
bf1536b7a is described below

commit bf1536b7ab70e1685f27a329da1560c7c11ec1bc
Author: Minh Vu <[email protected]>
AuthorDate: Fri Oct 30 09:22:14 2026 +0100

    refactor(puffin): centralize writer state checks (#2365)
---
 puffin/puffin_writer.go            | 48 +++++++++++-----------
 puffin/puffin_writer_state_test.go | 84 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 107 insertions(+), 25 deletions(-)

diff --git a/puffin/puffin_writer.go b/puffin/puffin_writer.go
index a5836662f..113206c11 100644
--- a/puffin/puffin_writer.go
+++ b/puffin/puffin_writer.go
@@ -90,15 +90,25 @@ func NewWriter(w io.Writer) (*Writer, error) {
        }, nil
 }
 
+// checkState keeps the writer's finalized and failed-state checks consistent
+// across operations. State checks must run before validating any input.
+func (w *Writer) checkState(action string) error {
+       if w.done {
+               return fmt.Errorf("puffin: cannot %s: writer already 
finalized", action)
+       }
+       if w.failed != nil {
+               return fmt.Errorf("puffin: cannot %s: writer previously failed: 
%w", action, w.failed)
+       }
+
+       return nil
+}
+
 // SetProperties merges the provided properties into the file-level properties
 // written to the footer. Repeated calls overwrite matching keys; use
 // ClearProperties to remove previously set custom properties.
 func (w *Writer) SetProperties(props map[string]string) error {
-       if w.done {
-               return errors.New("puffin: cannot set properties: writer 
already finalized")
-       }
-       if w.failed != nil {
-               return fmt.Errorf("puffin: cannot set properties: writer 
previously failed: %w", w.failed)
+       if err := w.checkState("set properties"); err != nil {
+               return err
        }
        maps.Copy(w.props, props)
 
@@ -107,11 +117,8 @@ func (w *Writer) SetProperties(props map[string]string) 
error {
 
 // ClearProperties removes all file-level properties.
 func (w *Writer) ClearProperties() error {
-       if w.done {
-               return errors.New("puffin: cannot clear properties: writer 
already finalized")
-       }
-       if w.failed != nil {
-               return fmt.Errorf("puffin: cannot clear properties: writer 
previously failed: %w", w.failed)
+       if err := w.checkState("clear properties"); err != nil {
+               return err
        }
        w.props = make(map[string]string)
 
@@ -121,11 +128,8 @@ func (w *Writer) ClearProperties() error {
 // SetCreatedBy overrides the default "created-by" property written to the 
footer.
 // The default value is "iceberg-go". Example: "MyApp version 1.2.3".
 func (w *Writer) SetCreatedBy(createdBy string) error {
-       if w.done {
-               return errors.New("puffin: cannot set created-by: writer 
already finalized")
-       }
-       if w.failed != nil {
-               return fmt.Errorf("puffin: cannot set created-by: writer 
previously failed: %w", w.failed)
+       if err := w.checkState("set created-by"); err != nil {
+               return err
        }
        if createdBy == "" {
                return errors.New("puffin: cannot set created-by: value cannot 
be empty")
@@ -139,11 +143,8 @@ func (w *Writer) SetCreatedBy(createdBy string) error {
 // Returns the complete BlobMetadata including the computed Offset and Length.
 // The input.Type is required; use constants like ApacheDataSketchesThetaV1.
 func (w *Writer) AddBlob(input BlobMetadataInput, data []byte) (BlobMetadata, 
error) {
-       if w.done {
-               return BlobMetadata{}, errors.New("puffin: cannot add blob: 
writer already finalized")
-       }
-       if w.failed != nil {
-               return BlobMetadata{}, fmt.Errorf("puffin: cannot add blob: 
writer previously failed: %w", w.failed)
+       if err := w.checkState("add blob"); err != nil {
+               return BlobMetadata{}, err
        }
        if input.Type == "" {
                return BlobMetadata{}, errors.New("puffin: cannot add blob: 
type is required")
@@ -216,11 +217,8 @@ func (w *Writer) AddBlob(input BlobMetadataInput, data 
[]byte) (BlobMetadata, er
 // Must be called exactly once after all blobs are written.
 // After Finish returns, no further operations are allowed on the writer.
 func (w *Writer) Finish() error {
-       if w.done {
-               return errors.New("puffin: cannot finish: writer already 
finalized")
-       }
-       if w.failed != nil {
-               return fmt.Errorf("puffin: cannot finish: writer previously 
failed: %w", w.failed)
+       if err := w.checkState("finish"); err != nil {
+               return err
        }
 
        // Build footer
diff --git a/puffin/puffin_writer_state_test.go 
b/puffin/puffin_writer_state_test.go
new file mode 100644
index 000000000..8e66695d5
--- /dev/null
+++ b/puffin/puffin_writer_state_test.go
@@ -0,0 +1,84 @@
+// 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 puffin
+
+import (
+       "errors"
+       "io"
+       "testing"
+)
+
+func TestWriterStateGuardErrors(t *testing.T) {
+       ops := []struct {
+               name   string
+               action string
+               call   func(*Writer) error
+       }{
+               {"SetProperties", "set properties", func(w *Writer) error {
+                       return w.SetProperties(map[string]string{"key": 
"value"})
+               }},
+               {"ClearProperties", "clear properties", func(w *Writer) error {
+                       return w.ClearProperties()
+               }},
+               {"SetCreatedBy", "set created-by", func(w *Writer) error {
+                       return w.SetCreatedBy("")
+               }},
+               {"AddBlob", "add blob", func(w *Writer) error {
+                       _, err := w.AddBlob(BlobMetadataInput{}, nil)
+
+                       return err
+               }},
+               {"Finish", "finish", func(w *Writer) error {
+                       return w.Finish()
+               }},
+       }
+
+       for _, op := range ops {
+               t.Run(op.name, func(t *testing.T) {
+                       finalized, err := NewWriter(io.Discard)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       if err := finalized.Finish(); err != nil {
+                               t.Fatal(err)
+                       }
+
+                       wantFinalized := "puffin: cannot " + op.action + ": 
writer already finalized"
+                       if got := op.call(finalized); got == nil || got.Error() 
!= wantFinalized {
+                               t.Fatalf("finalized: got %v, want %q", got, 
wantFinalized)
+                       }
+
+                       failed, err := NewWriter(io.Discard)
+                       if err != nil {
+                               t.Fatal(err)
+                       }
+                       underlying := errors.New("write failed")
+                       failed.failed = underlying
+                       wantFailed := "puffin: cannot " + op.action + ": writer 
previously failed: write failed"
+                       if got := op.call(failed); got == nil || got.Error() != 
wantFailed || !errors.Is(got, underlying) {
+                               t.Fatalf("failed: got %v, want error wrapping 
%q", got, wantFailed)
+                       }
+
+                       // When both flags are set, finalization still takes 
precedence.
+                       failed.done = true
+                       if got := op.call(failed); got == nil || got.Error() != 
wantFinalized {
+                               t.Fatalf("finalized and failed: got %v, want 
%q", got, wantFinalized)
+                       }
+               })
+       }
+}

Reply via email to