or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

blob-granules.mdc-api.mdgo-api.mdindex.mdjava-api.mdkey-encoding.mdmulti-tenancy.mdpython-api.mdruby-api.md

key-encoding.mddocs/

0

# Key Encoding and Subspaces

1

2

FoundationDB stores data as ordered key-value pairs where keys are byte sequences. To work effectively with structured data, FoundationDB provides tuple encoding and subspace utilities that allow you to create hierarchical, typed keys while maintaining sort order and efficient range queries.

3

4

## Core Concepts

5

6

- **Tuple Encoding**: Convert structured data (strings, numbers, nested tuples) into sortable byte sequences

7

- **Subspace**: Key prefix management for organizing hierarchical data

8

- **Directory Layer**: Named subspaces with automatic prefix allocation

9

- **Sort Order**: Encoded tuples maintain logical sort order

10

- **Type Preservation**: Round-trip encoding preserves data types

11

12

## Capabilities

13

14

### Tuple Encoding Across Languages

15

16

**Python API:**

17

18

```python { .api }

19

import fdb.tuple as tuple

20

21

def pack(items: Tuple[Any, ...]) -> bytes:

22

"""

23

Encode tuple to bytes for use as key.

24

25

Args:

26

items: Tuple of values to encode (str, int, float, bytes, tuple, None)

27

28

Returns:

29

Encoded bytes suitable for use as FoundationDB key

30

"""

31

32

def unpack(data: bytes) -> Tuple[Any, ...]:

33

"""

34

Decode bytes back to tuple.

35

36

Args:

37

data: Encoded tuple bytes

38

39

Returns:

40

Decoded tuple with original types preserved

41

"""

42

43

def range(prefix: Tuple[Any, ...]) -> Tuple[bytes, bytes]:

44

"""

45

Create key range for all tuples with given prefix.

46

47

Args:

48

prefix: Tuple prefix for range query

49

50

Returns:

51

(begin_key, end_key) tuple for range operations

52

"""

53

```

54

55

**Java API:**

56

57

```java { .api }

58

public class Tuple {

59

public static Tuple from(Object... items);

60

61

public byte[] pack();

62

public static Tuple fromBytes(byte[] data);

63

64

public Tuple add(Object item);

65

public Object get(int index);

66

public int size();

67

68

public Range range();

69

public static Range range(Tuple prefix);

70

}

71

```

72

73

**Go API:**

74

75

```go { .api }

76

// Package: fdb/tuple

77

78

type Tuple []TupleElement

79

80

func (t Tuple) Pack() []byte

81

func Unpack(b []byte) Tuple

82

func (t Tuple) FDBKey() Key

83

func (t Tuple) FDBRangeKeys() (Key, Key)

84

```

85

86

**Ruby API:**

87

88

```ruby { .api }

89

module FDB::Tuple

90

def self.pack(items)

91

def self.unpack(data)

92

def self.range(prefix)

93

end

94

```

95

96

### Supported Data Types

97

98

Tuple encoding supports the following types across all language bindings:

99

100

| Type | Python Example | Java Example | Go Example | Ruby Example | Sort Order |

101

|------|----------------|--------------|------------|--------------|------------|

102

| **Null** | `None` | `null` | `nil` | `nil` | First |

103

| **Bytes** | `b"data"` | `"data".getBytes()` | `[]byte("data")` | `"data".bytes` | Lexicographic |

104

| **String** | `"text"` | `"text"` | `"text"` | `"text"` | Lexicographic |

105

| **Integer** | `42`, `-7` | `42L`, `-7L` | `int64(42)` | `42` | Numeric |

106

| **Float** | `3.14`, `-2.5` | `3.14d` | `float64(3.14)` | `3.14` | Numeric |

107

| **Boolean** | `True`, `False` | `true`, `false` | `true`, `false` | `true`, `false` | false < true |

108

| **Tuple** | `(1, "a")` | `Tuple.from(1, "a")` | `tuple.Tuple{1, "a"}` | `[1, "a"]` | Element-wise |

109

| **UUID** | `uuid.UUID(...)` | `UUID.randomUUID()` | Custom | Custom | Lexicographic |

110

111

### Basic Tuple Encoding Examples

