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/paimon-rust.git


The following commit(s) were added to refs/heads/main by this push:
     new 4889e601 feat(go): add postpone fixed-bucket write bindings (#722)
4889e601 is described below

commit 4889e601924d0523fd33e699aaef9c31cec177d9
Author: XiaoHongbo <[email protected]>
AuthorDate: Thu Aug 20 10:54:48 2026 +0800

    feat(go): add postpone fixed-bucket write bindings (#722)
---
 bindings/go/postpone_fixed_bucket_write.go     | 335 +++++++++++++++++++++
 bindings/go/postpone_fixed_bucket_write_ffi.go | 394 +++++++++++++++++++++++++
 bindings/go/tests/paimon_test.go               | 155 ++++++++++
 bindings/go/types.go                           |  63 ++++
 dev/spark/provision.py                         |  15 +
 docs/src/go-binding.md                         |  57 ++++
 6 files changed, 1019 insertions(+)

diff --git a/bindings/go/postpone_fixed_bucket_write.go 
b/bindings/go/postpone_fixed_bucket_write.go
new file mode 100644
index 00000000..5b036d85
--- /dev/null
+++ b/bindings/go/postpone_fixed_bucket_write.go
@@ -0,0 +1,335 @@
+/*
+ * 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 paimon
+
+import (
+       "context"
+       "errors"
+       "sync"
+       "unsafe"
+
+       "github.com/apache/arrow-go/v18/arrow"
+)
+
+// PostponeFixedBucketWriteBuilder creates fixed-bucket writers for bucket=-2
+// tables. A resolved bucket plan must be supplied before NewWrite.
+type PostponeFixedBucketWriteBuilder struct {
+       ctx       context.Context
+       lib       *libRef
+       inner     *paimonPostponeFixedBucketWriteBuilder
+       closeOnce sync.Once
+}
+
+// NewPostponeFixedBucketWriteBuilder creates an explicitly selected
+// fixed-bucket builder for a postpone table.
+func (t *Table) NewPostponeFixedBucketWriteBuilder() 
(*PostponeFixedBucketWriteBuilder, error) {
+       if t.inner == nil {
+               return nil, ErrClosed
+       }
+       inner, err := 
ffiTableNewPostponeFixedBucketWriteBuilder.symbol(t.ctx)(t.inner)
+       if err != nil {
+               return nil, err
+       }
+       t.lib.acquire()
+       return &PostponeFixedBucketWriteBuilder{ctx: t.ctx, lib: t.lib, inner: 
inner}, nil
+}
+
+// NewPostponeFixedBucketWriteBuilderWithCommitUser creates a fixed-bucket
+// builder with a stable commit identity.
+func (t *Table) NewPostponeFixedBucketWriteBuilderWithCommitUser(
+       commitUser string,
+) (*PostponeFixedBucketWriteBuilder, error) {
+       if t.inner == nil {
+               return nil, ErrClosed
+       }
+       inner, err := 
ffiTableNewPostponeFixedBucketWriteBuilderWithCommitUser.symbol(t.ctx)(
+               t.inner,
+               commitUser,
+       )
+       if err != nil {
+               return nil, err
+       }
+       t.lib.acquire()
+       return &PostponeFixedBucketWriteBuilder{ctx: t.ctx, lib: t.lib, inner: 
inner}, nil
+}
+
+// Close releases the builder resources. Safe to call multiple times.
+func (wb *PostponeFixedBucketWriteBuilder) Close() {
+       wb.closeOnce.Do(func() {
+               ffiPostponeFixedBucketWriteBuilderFree.symbol(wb.ctx)(wb.inner)
+               wb.inner = nil
+               wb.lib.release()
+       })
+}
+
+// WithOverwrite enables overwrite mode for both writers and committers created
+// by this builder.
+func (wb *PostponeFixedBucketWriteBuilder) WithOverwrite() error {
+       if wb.inner == nil {
+               return ErrClosed
+       }
+       return 
ffiPostponeFixedBucketWriteBuilderWithOverwrite.symbol(wb.ctx)(wb.inner)
+}
+
+// WithBucketPlan sets a resolved partition-to-bucket-count plan. The plan must
+// contain the table partition columns followed by a non-null Int32
+// total_buckets column. The caller retains ownership of plan.
+func (wb *PostponeFixedBucketWriteBuilder) WithBucketPlan(plan arrow.Record) 
error {
+       if wb.inner == nil {
+               return ErrClosed
+       }
+       return withOwnedArrowRecord(
+               plan,
+               "paimon: bucket plan must not be nil",
+               func(array, schema unsafe.Pointer) error {
+                       return 
ffiPostponeFixedBucketWriteBuilderWithBucketPlan.symbol(wb.ctx)(
+                               wb.inner,
+                               array,
+                               schema,
+                       )
+               },
+       )
+}
+
+// NewWrite creates a fixed-bucket writer. WithBucketPlan must be called first.
+func (wb *PostponeFixedBucketWriteBuilder) NewWrite() 
(*PostponeFixedBucketTableWrite, error) {
+       if wb.inner == nil {
+               return nil, ErrClosed
+       }
+       inner, err := 
ffiPostponeFixedBucketWriteBuilderNewWrite.symbol(wb.ctx)(wb.inner)
+       if err != nil {
+               return nil, err
+       }
+       wb.lib.acquire()
+       return &PostponeFixedBucketTableWrite{ctx: wb.ctx, lib: wb.lib, inner: 
inner}, nil
+}
+
+// NewCommit creates a committer using this builder's commit identity and mode.
+func (wb *PostponeFixedBucketWriteBuilder) NewCommit() 
(*PostponeFixedBucketTableCommit, error) {
+       if wb.inner == nil {
+               return nil, ErrClosed
+       }
+       inner, err := 
ffiPostponeFixedBucketWriteBuilderNewCommit.symbol(wb.ctx)(wb.inner)
+       if err != nil {
+               return nil, err
+       }
+       wb.lib.acquire()
+       return &PostponeFixedBucketTableCommit{ctx: wb.ctx, lib: wb.lib, inner: 
inner}, nil
+}
+
+// PostponeFixedBucketTableWrite writes rows according to a resolved bucket 
plan.
+type PostponeFixedBucketTableWrite struct {
+       ctx       context.Context
+       lib       *libRef
+       inner     *paimonPostponeFixedBucketTableWrite
+       closeOnce sync.Once
+}
+
+// Close releases the writer resources. Safe to call multiple times.
+func (tw *PostponeFixedBucketTableWrite) Close() {
+       tw.closeOnce.Do(func() {
+               ffiPostponeFixedBucketTableWriteFree.symbol(tw.ctx)(tw.inner)
+               tw.inner = nil
+               tw.lib.release()
+       })
+}
+
+// WriteArrowBatch writes one Arrow record batch. The caller retains ownership.
+func (tw *PostponeFixedBucketTableWrite) WriteArrowBatch(record arrow.Record) 
error {
+       if tw.inner == nil {
+               return ErrClosed
+       }
+       return withOwnedArrowRecord(
+               record,
+               "paimon: record batch must not be nil",
+               func(array, schema unsafe.Pointer) error {
+                       return 
ffiPostponeFixedBucketTableWriteWriteArrowBatch.symbol(tw.ctx)(
+                               tw.inner,
+                               array,
+                               schema,
+                       )
+               },
+       )
+}
+
+// PrepareCommit finalizes pending writes and returns fixed-bucket messages.
+// It consumes the writer; subsequent operations return ErrClosed.
+func (tw *PostponeFixedBucketTableWrite) PrepareCommit() 
(*PostponeFixedBucketCommitMessages, error) {
+       if tw.inner == nil {
+               return nil, ErrClosed
+       }
+       inner, err := 
ffiPostponeFixedBucketTableWritePrepareCommit.symbol(tw.ctx)(tw.inner)
+       if err != nil {
+               tw.Close()
+               return nil, err
+       }
+       tw.lib.acquire()
+       tw.Close()
+       return &PostponeFixedBucketCommitMessages{ctx: tw.ctx, lib: tw.lib, 
inner: inner}, nil
+}
+
+// PostponeFixedBucketCommitMessages contains files produced by fixed-bucket
+// writers. It is a process-local native handle and cannot be transferred
+// between processes or passed to a standard TableCommit.
+type PostponeFixedBucketCommitMessages struct {
+       ctx       context.Context
+       lib       *libRef
+       inner     *paimonPostponeFixedBucketCommitMessages
+       closeOnce sync.Once
+}
+
+// Close releases the messages. Safe to call multiple times.
+func (m *PostponeFixedBucketCommitMessages) Close() {
+       m.closeOnce.Do(func() {
+               ffiPostponeFixedBucketCommitMessagesFree.symbol(m.ctx)(m.inner)
+               m.inner = nil
+               m.lib.release()
+       })
+}
+
+// Merge appends a copy of source's messages. Both handles must belong to the
+// same process, and both builders must use the same table, commit user, and
+// overwrite mode.
+func (m *PostponeFixedBucketCommitMessages) Merge(
+       source *PostponeFixedBucketCommitMessages,
+) error {
+       if m.inner == nil {
+               return ErrClosed
+       }
+       if source == nil {
+               return errors.New("paimon: source messages must not be nil")
+       }
+       if source.inner == nil {
+               return ErrClosed
+       }
+       return ffiPostponeFixedBucketCommitMessagesMerge.symbol(m.ctx)(m.inner, 
source.inner)
+}
+
+// PostponeFixedBucketTableCommit commits fixed-bucket messages using the mode
+// selected on its builder.
+type PostponeFixedBucketTableCommit struct {
+       ctx       context.Context
+       lib       *libRef
+       inner     *paimonPostponeFixedBucketTableCommit
+       closeOnce sync.Once
+}
+
+// Close releases the committer resources. Safe to call multiple times.
+func (tc *PostponeFixedBucketTableCommit) Close() {
+       tc.closeOnce.Do(func() {
+               ffiPostponeFixedBucketTableCommitFree.symbol(tc.ctx)(tc.inner)
+               tc.inner = nil
+               tc.lib.release()
+       })
+}
+
+func (tc *PostponeFixedBucketTableCommit) withMessages(
+       messages *PostponeFixedBucketCommitMessages,
+       operation func(
+               *paimonPostponeFixedBucketTableCommit,
+               *paimonPostponeFixedBucketCommitMessages,
+       ) error,
+) error {
+       if tc.inner == nil {
+               return ErrClosed
+       }
+       if messages == nil || messages.inner == nil {
+               return ErrClosed
+       }
+       return operation(tc.inner, messages.inner)
+}
+
+func (tc *PostponeFixedBucketTableCommit) withMessagesAndIdentifier(
+       messages *PostponeFixedBucketCommitMessages,
+       commitIdentifier int64,
+       operation func(
+               *paimonPostponeFixedBucketTableCommit,
+               *paimonPostponeFixedBucketCommitMessages,
+               int64,
+       ) error,
+) error {
+       if tc.inner == nil {
+               return ErrClosed
+       }
+       if messages == nil || messages.inner == nil {
+               return ErrClosed
+       }
+       return operation(tc.inner, messages.inner, commitIdentifier)
+}
+
+// Commit persists fixed-bucket messages using the builder's append or 
overwrite
+// mode.
+func (tc *PostponeFixedBucketTableCommit) Commit(
+       messages *PostponeFixedBucketCommitMessages,
+) error {
+       return tc.withMessages(messages, 
ffiPostponeFixedBucketTableCommitCommit.symbol(tc.ctx))
+}
+
+// CommitWithIdentifier commits with a stable identifier.
+func (tc *PostponeFixedBucketTableCommit) CommitWithIdentifier(
+       messages *PostponeFixedBucketCommitMessages,
+       commitIdentifier int64,
+) error {
+       return tc.withMessagesAndIdentifier(
+               messages,
+               commitIdentifier,
+               
ffiPostponeFixedBucketTableCommitCommitWithIdentifier.symbol(tc.ctx),
+       )
+}
+
+// FilterAndCommitWithIdentifier makes a retry idempotent.
+func (tc *PostponeFixedBucketTableCommit) FilterAndCommitWithIdentifier(
+       messages *PostponeFixedBucketCommitMessages,
+       commitIdentifier int64,
+) error {
+       return tc.withMessagesAndIdentifier(
+               messages,
+               commitIdentifier,
+               
ffiPostponeFixedBucketTableCommitFilterAndCommitWithIdentifier.symbol(tc.ctx),
+       )
+}
+
+// TruncateTable removes all table data.
+func (tc *PostponeFixedBucketTableCommit) TruncateTable() error {
+       if tc.inner == nil {
+               return ErrClosed
+       }
+       return 
ffiPostponeFixedBucketTableCommitTruncateTable.symbol(tc.ctx)(tc.inner)
+}
+
+// TruncateTableWithIdentifier removes all table data with a stable identifier.
+func (tc *PostponeFixedBucketTableCommit) TruncateTableWithIdentifier(
+       commitIdentifier int64,
+) error {
+       if tc.inner == nil {
+               return ErrClosed
+       }
+       return 
ffiPostponeFixedBucketTableCommitTruncateTableWithIdentifier.symbol(tc.ctx)(
+               tc.inner,
+               commitIdentifier,
+       )
+}
+
+// Abort performs best-effort cleanup of files in prepared messages.
+func (tc *PostponeFixedBucketTableCommit) Abort(
+       messages *PostponeFixedBucketCommitMessages,
+) error {
+       return tc.withMessages(messages, 
ffiPostponeFixedBucketTableCommitAbort.symbol(tc.ctx))
+}
diff --git a/bindings/go/postpone_fixed_bucket_write_ffi.go 
b/bindings/go/postpone_fixed_bucket_write_ffi.go
new file mode 100644
index 00000000..fff3bf0a
--- /dev/null
+++ b/bindings/go/postpone_fixed_bucket_write_ffi.go
@@ -0,0 +1,394 @@
+/*
+ * 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 paimon
+
+import (
+       "context"
+       "unsafe"
+
+       "github.com/jupiterrider/ffi"
+)
+
+var ffiTableNewPostponeFixedBucketWriteBuilder = newFFI(ffiOpts{
+       sym:    "paimon_table_new_postpone_fixed_bucket_write_builder",
+       rType:  &typeResultPostponeFixedBucketWriteBuilder,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonTable) (*paimonPostponeFixedBucketWriteBuilder, error) {
+       return func(table *paimonTable) 
(*paimonPostponeFixedBucketWriteBuilder, error) {
+               var result resultPostponeFixedBucketWriteBuilder
+               ffiCall(unsafe.Pointer(&result), unsafe.Pointer(&table))
+               if result.error != nil {
+                       return nil, parseError(ctx, result.error)
+               }
+               return result.writeBuilder, nil
+       }
+})
+
+var ffiTableNewPostponeFixedBucketWriteBuilderWithCommitUser = newFFI(ffiOpts{
+       sym:    
"paimon_table_new_postpone_fixed_bucket_write_builder_with_commit_user",
+       rType:  &typeResultPostponeFixedBucketWriteBuilder,
+       aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonTable, string) (*paimonPostponeFixedBucketWriteBuilder, error) {
+       return func(
+               table *paimonTable,
+               commitUser string,
+       ) (*paimonPostponeFixedBucketWriteBuilder, error) {
+               commitUserPtr, err := bytePtrFromString(commitUser)
+               if err != nil {
+                       return nil, err
+               }
+               var result resultPostponeFixedBucketWriteBuilder
+               ffiCall(
+                       unsafe.Pointer(&result),
+                       unsafe.Pointer(&table),
+                       unsafe.Pointer(&commitUserPtr),
+               )
+               if result.error != nil {
+                       return nil, parseError(ctx, result.error)
+               }
+               return result.writeBuilder, nil
+       }
+})
+
+var ffiPostponeFixedBucketWriteBuilderFree = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_write_builder_free",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       _ context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketWriteBuilder) {
+       return func(builder *paimonPostponeFixedBucketWriteBuilder) {
+               ffiCall(nil, unsafe.Pointer(&builder))
+       }
+})
+
+var ffiPostponeFixedBucketWriteBuilderWithOverwrite = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_write_builder_with_overwrite",
+       rType:  &ffi.TypePointer,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketWriteBuilder) error {
+       return func(builder *paimonPostponeFixedBucketWriteBuilder) error {
+               var ffiError *paimonError
+               ffiCall(unsafe.Pointer(&ffiError), unsafe.Pointer(&builder))
+               return parseError(ctx, ffiError)
+       }
+})
+
+var ffiPostponeFixedBucketWriteBuilderWithBucketPlan = newFFI(ffiOpts{
+       sym:   "paimon_postpone_fixed_bucket_write_builder_with_bucket_plan",
+       rType: &ffi.TypePointer,
+       aTypes: []*ffi.Type{
+               &ffi.TypePointer,
+               &ffi.TypePointer,
+               &ffi.TypePointer,
+       },
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketWriteBuilder, unsafe.Pointer, unsafe.Pointer) 
error {
+       return func(
+               builder *paimonPostponeFixedBucketWriteBuilder,
+               array unsafe.Pointer,
+               schema unsafe.Pointer,
+       ) error {
+               var ffiError *paimonError
+               ffiCall(
+                       unsafe.Pointer(&ffiError),
+                       unsafe.Pointer(&builder),
+                       unsafe.Pointer(&array),
+                       unsafe.Pointer(&schema),
+               )
+               return parseError(ctx, ffiError)
+       }
+})
+
+var ffiPostponeFixedBucketWriteBuilderNewWrite = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_write_builder_new_write",
+       rType:  &typeResultPostponeFixedBucketTableWrite,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketWriteBuilder) 
(*paimonPostponeFixedBucketTableWrite, error) {
+       return func(
+               builder *paimonPostponeFixedBucketWriteBuilder,
+       ) (*paimonPostponeFixedBucketTableWrite, error) {
+               var result resultPostponeFixedBucketTableWrite
+               ffiCall(unsafe.Pointer(&result), unsafe.Pointer(&builder))
+               if result.error != nil {
+                       return nil, parseError(ctx, result.error)
+               }
+               return result.write, nil
+       }
+})
+
+var ffiPostponeFixedBucketWriteBuilderNewCommit = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_write_builder_new_commit",
+       rType:  &typeResultPostponeFixedBucketTableCommit,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketWriteBuilder) 
(*paimonPostponeFixedBucketTableCommit, error) {
+       return func(
+               builder *paimonPostponeFixedBucketWriteBuilder,
+       ) (*paimonPostponeFixedBucketTableCommit, error) {
+               var result resultPostponeFixedBucketTableCommit
+               ffiCall(unsafe.Pointer(&result), unsafe.Pointer(&builder))
+               if result.error != nil {
+                       return nil, parseError(ctx, result.error)
+               }
+               return result.commit, nil
+       }
+})
+
+var ffiPostponeFixedBucketTableWriteFree = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_table_write_free",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       _ context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketTableWrite) {
+       return func(write *paimonPostponeFixedBucketTableWrite) {
+               ffiCall(nil, unsafe.Pointer(&write))
+       }
+})
+
+var ffiPostponeFixedBucketTableWriteWriteArrowBatch = newFFI(ffiOpts{
+       sym:   "paimon_postpone_fixed_bucket_table_write_write_arrow_batch",
+       rType: &ffi.TypePointer,
+       aTypes: []*ffi.Type{
+               &ffi.TypePointer,
+               &ffi.TypePointer,
+               &ffi.TypePointer,
+       },
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketTableWrite, unsafe.Pointer, unsafe.Pointer) 
error {
+       return func(
+               write *paimonPostponeFixedBucketTableWrite,
+               array unsafe.Pointer,
+               schema unsafe.Pointer,
+       ) error {
+               var ffiError *paimonError
+               ffiCall(
+                       unsafe.Pointer(&ffiError),
+                       unsafe.Pointer(&write),
+                       unsafe.Pointer(&array),
+                       unsafe.Pointer(&schema),
+               )
+               return parseError(ctx, ffiError)
+       }
+})
+
+var ffiPostponeFixedBucketTableWritePrepareCommit = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_table_write_prepare_commit",
+       rType:  &typeResultPostponeFixedBucketPrepareCommit,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketTableWrite) 
(*paimonPostponeFixedBucketCommitMessages, error) {
+       return func(
+               write *paimonPostponeFixedBucketTableWrite,
+       ) (*paimonPostponeFixedBucketCommitMessages, error) {
+               var result resultPostponeFixedBucketPrepareCommit
+               ffiCall(unsafe.Pointer(&result), unsafe.Pointer(&write))
+               if result.error != nil {
+                       return nil, parseError(ctx, result.error)
+               }
+               return result.messages, nil
+       }
+})
+
+var ffiPostponeFixedBucketTableCommitFree = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_table_commit_free",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       _ context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketTableCommit) {
+       return func(commit *paimonPostponeFixedBucketTableCommit) {
+               ffiCall(nil, unsafe.Pointer(&commit))
+       }
+})
+
+var ffiPostponeFixedBucketCommitMessagesFree = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_commit_messages_free",
+       rType:  &ffi.TypeVoid,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       _ context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketCommitMessages) {
+       return func(messages *paimonPostponeFixedBucketCommitMessages) {
+               ffiCall(nil, unsafe.Pointer(&messages))
+       }
+})
+
+var ffiPostponeFixedBucketCommitMessagesMerge = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_commit_messages_merge",
+       rType:  &ffi.TypePointer,
+       aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketCommitMessages, 
*paimonPostponeFixedBucketCommitMessages) error {
+       return func(
+               target *paimonPostponeFixedBucketCommitMessages,
+               source *paimonPostponeFixedBucketCommitMessages,
+       ) error {
+               var ffiError *paimonError
+               ffiCall(
+                       unsafe.Pointer(&ffiError),
+                       unsafe.Pointer(&target),
+                       unsafe.Pointer(&source),
+               )
+               return parseError(ctx, ffiError)
+       }
+})
+
+var ffiPostponeFixedBucketTableCommitCommit = newFixedCommitMessagesFFI(
+       "paimon_postpone_fixed_bucket_table_commit_commit",
+)
+var ffiPostponeFixedBucketTableCommitCommitWithIdentifier = 
newFixedCommitMessagesIdentifierFFI(
+       "paimon_postpone_fixed_bucket_table_commit_commit_with_identifier",
+)
+var ffiPostponeFixedBucketTableCommitFilterAndCommitWithIdentifier = 
newFixedCommitMessagesIdentifierFFI(
+       
"paimon_postpone_fixed_bucket_table_commit_filter_and_commit_with_identifier",
+)
+var ffiPostponeFixedBucketTableCommitAbort = newFixedCommitMessagesFFI(
+       "paimon_postpone_fixed_bucket_table_commit_abort",
+)
+
+func newFixedCommitMessagesFFI(
+       symbol contextKey,
+) *FFI[func(
+       *paimonPostponeFixedBucketTableCommit,
+       *paimonPostponeFixedBucketCommitMessages,
+) error] {
+       return newFFI(ffiOpts{
+               sym:    symbol,
+               rType:  &ffi.TypePointer,
+               aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+       }, func(
+               ctx context.Context,
+               ffiCall ffiCall,
+       ) func(
+               *paimonPostponeFixedBucketTableCommit,
+               *paimonPostponeFixedBucketCommitMessages,
+       ) error {
+               return func(
+                       commit *paimonPostponeFixedBucketTableCommit,
+                       messages *paimonPostponeFixedBucketCommitMessages,
+               ) error {
+                       var ffiError *paimonError
+                       ffiCall(
+                               unsafe.Pointer(&ffiError),
+                               unsafe.Pointer(&commit),
+                               unsafe.Pointer(&messages),
+                       )
+                       return parseError(ctx, ffiError)
+               }
+       })
+}
+
+func newFixedCommitMessagesIdentifierFFI(
+       symbol contextKey,
+) *FFI[func(
+       *paimonPostponeFixedBucketTableCommit,
+       *paimonPostponeFixedBucketCommitMessages,
+       int64,
+) error] {
+       return newFFI(ffiOpts{
+               sym:    symbol,
+               rType:  &ffi.TypePointer,
+               aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer, 
&ffi.TypeSint64},
+       }, func(
+               ctx context.Context,
+               ffiCall ffiCall,
+       ) func(
+               *paimonPostponeFixedBucketTableCommit,
+               *paimonPostponeFixedBucketCommitMessages,
+               int64,
+       ) error {
+               return func(
+                       commit *paimonPostponeFixedBucketTableCommit,
+                       messages *paimonPostponeFixedBucketCommitMessages,
+                       identifier int64,
+               ) error {
+                       var ffiError *paimonError
+                       ffiCall(
+                               unsafe.Pointer(&ffiError),
+                               unsafe.Pointer(&commit),
+                               unsafe.Pointer(&messages),
+                               unsafe.Pointer(&identifier),
+                       )
+                       return parseError(ctx, ffiError)
+               }
+       })
+}
+
+var ffiPostponeFixedBucketTableCommitTruncateTable = newFFI(ffiOpts{
+       sym:    "paimon_postpone_fixed_bucket_table_commit_truncate_table",
+       rType:  &ffi.TypePointer,
+       aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketTableCommit) error {
+       return func(commit *paimonPostponeFixedBucketTableCommit) error {
+               var ffiError *paimonError
+               ffiCall(unsafe.Pointer(&ffiError), unsafe.Pointer(&commit))
+               return parseError(ctx, ffiError)
+       }
+})
+
+var ffiPostponeFixedBucketTableCommitTruncateTableWithIdentifier = 
newFFI(ffiOpts{
+       sym:    
"paimon_postpone_fixed_bucket_table_commit_truncate_table_with_identifier",
+       rType:  &ffi.TypePointer,
+       aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypeSint64},
+}, func(
+       ctx context.Context,
+       ffiCall ffiCall,
+) func(*paimonPostponeFixedBucketTableCommit, int64) error {
+       return func(commit *paimonPostponeFixedBucketTableCommit, identifier 
int64) error {
+               var ffiError *paimonError
+               ffiCall(
+                       unsafe.Pointer(&ffiError),
+                       unsafe.Pointer(&commit),
+                       unsafe.Pointer(&identifier),
+               )
+               return parseError(ctx, ffiError)
+       }
+})
diff --git a/bindings/go/tests/paimon_test.go b/bindings/go/tests/paimon_test.go
index ed5054ce..621292a6 100644
--- a/bindings/go/tests/paimon_test.go
+++ b/bindings/go/tests/paimon_test.go
@@ -24,6 +24,7 @@ import (
        "io"
        "os"
        "path/filepath"
+       "reflect"
        "sort"
        "strings"
        "testing"
@@ -39,6 +40,12 @@ type row struct {
        name string
 }
 
+type partitionedRow struct {
+       id   int32
+       name string
+       dt   string
+}
+
 func testWarehouse() string {
        warehouse := os.Getenv("PAIMON_TEST_WAREHOUSE")
        if warehouse == "" {
@@ -157,6 +164,40 @@ func makeRecord(t *testing.T, rows []row) arrow.Record {
        return builder.NewRecord()
 }
 
+func makePartitionedRecord(t *testing.T, value partitionedRow) arrow.Record {
+       t.Helper()
+
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "id", Type: arrow.PrimitiveTypes.Int32, Nullable: false},
+               {Name: "name", Type: arrow.BinaryTypes.String, Nullable: true},
+               {Name: "dt", Type: arrow.BinaryTypes.String, Nullable: true},
+       }, nil)
+       builder := array.NewRecordBuilder(memory.DefaultAllocator, schema)
+       defer builder.Release()
+       builder.Field(0).(*array.Int32Builder).Append(value.id)
+       builder.Field(1).(*array.StringBuilder).Append(value.name)
+       builder.Field(2).(*array.StringBuilder).Append(value.dt)
+       return builder.NewRecord()
+}
+
+func makePartitionedBucketPlan(t *testing.T, partitions []string, totalBuckets 
int32) arrow.Record {
+       t.Helper()
+
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "dt", Type: arrow.BinaryTypes.String, Nullable: true},
+               {Name: "total_buckets", Type: arrow.PrimitiveTypes.Int32, 
Nullable: false},
+       }, nil)
+       builder := array.NewRecordBuilder(memory.DefaultAllocator, schema)
+       defer builder.Release()
+       partitionBuilder := builder.Field(0).(*array.StringBuilder)
+       countBuilder := builder.Field(1).(*array.Int32Builder)
+       for _, partition := range partitions {
+               partitionBuilder.Append(partition)
+               countBuilder.Append(totalBuckets)
+       }
+       return builder.NewRecord()
+}
+
 func readTableRows(t *testing.T, table *paimon.Table) []row {
        t.Helper()
        rb, err := table.NewReadBuilder()
@@ -549,6 +590,120 @@ func TestAppendOnlyWriteMergeAndIdempotentCommit(t 
*testing.T) {
        }
 }
 
+// TestPostponeFixedBucketTypesAreIsolated pins the compile-time separation
+// between the standard and postpone fixed-bucket handles: neither commit
+// method accepts the other's messages, so the two paths cannot be mixed.
+func TestPostponeFixedBucketTypesAreIsolated(t *testing.T) {
+       standard, ok := 
reflect.TypeOf(&paimon.TableCommit{}).MethodByName("Commit")
+       if !ok {
+               t.Fatal("TableCommit.Commit not found")
+       }
+       fixed, ok := 
reflect.TypeOf(&paimon.PostponeFixedBucketTableCommit{}).MethodByName("Commit")
+       if !ok {
+               t.Fatal("PostponeFixedBucketTableCommit.Commit not found")
+       }
+
+       standardMessages := standard.Type.In(1)
+       fixedMessages := fixed.Type.In(1)
+       if standardMessages == fixedMessages {
+               t.Fatalf("Commit message types must stay distinct, both are 
%s", standardMessages)
+       }
+       if standardMessages != reflect.TypeOf(&paimon.CommitMessages{}) {
+               t.Errorf("TableCommit.Commit takes %s", standardMessages)
+       }
+       if fixedMessages != 
reflect.TypeOf(&paimon.PostponeFixedBucketCommitMessages{}) {
+               t.Errorf("PostponeFixedBucketTableCommit.Commit takes %s", 
fixedMessages)
+       }
+}
+
+func TestMultiplePostponeFixedBucketWritersSharePlan(t *testing.T) {
+       table := openCopiedTable(t, "postpone_fixed_bucket_pk_table")
+       const commitUser = "go-postpone-fixed-bucket-write"
+
+       builders := make([]*paimon.PostponeFixedBucketWriteBuilder, 2)
+       for index := range builders {
+               builder, err := 
table.NewPostponeFixedBucketWriteBuilderWithCommitUser(commitUser)
+               if err != nil {
+                       t.Fatalf("Failed to create fixed-bucket builder %d: 
%v", index, err)
+               }
+               builders[index] = builder
+               defer builder.Close()
+       }
+
+       if _, err := builders[0].NewWrite(); err == nil || 
!strings.Contains(err.Error(), "bucket plan is required") {
+               t.Fatalf("Expected missing bucket plan error, got: %v", err)
+       }
+       plan := makePartitionedBucketPlan(t, []string{"2026-08-14", 
"2026-08-15"}, 1)
+       for index, builder := range builders {
+               if err := builder.WithBucketPlan(plan); err != nil {
+                       plan.Release()
+                       t.Fatalf("Failed to set shared bucket plan on builder 
%d: %v", index, err)
+               }
+       }
+       plan.Release()
+
+       writeAndPrepare := func(
+               builder *paimon.PostponeFixedBucketWriteBuilder,
+               value partitionedRow,
+       ) *paimon.PostponeFixedBucketCommitMessages {
+               write, err := builder.NewWrite()
+               if err != nil {
+                       t.Fatalf("Failed to create fixed-bucket writer: %v", 
err)
+               }
+               defer write.Close()
+
+               record := makePartitionedRecord(t, value)
+               if err := write.WriteArrowBatch(record); err != nil {
+                       record.Release()
+                       t.Fatalf("Failed to write Arrow record batch: %v", err)
+               }
+               record.Release()
+               messages, err := write.PrepareCommit()
+               if err != nil {
+                       t.Fatalf("Failed to prepare fixed-bucket commit: %v", 
err)
+               }
+               if err := write.WriteArrowBatch(nil); !errors.Is(err, 
paimon.ErrClosed) {
+                       t.Fatalf("Expected consumed writer to reject writes 
with ErrClosed, got: %v", err)
+               }
+               if _, err := write.PrepareCommit(); !errors.Is(err, 
paimon.ErrClosed) {
+                       t.Fatalf("Expected consumed writer to reject 
PrepareCommit with ErrClosed, got: %v", err)
+               }
+               return messages
+       }
+
+       messages1 := writeAndPrepare(builders[0], partitionedRow{4, "dave", 
"2026-08-14"})
+       defer messages1.Close()
+       messages2 := writeAndPrepare(builders[1], partitionedRow{5, "eve", 
"2026-08-15"})
+       defer messages2.Close()
+       if err := messages1.Merge(nil); err == nil || err.Error() != "paimon: 
source messages must not be nil" {
+               t.Fatalf("Expected a specific nil source error, got: %v", err)
+       }
+       if err := messages1.Merge(messages2); err != nil {
+               t.Fatalf("Failed to merge fixed-bucket commit messages: %v", 
err)
+       }
+
+       commit, err := builders[0].NewCommit()
+       if err != nil {
+               t.Fatalf("Failed to create fixed-bucket table commit: %v", err)
+       }
+       defer commit.Close()
+       if err := commit.Commit(messages1); err != nil {
+               t.Fatalf("Failed to commit fixed-bucket write: %v", err)
+       }
+
+       rows := readTableRows(t, table)
+       sort.Slice(rows, func(i, j int) bool { return rows[i].id < rows[j].id })
+       expected := []row{{4, "dave"}, {5, "eve"}}
+       if len(rows) != len(expected) {
+               t.Fatalf("Expected %d rows, got %d: %v", len(expected), 
len(rows), rows)
+       }
+       for index := range expected {
+               if rows[index] != expected[index] {
+                       t.Errorf("Row %d: expected %v, got %v", index, 
expected[index], rows[index])
+               }
+       }
+}
+
 // TestReadLogTable reads the test table and verifies the data matches 
expected values.
 //
 // The table was populated by Docker provisioning with:
diff --git a/bindings/go/types.go b/bindings/go/types.go
index a8cec54e..04fc57ae 100644
--- a/bindings/go/types.go
+++ b/bindings/go/types.go
@@ -167,6 +167,45 @@ var (
                }[0],
        }
 
+       // Postpone fixed-bucket write result types also contain an opaque 
pointer
+       // followed by *paimon_error. Keep dedicated descriptors so each Go 
result
+       // mirror is paired with the exact C result type returned by its symbol.
+       typeResultPostponeFixedBucketWriteBuilder = ffi.Type{
+               Type: ffi.Struct,
+               Elements: &[]*ffi.Type{
+                       &ffi.TypePointer,
+                       &ffi.TypePointer,
+                       nil,
+               }[0],
+       }
+
+       typeResultPostponeFixedBucketTableWrite = ffi.Type{
+               Type: ffi.Struct,
+               Elements: &[]*ffi.Type{
+                       &ffi.TypePointer,
+                       &ffi.TypePointer,
+                       nil,
+               }[0],
+       }
+
+       typeResultPostponeFixedBucketTableCommit = ffi.Type{
+               Type: ffi.Struct,
+               Elements: &[]*ffi.Type{
+                       &ffi.TypePointer,
+                       &ffi.TypePointer,
+                       nil,
+               }[0],
+       }
+
+       typeResultPostponeFixedBucketPrepareCommit = ffi.Type{
+               Type: ffi.Struct,
+               Elements: &[]*ffi.Type{
+                       &ffi.TypePointer,
+                       &ffi.TypePointer,
+                       nil,
+               }[0],
+       }
+
        // paimon_datum { tag: i32, int_val: i64, double_val: f64, str_data: 
*u8, str_len: usize,
        //                int_val2: i64, uint_val: u32, uint_val2: u32 }
        typePaimonDatum = ffi.Type{
@@ -223,6 +262,10 @@ type paimonWriteBuilder struct{}
 type paimonTableWrite struct{}
 type paimonTableCommit struct{}
 type paimonCommitMessages struct{}
+type paimonPostponeFixedBucketWriteBuilder struct{}
+type paimonPostponeFixedBucketTableWrite struct{}
+type paimonPostponeFixedBucketTableCommit struct{}
+type paimonPostponeFixedBucketCommitMessages struct{}
 
 // Result types matching the C repr structs
 type resultCatalogNew struct {
@@ -290,6 +333,26 @@ type resultPrepareCommit struct {
        error    *paimonError
 }
 
+type resultPostponeFixedBucketWriteBuilder struct {
+       writeBuilder *paimonPostponeFixedBucketWriteBuilder
+       error        *paimonError
+}
+
+type resultPostponeFixedBucketTableWrite struct {
+       write *paimonPostponeFixedBucketTableWrite
+       error *paimonError
+}
+
+type resultPostponeFixedBucketTableCommit struct {
+       commit *paimonPostponeFixedBucketTableCommit
+       error  *paimonError
+}
+
+type resultPostponeFixedBucketPrepareCommit struct {
+       messages *paimonPostponeFixedBucketCommitMessages
+       error    *paimonError
+}
+
 // paimonDatumC mirrors the C paimon_datum struct.
 type paimonDatumC struct {
        tag      int32
diff --git a/dev/spark/provision.py b/dev/spark/provision.py
index 2ce1c724..0b209d0e 100644
--- a/dev/spark/provision.py
+++ b/dev/spark/provision.py
@@ -989,6 +989,21 @@ def main():
         """
     )
 
+    # Empty postpone table for Go fixed-bucket write tests.
+    spark.sql(
+        """
+        CREATE TABLE IF NOT EXISTS postpone_fixed_bucket_pk_table (
+            id INT,
+            name STRING,
+            dt STRING
+        ) USING paimon
+        PARTITIONED BY (dt)
+        TBLPROPERTIES (
+            'primary-key' = 'id,dt',
+            'bucket' = '-2'
+        )
+        """
+    )
 
     # ===== Dynamic bucket PK table (bucket=-1) =====
     # Two commits with overlapping keys to exercise dynamic bucket assignment
diff --git a/docs/src/go-binding.md b/docs/src/go-binding.md
index 3524e844..26999d9b 100644
--- a/docs/src/go-binding.md
+++ b/docs/src/go-binding.md
@@ -125,6 +125,63 @@ process-local and cannot be sent to another process. For 
primary-key
 fixed-bucket tables, assign each `(partition, bucket)` to one writer before
 writing; merging messages does not establish ownership.
 
+### Postpone Fixed-Bucket Writes
+
+For a `bucket = -2` table, the ordinary builder writes postpone files. To write
+real buckets directly, use the dedicated builder with a plan mapping each
+partition to its bucket count:
+
+```go
+// Plan schema: partition keys in order, then a non-null Int32 total_buckets.
+schema := arrow.NewSchema([]arrow.Field{
+    {Name: "dt", Type: arrow.BinaryTypes.String, Nullable: true},
+    {Name: "total_buckets", Type: arrow.PrimitiveTypes.Int32, Nullable: false},
+}, nil)
+rb := array.NewRecordBuilder(memory.DefaultAllocator, schema)
+defer rb.Release()
+rb.Field(0).(*array.StringBuilder).AppendValues([]string{"2026-08-14", 
"2026-08-15"}, nil)
+rb.Field(1).(*array.Int32Builder).AppendValues([]int32{1, 1}, nil)
+plan := rb.NewRecord()
+defer plan.Release()
+
+builder, err := table.NewPostponeFixedBucketWriteBuilderWithCommitUser("job-1")
+if err != nil {
+    log.Fatal(err)
+}
+defer builder.Close()
+if err := builder.WithBucketPlan(plan); err != nil {
+    log.Fatal(err)
+}
+writer, err := builder.NewWrite() // requires WithBucketPlan to be called first
+if err != nil {
+    log.Fatal(err)
+}
+defer writer.Close()
+
+if err := writer.WriteArrowBatch(record); err != nil {
+    log.Fatal(err)
+}
+messages, err := writer.PrepareCommit()
+if err != nil {
+    log.Fatal(err)
+}
+defer messages.Close()
+
+commit, err := builder.NewCommit()
+if err != nil {
+    log.Fatal(err)
+}
+defer commit.Close()
+if err := commit.Commit(messages); err != nil {
+    log.Fatal(err)
+}
+```
+
+An unpartitioned plan holds only `total_buckets`. Multiple writers in one 
process must share the plan
+and commit user and assign each `(partition, bucket)` to one writer. Commit
+messages are process-local. A fixed-bucket writer is single-use; create a new
+writer after `PrepareCommit`.
+
 ## Reading a Table
 
 Paimon Go uses a **scan-then-read** pattern: first scan the table to produce 
splits, then read data from those splits as Arrow RecordBatches.

Reply via email to