or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

data-operations.mddocs/reference/

Data Operations

Read, write, clear, and atomically mutate key-value data with support for range queries, streaming results, and various atomic operations.

Capabilities

Basic Write Operations

Set and clear individual keys and key ranges.

/**
 * Set a key to a value. Overwrites any existing value.
 * 
 * Parameters:
 * - key: byte[] - Key to set (max 10KB)
 * - value: byte[] - Value to store (max 100KB)
 */
void Transaction.set(byte[] key, byte[] value);

/**
 * Remove a key from the database.
 * No effect if key does not exist.
 * 
 * Parameters:
 * - key: byte[] - Key to clear
 */
void Transaction.clear(byte[] key);

/**
 * Remove all keys in the range [beginKey, endKey).
 * Range is half-open: beginKey is inclusive, endKey is exclusive.
 * 
 * Parameters:
 * - beginKey: byte[] - Start of range (inclusive)
 * - endKey: byte[] - End of range (exclusive)
 */
void Transaction.clear(byte[] beginKey, byte[] endKey);

/**
 * Remove all keys in the specified range.
 *
 * Parameters:
 * - range: Range - Range object specifying keys to clear
 */
void Transaction.clear(Range range);

/**
 * Remove all keys starting with the specified prefix.
 *
 * @deprecated Use Transaction.clear(Range.startsWith(prefix)) instead.
 *
 * Parameters:
 * - prefix: byte[] - Prefix of keys to clear
 *
 * Throws:
 * FDBException - If the clear-range operation fails
 */
@Deprecated
void Transaction.clearRangeStartsWith(byte[] prefix);

Usage examples:

import com.apple.foundationdb.*;

// Set a key
db.run(tr -> {
    tr.set("user:1001".getBytes(), "Alice".getBytes());
    return null;
});

// Clear a key
db.run(tr -> {
    tr.clear("user:1001".getBytes());
    return null;
});

// Clear a range
db.run(tr -> {
    // Remove all keys starting with "temp:"
    tr.clear("temp:".getBytes(), "temp;".getBytes());
    return null;
});

// Clear using Range object
db.run(tr -> {
    Range range = Range.startsWith("cache:".getBytes());
    tr.clear(range);
    return null;
});

Basic Read Operations

Retrieve individual values by key.

/**
 * Get the value for a key.
 * 
 * Parameters:
 * - key: byte[] - Key to retrieve
 * 
 * Returns:
 * CompletableFuture<byte[]> - Value, or null if key does not exist
 */
CompletableFuture<byte[]> ReadTransaction.get(byte[] key);

Usage examples:

// Blocking read
byte[] value = db.run(tr -> {
    return tr.get("user:1001".getBytes()).join();
});

if (value != null) {
    System.out.println("User: " + new String(value));
}

// Async read
db.runAsync(tr -> {
    return tr.get("user:1001".getBytes())
        .thenAccept(value -> {
            if (value != null) {
                System.out.println("User: " + new String(value));
            }
        });
});

// Multiple reads
db.run(tr -> {
    CompletableFuture<byte[]> f1 = tr.get("key1".getBytes());
    CompletableFuture<byte[]> f2 = tr.get("key2".getBytes());
    CompletableFuture<byte[]> f3 = tr.get("key3".getBytes());
    
    // Wait for all
    CompletableFuture.allOf(f1, f2, f3).join();
    
    // Process results
    byte[] v1 = f1.join();
    byte[] v2 = f2.join();
    byte[] v3 = f3.join();
    
    return null;
});

Range Queries

Query ranges of keys with streaming results and various options.

/**
 * Get all key-value pairs in range [begin, end).
 * Returns iterator that fetches results in batches.
 * 
 * Parameters:
 * - begin: byte[] - Start key (inclusive)
 * - end: byte[] - End key (exclusive)
 * 
 * Returns:
 * AsyncIterable<KeyValue> - Iterable over key-value pairs in range
 */
AsyncIterable<KeyValue> ReadTransaction.getRange(byte[] begin, byte[] end);

