The tuple package provides a layer for encoding and decoding multi-element tuples into keys usable by FoundationDB. The encoded key maintains the same sort order as the original tuple: sorted first by the first element, then by the second element, etc. This makes the tuple layer ideal for building a variety of higher-level data models.
Package Path: github.com/apple/foundationdb/bindings/go/src/fdb/tuple
Key Features:
For general guidance on tuple usage, see the Tuple section of Data Modeling.
FoundationDB tuples can encode the following types in Go:
| Go Type | Description |
|---|---|
[]byte | Byte strings |
fdb.KeyConvertible | Any type implementing KeyConvertible |
string | Unicode strings |
int, int64 | Signed integers |
uint, uint64 | Unsigned integers |
*big.Int, big.Int | Large integers (up to 255 bytes) |
float32 | Single-precision floats |
float64 | Double-precision floats |
bool | Boolean values |
UUID | 128-bit UUIDs |
Versionstamp | FoundationDB versionstamps |
Tuple | Nested tuples |
nil | NULL values |
Integer Range: [-22040+1, 22040-1]
Type Normalization: When unpacking, types are normalized to []byte, uint64, and int64.
{ .api }
type Tuple []TupleElementTuple is a slice of objects that can be encoded as FoundationDB tuples. If any of the TupleElements are of unsupported types, a runtime panic will occur when the Tuple is packed.
Given a Tuple T containing objects only of supported types, then T will be identical to the Tuple returned by unpacking the byte slice obtained by packing T (modulo type normalization to []byte, uint64, and int64).
{ .api }
func (t Tuple) Pack() []byteReturns a new byte slice encoding the provided tuple. Pack will panic if the tuple contains an element of any type other than []byte, fdb.KeyConvertible, string, int64, int, uint64, uint, *big.Int, big.Int, float32, float64, bool, tuple.UUID, tuple.Versionstamp, nil, or a Tuple with elements of valid types.
Panics:
Returns: Packed byte slice representing the tuple
Example:
t := tuple.Tuple{"user", 42, "email@example.com"}
key := t.Pack()
// key can be used as a FoundationDB keyNote: Tuple satisfies the fdb.KeyConvertible interface, so it is not necessary to call Pack when using a Tuple with a FoundationDB API function that requires a key.
func (t Tuple) PackWithVersionstamp(prefix []byte) ([]byte, error)Packs the specified tuple into a key for versionstamp operations. This function must be used instead of Pack when the tuple contains an incomplete Versionstamp.
Parameters:
prefix: Optional byte prefix to prepend to the packed tuple (can be nil)Returns:
Example:
vs := tuple.IncompleteVersionstamp(0)
t := tuple.Tuple{"user", vs, "metadata"}
key, err := t.PackWithVersionstamp(nil)
if err != nil {
log.Fatal(err)
}
// Use key with SetVersionstampedKey or SetVersionstampedValueImportant: This function appends the versionstamp position to the end of the packed key. For API versions < 520, the position is 2 bytes; for 520+, it's 4 bytes.
func (t Tuple) FDBKey() fdb.KeyReturns the packed representation of a Tuple, and allows Tuple to satisfy the fdb.KeyConvertible interface. FDBKey will panic in the same circumstances as Pack.
Returns: Packed byte slice as fdb.Key
Example:
t := tuple.Tuple{"users", "alice"}
tr.Set(t, []byte("data")) // t is automatically converted via FDBKey()func (t Tuple) FDBRangeKeys() (fdb.KeyConvertible, fdb.KeyConvertible)Allows Tuple to satisfy the fdb.ExactRange interface. The range represents all keys that encode tuples strictly starting with this Tuple (that is, all tuples of greater length than the Tuple of which the Tuple is a prefix).
Returns:
Example:
prefix := tuple.Tuple{"users"}
begin, end := prefix.FDBRangeKeys()
// Range includes ("users", ...) but not ("users")func (t Tuple) FDBRangeKeySelectors() (fdb.Selectable, fdb.Selectable)Allows Tuple to satisfy the fdb.Range interface. The range represents all keys that encode tuples strictly starting with a Tuple (that is, all tuples of greater length than the Tuple of which the Tuple is a prefix).
Returns:
Example:
prefix := tuple.Tuple{"users"}
kvs, err := tr.GetRange(prefix, fdb.RangeOptions{}).GetSliceWithError()
// Retrieves all tuples starting with ("users", ...)func (t Tuple) HasIncompleteVersionstamp() (bool, error)Determines if there is at least one incomplete versionstamp in a tuple. This function will return an error if this tuple has more than one versionstamp.
Returns:
Example:
vs := tuple.IncompleteVersionstamp(0)
t := tuple.Tuple{"key", vs}
hasVS, err := t.HasIncompleteVersionstamp()
if err != nil {
log.Fatal(err)
}
if hasVS {
key, _ := t.PackWithVersionstamp(nil)
// Use with versionstamp operations
}func (t Tuple) String() stringImplements the fmt.Stringer interface and returns human-readable string representation of this tuple. For most elements, the default string representation is used.
Returns: Human-readable tuple representation
Example:
t := tuple.Tuple{"user", 42, nil}
fmt.Println(t)
// Output: ("user", 42, <nil>){ .api }
func Unpack(b []byte) (Tuple, error)Returns the tuple encoded by the provided byte slice, or an error if the key does not correctly encode a FoundationDB tuple.
Parameters:
b: Byte slice containing a packed tupleReturns:
Example:
key := []byte{0x02, 0x75, 0x73, 0x65, 0x72, 0x00, 0x15, 0x2A}
t, err := tuple.Unpack(key)
if err != nil {
log.Fatal(err)
}
// t is Tuple{"user", 42}{ .api }
type TupleElement interface{}A TupleElement is one of the types that may be encoded in FoundationDB tuples. Although the Go compiler cannot enforce this, it is a programming error to use an unsupported type as a TupleElement (and will typically result in a runtime panic).
The valid types for TupleElement are []byte (or fdb.KeyConvertible), string, int64 (or int, uint, uint64), *big.Int (or big.Int), float32, float64, bool, UUID, Tuple, and nil.
{ .api }
type UUID [16]byteUUID wraps a basic byte array as a UUID. We do not provide any special methods for accessing or generating the UUID, but as Go does not provide a built-in UUID type, this simple wrapper allows for other libraries to write the output of their UUID type as a 16-byte array into an instance of this type.
{ .api }
func (uuid UUID) String() stringReturns a human-readable string representation of the UUID in standard format.
Returns: UUID formatted as "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
Example:
var uuid tuple.UUID
copy(uuid[:], someUUIDBytes)
fmt.Println(uuid.String())
// Output: 550e8400-e29b-41d4-a716-446655440000{ .api }
type Versionstamp struct {
TransactionVersion [10]byte
UserVersion uint16
}Versionstamp is a struct for a FoundationDB versionstamp. Versionstamps are 12 bytes long composed of a 10-byte transaction version and a 2-byte user version. The transaction version is filled in at commit time and the user version is provided by the application to order results within a transaction.
Fields:
TransactionVersion: 10-byte transaction version (set by FoundationDB at commit time)UserVersion: 2-byte user-defined version for ordering within a transaction{ .api }
func IncompleteVersionstamp(userVersion uint16) VersionstampIncompleteVersionstamp is the constructor you should use to make an incomplete versionstamp to use in a tuple. The transaction version will be set to the incomplete marker (all 0xFF bytes) and will be filled in by FoundationDB at commit time.
Parameters:
userVersion: User-defined version number for orderingReturns: Versionstamp with incomplete transaction version
Example:
// Create versionstamp with user version 0
vs := tuple.IncompleteVersionstamp(0)
t := tuple.Tuple{"index", vs}
key, _ := t.PackWithVersionstamp(nil)
// Use in transaction
tr.Set(fdb.Key("versionstamped_key"), key){ .api }
func (v Versionstamp) Bytes() []byteConverts a Versionstamp struct to a byte slice for encoding in a tuple.
Returns: 12-byte slice containing transaction version (10 bytes) + user version (2 bytes)
Example:
vs := tuple.IncompleteVersionstamp(42)
bytes := vs.Bytes()
// bytes is 12-byte slice: [0xFF, 0xFF, ..., 0xFF, 0x00, 0x2A]func (vs Versionstamp) String() stringReturns a human-readable string representation of this Versionstamp.
Returns: Formatted string showing transaction version and user version
Example:
vs := tuple.IncompleteVersionstamp(100)
fmt.Println(vs.String())
// Output: Versionstamp(\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff, 100)package main
import (
"fmt"
"log"
"github.com/apple/foundationdb/bindings/go/src/fdb"
"github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)
func main() {
fdb.MustAPIVersion(730)
db := fdb.MustOpenDefault()
// Create a tuple
t := tuple.Tuple{"users", "alice", 42}
// Pack the tuple into a key
key := t.Pack()
fmt.Printf("Packed key: %x\n", key)
// Unpack the key back into a tuple
unpacked, err := tuple.Unpack(key)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Unpacked tuple: %v\n", unpacked)
}// Store data with tuple keys
_, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// User data indexed by (table, user_id)
userKey := tuple.Tuple{"users", 12345}
tr.Set(userKey, []byte(`{"name": "Alice", "email": "alice@example.com"}`))
// Email index: (email_index, email) -> user_id
emailKey := tuple.Tuple{"email_index", "alice@example.com"}
tr.Set(emailKey, []byte("12345"))
return nil, nil
})// Query all users
prefix := tuple.Tuple{"users"}
result, err := db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// Get range returns all keys starting with prefix
kvs, err := tr.GetRange(prefix, fdb.RangeOptions{}).GetSliceWithError()
if err != nil {
return nil, err
}
// Process results
for _, kv := range kvs {
keyTuple, _ := tuple.Unpack(kv.Key)
fmt.Printf("User ID: %v, Data: %s\n", keyTuple[1], kv.Value)
}
return nil, nil
})// Create nested tuple structure
nested := tuple.Tuple{
"document",
123,
tuple.Tuple{"metadata", "v1", 2024},
}
// Pack and unpack
packed := nested.Pack()
unpacked, _ := tuple.Unpack(packed)
fmt.Println(unpacked)
// Output: ("document", 123, ("metadata", "v1", 2024))// Create tuple with incomplete versionstamp
vs := tuple.IncompleteVersionstamp(0)
t := tuple.Tuple{"log_entry", vs, "message"}
// Pack with versionstamp support
packedKey, err := t.PackWithVersionstamp(nil)
if err != nil {
log.Fatal(err)
}
// Use in transaction with atomic versionstamp operation
_, err = db.Transact(func(tr fdb.Transaction) (interface{}, error) {
// SetVersionstampedKey will fill in the versionstamp at commit time
tr.SetVersionstampedKey(packedKey, []byte("log data"))
return nil, nil
})// Use user version to order multiple items in same transaction
for i := 0; i < 10; i++ {
vs := tuple.IncompleteVersionstamp(uint16(i))
t := tuple.Tuple{"events", vs, fmt.Sprintf("event_%d", i)}
key, _ := t.PackWithVersionstamp(nil)
// Each will have same transaction version but different user versions
// allowing proper ordering within the transaction
}import "math/big"
// All supported types
t := tuple.Tuple{
[]byte{0x01, 0x02, 0x03}, // byte slice
"hello", // string
int64(42), // int64
uint64(100), // uint64
big.NewInt(999999999999999), // big.Int pointer
float32(3.14), // float32
float64(2.718281828), // float64
true, // bool
tuple.UUID{}, // UUID
tuple.IncompleteVersionstamp(0), // Versionstamp
tuple.Tuple{"nested", "tuple"}, // nested tuple
nil, // nil
}
packed := t.Pack()
unpacked, _ := tuple.Unpack(packed)
// Round-trip encoding preserves values (with type normalization)// Handling invalid tuples
invalidKey := []byte{0xFF, 0xFF} // Invalid encoding
t, err := tuple.Unpack(invalidKey)
if err != nil {
fmt.Printf("Failed to unpack: %v\n", err)
}
// Detecting versionstamp issues
vs1 := tuple.IncompleteVersionstamp(0)
vs2 := tuple.IncompleteVersionstamp(1)
invalidTuple := tuple.Tuple{vs1, vs2}
hasVS, err := invalidTuple.HasIncompleteVersionstamp()
if err != nil {
fmt.Printf("Error: %v\n", err)
// Error: Tuple can only contain one incomplete versionstamp
}
// Attempting to pack tuple with incomplete versionstamp using Pack
// will panic - must use PackWithVersionstamp
defer func() {
if r := recover(); r != nil {
fmt.Printf("Caught panic: %v\n", r)
}
}()
invalidTuple = tuple.Tuple{tuple.IncompleteVersionstamp(0)}
invalidTuple.Pack() // This will panicThe tuple encoding preserves sort order, making it ideal for range queries and indexes:
// These tuples will be sorted in this order when packed
tuples := []tuple.Tuple{
tuple.Tuple{"a", 1},
tuple.Tuple{"a", 2},
tuple.Tuple{"a", 10},
tuple.Tuple{"b", 1},
}
// When packed and used as keys, they maintain this order
for _, t := range tuples {
packed := t.Pack()
fmt.Printf("%v -> %x\n", t, packed)
}Sort Order Rules:
Use Tuples for Structured Keys: Instead of manual key construction, use tuples for better maintainability
// Good
key := tuple.Tuple{"users", userID, "email"}
// Avoid
key := []byte(fmt.Sprintf("users:%d:email", userID))Leverage Range Queries: Use tuple prefixes for efficient range scans
// Get all data for a user
prefix := tuple.Tuple{"users", userID}
kvs := tr.GetRange(prefix, fdb.RangeOptions{})Versionstamp User Versions: Use user versions to maintain order within a transaction
for i, event := range events {
vs := tuple.IncompleteVersionstamp(uint16(i))
key := tuple.Tuple{"events", vs}
// Events will be ordered by user version
}Type Consistency: Maintain consistent types across tuple positions for predictable sorting
// Consistent: always use int64 for IDs
tuple.Tuple{"users", int64(id1)}
tuple.Tuple{"users", int64(id2)}Handle Errors: Always check errors when unpacking or using versionstamps
t, err := tuple.Unpack(key)
if err != nil {
// Handle invalid encoding
}big.Int values (> 8 bytes) have additional overhead