112

113

**Python:**

114

115

```python

116

import fdb.tuple as tuple

117

118

# Encode various data types

119

user_key = tuple.pack(("users", "user123", "profile"))

120

# Result: b'\x02users\x00\x02user123\x00\x02profile\x00'

121

122

score_key = tuple.pack(("game", "chess", 1500, "player456"))

123

timestamp_key = tuple.pack(("events", 1634567890.123, "login"))

124

125

# Hierarchical keys maintain sort order

126

keys = [

127

tuple.pack(("users", "alice")),

128

tuple.pack(("users", "alice", "email")),

129

tuple.pack(("users", "alice", "name")),

130

tuple.pack(("users", "bob")),

131

tuple.pack(("users", "bob", "email"))

132

]

133

# Keys are naturally sorted: alice < alice/email < alice/name < bob < bob/email

134

135

# Decode back to tuple

136

decoded = tuple.unpack(user_key)

137

print(decoded) # ('users', 'user123', 'profile')

138

139

# Create ranges for prefix queries

140

user_range = tuple.range(("users", "alice"))

141

begin_key, end_key = user_range

142

# Finds all keys starting with ("users", "alice")

143

```

144

145

**Java:**

146

147

```java

148

import com.apple.foundationdb.tuple.Tuple;

149

150

// Encode structured keys

151

byte[] userKey = Tuple.from("users", "user123", "profile").pack();

152

byte[] scoreKey = Tuple.from("game", "chess", 1500L, "player456").pack();

153

154

// Hierarchical organization

155

Tuple userPrefix = Tuple.from("users", "alice");

156

Tuple emailKey = userPrefix.add("email");

157

Tuple nameKey = userPrefix.add("name");

158

159

// Range operations

160

Range userRange = Tuple.from("users", "alice").range();

161

162

// Decode

163

Tuple decoded = Tuple.fromBytes(userKey);

164

String table = (String) decoded.get(0); // "users"

165

String userId = (String) decoded.get(1); // "user123"

166

String field = (String) decoded.get(2); // "profile"

167

```

168

169

**Go:**

170

171

```go

172

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

173

174

// Encode keys

175

userKey := tuple.Tuple{"users", "user123", "profile"}.Pack()

176

scoreKey := tuple.Tuple{"game", "chess", int64(1500), "player456"}.Pack()

177

178

// Hierarchical keys

179

userPrefix := tuple.Tuple{"users", "alice"}

180

emailKey := append(userPrefix, "email").Pack()

181

nameKey := append(userPrefix, "name").Pack()

182

183

// Range operations

184

beginKey, endKey := userPrefix.FDBRangeKeys()

185

186

// Decode

187

decoded := tuple.Unpack(userKey)

188

table := decoded[0].(string) // "users"

189

userID := decoded[1].(string) // "user123"

190

field := decoded[2].(string) // "profile"

191

```

192

193

**Ruby:**

194

195

```ruby

196

require 'fdb/tuple'

197

198

# Encode keys

199

user_key = FDB::Tuple.pack(['users', 'user123', 'profile'])

200

score_key = FDB::Tuple.pack(['game', 'chess', 1500, 'player456'])

201

202

# Hierarchical organization

203

user_prefix = ['users', 'alice']

204

email_key = FDB::Tuple.pack(user_prefix + ['email'])

205

name_key = FDB::Tuple.pack(user_prefix + ['name'])

206

207

# Range operations

208

begin_key, end_key = FDB::Tuple.range(['users', 'alice'])

209

210

# Decode

211

decoded = FDB::Tuple.unpack(user_key)

212

table, user_id, field = decoded # ['users', 'user123', 'profile']

213

```

214

215

### Subspace Operations

216

217

Subspaces provide key prefix management for organizing hierarchical data:

218

219

**Python:**

220

221