/**
 * Get key-value pairs in range with options.
 * 
 * Parameters:
 * - begin: byte[] - Start key (inclusive)
 * - end: byte[] - End key (exclusive)
 * - limit: int - Maximum results (ReadTransaction.ROW_LIMIT_UNLIMITED for no limit)
 * - reverse: boolean - If true, return results in reverse order
 * - mode: StreamingMode - Hint for result fetching strategy
 * 
 * Returns:
 * AsyncIterable<KeyValue> - Iterable over key-value pairs
 */
AsyncIterable<KeyValue> ReadTransaction.getRange(
    byte[] begin, byte[] end, int limit, boolean reverse, StreamingMode mode
);

/**
 * Get key-value pairs using Range object.
 * 
 * Parameters:
 * - range: Range - Range to query
 * 
 * Returns:
 * AsyncIterable<KeyValue> - Iterable over key-value pairs
 */
AsyncIterable<KeyValue> ReadTransaction.getRange(Range range);

/**
 * Get key-value pairs using Range object with options.
 * 
 * Parameters:
 * - range: Range - Range to query
 * - limit: int - Maximum results
 * - reverse: boolean - If true, return in reverse order
 * - mode: StreamingMode - Hint for fetching strategy
 * 
 * Returns:
 * AsyncIterable<KeyValue> - Iterable over key-value pairs
 */
AsyncIterable<KeyValue> ReadTransaction.getRange(
    Range range, int limit, boolean reverse, StreamingMode mode
);

/**
 * Get key-value pairs using KeySelector bounds.
 * KeySelectors enable relative key positioning.
 * 
 * Parameters:
 * - begin: KeySelector - Start position
 * - end: KeySelector - End position
 * 
 * Returns:
 * AsyncIterable<KeyValue> - Iterable over key-value pairs
 */
AsyncIterable<KeyValue> ReadTransaction.getRange(KeySelector begin, KeySelector end);

/**
 * Get key-value pairs using KeySelector bounds with options.
 * 
 * Parameters:
 * - begin: KeySelector - Start position
 * - end: KeySelector - End position
 * - limit: int - Maximum results
 * - reverse: boolean - If true, return in reverse order
 * - mode: StreamingMode - Hint for fetching strategy
 * 
 * Returns:
 * AsyncIterable<KeyValue> - Iterable over key-value pairs
 */
AsyncIterable<KeyValue> ReadTransaction.getRange(
    KeySelector begin, KeySelector end, int limit, boolean reverse, StreamingMode mode
);

Usage examples:

import com.apple.foundationdb.*;

// Simple range query
db.read(tr -> {
    for (KeyValue kv : tr.getRange("user:".getBytes(), "user;".getBytes())) {
        String key = new String(kv.getKey());
        String value = new String(kv.getValue());
        System.out.println(key + " = " + value);
    }
    return null;
});

// Range query with limit
db.read(tr -> {
    AsyncIterable<KeyValue> range = tr.getRange(
        "item:".getBytes(),
        "item;".getBytes(),
        10,  // Limit to 10 results
        false,  // Forward order
        StreamingMode.ITERATOR
    );
    
    for (KeyValue kv : range) {
        System.out.println(new String(kv.getKey()));
    }
    return null;
});

// Reverse range query
db.read(tr -> {
    AsyncIterable<KeyValue> range = tr.getRange(
        "log:".getBytes(),
        "log;".getBytes(),
        100,
        true,  // Reverse order (newest first)
        StreamingMode.WANT_ALL
    );
    
    for (KeyValue kv : range) {
        System.out.println("Log entry: " + new String(kv.getValue()));
    }
    return null;
});

// Range query using Range.startsWith
db.read(tr -> {
    Range range = Range.startsWith("config:".getBytes());
    for (KeyValue kv : tr.getRange(range)) {
        System.out.println(new String(kv.getKey()));
    }
    return null;
});

// Using KeySelector for pagination
db.read(tr -> {
    byte[] lastKey = "user:1000".getBytes();
    
    // Get next page after lastKey
    KeySelector begin = KeySelector.firstGreaterThan(lastKey);
    KeySelector end = KeySelector.firstGreaterOrEqual("user;".getBytes());
    
    AsyncIterable<KeyValue> range = tr.getRange(begin, end, 50, false, 
                                                  StreamingMode.WANT_ALL);
    
    for (KeyValue kv : range) {
        System.out.println(new String(kv.getKey()));
    }
    return null;
});

