CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/golang-github-com-apple-foundationdb-bindings-go

Go language bindings for FoundationDB, a distributed key-value store with ACID transactions

Pending
Overview
Eval results
Files

subspace.mddocs/

Subspace Package - Namespace Management for FoundationDB

Overview

The subspace package provides a convenient way to use FoundationDB tuples to define namespaces for different categories of data. The namespace is specified by a prefix tuple which is prepended to all tuples packed by the subspace. When unpacking a key with the subspace, the prefix tuple will be removed from the result.

Package Path: github.com/apple/foundationdb/bindings/go/src/fdb/subspace

Subspaces partition the keyspace into logical regions, enabling:

  • Multi-tenancy within a single database
  • Application-level key organization
  • Collision-free key allocation across different data types
  • Hierarchical namespace structures

As a best practice, API clients should use at least one subspace for application data. For general guidance on subspace usage, see the Subspaces section of the Developer Guide.


Subspace Interface

The Subspace interface represents a well-defined region of keyspace in a FoundationDB database.

type Subspace interface {
	// Sub returns a new Subspace whose prefix extends this Subspace with the
	// encoding of the provided element(s). If any of the elements are not a
	// valid tuple.TupleElement, Sub will panic.
	Sub(el ...tuple.TupleElement) Subspace

	// Bytes returns the literal bytes of the prefix of this Subspace.
	Bytes() []byte

	// Pack returns the key encoding the specified Tuple with the prefix of this
	// Subspace prepended.
	Pack(t tuple.Tuple) fdb.Key

	// PackWithVersionstamp returns the key encoding the specified tuple in
	// the subspace so that it may be used as the key in fdb.Transaction's
	// SetVersionstampedKey() method. The passed tuple must contain exactly
	// one incomplete tuple.Versionstamp instance or the method will return
	// with an error. The behavior here is the same as if one used the
	// tuple.PackWithVersionstamp() method to appropriately pack together this
	// subspace and the passed tuple.
	PackWithVersionstamp(t tuple.Tuple) (fdb.Key, error)

	// Unpack returns the Tuple encoded by the given key with the prefix of this
	// Subspace removed. Unpack will return an error if the key is not in this
	// Subspace or does not encode a well-formed Tuple.
	Unpack(k fdb.KeyConvertible) (tuple.Tuple, error)

	// Contains returns true if the provided key starts with the prefix of this
	// Subspace, indicating that the Subspace logically contains the key.
	Contains(k fdb.KeyConvertible) bool

	// All Subspaces implement fdb.KeyConvertible and may be used as
	// FoundationDB keys (corresponding to the prefix of this Subspace).
	fdb.KeyConvertible

	// All Subspaces implement fdb.ExactRange and fdb.Range, and describe all
	// keys strictly within the subspace that encode tuples. Specifically,
	// this will include all keys in [prefix + '\x00', prefix + '\xff').
	fdb.ExactRange
}

Constructor Functions

AllKeys

func AllKeys() Subspace

Returns the Subspace corresponding to all keys in a FoundationDB database. This creates a subspace with an empty prefix that contains all possible keys.

Returns: A Subspace representing the entire keyspace

Example:

// Create a subspace that spans the entire database
allKeys := subspace.AllKeys()

// This can be used as a base for creating other subspaces
userSubspace := allKeys.Sub("users")

FromBytes

func FromBytes(b []byte) Subspace

Returns a new Subspace from the provided bytes. This is useful when you have a raw byte prefix and want to create a subspace from it.

Parameters:

  • b: Raw byte slice to use as the subspace prefix

Returns: A Subspace with the specified byte prefix

Example:

// Create a subspace from raw bytes
prefix := []byte{0x01, 0x02, 0x03}
ss := subspace.FromBytes(prefix)

// Now use it to pack keys
key := ss.Pack(tuple.Tuple{"user", 123})

Sub

func Sub(el ...tuple.TupleElement) Subspace

Returns a new Subspace whose prefix is the encoding of the provided element(s). If any of the elements are not a valid tuple.TupleElement, a runtime panic will occur.

Parameters:

  • el: One or more tuple elements to encode as the subspace prefix

Returns: A Subspace with the encoded prefix

Valid tuple elements include:

  • []byte: Byte strings
  • string: Unicode strings
  • int, int8, int16, int32, int64: Signed integers
  • uint, uint8, uint16, uint32, uint64: Unsigned integers
  • float32, float64: Floating-point numbers
  • bool: Boolean values
  • tuple.UUID: UUIDs
  • tuple.Versionstamp: Versionstamps
  • nil: Null values

