-
Notifications
You must be signed in to change notification settings - Fork 0
Add support for Merge operator #42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
08a2eb2 to
81f3746
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds comprehensive support for RocksDB merge operators, enabling atomic read-modify-write operations without requiring separate read-before-write operations. This feature is particularly useful for counters, list appends, and other accumulative data structures.
Key Changes
- Introduces new abstractions for merge operations (
IMergeOperator,IMergeAccessor,MergeableRocksDbStore) - Implements two concrete merge operators:
ListAppendMergeOperator(for simple list appends) andListMergeOperator(for add/remove operations) - Upgrades RocksDB package from version 9.10.0 to 10.4.2, which includes API changes
Reviewed changes
Copilot reviewed 17 out of 18 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
src/RocksDb.Extensions/IMergeOperator.cs |
Core interface defining merge operator contract with FullMerge and PartialMerge methods |
src/RocksDb.Extensions/IMergeAccessor.cs |
Accessor interface extending IRocksDbAccessor with Merge operation support |
src/RocksDb.Extensions/MergeableRocksDbStore.cs |
Base class for stores that support merge operations, providing common CRUD operations |
src/RocksDb.Extensions/MergeAccessor.cs |
Implementation of merge accessor with proper serialization for key and operand |
src/RocksDb.Extensions/MergeOperatorConfig.cs |
Internal configuration class for merge operators per column family |
src/RocksDb.Extensions/MergeOperators/ListAppendMergeOperator.cs |
Merge operator for appending items to lists |
src/RocksDb.Extensions/MergeOperators/ListMergeOperator.cs |
Advanced merge operator supporting both add and remove operations on lists |
src/RocksDb.Extensions/MergeOperators/ListOperation.cs |
Data class representing list add/remove operations |
src/RocksDb.Extensions/MergeOperators/OperationType.cs |
Enum for operation types (Add/Remove) |
src/RocksDb.Extensions/ListOperationSerializer.cs |
Serializer for ListOperation with size-prefixed format |
src/RocksDb.Extensions/IRocksDbBuilder.cs |
Added AddMergeableStore method to builder interface |
src/RocksDb.Extensions/RocksDbBuilder.cs |
Implementation of mergeable store registration with merge operator configuration |
src/RocksDb.Extensions/RocksDbContext.cs |
Updated column family creation to support merge operators; changed API method name for RocksDB 10.x |
src/RocksDb.Extensions/RocksDbOptions.cs |
Added dictionary to track merge operators per column family |
src/RocksDb.Extensions/RocksDbAccessor.cs |
Changed field visibility modifiers and added Merge method implementation |
src/RocksDb.Extensions/RocksDbStore.cs |
New base class file providing standard store operations |
src/RocksDb.Extensions/RocksDb.Extensions.csproj |
Upgraded RocksDB package to version 10.4.2 |
test/RocksDb.Extensions.Tests/MergeOperatorTests.cs |
Comprehensive test suite covering list append, add/remove operations, and edge cases |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| protected private readonly ISerializer<TValue> _valueSerializer; | ||
| protected private readonly RocksDbContext _rocksDbContext; |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invalid access modifier combination: protected private is not a valid C# modifier combination. This should be either protected, private, or private protected.
| protected private readonly ISerializer<TValue> _valueSerializer; | |
| protected private readonly RocksDbContext _rocksDbContext; | |
| private protected readonly ISerializer<TValue> _valueSerializer; | |
| private protected readonly RocksDbContext _rocksDbContext; |
| protected private readonly ISerializer<TValue> _valueSerializer; | ||
| protected private readonly RocksDbContext _rocksDbContext; |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invalid access modifier combination: private protected should be private protected (in that order). The correct syntax is private protected, not protected private.
| protected private readonly ISerializer<TValue> _valueSerializer; | |
| protected private readonly RocksDbContext _rocksDbContext; | |
| private protected readonly ISerializer<TValue> _valueSerializer; | |
| private protected readonly RocksDbContext _rocksDbContext; |
|
|
||
| public void WriteTo(ref ListOperation<T> value, IBufferWriter<byte> buffer) | ||
| { | ||
| throw new NotImplementedException(); |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WriteTo(ref ListOperation<T> value, IBufferWriter<byte> buffer) method throws NotImplementedException, but this is a required method for the serializer interface. This will cause runtime failures when the buffer writer path is used instead of the span-based path. This method should be implemented to support variable-sized serialization.
| throw new NotImplementedException(); | |
| // Write operation type (1 byte) | |
| var opSpan = buffer.GetSpan(sizeof(byte)); | |
| opSpan[0] = (byte)value.Type; | |
| buffer.Advance(sizeof(byte)); | |
| // Write count (4 bytes) | |
| var countSpan = buffer.GetSpan(sizeof(int)); | |
| BitConverter.TryWriteBytes(countSpan, value.Items.Count); | |
| buffer.Advance(sizeof(int)); | |
| // Write each item with size prefix and data | |
| for (int i = 0; i < value.Items.Count; i++) | |
| { | |
| var item = value.Items[i]; | |
| if (_itemSerializer.TryCalculateSize(ref item, out var itemSize)) | |
| { | |
| // Write size prefix (4 bytes) | |
| var sizeSpan = buffer.GetSpan(sizeof(int)); | |
| BitConverter.TryWriteBytes(sizeSpan, itemSize); | |
| buffer.Advance(sizeof(int)); | |
| // Write item data | |
| var itemSpan = buffer.GetSpan(itemSize); | |
| var tmpSpan = itemSpan.Slice(0, itemSize); | |
| _itemSerializer.WriteTo(ref item, ref tmpSpan); | |
| buffer.Advance(itemSize); | |
| } | |
| } |
| // Write each item with size prefix | ||
| for (int i = 0; i < value.Items.Count; i++) | ||
| { | ||
| var item = value.Items[i]; | ||
| if (_itemSerializer.TryCalculateSize(ref item, out var itemSize)) | ||
| { | ||
| slice = span.Slice(offset, sizeof(int)); | ||
| BitConverter.TryWriteBytes(slice, itemSize); | ||
| offset += sizeof(int); | ||
|
|
||
| slice = span.Slice(offset, itemSize); | ||
| _itemSerializer.WriteTo(ref item, ref slice); | ||
| offset += itemSize; | ||
| } | ||
| } |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WriteTo method doesn't handle the case where _itemSerializer.TryCalculateSize returns false. When this happens, the item cannot be serialized using the span-based approach, but the method silently skips these items, leading to data loss. The method should fall back to using a buffer writer for items that don't support fixed-size serialization.
| protected private readonly ISerializer<TValue> _valueSerializer; | ||
| protected private readonly RocksDbContext _rocksDbContext; |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invalid access modifier combination: protected private is not a valid C# modifier combination. This should be either protected, private, or private protected.
| protected private readonly ISerializer<TValue> _valueSerializer; | |
| protected private readonly RocksDbContext _rocksDbContext; | |
| private protected readonly ISerializer<TValue> _valueSerializer; | |
| private protected readonly RocksDbContext _rocksDbContext; |
| : Span<byte>.Empty; | ||
|
|
||
| ReadOnlySpan<byte> keySpan = keyBuffer; | ||
| ArrayPoolBufferWriter<byte>? keyBufferWriter = null; |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.
|
|
||
|
|
||
| ReadOnlySpan<byte> valueSpan = valueBuffer; | ||
| ArrayPoolBufferWriter<byte>? valueBufferWriter = null; |
Copilot
AI
Dec 4, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.
|
@copilot try to fix the build. it's passing on MacOS |
460c597 to
c985414
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 16 out of 17 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| public void WriteTo(ref ListOperation<T> value, ref Span<byte> span) | ||
| { | ||
| int offset = 0; | ||
|
|
||
| // Write operation type (1 byte) | ||
| span[offset] = (byte)value.Type; | ||
| offset += sizeof(byte); | ||
|
|
||
| // Write count | ||
| var slice = span.Slice(offset, sizeof(int)); | ||
| BitConverter.TryWriteBytes(slice, value.Items.Count); | ||
| offset += sizeof(int); | ||
|
|
||
| // Write each item with size prefix | ||
| for (int i = 0; i < value.Items.Count; i++) | ||
| { | ||
| var item = value.Items[i]; | ||
| if (_itemSerializer.TryCalculateSize(ref item, out var itemSize)) | ||
| { | ||
| slice = span.Slice(offset, sizeof(int)); | ||
| BitConverter.TryWriteBytes(slice, itemSize); | ||
| offset += sizeof(int); | ||
|
|
||
| slice = span.Slice(offset, itemSize); | ||
| _itemSerializer.WriteTo(ref item, ref slice); | ||
| offset += itemSize; | ||
| } | ||
| } | ||
| } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The WriteTo method only writes items when TryCalculateSize succeeds for each item, but there's no handling for the case when TryCalculateSize returns false. This creates an inconsistency with the TryCalculateSize method (which also has this issue) and could result in incomplete serialization where the count written doesn't match the actual number of items serialized.
| /// public class CounterStore : MergeableRocksDbStore<string, long, long> | ||
| /// { | ||
| /// public CounterStore(IRocksDbAccessor<string, long> accessor, IMergeAccessor<string, long> mergeAccessor) | ||
| /// : base(accessor, mergeAccessor) { } | ||
| /// | ||
| /// public void Increment(string key, long delta = 1) => Merge(key, delta); | ||
| /// } |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example in the documentation shows a CounterStore constructor that accepts two parameters (accessor and mergeAccessor), but based on the actual implementation pattern in the codebase (see test file), stores should only accept the mergeAccessor parameter. This inconsistency could confuse developers trying to implement their own mergeable stores.
| } | ||
|
|
||
| if (_checkIfExists && _rocksDbContext.Db.HasKey(keySpan, _columnFamily.Handle) == false) | ||
| if (_checkIfExists && RocksDbContext.Db.HasKey(keySpan, ColumnFamily.Handle) == false) |
Copilot
AI
Dec 11, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The expression 'A == false' can be simplified to '!A'.
| if (_checkIfExists && RocksDbContext.Db.HasKey(keySpan, ColumnFamily.Handle) == false) | |
| if (_checkIfExists && !RocksDbContext.Db.HasKey(keySpan, ColumnFamily.Handle)) |
No description provided.