// Convert range to list
db.read(tr -> {
    AsyncIterable<KeyValue> range = tr.getRange("data:".getBytes(), "data;".getBytes());
    List<KeyValue> list = range.asList().join();
    System.out.println("Found " + list.size() + " items");
    return null;
});

Key Selection

Select keys by relative position using KeySelector.

/**
 * Get the key selected by a KeySelector.
 * 
 * Parameters:
 * - selector: KeySelector - Selector specifying which key to retrieve
 * 
 * Returns:
 * CompletableFuture<byte[]> - Selected key
 */
CompletableFuture<byte[]> ReadTransaction.getKey(KeySelector selector);

Usage example:

import com.apple.foundationdb.*;

// Get first key >= "user:1000"
db.read(tr -> {
    KeySelector selector = KeySelector.firstGreaterOrEqual("user:1000".getBytes());
    byte[] key = tr.getKey(selector).join();
    System.out.println("First key: " + new String(key));
    return null;
});

// Get last key < "user:2000"
db.read(tr -> {
    KeySelector selector = KeySelector.lastLessThan("user:2000".getBytes());
    byte[] key = tr.getKey(selector).join();
    System.out.println("Last key: " + new String(key));
    return null;
});

Atomic Operations

Perform atomic mutations without reading current value.

/**
 * Perform atomic operation on a key.
 * Modifies value without reading it first.
 * 
 * Parameters:
 * - optype: MutationType - Type of atomic operation
 * - key: byte[] - Key to mutate
 * - param: byte[] - Operation parameter (interpretation depends on optype)
 */
void Transaction.mutate(MutationType optype, byte[] key, byte[] param);

Usage examples:

import com.apple.foundationdb.*;
import java.nio.ByteBuffer;

// Atomic increment (ADD)
db.run(tr -> {
    byte[] key = "counter".getBytes();
    byte[] one = ByteBuffer.allocate(8).putLong(1).array();
    tr.mutate(MutationType.ADD, key, one);
    return null;
});

// Atomic append
db.run(tr -> {
    byte[] key = "log".getBytes();
    byte[] entry = "New entry\n".getBytes();
    tr.mutate(MutationType.APPEND_IF_FITS, key, entry);
    return null;
});

// Bitwise OR
db.run(tr -> {
    byte[] key = "flags".getBytes();
    byte[] mask = new byte[]{0x04}; // Set bit 2
    tr.mutate(MutationType.BIT_OR, key, mask);
    return null;
});

// Atomic MAX (keep larger value)
db.run(tr -> {
    byte[] key = "high_score".getBytes();
    byte[] score = ByteBuffer.allocate(8).putLong(1000).array();
    tr.mutate(MutationType.MAX, key, score);
    return null;
});

// Atomic MIN (keep smaller value)
db.run(tr -> {
    byte[] key = "low_temp".getBytes();
    byte[] temp = ByteBuffer.allocate(8).putLong(-5).array();
    tr.mutate(MutationType.MIN, key, temp);
    return null;
});

// Versionstamped key
db.run(tr -> {
    // Key with incomplete versionstamp placeholder
    Tuple keyTuple = Tuple.from("events", Versionstamp.incomplete());
    byte[] key = keyTuple.packWithVersionstamp();
    
    tr.mutate(MutationType.SET_VERSIONSTAMPED_KEY, key, "event data".getBytes());
    return null;
});

// Versionstamped value
db.run(tr -> {
    byte[] key = "versioned_data".getBytes();
    
    // Value with incomplete versionstamp placeholder
    Tuple valueTuple = Tuple.from("data", Versionstamp.incomplete());
    byte[] value = valueTuple.packWithVersionstamp();
    
    tr.mutate(MutationType.SET_VERSIONSTAMPED_VALUE, key, value);
    return null;
});

Range Metadata Operations

