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
58 changes: 54 additions & 4 deletions src/SharpCoreDB/DataStructures/Table.CRUD.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1946,6 +1946,13 @@
// from the file INCLUDING the stale record).
EnsureAllRegisteredIndexesLoaded();

// Whole-file snapshot for the per-row fastPatch reads below (same guard as the DELETE
// path): reading the small plaintext file once replaces one pread pair per updated row.
// A position that already has a buffered in-place overwrite in this transaction is
// NEVER served from the snapshot (its disk bytes would be stale) — it falls back to the
// per-record read, which honors the write-behind buffer.
byte[]? wholeFile = TryLoadWholeFileForRowAccess();

// B8: single-pass contiguous UPDATE — when every operation is a `pk = <literal>` match on a
// plaintext fixed-width table with physically adjacent PK-ordered records, the old records
// are read as ONE contiguous byte range and patched in memory (no per-row pread). Strictly
Expand Down Expand Up @@ -2007,7 +2014,17 @@
var fastSearch = this.Index.Search(pkWhereVal?.ToString() ?? string.Empty);
if (fastSearch.Found)
{
var fastData = engine.Read(Name, fastSearch.Value);
byte[]? fastData;
if (fastPatch && wholeFile != null &&
(this.storage is null || !this.storage.HasBufferedOverwriteAt(DataFile, fastSearch.Value)))
{
fastData = TrySlicePayloadFromFile(wholeFile, fastSearch.Value);
}
else
{
fastData = engine.Read(Name, fastSearch.Value);
}

if (fastData != null)
{
rows = fastPatch
Expand Down Expand Up @@ -2036,7 +2053,17 @@
rows = [];
foreach (var pos in hashIndex.LookupPositionsUnsafe(key))
{
var data = engine.Read(Name, pos);
byte[]? data;
if (fastPatch && wholeFile != null &&
(this.storage is null || !this.storage.HasBufferedOverwriteAt(DataFile, pos)))
{
data = TrySlicePayloadFromFile(wholeFile, pos);
}
else
{
data = engine.Read(Name, pos);
}

if (data != null)
{
if (fastPatch)
Expand Down Expand Up @@ -3064,7 +3091,7 @@

// A1: when the (plaintext, legacy variable-length) data file is small enough, read it
// ONCE and resolve every target from memory instead of one pread pair per deleted row.
byte[]? wholeFile = TryLoadWholeFileForDeleteResolution();
byte[]? wholeFile = TryLoadWholeFileForRowAccess();

var recordsToDelete = new List<(long storagePosition, Dictionary<string, object> row)>();

Expand Down Expand Up @@ -3317,7 +3344,7 @@
// per deleted row. Guarded to plaintext legacy variable-length files below this size.
private const long WholeFileDeleteResolutionLimitBytes = 32 * 1024 * 1024;

private byte[]? TryLoadWholeFileForDeleteResolution()
private byte[]? TryLoadWholeFileForRowAccess()
{
if (_fixedWidthRecords ||
this.storage is null ||
Expand Down Expand Up @@ -3351,6 +3378,29 @@
return DeserializeDeleteKeyRow(wholeFile.AsSpan((int)position + 4, recordLength), wanted);
}

/// <summary>
/// Copies the raw (plaintext) payload of the record at <paramref name="position"/> out of a
/// whole-file snapshot. Returns null when the position/length is not fully contained in the
/// snapshot (caller falls back to the per-record read).
/// </summary>
private byte[]? TrySlicePayloadFromFile(byte[] wholeFile, long position)

Check warning on line 3386 in src/SharpCoreDB/DataStructures/Table.CRUD.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'TrySlicePayloadFromFile' a static method.

See more on https://sonarcloud.io/project/issues?id=MPCoreDeveloper_SharpCoreDB&issues=AaBqyd7m7dBg3zLCZ3mx&open=AaBqyd7m7dBg3zLCZ3mx&pullRequest=374
{
if (position < 0 || position + 4 > wholeFile.Length)
{
return null;
}

int recordLength = System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(wholeFile.AsSpan((int)position, 4));
if (recordLength <= 0 || position + 4 + recordLength > wholeFile.Length)
{
return null;
}

var payload = new byte[recordLength];
wholeFile.AsSpan((int)position + 4, recordLength).CopyTo(payload);
return payload;
}

/// <summary>
/// Shared B8/B9 probe: resolves each key's record position through the PK B-tree, requires the
/// positions to be physically adjacent at the fixed-width stride, reads the whole contiguous
Expand Down
9 changes: 9 additions & 0 deletions src/SharpCoreDB/Interfaces/IStorage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ bool OverwriteRecordAtSameLength(string path, long offset, byte[] data) =>
/// </summary>
bool HasBufferedOverwrite(string path) => false;

/// <summary>
/// True when an in-place overwrite is currently buffered for exactly <paramref name="offset"/>
/// of <paramref name="path"/>. Used by whole-file row-resolution fast paths (DELETE/UPDATE) to
/// decide whether a disk snapshot slice of a position is fresh or must go through the
/// per-record read path (which honors the write-behind buffer). Defaults to false (mock/
/// alternative storages have no write-behind buffer to bypass).
/// </summary>
bool HasBufferedOverwriteAt(string path, long offset) => false;

/// <summary>
/// Appends multiple binary data blocks to a file in a single batch operation (used for batch inserts).
/// </summary>
Expand Down
4 changes: 4 additions & 0 deletions src/SharpCoreDB/Services/Storage.Append.cs
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,10 @@ public bool OverwriteRecordAtSameLength(string path, long offset, byte[] data)
public bool HasBufferedOverwrite(string path) =>
!bufferedOverwrites.IsEmpty && bufferedOverwrites.ContainsKey(path);

/// <inheritdoc />
public bool HasBufferedOverwriteAt(string path, long offset) =>
bufferedOverwrites.TryGetValue(path, out var overwrites) && overwrites.ContainsKey(offset);

/// <inheritdoc />
public bool AreRecordsEncrypted(string path) => UseRecordEncryption && FileHasEncryptedHeader(path);

Expand Down
Loading