[1/4] Multi-hop query: Mutation + multi-get query#226
Conversation
Introduces a wide row structure that stores multiple edges per source in a single HBase row, enabling efficient multi-get queries via ColumnRangeFilter. - EdgeCacheRecord with encoder/decoder - Mutation pipeline support (CREATE/DELETE/UPDATE) - Seek API with cursor-based pagination
| offset?.let { | ||
| val decoded = CryptoUtils.decodeAndDecryptUrlSafe(it) | ||
| decoded + 0x00.toByte() | ||
| } |
There was a problem hiding this comment.
@em3s
Used qualifier + 0x00 to express "start after" instead of ColumnRangeFilter's minColumnInclusive=false.
// current: byte-append trick
val offsetNext = decoded + 0x00.toByte()
ColumnRangeFilter(min, minColumnInclusive = true, max, maxColumnInclusive = false)
// alternative: use native exclusive flag
val offsetNext = decoded
ColumnRangeFilter(min, minColumnInclusive = false, max, maxColumnInclusive = false)The byte-append keeps hbaseGetWideRow simpler (no inclusive params), but the native flag avoids byte-level assumptions. Open to either approach.
There was a problem hiding this comment.
@em3s
Done. Switched to native exclusive flag and removed the byte-append trick.
| val nextOffset = | ||
| if (hasNext) { | ||
| results.lastOrNull()?.qualifier?.let { | ||
| CryptoUtils.encryptAndEncodeUrlSafe(it) |
There was a problem hiding this comment.
OK, use CryptoUtils and will address in a separate task.
|
@em3s
Happy to split sooner if preferred. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
| val nextOffset = | ||
| if (hasNext) { | ||
| results.lastOrNull()?.qualifier?.let { | ||
| CryptoUtils.encryptAndEncodeUrlSafe(it) |
There was a problem hiding this comment.
OK, use CryptoUtils and will address in a separate task.
|
|
||
| while (buffer.hasRemaining()) { | ||
| val propertyHashKey: Int = buffer.getValue() | ||
| val propertyValue: Any? = buffer.getValueOrNull() |
There was a problem hiding this comment.
decodeValue in EdgeIndexRecordMapper uses buffer.getValue() — why the different approach here?
There was a problem hiding this comment.
@em3s
Nullable properties end up as null in HBase (specialStateValueToNull() converts __UNSET__ → null before write).
e.g. schema has receivedFrom (nullable=true), mutation omits it → stored as null in EdgeIndex value.
V2 decoder handles this fine (ValueUtils.deserialize() returns null), but V3 EdgeIndexRecordMapper.decodeValue() uses getValue() which throws. Should use getValueOrNull() like EdgeCacheRecordMapper.
| import org.junit.jupiter.api.Test | ||
|
|
||
| /** | ||
| * EdgeCache (Wide Row) HBase layout: |
There was a problem hiding this comment.
just EdgeCache (Wide Row) layout:
|
Large changes OK. This PR is not production-facing, so I suggest we merge first and iterate. |
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Null properties are stored as "__UNSET__", not raw null. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This reverts commit 3c8edce.
Replace qualifier + 0x00 byte trick with ColumnRangeFilter's minColumnInclusive=false for "start after" semantics. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@em3s |
Part of #214
Summary
Introduces EdgeCache, a wide row structure that stores multiple edges per source in a single HBase row. Adds mutation support (CREATE/DELETE/UPDATE) and a seek API for range queries via
ColumnRangeFilter.Data Model
Unlike EdgeIndex (narrow row: one row per edge), EdgeCache stores multiple edges as qualifiers within a single row. Supports range queries via
ColumnRangeFilter.Test Plan
EdgeCacheRecordMapperTestEdgeMutationBuilderTestV2BackedTableBindingTestEdgeCacheQueryE2ETestRun:
./gradlew test