Query metadata about key ranges.

/**
 * Estimate size in bytes of a key range.
 * Useful for determining if range fits in transaction size limit.
 *
 * Parameters:
 * - begin: byte[] - Start of range (inclusive)
 * - end: byte[] - End of range (exclusive)
 *
 * Returns:
 * CompletableFuture<Long> - Estimated size in bytes
 */
CompletableFuture<Long> ReadTransaction.getEstimatedRangeSizeBytes(byte[] begin, byte[] end);

/**
 * Estimate size in bytes of a key range using Range object.
 *
 * Parameters:
 * - range: Range - Range to estimate
 *
 * Returns:
 * CompletableFuture<Long> - Estimated size in bytes
 */
CompletableFuture<Long> ReadTransaction.getEstimatedRangeSizeBytes(Range range);

/**
 * Get keys that split a range into chunks of approximately chunkSize bytes.
 * Useful for parallel processing of large ranges.
 *
 * Parameters:
 * - begin: byte[] - Start of range (inclusive)
 * - end: byte[] - End of range (exclusive)
 * - chunkSize: long - Desired chunk size in bytes
 *
 * Returns:
 * CompletableFuture<KeyArrayResult> - Array of split point keys
 */
CompletableFuture<KeyArrayResult> ReadTransaction.getRangeSplitPoints(
    byte[] begin, byte[] end, long chunkSize
);

/**
 * Get keys that split a range into chunks using Range object.
 *
 * Parameters:
 * - range: Range - Range to split
 * - chunkSize: long - Desired chunk size in bytes
 *
 * Returns:
 * CompletableFuture<KeyArrayResult> - Array of split point keys
 */
CompletableFuture<KeyArrayResult> ReadTransaction.getRangeSplitPoints(Range range, long chunkSize);

Usage examples:

// Check if range fits in transaction
db.read(tr -> {
    long size = tr.getEstimatedRangeSizeBytes(
        "data:".getBytes(),
        "data;".getBytes()
    ).join();
    
    if (size > 9_000_000) {  // Near 10MB limit
        System.out.println("Range too large for single transaction");
    }
    return null;
});

// Split range for parallel processing
db.read(tr -> {
    KeyArrayResult splits = tr.getRangeSplitPoints(
        "data:".getBytes(),
        "data;".getBytes(),
        1_000_000  // ~1MB chunks
    ).join();
    
    byte[][] splitKeys = splits.getKeys();
    System.out.println("Range can be split into " + (splitKeys.length + 1) + " chunks");
    
    // Process each chunk in parallel
    byte[] begin = "data:".getBytes();
    for (byte[] splitKey : splitKeys) {
        processChunk(begin, splitKey);
        begin = splitKey;
    }
    processChunk(begin, "data;".getBytes());
    
    return null;
});

Mapped Range Queries (Experimental)

Query ranges with mapped results from secondary indexes.

/**
 * Get mapped range results (experimental feature).
 * Retrieves primary data and associated secondary index data in single query.
 * 
 * Parameters:
 * - begin: KeySelector - Start position
 * - end: KeySelector - End position
 * - mapper: byte[] - Mapper specification for secondary lookups
 * - limit: int - Maximum results
 * - reverse: boolean - If true, return in reverse order
 * - mode: StreamingMode - Hint for fetching strategy
 * 
 * Returns:
 * AsyncIterable<MappedKeyValue> - Iterable over mapped results
 */
AsyncIterable<MappedKeyValue> ReadTransaction.getMappedRange(
    KeySelector begin, KeySelector end, byte[] mapper,
    int limit, boolean reverse, StreamingMode mode
);

Blob Granule Range Queries

Query blob granule boundaries for ranges stored in blob storage.

/**
 * Get blob granule ranges within specified bounds.
 * Used for querying data stored in blob storage.
 * 
 * Parameters:
 * - begin: byte[] - Start of query range (inclusive)
 * - end: byte[] - End of query range (exclusive)
 * - rowLimit: int - Maximum number of granule ranges to return
 * 
 * Returns:
 * CompletableFuture<KeyRangeArrayResult> - Array of blob granule ranges
 */
