or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

directory.mdfdb.mdindex.mdsubspace.mdtuple.md
tile.json

tuple.mddocs/

Tuple Package - FoundationDB Tuple Layer

Overview

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:

  • Sort order preservation during encoding
  • Support for multiple data types
  • Nested tuple support
  • Versionstamp support for atomic versioning
  • Round-trip encoding/decoding guarantee

For general guidance on tuple usage, see the Tuple section of Data Modeling.

Supported Element Types

FoundationDB tuples can encode the following types in Go:

Go TypeDescription
[]byteByte strings
fdb.KeyConvertibleAny type implementing KeyConvertible
stringUnicode strings
int, int64Signed integers
uint, uint64Unsigned integers
*big.Int, big.IntLarge integers (up to 255 bytes)
float32Single-precision floats
float64Double-precision floats
boolBoolean values
UUID128-bit UUIDs
VersionstampFoundationDB versionstamps
TupleNested tuples
nilNULL values

Integer Range: [-22040+1, 22040-1]

Type Normalization: When unpacking, types are normalized to []byte, uint64, and int64.

Tuple Type

Type Definition

{ .api }

type Tuple []TupleElement

Tuple 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).

Methods

{ .api }

Pack

func (t Tuple) Pack() []byte

Returns 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:

  • If tuple contains unsupported types
  • If integer value is outside range [-22040+1, 22040-1]
  • If tuple contains an incomplete Versionstamp (use PackWithVersionstamp instead)

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 key

Note: 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.

PackWithVersionstamp

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:

  • Packed byte slice with versionstamp position appended
  • Error if tuple contains more than one versionstamp
  • Error if versionstamp position > uint16 and API version < 520

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 SetVersionstampedValue

Important: 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.

FDBKey

func (t Tuple) FDBKey() fdb.Key

Returns 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()

FDBRangeKeys

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:

  • Begin key: Tuple packed + 0x00
  • End key: Tuple packed + 0xFF

Example:

prefix := tuple.Tuple{"users"}
begin, end := prefix.FDBRangeKeys()
// Range includes ("users", ...) but not ("users")

FDBRangeKeySelectors

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:

  • Begin selector: FirstGreaterOrEqual(begin key)
  • End selector: FirstGreaterOrEqual(end key)

Example:

prefix := tuple.Tuple{"users"}
kvs, err := tr.GetRange(prefix, fdb.RangeOptions{}).GetSliceWithError()
// Retrieves all tuples starting with ("users", ...)

HasIncompleteVersionstamp

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:

  • true if tuple contains at least one incomplete versionstamp
  • Error if tuple contains more than one versionstamp

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
}

String

func (t Tuple) String() string

Implements 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>)

Unpack Function

Function

{ .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 tuple

Returns:

  • Decoded Tuple
  • Error if byte slice is not a valid tuple encoding

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}

TupleElement Interface

Type Definition

{ .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.

UUID Type

Type Definition

{ .api }

type UUID [16]byte

UUID 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.

Methods

{ .api }

String

func (uuid UUID) String() string

Returns 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

Versionstamp Type

Type Definition

{ .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

Constructor

{ .api }

func IncompleteVersionstamp(userVersion uint16) Versionstamp

IncompleteVersionstamp 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 ordering

Returns: 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)

Methods

{ .api }

Bytes

func (v Versionstamp) Bytes() []byte

Converts 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]

String

func (vs Versionstamp) String() string

Returns 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)

Usage Examples

Basic Tuple Operations

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)
}

Using Tuples with FoundationDB

// 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
})

Range Queries with Tuples

// 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
})

Nested Tuples

// 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))

Using Versionstamps

// 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
})

Multiple User Versions

// 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
}

Type Support Examples

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)

Error Handling

// 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 panic

Sort Order Preservation

The 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:

  1. Elements are compared in order (first element, then second, etc.)
  2. Different types have a defined sort order
  3. Within same type, natural ordering is preserved
  4. Nested tuples are supported

Best Practices

  1. 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))
  2. 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{})
  3. 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
    }
  4. 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)}
  5. Handle Errors: Always check errors when unpacking or using versionstamps

    t, err := tuple.Unpack(key)
    if err != nil {
        // Handle invalid encoding
    }

Performance Considerations

  • Packing is Fast: Tuple packing is optimized for performance
  • Unpacking Validates: Unpacking includes validation, may be slower than packing
  • Nested Tuples: Each nesting level adds encoding overhead
  • Large Integers: Very large big.Int values (> 8 bytes) have additional overhead
  • Versionstamps: Adding versionstamps requires PackWithVersionstamp and position tracking

See Also

  • FoundationDB Data Modeling Guide
  • FDB Package Documentation
  • Subspace Package (builds on tuples)