Example:

// Create a simple subspace
ss := subspace.Sub("myapp")

// Create a multi-element subspace
userSubspace := subspace.Sub("myapp", "users")

// Create a subspace with mixed types
dataSubspace := subspace.Sub("myapp", "data", 1, "version")

Core Methods

Sub

func (s Subspace) Sub(el ...tuple.TupleElement) Subspace

Returns a new Subspace whose prefix extends this Subspace with the encoding of the provided element(s). This enables hierarchical subspace creation.

Parameters:

  • el: One or more tuple elements to append to the current prefix

Returns: A new Subspace with extended prefix

Example:

// Create base application subspace
app := subspace.Sub("myapp")

// Create nested subspaces for different data types
users := app.Sub("users")
posts := app.Sub("posts")
comments := app.Sub("comments")

// Create deeply nested subspaces
userMetadata := users.Sub("metadata")
userPosts := users.Sub("posts")

Bytes

func (s Subspace) Bytes() []byte

Returns the literal bytes of the prefix of this Subspace. Useful for debugging or when you need the raw prefix.

Returns: Byte slice containing the subspace prefix

Example:

ss := subspace.Sub("users")
prefix := ss.Bytes()
fmt.Printf("Prefix: %x\n", prefix) // Prints hex representation

Pack

func (s Subspace) Pack(t tuple.Tuple) fdb.Key

Returns the key encoding the specified Tuple with the prefix of this Subspace prepended. This is the primary method for creating keys within a subspace.

Parameters:

  • t: Tuple to encode and prepend with subspace prefix

Returns: FoundationDB key with subspace prefix

Example:

users := subspace.Sub("users")

// Pack a user ID
userKey := users.Pack(tuple.Tuple{123})

// Pack a composite key
userEmailKey := users.Pack(tuple.Tuple{123, "email"})

// Use in transaction
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
    tr.Set(userKey, []byte("John Doe"))
    return nil, nil
})

PackWithVersionstamp

func (s Subspace) PackWithVersionstamp(t tuple.Tuple) (fdb.Key, error)

Returns the key encoding the specified tuple in the subspace so that it may be used as the key in fdb.Transaction's SetVersionstampedKey() method. The passed tuple must contain exactly one incomplete tuple.Versionstamp instance or the method will return with an error.

Parameters:

  • t: Tuple containing exactly one incomplete Versionstamp

Returns:

  • Encoded key suitable for SetVersionstampedKey()
  • Error if tuple doesn't contain exactly one incomplete Versionstamp

Example:

events := subspace.Sub("events")

// Create an incomplete versionstamp
vs := tuple.IncompleteVersionstamp(0)

// Pack with versionstamp
key, err := events.PackWithVersionstamp(tuple.Tuple{vs})
if err != nil {
    log.Fatal(err)
}

// Use with SetVersionstampedKey
db.Transact(func(tr fdb.Transaction) (interface{}, error) {
    tr.SetVersionstampedKey(key, []byte("event data"))
    return nil, nil
})

Unpack

func (s Subspace) Unpack(k fdb.KeyConvertible) (tuple.Tuple, error)

Returns the Tuple encoded by the given key with the prefix of this Subspace removed. Unpack will return an error if the key is not in this Subspace or does not encode a well-formed Tuple.

Parameters:

  • k: Key to unpack (must be within this subspace)

Returns:

  • Decoded tuple with subspace prefix removed
  • Error if key is not in subspace or not a valid tuple

Example:

users := subspace.Sub("users")

// Pack a key
key := users.Pack(tuple.Tuple{123, "email"})

// Later, unpack it
t, err := users.Unpack(key)
if err != nil {
    log.Fatal(err)
}

// Access tuple elements
userID := t[0].(int64)  // 123
field := t[1].(string)  // "email"

Contains

func (s Subspace) Contains(k fdb.KeyConvertible) bool

Returns true if the provided key starts with the prefix of this Subspace, indicating that the Subspace logically contains the key.

Parameters:

  • k: Key to check

Returns: true if key is within this subspace, false otherwise

Example:

users := subspace.Sub("users")
posts := subspace.Sub("posts")

userKey := users.Pack(tuple.Tuple{123})

fmt.Println(users.Contains(userKey)) // true
fmt.Println(posts.Contains(userKey)) // false