```python { .api }

222

class Subspace:

223

def __init__(self, prefixTuple: Tuple[Any, ...] = tuple(), rawPrefix: bytes = b''):

224

"""Create subspace with tuple or raw prefix."""

225

226

def pack(self, t: Tuple[Any, ...] = tuple()) -> bytes:

227

"""Pack tuple with subspace prefix."""

228

229

def unpack(self, key: bytes) -> Tuple[Any, ...]:

230

"""Unpack key and remove subspace prefix."""

231

232

def range(self, t: Tuple[Any, ...] = tuple()) -> Tuple[bytes, bytes]:

233

"""Create range within this subspace."""

234

235

def contains(self, key: bytes) -> bool:

236

"""Check if key belongs to this subspace."""

237

238

def subspace(self, t: Tuple[Any, ...]) -> Subspace:

239

"""Create child subspace with extended prefix."""

240

```

241

242

**Subspace Usage Examples:**

243

244

```python

245

import fdb.subspace

246

247

# Create application subspaces

248

app_space = fdb.Subspace(("myapp",))

249

user_space = app_space.subspace(("users",))

250

session_space = app_space.subspace(("sessions",))

251

252

# Work with individual users

253

alice_space = user_space.subspace(("alice",))

254

255

# Generate keys within subspaces

256

profile_key = alice_space.pack(("profile",))

257

settings_key = alice_space.pack(("settings",))

258

preferences_key = alice_space.pack(("preferences", "theme"))

259

260

# Range queries within subspaces

261

@fdb.transactional

262

def get_user_data(tr, user_id):

263

user = user_space.subspace((user_id,))

264

begin, end = user.range()

265

266

data = {}

267

for kv in tr.get_range(begin, end):

268

field_tuple = user.unpack(kv.key)

269

field_name = field_tuple[0] if field_tuple else "unknown"

270

data[field_name] = kv.value.decode()

271

272

return data

273

274

# Check key membership

275

assert alice_space.contains(profile_key)

276

assert not session_space.contains(profile_key)

277

```

278

279

### Complex Data Modeling Patterns

280

281

**User Profile with Nested Data:**

282

283

```python

284

class UserProfileManager:

285

def __init__(self, database):

286

self.db = database

287

self.users = fdb.Subspace(("users",))

288

289

@fdb.transactional

290

def create_user(self, tr, user_id, profile_data):

291

user = self.users.subspace((user_id,))

292

293

# Basic profile

294

tr.set(user.pack(("email",)), profile_data["email"].encode())

295

tr.set(user.pack(("name",)), profile_data["name"].encode())

296

tr.set(user.pack(("created",)), str(time.time()).encode())

297

298

# Preferences with nested structure

299

prefs = profile_data.get("preferences", {})

300

for category, settings in prefs.items():

301

if isinstance(settings, dict):

302

for key, value in settings.items():

303

pref_key = user.pack(("preferences", category, key))

304

tr.set(pref_key, json.dumps(value).encode())

305

else:

306

pref_key = user.pack(("preferences", category))

307

tr.set(pref_key, json.dumps(settings).encode())

308

309

# Tags with individual keys for efficient queries

310

tags = profile_data.get("tags", [])

311

for tag in tags:

312

tag_key = user.pack(("tags", tag))

313

tr.set(tag_key, b"") # Existence key

314

315

@fdb.transactional

316

def get_user_preferences(self, tr, user_id):

317

user = self.users.subspace((user_id,))

318

pref_prefix = user.subspace(("preferences",))

319

begin, end = pref_prefix.range()

320

321

preferences = {}

322

for kv in tr.get_range(begin, end):

323

pref_path = pref_prefix.unpack(kv.key)

324

325

# Reconstruct nested structure

326

current = preferences

327

for part in pref_path[:-1]:

328

if part not in current:

329

current[part] = {}

330

current = current[part]

331

332

current[pref_path[-1]] = json.loads(kv.value.decode())

333

334

return preferences

335

336

@fdb.transactional

337

def find_users_with_tag(self, tr, tag):

338

# Efficient tag-based queries

339

results = []

340

341

begin, end = self.users.range()

342

for kv in tr.get_range(begin, end):

343

user_key_parts = self.users.unpack(kv.key)

344

if len(user_key_parts) >= 3 and user_key_parts[1] == "tags" and user_key_parts[2] == tag:

345

user_id = user_key_parts[0]

346

results.append(user_id)

347

348

return list(set(results)) # Remove duplicates

349

```

350

351

**Time-Series Data with Efficient Queries:**

352

353

