Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,20 @@
## [Unreleased]

### Added
- [#91] `ReadTransaction::getMappedRange()` — single-round-trip index lookups
backed by `fdb_transaction_get_mapped_range` and
`fdb_future_get_mappedkeyvalue_array` (the latter through the new
`CrazyGoat\FoundationDB\Future\FutureMappedKeyValueArray` future type).
Each resolved index row is returned as a `MappedKeyValue` pairing the index
key/value with the list of records described by the mapper tuple template
(`{K[N]}`, `{V[N]}`, and the `{...}` range descriptor that must be the last
element). Only available on non-snapshot (read-your-writes) reads — calling
it on a `Snapshot` throws a `LogicException`. The FFI layer also corrects
the `FDBMappedKeyValue` memory layout: the native reply stores either a
point lookup or a range lookup per row (selected by a variant index at
offset 104), which the public `fdb_c.h` declaration does not model.
Integration tests in `tests/Integration/MappedRangeTest.php`;
`docs/range-reads.md` updated with the mapper syntax.
- [#90] `ReadTransaction::getTotalCost()` and
`ReadTransaction::getTagThrottledDuration()` (backed by
`fdb_transaction_get_total_cost` and
Expand Down
8 changes: 7 additions & 1 deletion docker/php/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
FROM php:8.2-cli-bookworm

ARG FDB_VERSION=7.3.75
ARG TARGETARCH

RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
Expand All @@ -11,7 +12,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
procps \
&& rm -rf /var/lib/apt/lists/*

RUN wget -q "https://github.com/apple/foundationdb/releases/download/${FDB_VERSION}/foundationdb-clients_${FDB_VERSION}-1_amd64.deb" \
# Map Docker's TARGETARCH (arm64/amd64) to the FoundationDB deb suffix (aarch64/amd64).
RUN case "$TARGETARCH" in \
arm64) FDB_ARCH=aarch64 ;; \
*) FDB_ARCH=amd64 ;; \
esac \
&& wget -q "https://github.com/apple/foundationdb/releases/download/${FDB_VERSION}/foundationdb-clients_${FDB_VERSION}-1_${FDB_ARCH}.deb" \
-O /tmp/fdb-clients.deb \
&& dpkg -i /tmp/fdb-clients.deb \
&& rm /tmp/fdb-clients.deb
Expand Down
63 changes: 63 additions & 0 deletions docs/range-reads.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,69 @@ $results = $db->getRangeAllStartsWith('users/'); // list<KeyValue>

Note: Database-level methods always return arrays (not lazy iterators).

## Mapped Range Reads (single-round-trip index lookups)

`getMappedRange()` performs an index lookup and the corresponding record
fetches in a single round trip to the storage servers (backed by
`fdb_transaction_get_mapped_range`). Without it, an index-backed read requires
a `getRange()` over the index followed by N separate `get()` calls; with it,
every index row comes back together with its records in one response.

The index and the records must be laid out so that the mapper can address the
records from an index row. The mapper is a **packed tuple template** whose
elements are resolved per index row:

| Element | Resolves to |
|--------------|----------------------------------------------------------|
| `'literal'` | Copied verbatim into the record key/value |
| `{K[N]}` | The N-th element (0-based) of the index key tuple |
| `{V[N]}` | The N-th element (0-based) of the index value tuple |
| `{...}` | Range descriptor — must be the **last** element |

- Without `{...}`, the mapper describes a **point lookup**: exactly one
record (the key built from the template) is fetched per index row. A
missing record yields an empty `results` list.
- With `{...}` as the last element, the mapper describes a **range lookup**:
every record whose key has the built key as a prefix is fetched per index
row.
- Literal braces can be escaped by doubling them (`{{`, `}}`).

Example layout — index `('user', $userId) -> $name`, records under
`('records', $userId)` (multiple keys per user):

```php
use CrazyGoat\FoundationDB\Tuple\Tuple;

$tr = $db->createTransaction();
[$begin, $end] = Tuple::range(['user']); // index range

// mapper: ('records', {K[1]}, {...}) — every record under the
// ('records', $userId) prefix is fetched together with its index row
$mapper = Tuple::pack(['records', '{K[1]}', '{...}']);

$result = $tr->getMappedRange($begin, $end, $mapper)->await();

foreach ($result->entries as $mapped) {
// $mapped->key / $mapped->value — the index row
// $mapped->results — list<KeyValue> fetched for this row
}

// Pagination: $result->more tells whether more index rows exist,
// $result->count the number of rows in this batch. Options (limit,
// reverse, streaming mode) are passed as a RangeOptions.
```

Notes:

- Mapped ranges are **not** supported on snapshot reads on current
FoundationDB versions — call `getMappedRange()` on a plain `Transaction`
(read-your-writes). Calling it on `Transaction::snapshot()` throws a
`LogicException`.
- An invalid mapper (unparseable tuple, bad placeholder, or a `{...}`
descriptor that is not the last element) surfaces as an `FDBException`.
- `getRange()`-style lazy pagination is not wrapped for mapped reads; use the
`RangeOptions` limit together with `more` to page manually.

## Range Size Estimation

```php
Expand Down
103 changes: 103 additions & 0 deletions src/Future/FutureMappedKeyValueArray.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\FoundationDB\Future;

use CrazyGoat\FoundationDB\KeyValue;
use CrazyGoat\FoundationDB\MappedKeyValue;
use FFI;

/**
* Result of fdb_transaction_get_mapped_range.
*
* The native reply carries, per index row, either a point lookup
* (GetValueReqAndResultRef, variant index 0) or a range lookup
* (GetRangeReqAndResultRef, variant index 1) — depending on whether the
* mapper tuple template ends with a "{...}" range descriptor.
*
* NOTE: the public fdb_c.h FDBMappedKeyValue only models the getRange
* alternative. The real C++ object (MappedKeyValueRef) is laid out as:
* index key (12B), index value (12B), 80-byte variant union, 4-byte
* variant index at offset 104 (padded to a 112-byte stride).
*/
final class FutureMappedKeyValueArray extends Future
{
private const VARIANT_GET_VALUE = 0;
private const VARIANT_GET_RANGE = 1;

private ?MappedRangeResult $cachedResult = null;

public function await(): MappedRangeResult
{
if ($this->resolved && $this->cachedResult instanceof MappedRangeResult) {
return $this->cachedResult;
}

$this->blockUntilReady();

$outKvm = $this->client->fdb->new('FDBMappedKeyValue*');
$outCount = $this->client->fdb->new('int');
$outMore = $this->client->fdb->new('fdb_bool_t');

$this->client->checkError(
$this->client->fdb->fdb_future_get_mappedkeyvalue_array(
$this->fpointer,
FFI::addr($outKvm),
FFI::addr($outCount),
FFI::addr($outMore),
),
);

$count = $outCount->cdata;
$entries = [];

for ($i = 0; $i < $count; $i++) {
$mapped = $outKvm[$i];

$indexKey = $this->stringFromKey($mapped->key);
$indexValue = $this->stringFromKey($mapped->value);

$variant = $mapped->variant_index;

if ($variant === self::VARIANT_GET_VALUE) {
$results = [];
if ($mapped->reqAndResult->getValue->present) {
$results[] = new KeyValue(
$this->stringFromKey($mapped->reqAndResult->getValue->key),
$this->stringFromKey($mapped->reqAndResult->getValue->value),
);
}
} elseif ($variant === self::VARIANT_GET_RANGE) {
$results = [];
$reqAndResult = $mapped->reqAndResult->getRange->reqAndResult;
$resultCount = $reqAndResult->m_size;
for ($j = 0; $j < $resultCount; $j++) {
$kv = $reqAndResult->data[$j];
$results[] = new KeyValue(
FFI::string($kv->key, $kv->key_length),
FFI::string($kv->value, $kv->value_length),
);
}
} else {
throw new \RuntimeException(sprintf(
'Unknown mapped key value variant index: %d',
$variant,
));
}

$entries[] = new MappedKeyValue($indexKey, $indexValue, $results);
}

$this->cachedResult = new MappedRangeResult($entries, $count, $outMore->cdata !== 0);
$this->releaseMemory();
$this->resolved = true;

return $this->cachedResult;
}

private function stringFromKey(FFI\CData $key): string
{
return FFI::string($key->key, $key->key_length);
}
}
23 changes: 23 additions & 0 deletions src/Future/MappedRangeResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\FoundationDB\Future;

/**
* Result of a mapped range read batch: the mapped index rows plus the
* pagination hint.
*/
final readonly class MappedRangeResult
{
/**
* @param list<\CrazyGoat\FoundationDB\MappedKeyValue> $entries
* @param int<0, max> $count
*/
public function __construct(
public array $entries,
public int $count,
public bool $more,
) {
}
}
25 changes: 25 additions & 0 deletions src/MappedKeyValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace CrazyGoat\FoundationDB;

/**
* A single row of a mapped range read: one index key/value together with the
* records fetched for it by the mapper (single round trip to the storage
* servers).
*/
final readonly class MappedKeyValue
{
/**
* @param string $key The index key.
* @param string $value The index value.
* @param list<KeyValue> $results The records mapped from this index row.
*/
public function __construct(
public string $key,
public string $value,
public array $results,
) {
}
}
69 changes: 69 additions & 0 deletions src/NativeClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,62 @@ final class NativeClient
int key_length;
} FDBKey;

/* Memory layout of KeySelectorRef (packed via #pragma pack(4) in fdb_c.h). */
typedef struct __attribute__((packed)) {
FDBKey key;
fdb_bool_t orEqual;
int offset;
} FDBKeySelector;

/* Memory layout of GetRangeReqAndResultRef (packed via #pragma pack(4) in fdb_c.h). */
typedef struct __attribute__((packed)) {
FDBKeySelector begin;
FDBKeySelector end;
FDBKeyValue* data;
int m_size;
int m_capacity;
} FDBGetRangeReqAndResult;

/* Memory layout of MappedKeyValueRef (packed via #pragma pack(4) in fdb_c.h). */
/* Memory layout of MappedKeyValueRef (not packed in fdb_c.h).
*
* The public fdb_c.h FDBMappedKeyValue only models the getRange
* alternative of the underlying std::variant, which is not what the
* native client actually produces: the reply carries either a point
* lookup (GetValueReqAndResultRef, variant index 0) or a range
* lookup (GetRangeReqAndResultRef, variant index 1). The real C++
* object is laid out as: index key (12B), index value (12B), an
* 80-byte variant union at offset 24, and a 4-byte variant index at
* offset 104 (padded to a 112-byte stride). FDBGetValue /
* FDBGetRangeReqAndResultFull below mirror both alternatives over
* the 80-byte union, so the struct declared here mirrors the real
* memory layout rather than the one from fdb_c.h.
*/
typedef struct __attribute__((packed)) {
FDBKey key;
FDBKey value;
bool present;
unsigned char tail[55];
} FDBGetValueReqAndResult;

typedef struct __attribute__((packed)) {
FDBGetRangeReqAndResult reqAndResult;
unsigned char tail[24];
} FDBGetRangeReqAndResultFull;

typedef union __attribute__((packed)) {
FDBGetValueReqAndResult getValue;
FDBGetRangeReqAndResultFull getRange;
} FDBMappedReqAndResult;

typedef struct __attribute__((packed)) {
FDBKey key;
FDBKey value;
FDBMappedReqAndResult reqAndResult;
int variant_index;
unsigned char tail[4];
} FDBMappedKeyValue;

fdb_error_t fdb_select_api_version_impl(int runtime_version, int header_version);
int fdb_get_max_api_version();
const char* fdb_get_error(fdb_error_t code);
Expand Down Expand Up @@ -102,6 +158,19 @@ final class NativeClient
int limit, int target_bytes, int streaming_mode, int iteration,
fdb_bool_t snapshot, fdb_bool_t reverse
);
FDBFuture* fdb_transaction_get_mapped_range(
FDBTransaction* tr,
const char* begin_key_name, int begin_key_name_length,
fdb_bool_t begin_or_equal, int begin_offset,
const char* end_key_name, int end_key_name_length,
fdb_bool_t end_or_equal, int end_offset,
const char* mapper_name, int mapper_name_length,
int limit, int target_bytes, int streaming_mode, int iteration,
fdb_bool_t snapshot, fdb_bool_t reverse
);
fdb_error_t fdb_future_get_mappedkeyvalue_array(
FDBFuture* f, const FDBMappedKeyValue** out_kvm, int* out_count, fdb_bool_t* out_more
);
FDBFuture* fdb_transaction_get_estimated_range_size_bytes(
FDBTransaction* tr,
const char* begin_key_name, int begin_key_name_length,
Expand Down
Loading
Loading