Skip to content

Conversation

@Havret
Copy link
Owner

@Havret Havret commented Dec 4, 2025

No description provided.

@Havret Havret force-pushed the merge_support_wip branch from 08a2eb2 to 81f3746 Compare December 4, 2025 09:35
Copy link

Copilot AI left a 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) and ListMergeOperator (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.

Comment on lines 13 to 14
protected private readonly ISerializer<TValue> _valueSerializer;
protected private readonly RocksDbContext _rocksDbContext;
Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
protected private readonly ISerializer<TValue> _valueSerializer;
protected private readonly RocksDbContext _rocksDbContext;
private protected readonly ISerializer<TValue> _valueSerializer;
private protected readonly RocksDbContext _rocksDbContext;

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 14
protected private readonly ISerializer<TValue> _valueSerializer;
protected private readonly RocksDbContext _rocksDbContext;
Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
protected private readonly ISerializer<TValue> _valueSerializer;
protected private readonly RocksDbContext _rocksDbContext;
private protected readonly ISerializer<TValue> _valueSerializer;
private protected readonly RocksDbContext _rocksDbContext;

Copilot uses AI. Check for mistakes.

public void WriteTo(ref ListOperation<T> value, IBufferWriter<byte> buffer)
{
throw new NotImplementedException();
Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
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);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines 57 to 71
// 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;
}
}
Copy link

Copilot AI Dec 4, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 13 to 14
protected private readonly ISerializer<TValue> _valueSerializer;
protected private readonly RocksDbContext _rocksDbContext;
Copy link

Copilot AI Dec 4, 2025

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.

Suggested change
protected private readonly ISerializer<TValue> _valueSerializer;
protected private readonly RocksDbContext _rocksDbContext;
private protected readonly ISerializer<TValue> _valueSerializer;
private protected readonly RocksDbContext _rocksDbContext;

Copilot uses AI. Check for mistakes.
: Span<byte>.Empty;

ReadOnlySpan<byte> keySpan = keyBuffer;
ArrayPoolBufferWriter<byte>? keyBufferWriter = null;
Copy link

Copilot AI Dec 4, 2025

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 uses AI. Check for mistakes.


ReadOnlySpan<byte> valueSpan = valueBuffer;
ArrayPoolBufferWriter<byte>? valueBufferWriter = null;
Copy link

Copilot AI Dec 4, 2025

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 uses AI. Check for mistakes.
@Havret
Copy link
Owner Author

Havret commented Dec 5, 2025

@copilot try to fix the build. it's passing on MacOS

Copy link

Copilot AI commented Dec 5, 2025

@Havret I've opened a new pull request, #43, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

Copilot AI left a 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.

Comment on lines 44 to 72
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;
}
}
}
Copy link

Copilot AI Dec 11, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines 28 to 34
/// public class CounterStore : MergeableRocksDbStore&lt;string, long, long&gt;
/// {
/// public CounterStore(IRocksDbAccessor&lt;string, long&gt; accessor, IMergeAccessor&lt;string, long&gt; mergeAccessor)
/// : base(accessor, mergeAccessor) { }
///
/// public void Increment(string key, long delta = 1) => Merge(key, delta);
/// }
Copy link

Copilot AI Dec 11, 2025

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.

Copilot uses AI. Check for mistakes.
}

if (_checkIfExists && _rocksDbContext.Db.HasKey(keySpan, _columnFamily.Handle) == false)
if (_checkIfExists && RocksDbContext.Db.HasKey(keySpan, ColumnFamily.Handle) == false)
Copy link

Copilot AI Dec 11, 2025

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'.

Suggested change
if (_checkIfExists && RocksDbContext.Db.HasKey(keySpan, ColumnFamily.Handle) == false)
if (_checkIfExists && !RocksDbContext.Db.HasKey(keySpan, ColumnFamily.Handle))

Copilot uses AI. Check for mistakes.
@Havret Havret closed this Dec 17, 2025
@Havret Havret deleted the merge_support_wip branch December 17, 2025 23:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants