Skip to content
Open
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
Binary file added docs/images/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions src/CSharpDB.Data/CSharpDbDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ namespace CSharpDB.Data;

public sealed class CSharpDbDataReader : DbDataReader
{
private const int OrdinalLookupThreshold = 8;
private readonly QueryResult _queryResult;
private readonly CommandBehavior _behavior;
private readonly CSharpDbConnection? _connection;
private readonly ColumnDefinition[] _schema;
private readonly Dictionary<string, int>? _ordinalLookup;

private DbValue[]? _currentRow;
private int _currentRowIndex = -1;
Expand All @@ -30,6 +32,7 @@ internal CSharpDbDataReader(
_behavior = behavior;
_connection = connection;
_schema = queryResult.Schema;
_ordinalLookup = BuildOrdinalLookupIfNeeded(_schema);
}

private DbValue[] CurrentRow
Expand Down Expand Up @@ -80,6 +83,9 @@ public override Task<bool> NextResultAsync(CancellationToken cancellationToken)

public override int GetOrdinal(string name)
{
if (_ordinalLookup != null && _ordinalLookup.TryGetValue(name, out int ordinal))
return ordinal;

for (int i = 0; i < _schema.Length; i++)
{
if (string.Equals(_schema[i].Name, name, StringComparison.OrdinalIgnoreCase))
Expand All @@ -88,6 +94,22 @@ public override int GetOrdinal(string name)
throw new IndexOutOfRangeException($"Column '{name}' not found.");
}

private static Dictionary<string, int>? BuildOrdinalLookupIfNeeded(ColumnDefinition[] schema)
{
if (schema.Length < OrdinalLookupThreshold)
return null;

var lookup = new Dictionary<string, int>(schema.Length, StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < schema.Length; i++)
{
string columnName = schema[i].Name;
if (!lookup.ContainsKey(columnName))
lookup[columnName] = i;
}

return lookup;
}

public override string GetDataTypeName(int ordinal)
=> TypeMapper.ToDataTypeName(_schema[ordinal].Type);

Expand Down
44 changes: 34 additions & 10 deletions src/CSharpDB.Storage/BTree/BTree.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,29 +319,53 @@ private async ValueTask<InsertResult> InsertIntoLeafAsync(uint pageId, byte[] pa
// Preserve the stack-built cell for split handling.
byte[] splitCell = GC.AllocateUninitializedArray<byte>(leafCellLength);
stackCell.CopyTo(splitCell);
return await SplitLeafAsync(pageId, page, sp, insertIdx, splitCell, ct);
return await SplitLeafAsync(pageId, page, sp, insertIdx, splitCell, splitCell.Length, ct);
}

byte[] heapCell = BuildLeafCell(key, payload.Span);
if (sp.InsertCell(insertIdx, heapCell))
// For larger payloads, rent a temporary buffer instead of allocating per insert.
byte[] pooledCell = ArrayPool<byte>.Shared.Rent(leafCellLength);
try
{
await _pager.MarkDirtyAsync(pageId, ct);
return new InsertResult { Split = false };
}
var pooledCellSpan = pooledCell.AsSpan(0, leafCellLength);
WriteLeafCell(pooledCellSpan, key, payload.Span);

if (sp.InsertCell(insertIdx, pooledCellSpan))
{
await _pager.MarkDirtyAsync(pageId, ct);
return new InsertResult { Split = false };
}

// Page is full — split
return await SplitLeafAsync(pageId, page, sp, insertIdx, heapCell, ct);
// Page is full — split
return await SplitLeafAsync(pageId, page, sp, insertIdx, pooledCell, leafCellLength, ct);
}
finally
{
ArrayPool<byte>.Shared.Return(pooledCell, clearArray: false);
}
}