```java

354

public class TimeSeriesStore {

355

private final Database db;

356

private final Subspace metricsSpace;

357

358

public TimeSeriesStore(Database db) {

359

this.db = db;

360

this.metricsSpace = new Subspace(Tuple.from("metrics"));

361

}

362

363

public void recordMetric(String metricName, long timestamp, double value, Map<String, String> tags) {

364

db.run(tr -> {

365

// Primary key: metric name, timestamp, then tag hash for uniqueness

366

String tagHash = computeTagHash(tags);

367

Tuple keyTuple = Tuple.from(metricName, timestamp, tagHash);

368

byte[] key = metricsSpace.pack(keyTuple);

369

370

// Store value and tags as JSON

371

Map<String, Object> data = new HashMap<>();

372

data.put("value", value);

373

data.put("tags", tags);

374

data.put("timestamp", timestamp);

375

376

String jsonData = new Gson().toJson(data);

377

tr.set(key, jsonData.getBytes());

378

379

// Secondary indexes for efficient tag queries

380

for (Map.Entry<String, String> tag : tags.entrySet()) {

381

Tuple indexKey = Tuple.from("by_tag", tag.getKey(), tag.getValue(), timestamp, metricName, tagHash);

382

tr.set(metricsSpace.pack(indexKey), key);

383

}

384

385

return null;

386

});

387

}

388

389

public List<DataPoint> queryRange(String metricName, long startTime, long endTime) {

390

return db.read(tr -> {

391

List<DataPoint> results = new ArrayList<>();

392

393

Tuple beginTuple = Tuple.from(metricName, startTime);

394

Tuple endTuple = Tuple.from(metricName, endTime + 1); // Exclusive end

395

396

Range queryRange = new Range(

397

metricsSpace.pack(beginTuple),

398

metricsSpace.pack(endTuple)

399

);

400

401

for (KeyValue kv : tr.getRange(queryRange)) {

402

String jsonData = new String(kv.getValue());

403

DataPoint point = new Gson().fromJson(jsonData, DataPoint.class);

404

results.add(point);

405

}

406

407

return results;

408

});

409

}

410

411

public List<DataPoint> queryByTag(String tagKey, String tagValue, long startTime, long endTime) {

412

return db.read(tr -> {

413

List<DataPoint> results = new ArrayList<>();

414

415

Tuple beginTuple = Tuple.from("by_tag", tagKey, tagValue, startTime);

416

Tuple endTuple = Tuple.from("by_tag", tagKey, tagValue, endTime + 1);

417

418

Range indexRange = new Range(

419

metricsSpace.pack(beginTuple),

420

metricsSpace.pack(endTuple)

421

);

422

423

for (KeyValue indexKv : tr.getRange(indexRange)) {

424

// Get actual data using the stored key

425

byte[] dataKey = indexKv.getValue();

426

byte[] data = tr.get(dataKey).join();

427

428

if (data != null) {

429

String jsonData = new String(data);

430

DataPoint point = new Gson().fromJson(jsonData, DataPoint.class);

431

results.add(point);

432

}

433

}

434

435

return results;

436

});

437

}

438

439

private String computeTagHash(Map<String, String> tags) {

440

// Create deterministic hash of sorted tags

441

return tags.entrySet().stream()

442

.sorted(Map.Entry.comparingByKey())

443

.map(e -> e.getKey() + "=" + e.getValue())

444

.collect(Collectors.joining(","))

445

.hashCode() + "";

446

}

447

448

public static class DataPoint {

449

public double value;

450

public long timestamp;

451

public Map<String, String> tags;

452

}

453

}

454

```

455

456

### Advanced Subspace Patterns

457

458

**Multi-Level Hierarchy with Go:**

459

460