Usage Examples

Basic Subspace Creation

package main

import (
    "fmt"
    "github.com/apple/foundationdb/bindings/go/src/fdb"
    "github.com/apple/foundationdb/bindings/go/src/fdb/subspace"
    "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)

func main() {
    fdb.MustAPIVersion(730)
    db := fdb.MustOpenDefault()

    // Create application subspace
    myApp := subspace.Sub("myapp")

    // Create nested subspaces for different data types
    users := myApp.Sub("users")
    posts := myApp.Sub("posts")

    db.Transact(func(tr fdb.Transaction) (interface{}, error) {
        // Write user data
        tr.Set(users.Pack(tuple.Tuple{1, "name"}), []byte("Alice"))
        tr.Set(users.Pack(tuple.Tuple{1, "email"}), []byte("alice@example.com"))

        // Write post data
        tr.Set(posts.Pack(tuple.Tuple{100, "title"}), []byte("First Post"))
        tr.Set(posts.Pack(tuple.Tuple{100, "author"}), []byte("Alice"))

        return nil, nil
    })
}

Packing and Unpacking Keys

package main

import (
    "fmt"
    "log"
    "github.com/apple/foundationdb/bindings/go/src/fdb"
    "github.com/apple/foundationdb/bindings/go/src/fdb/subspace"
    "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)

func main() {
    fdb.MustAPIVersion(730)
    db := fdb.MustOpenDefault()

    users := subspace.Sub("users")

    // Pack a composite key
    userID := 123
    field := "email"
    key := users.Pack(tuple.Tuple{userID, field})

    // Write data
    db.Transact(func(tr fdb.Transaction) (interface{}, error) {
        tr.Set(key, []byte("user@example.com"))
        return nil, nil
    })

    // Later, unpack the key
    t, err := users.Unpack(key)
    if err != nil {
        log.Fatal(err)
    }

    // Extract components
    id := t[0].(int64)
    fieldName := t[1].(string)
    fmt.Printf("User ID: %d, Field: %s\n", id, fieldName)
}

Hierarchical Subspaces

package main

import (
    "github.com/apple/foundationdb/bindings/go/src/fdb"
    "github.com/apple/foundationdb/bindings/go/src/fdb/subspace"
    "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)

func main() {
    fdb.MustAPIVersion(730)
    db := fdb.MustOpenDefault()

    // Create hierarchical organization
    app := subspace.Sub("myapp")
    users := app.Sub("users")

    // Further subdivide user data
    userProfiles := users.Sub("profiles")
    userSettings := users.Sub("settings")
    userSessions := users.Sub("sessions")

    db.Transact(func(tr fdb.Transaction) (interface{}, error) {
        userID := 42

        // Store in different subdivisions
        tr.Set(
            userProfiles.Pack(tuple.Tuple{userID, "name"}),
            []byte("Bob"),
        )
        tr.Set(
            userSettings.Pack(tuple.Tuple{userID, "theme"}),
            []byte("dark"),
        )
        tr.Set(
            userSessions.Pack(tuple.Tuple{userID, "token"}),
            []byte("abc123"),
        )

        return nil, nil
    })
}

Multi-Tenancy Pattern

package main

import (
    "github.com/apple/foundationdb/bindings/go/src/fdb"
    "github.com/apple/foundationdb/bindings/go/src/fdb/subspace"
    "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)

// Tenant represents a single tenant in a multi-tenant application
type Tenant struct {
    ID       string
    Subspace subspace.Subspace
}

// NewTenant creates a new tenant with isolated keyspace
func NewTenant(tenantID string) *Tenant {
    // Each tenant gets its own subspace
    ss := subspace.Sub("tenants", tenantID)
    return &Tenant{
        ID:       tenantID,
        Subspace: ss,
    }
}

// GetUserKey returns a key for a user within this tenant
func (t *Tenant) GetUserKey(userID int64, field string) fdb.Key {
    return t.Subspace.Sub("users").Pack(tuple.Tuple{userID, field})
}

// GetPostKey returns a key for a post within this tenant
func (t *Tenant) GetPostKey(postID int64, field string) fdb.Key {
    return t.Subspace.Sub("posts").Pack(tuple.Tuple{postID, field})
}

func main() {
    fdb.MustAPIVersion(730)
    db := fdb.MustOpenDefault()

    // Create tenants
    acmeCorp := NewTenant("acme-corp")
    techStartup := NewTenant("tech-startup")

    db.Transact(func(tr fdb.Transaction) (interface{}, error) {
        // Each tenant's data is completely isolated

        // ACME Corp's data
        tr.Set(acmeCorp.GetUserKey(1, "name"), []byte("Alice"))
        tr.Set(acmeCorp.GetPostKey(100, "title"), []byte("ACME News"))

        // Tech Startup's data (separate keyspace)
        tr.Set(techStartup.GetUserKey(1, "name"), []byte("Bob"))
        tr.Set(techStartup.GetPostKey(100, "title"), []byte("Startup Blog"))

        return nil, nil
    })

    // Range queries are automatically scoped to tenant
    db.ReadTransact(func(rtr fdb.ReadTransaction) (interface{}, error) {
        // Only reads ACME Corp's users
        rng := acmeCorp.Subspace.Sub("users")
        ri := rtr.GetRange(rng, fdb.RangeOptions{}).Iterator()

        for ri.Advance() {
            kv, err := ri.Get()
            if err != nil {
                return nil, err
            }
            // Process ACME Corp user data
            _ = kv
        }

        return nil, nil
    })
}

Key Organization with Subspaces

package main

import (
    "fmt"
    "github.com/apple/foundationdb/bindings/go/src/fdb"
    "github.com/apple/foundationdb/bindings/go/src/fdb/subspace"
    "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)

func main() {
    fdb.MustAPIVersion(730)
    db := fdb.MustOpenDefault()

    // Organize keys by data type and access pattern
    app := subspace.Sub("social-network")

    // User data organized by ID
    users := app.Sub("users")
    usersByID := users.Sub("by-id")
    usersByEmail := users.Sub("by-email")

    // Post data with multiple indexes
    posts := app.Sub("posts")
    postsByID := posts.Sub("by-id")
    postsByAuthor := posts.Sub("by-author")
    postsByTimestamp := posts.Sub("by-timestamp")

    // Relationship data
    relationships := app.Sub("relationships")
    followers := relationships.Sub("followers")
    following := relationships.Sub("following")

    db.Transact(func(tr fdb.Transaction) (interface{}, error) {
        // Store user with multiple indexes
        userID := 123
        email := "alice@example.com"

        // Primary record
        tr.Set(usersByID.Pack(tuple.Tuple{userID}), []byte("Alice"))

        // Secondary index
        tr.Set(usersByEmail.Pack(tuple.Tuple{email}), []byte{123})

        // Store post with multiple indexes
        postID := 1001
        authorID := 123
        timestamp := 1640000000

        // Primary record
        tr.Set(postsByID.Pack(tuple.Tuple{postID}), []byte("Post content"))

        // Index by author
        tr.Set(postsByAuthor.Pack(tuple.Tuple{authorID, postID}), []byte{})

        // Index by timestamp
        tr.Set(postsByTimestamp.Pack(tuple.Tuple{timestamp, postID}), []byte{})

        // Store relationships
        followerID := 456
        followingID := 123

        // Bidirectional relationship indexes
        tr.Set(followers.Pack(tuple.Tuple{followingID, followerID}), []byte{})
        tr.Set(following.Pack(tuple.Tuple{followerID, followingID}), []byte{})

        return nil, nil
    })

    // Query posts by author
    db.ReadTransact(func(rtr fdb.ReadTransaction) (interface{}, error) {
        authorID := 123

        // Create range for this author's posts
        authorRange := postsByAuthor.Sub(authorID)
        ri := rtr.GetRange(authorRange, fdb.RangeOptions{}).Iterator()

        fmt.Printf("Posts by author %d:\n", authorID)
        for ri.Advance() {
            kv, err := ri.Get()
            if err != nil {
                return nil, err
            }

            // Unpack to get post ID
            t, err := postsByAuthor.Unpack(kv.Key)
            if err != nil {
                return nil, err
            }

            postID := t[1].(int64)
            fmt.Printf("  Post ID: %d\n", postID)
        }

        return nil, nil
    })
}

Using Contains for Validation

package main

import (
    "fmt"
    "github.com/apple/foundationdb/bindings/go/src/fdb"
    "github.com/apple/foundationdb/bindings/go/src/fdb/subspace"
    "github.com/apple/foundationdb/bindings/go/src/fdb/tuple"
)

func main() {
    fdb.MustAPIVersion(730)
    db := fdb.MustOpenDefault()

    app := subspace.Sub("myapp")
    users := app.Sub("users")
    posts := app.Sub("posts")
    admin := app.Sub("admin")

    // Check if a key belongs to a specific subspace
    userKey := users.Pack(tuple.Tuple{123})
    adminKey := admin.Pack(tuple.Tuple{"config"})

    fmt.Println(users.Contains(userKey))  // true
    fmt.Println(posts.Contains(userKey))  // false
    fmt.Println(admin.Contains(adminKey)) // true

    // Use Contains for access control
    db.Transact(func(tr fdb.Transaction) (interface{}, error) {
        key := userKey

        // Verify key is in allowed subspace before operation
        if !users.Contains(key) {
            return nil, fmt.Errorf("key not in users subspace")
        }

        tr.Set(key, []byte("user data"))
        return nil, nil
    })

    // Use Contains to route operations
    processKey := func(key fdb.Key) {
        switch {
        case users.Contains(key):
            fmt.Println("Processing user key")
        case posts.Contains(key):
            fmt.Println("Processing post key")
        case admin.Contains(key):
            fmt.Println("Processing admin key")
        default:
            fmt.Println("Unknown key type")
        }
    }

    processKey(userKey)
    processKey(adminKey)
}

Best Practices

Namespace Organization

  1. Use descriptive prefixes: Choose clear, meaningful names for subspaces

    // Good
    users := app.Sub("users")
    posts := app.Sub("posts")
    
    // Avoid
    ss1 := app.Sub("s1")
    ss2 := app.Sub("s2")
  2. Plan your hierarchy: Design your subspace structure before implementation

    // Hierarchical organization
    app := subspace.Sub("myapp")
    data := app.Sub("data")
    users := data.Sub("users")
    posts := data.Sub("posts")
    indexes := app.Sub("indexes")
  3. Separate concerns: Use different subspaces for different data types

    userData := app.Sub("users")
    postData := app.Sub("posts")
    commentData := app.Sub("comments")
    analyticsData := app.Sub("analytics")

Multi-Tenancy

  1. Isolate tenant data: Each tenant should have its own subspace

    tenantSpace := subspace.Sub("tenants", tenantID)
  2. Consistent structure: Use the same subspace structure for all tenants

    func getTenantSubspace(tenantID string) subspace.Subspace {
        return subspace.Sub("tenants", tenantID)
    }
  3. Validate tenant access: Always verify keys belong to the correct tenant

    if !tenantSpace.Contains(key) {
        return fmt.Errorf("unauthorized access")
    }

Key Design

  1. Use Pack for key creation: Always use Pack() to create keys

    // Correct
    key := ss.Pack(tuple.Tuple{userID, "email"})
    
    // Don't manually construct keys
    // key := append(ss.Bytes(), ...)
  2. Use Unpack for key parsing: Always use Unpack() to parse keys

    t, err := ss.Unpack(key)
    if err != nil {
        log.Fatal(err)
    }
  3. Check Contains before Unpack: Validate keys belong to subspace

    if !ss.Contains(key) {
        return fmt.Errorf("key not in subspace")
    }
    t, err := ss.Unpack(key)

Performance

  1. Reuse subspace objects: Create subspaces once and reuse them

    // Good: Create once
    var userSubspace = subspace.Sub("users")
    
    // Avoid: Creating repeatedly
    func getUser(id int) {
        ss := subspace.Sub("users") // Don't recreate each time
        key := ss.Pack(tuple.Tuple{id})
    }
  2. Plan for range queries: Organize keys to support efficient queries

    // Keys naturally grouped for range queries
    posts := app.Sub("posts")
    postsByAuthor := posts.Sub("by-author")
    key := postsByAuthor.Pack(tuple.Tuple{authorID, postID})
  3. Use appropriate tuple elements: Choose types that match your needs

    // Integers for IDs (more efficient)
    key := ss.Pack(tuple.Tuple{123})
    
    // Strings when needed
    key := ss.Pack(tuple.Tuple{"user-uuid-here"})

Related Documentation

  • Core FDB Package - Database connection and transaction management
  • Tuple Package - Encoding multi-element keys with sort order preservation
  • Directory Package - Hierarchical path-based subspace organization
  • FoundationDB Developer Guide - Subspaces

Install with Tessl CLI

npx tessl i tessl/golang-github-com-apple-foundationdb-bindings-go

docs

directory.md

fdb.md

index.md

subspace.md

tuple.md

tile.json