private async ValueTask<InsertResult> SplitLeafAsync(uint pageId, byte[] page, SlottedPage sp, int insertIdx, byte[] newCell, CancellationToken ct)
private async ValueTask<InsertResult> SplitLeafAsync(
uint pageId,
byte[] page,
SlottedPage sp,
int insertIdx,
byte[] newCell,
int newCellLength,
CancellationToken ct)
{
int existingCellCount = sp.CellCount;
int totalCellCount = existingCellCount + 1;
int[] cellOffsets = ArrayPool<int>.Shared.Rent(totalCellCount + 1);
byte[]? splitCellBuffer = null;
try
{
splitCellBuffer = BuildSplitCellBuffer(page, sp, insertIdx, newCell, cellOffsets, out int totalCellBytes);
splitCellBuffer = BuildSplitCellBuffer(
page,
sp,
insertIdx,
newCell.AsSpan(0, newCellLength),
cellOffsets,
out int totalCellBytes);
cellOffsets[totalCellCount] = totalCellBytes;
int mid = totalCellCount / 2;

Expand Down
26 changes: 23 additions & 3 deletions src/CSharpDB.Storage/Caching/DictionaryPageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,41 @@ namespace CSharpDB.Storage.Caching;
/// Default in-memory page cache backed by a dictionary.
/// Maintains current behavior (unbounded, no eviction).
/// </summary>
public sealed class DictionaryPageCache : IPageCache
public sealed class DictionaryPageCache : IPageCache, IPageCacheEvictionEvents
{
private readonly Dictionary<uint, byte[]> _pages = new();
public event Action<uint, byte[]>? PageEvicted;

public bool TryGet(uint pageId, out byte[] page) =>
_pages.TryGetValue(pageId, out page!);

public void Set(uint pageId, byte[] page)
{
if (_pages.TryGetValue(pageId, out var existing) && !ReferenceEquals(existing, page))
PageEvicted?.Invoke(pageId, existing);

_pages[pageId] = page;
}

public bool Contains(uint pageId) => _pages.ContainsKey(pageId);

public bool Remove(uint pageId) => _pages.Remove(pageId);
public bool Remove(uint pageId)
{
if (!_pages.Remove(pageId, out var page))
return false;

PageEvicted?.Invoke(pageId, page);
return true;
}

public void Clear() => _pages.Clear();
public void Clear()
{
if (PageEvicted != null)
{
foreach (var entry in _pages)
PageEvicted(entry.Key, entry.Value);
}

_pages.Clear();
}
}
9 changes: 9 additions & 0 deletions src/CSharpDB.Storage/Caching/IPageCacheEvictionEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace CSharpDB.Storage.Caching;

/// <summary>
/// Optional event surface for caches that can report page removals/evictions.
/// </summary>
public interface IPageCacheEvictionEvents
{
event Action<uint, byte[]>? PageEvicted;
}
17 changes: 15 additions & 2 deletions src/CSharpDB.Storage/Caching/LruPageCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ namespace CSharpDB.Storage.Caching;
/// <summary>
/// Bounded page cache with LRU eviction semantics.
/// </summary>
public sealed class LruPageCache : IPageCache
public sealed class LruPageCache : IPageCache, IPageCacheEvictionEvents
{
private readonly int _capacity;
private readonly Dictionary<uint, CacheEntry> _entries;
private readonly LinkedList<uint> _usageOrder = new();
public event Action<uint, byte[]>? PageEvicted;

private sealed class CacheEntry
{
Expand Down Expand Up @@ -41,6 +42,9 @@ public void Set(uint pageId, byte[] page)
{
if (_entries.TryGetValue(pageId, out var existing))
{
if (!ReferenceEquals(existing.Page, page))
PageEvicted?.Invoke(pageId, existing.Page);

_entries[pageId] = new CacheEntry
{
Page = page,
Expand Down Expand Up @@ -70,11 +74,18 @@ public bool Remove(uint pageId)

_usageOrder.Remove(entry.Node);
_entries.Remove(pageId);
PageEvicted?.Invoke(pageId, entry.Page);
return true;
}

public void Clear()
{
if (PageEvicted != null)
{
foreach (var entry in _entries)
PageEvicted(entry.Key, entry.Value.Page);
}

_usageOrder.Clear();
_entries.Clear();
}
Expand All @@ -86,7 +97,9 @@ private void EvictLeastRecentlyUsed()
return;

_usageOrder.RemoveFirst();
_entries.Remove(first.Value);
uint pageId = first.Value;
if (_entries.Remove(pageId, out var entry))
PageEvicted?.Invoke(pageId, entry.Page);
}

private void Touch(LinkedListNode<uint> node)
Expand Down
Loading