CompletableFuture<KeyRangeArrayResult> ReadTransaction.getBlobGranuleRanges(
    byte[] begin, byte[] end, int rowLimit
);

Types

class KeyValue {
    KeyValue(byte[] key, byte[] value);
    byte[] getKey();
    byte[] getValue();
    boolean equals(Object obj);
    int hashCode();
    String toString();
}

class Range {
    byte[] begin;  // Inclusive
    byte[] end;    // Exclusive
    
    Range(byte[] begin, byte[] end);
    static Range startsWith(byte[] prefix);
    boolean equals(Object o);
    int hashCode();
    String toString();
}

class KeySelector {
    KeySelector(byte[] key, boolean orEqual, int offset);
    
    static KeySelector lastLessThan(byte[] key);
    static KeySelector lastLessOrEqual(byte[] key);
    static KeySelector firstGreaterThan(byte[] key);
    static KeySelector firstGreaterOrEqual(byte[] key);
    
    KeySelector add(int offset);
    byte[] getKey();
    boolean orEqual();
    int getOffset();
    String toString();
}

class MappedKeyValue {
    byte[] getKey();
    byte[] getValue();
    byte[] getParent();
    int getIndex();
    List<KeyValue> getRangeResults();
}

class KeyArrayResult {
    byte[][] getKeys();
}

class KeyRangeArrayResult {
    Range[] getRanges();
}

enum StreamingMode {
    WANT_ALL,    // Client wants all results
    ITERATOR,    // Iterator mode (default, adaptive)
    EXACT,       // Exact row limit
    SMALL,       // Small result set expected
    MEDIUM,      // Medium result set expected
    LARGE,       // Large result set expected
    SERIAL       // Serial mode
}

enum MutationType {
    ADD,                      // Atomic addition (little-endian)
    BIT_AND,                  // Bitwise AND
    BIT_OR,                   // Bitwise OR
    BIT_XOR,                  // Bitwise XOR
    APPEND_IF_FITS,           // Append if under value size limit
    MAX,                      // Keep maximum value (little-endian)
    MIN,                      // Keep minimum value (little-endian)
    SET_VERSIONSTAMPED_KEY,   // Set key with versionstamp
    SET_VERSIONSTAMPED_VALUE, // Set value with versionstamp
    BYTE_MIN,                 // Byte-wise minimum
    BYTE_MAX,                 // Byte-wise maximum
    COMPARE_AND_CLEAR         // Clear if value matches param
}

interface ReadTransaction extends ReadTransactionContext {
    int ROW_LIMIT_UNLIMITED = 0;
}

Important Notes

Range Query Iteration

  • AsyncIterable fetches results in batches for memory efficiency
  • Use for-each loop for automatic iteration
  • Call asList() to fetch all results at once
  • Large ranges should use streaming iteration

StreamingMode Hints

  • ITERATOR: Default, adaptive batching
  • WANT_ALL: Fetch all results in fewer requests
  • SMALL/MEDIUM/LARGE: Hint at expected result size
  • EXACT: Respect row limit exactly
  • SERIAL: Fetch results one at a time

Atomic Operations

  • Execute without reading current value
  • More efficient than read-modify-write
  • ADD/MIN/MAX use little-endian integer encoding
  • Versionstamped operations get transaction commit version

Key Size Limits

  • Maximum key size: 10,000 bytes (10KB)
  • Maximum value size: 100,000 bytes (100KB)
  • Keys starting with 0xFF are reserved for system use
  • Use Tuple encoding for structured keys

Range Semantics

  • Ranges are half-open: [begin, end)
  • Begin key is inclusive, end key is exclusive
  • Empty range if begin >= end
  • Reverse order supported for all range queries

Transaction Size Limits

  • Default limit: 10MB per transaction
  • Includes all keys, values, and metadata
  • Use getEstimatedRangeSizeBytes() to check
  • Use getRangeSplitPoints() for large ranges

Read Your Writes

  • By default, reads see writes in same transaction
  • Disable with TransactionOptions.setReadYourWritesDisable()
  • Snapshot reads bypass read-your-writes