```go

461

type DocumentStore struct {

462

db fdb.Database

463

docsSpace subspace.Subspace

464

indexSpace subspace.Subspace

465

}

466

467

func NewDocumentStore(db fdb.Database) *DocumentStore {

468

root := subspace.Sub(tuple.Tuple{"documents"}.Pack())

469

return &DocumentStore{

470

db: db,

471

docsSpace: root.Sub(tuple.Tuple{"data"}),

472

indexSpace: root.Sub(tuple.Tuple{"indexes"}),

473

}

474

}

475

476

func (ds *DocumentStore) StoreDocument(collection, docID string, doc map[string]interface{}) error {

477

_, err := ds.db.Transact(func(tr fdb.Transaction) (interface{}, error) {

478

// Store document data

479

docSpace := ds.docsSpace.Sub(tuple.Tuple{collection, docID})

480

481

for field, value := range doc {

482

key := docSpace.Pack(tuple.Tuple{field})

483

jsonValue, _ := json.Marshal(value)

484

tr.Set(key, jsonValue)

485

}

486

487

// Build indexes

488

for field, value := range doc {

489

if ds.shouldIndex(field) {

490

indexKey := ds.indexSpace.Pack(tuple.Tuple{collection, field, value, docID})

491

tr.Set(indexKey, []byte{})

492

}

493

}

494

495

return nil, nil

496

})

497

498

return err

499

}

500

501

func (ds *DocumentStore) QueryByField(collection, field string, value interface{}) ([]string, error) {

502

result, err := ds.db.ReadTransact(func(tr fdb.ReadTransaction) (interface{}, error) {

503

var docIDs []string

504

505

indexPrefix := ds.indexSpace.Sub(tuple.Tuple{collection, field, value})

506

begin, end := indexPrefix.FDBRangeKeys()

507

508

ri := tr.GetRange(fdb.Range{begin, end}, fdb.RangeOptions{}).Iterator()

509

for ri.Advance() {

510

kv, err := ri.Get()

511

if err != nil {

512

return nil, err

513

}

514

515

// Extract document ID from index key

516

keyParts := ds.indexSpace.Unpack(kv.Key)

517

if len(keyParts) >= 4 {

518

docID := keyParts[3].(string)

519

docIDs = append(docIDs, docID)

520

}

521

}

522

523

return docIDs, nil

524

})

525

526

if err != nil {

527

return nil, err

528

}

529

530

return result.([]string), nil

531

}

532

533

func (ds *DocumentStore) shouldIndex(field string) bool {

534

// Define which fields should be indexed

535

indexedFields := map[string]bool{

536

"status": true,

537

"category": true,

538

"author": true,

539

"published": true,

540

}

541

return indexedFields[field]

542

}

543

```

544

545

### Performance Considerations

546

547

**Efficient Key Design:**

548

549

```ruby

550

class EfficientKeyDesign

551

def initialize(database)

552

@db = database

553

@metrics = FDB::Subspace.new(['metrics'])

554

end

555

556

# Good: Use tuple encoding for structured keys

557

def store_metric_good(metric_name, timestamp, value, host)

558

key = @metrics.pack([metric_name, timestamp, host])

559

560

FDB.transactional do |tr|

561

tr[key] = value.to_s

562

end

563

end

564

565

# Bad: String concatenation loses sort order and type info

566

def store_metric_bad(metric_name, timestamp, value, host)

567

key = "#{metric_name}:#{timestamp}:#{host}"

568

569

FDB.transactional do |tr|

570

tr[key] = value.to_s

571

end

572

end

573

574

# Good: Hierarchical subspaces for range queries

575

def query_metric_range_good(metric_name, start_time, end_time)

576

metric_space = @metrics.subspace([metric_name])

577

578

FDB.transactional do |tr|

579

results = []

580

581

tr.get_range(

582

metric_space.pack([start_time]),

583

metric_space.pack([end_time + 1]) # Exclusive end

584

).each do |kv|

585

timestamp, host = metric_space.unpack(kv.key)

586

results << {

587

timestamp: timestamp,

588

host: host,

589

value: kv.value.to_f

590

}

591

end

592

593

results

594

end

595

end

596

597

# Good: Batch operations for better performance

598

def store_metrics_batch(metrics_data)

599

FDB.transactional do |tr|

600

metrics_data.each do |data|

601

key = @metrics.pack([data[:name], data[:timestamp], data[:host]])

602

tr[key] = data[:value].to_s

603

end

604

end

605

end

606

end

607

```

608

609

Key encoding and subspaces are fundamental to efficient FoundationDB usage, providing type-safe, sortable keys that enable complex queries while maintaining high performance across all language bindings.