Go language bindings for FoundationDB, a distributed key-value store with ACID transactions
—
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:
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.
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
}func AllKeys() SubspaceReturns 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")func FromBytes(b []byte) SubspaceReturns 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 prefixReturns: 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})func Sub(el ...tuple.TupleElement) SubspaceReturns 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 prefixReturns: A Subspace with the encoded prefix
Valid tuple elements include:
[]byte: Byte stringsstring: Unicode stringsint, int8, int16, int32, int64: Signed integersuint, uint8, uint16, uint32, uint64: Unsigned integersfloat32, float64: Floating-point numbersbool: Boolean valuestuple.UUID: UUIDstuple.Versionstamp: Versionstampsnil: Null valuesExample:
// 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")func (s Subspace) Sub(el ...tuple.TupleElement) SubspaceReturns 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 prefixReturns: 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")func (s Subspace) Bytes() []byteReturns 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 representationfunc (s Subspace) Pack(t tuple.Tuple) fdb.KeyReturns 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 prefixReturns: 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
})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 VersionstampReturns:
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
})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:
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"func (s Subspace) Contains(k fdb.KeyConvertible) boolReturns true if the provided key starts with the prefix of this Subspace, indicating that the Subspace logically contains the key.
Parameters:
k: Key to checkReturns: 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)) // falsepackage 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
})
}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)
}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
})
}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
})
}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
})
}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)
}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")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")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")Isolate tenant data: Each tenant should have its own subspace
tenantSpace := subspace.Sub("tenants", tenantID)Consistent structure: Use the same subspace structure for all tenants
func getTenantSubspace(tenantID string) subspace.Subspace {
return subspace.Sub("tenants", tenantID)
}Validate tenant access: Always verify keys belong to the correct tenant
if !tenantSpace.Contains(key) {
return fmt.Errorf("unauthorized access")
}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(), ...)Use Unpack for key parsing: Always use Unpack() to parse keys
t, err := ss.Unpack(key)
if err != nil {
log.Fatal(err)
}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)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})
}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})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"})Install with Tessl CLI
npx tessl i tessl/golang-github-com-apple-foundationdb-bindings-go