Skip to content

Commit 7ab07bb

Browse files
author
MPCoreDeveloper
committed
perf: run the contiguous UPDATE/DELETE fast paths on plaintext default-config tables
The bulk fast paths previously required DatabaseConfig.NoEncryptMode, although per-record encryption is opt-in (EnableAtRestRecordEncryption): default-config databases already store plaintext records. Gate on the storage layer's runtime AreRecordsEncrypted (UseRecordEncryption && file carries the encrypted magic header) instead of the config flag, so default-config users get the single-pass UPDATE/DELETE path too; genuinely encrypted tables still fall back to the generic loop.
1 parent 668ea8e commit 7ab07bb

4 files changed

Lines changed: 48 additions & 9 deletions

File tree

‎src/SharpCoreDB/DataStructures/Table.CRUD.cs‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2356,15 +2356,16 @@ private bool TryBulkUpdateContiguousFixedWidth(
23562356
}
23572357

23582358
// Narrow, conservative gate: fixed-width columnar table with an explicit PK, plaintext
2359-
// records only (a raw contiguous read must equal the logical record bytes), no buffered
2360-
// overwrites for this file, and no CHECK constraints (mirrors the generic fastPatch gate).
2359+
// records only (no per-record encryption magic — a raw contiguous read must equal the
2360+
// logical record bytes), no buffered overwrites for this file, and no CHECK constraints
2361+
// (mirrors the generic fastPatch gate).
23612362
if (!_fixedWidthRecords ||
23622363
StorageMode != StorageMode.Columnar ||
23632364
this.PrimaryKeyIndex < 0 ||
23642365
this.TableCheckConstraints.Count > 0 ||
23652366
HasColumnCheckConstraints() ||
23662367
this.storage is null ||
2367-
this._config is not { NoEncryptMode: true } ||
2368+
this.storage.AreRecordsEncrypted(DataFile) ||
23682369
this.storage.HasBufferedOverwrite(DataFile))
23692370
{
23702371
return false;
@@ -3047,13 +3048,14 @@ private bool TryBulkDeleteContiguousFixedWidth(List<string> whereConditions)
30473048
}
30483049

30493050
// Identical safety gate to the UPDATE fast path: fixed-width columnar table with an explicit
3050-
// PK, plaintext records only, and no buffered overwrites (a raw range read must equal the
3051-
// logical record bytes). DeleteMultiple loads every registered hash index before calling this.
3051+
// PK, plaintext records only (no per-record encryption magic — a raw range read must equal
3052+
// the logical record bytes), and no buffered overwrites. DeleteMultiple loads every
3053+
// registered hash index before calling this.
30523054
if (!_fixedWidthRecords ||
30533055
StorageMode != StorageMode.Columnar ||
30543056
this.PrimaryKeyIndex < 0 ||
30553057
this.storage is null ||
3056-
this._config is not { NoEncryptMode: true } ||
3058+
this.storage.AreRecordsEncrypted(DataFile) ||
30573059
this.storage.HasBufferedOverwrite(DataFile))
30583060
{
30593061
return false;

‎src/SharpCoreDB/Interfaces/IStorage.cs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,13 @@ bool OverwriteRecordAtSameLength(string path, long offset, byte[] data) =>
163163
/// </summary>
164164
byte[]? ReadBytesRange(string path, long offset, int length) => null;
165165

166+
/// <summary>
167+
/// True when the file at <paramref name="path"/> stores per-record encrypted (ciphertext)
168+
/// payloads (it carries the encrypted-table magic header). Raw range reads must never be used on
169+
/// such files; the default returns false (plaintext / legacy layouts).
170+
/// </summary>
171+
bool AreRecordsEncrypted(string path) => false;
172+
166173
/// <summary>
167174
/// Enumerates every record in a table data file, yielding the literal file offset of the
168175
/// 4-byte length prefix (the offset returned by <see cref="AppendBytes"/>) together with the

‎src/SharpCoreDB/Services/Storage.Append.cs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,9 @@ public bool OverwriteRecordAtSameLength(string path, long offset, byte[] data)
560560
public bool HasBufferedOverwrite(string path) =>
561561
!bufferedOverwrites.IsEmpty && bufferedOverwrites.ContainsKey(path);
562562

563+
/// <inheritdoc />
564+
public bool AreRecordsEncrypted(string path) => UseRecordEncryption && FileHasEncryptedHeader(path);
565+
563566
/// <inheritdoc />
564567
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
565568
public long[] AppendBytesMultiple(string path, List<byte[]> dataBlocks)

‎tests/SharpCoreDB.Tests/FixedWidthBulkUpdateTests.cs‎

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public void Dispose()
4242
private IDatabase CreateDb(bool noEncrypt = true) => _factory.Create(_dirPath, "pw", isReadOnly: false,
4343
config: new DatabaseConfig { NoEncryptMode = noEncrypt });
4444

45+
private IDatabase CreateEncryptedDb() => _factory.Create(_dirPath, "pw", isReadOnly: false,
46+
config: new DatabaseConfig { NoEncryptMode = false, EnableAtRestRecordEncryption = true });
47+
4548
private static Table TableOf(IDatabase db, string tableName)
4649
{
4750
Assert.True(db.TryGetTable(tableName, out var t));
@@ -209,9 +212,11 @@ public void IndexedColumnUpdate_FallsBack_AndRepointsIndex()
209212
}
210213

211214
[Fact]
212-
public void EncryptedOrDefaultConfig_FallsBack_AndStaysCorrect()
215+
public void DefaultPlaintextConfig_EngagesBulkPath_AndStaysCorrect()
213216
{
214-
var db = CreateDb(noEncrypt: false); // default config: the gate requires NoEncryptMode
217+
// Default config (no NoEncryptMode) stores plaintext records unless per-record encryption
218+
// is opted in, so the runtime magic-header gate now admits the fast path here too.
219+
var db = CreateDb(noEncrypt: false);
215220
try
216221
{
217222
db.ExecuteSQL("CREATE TABLE docs (id INTEGER PRIMARY KEY, name TEXT, score REAL)");
@@ -221,7 +226,29 @@ public void EncryptedOrDefaultConfig_FallsBack_AndStaysCorrect()
221226
var table = TableOf(db, "docs");
222227
db.ExecuteBatchSQL(BuildUpdates(1, 300, "score = 4.25"));
223228
db.Flush();
224-
Assert.Equal(0, table.BulkContiguousUpdateBatches);
229+
Assert.Equal(1, table.BulkContiguousUpdateBatches);
230+
231+
var rows = db.ExecuteQuery("SELECT score FROM docs WHERE id = 300");
232+
Assert.Single(rows);
233+
Assert.Equal(4.25, Convert.ToDouble(rows[0]["score"]));
234+
}
235+
finally { (db as IDisposable)?.Dispose(); }
236+
}
237+
238+
[Fact]
239+
public void PerRecordEncryption_FallsBackToGenericLoop_AndStaysCorrect()
240+
{
241+
var db = CreateEncryptedDb();
242+
try
243+
{
244+
db.ExecuteSQL("CREATE TABLE docs (id INTEGER PRIMARY KEY, name TEXT, score REAL)");
245+
InsertDocs(db, 1, 300);
246+
db.Flush();
247+
248+
var table = TableOf(db, "docs");
249+
db.ExecuteBatchSQL(BuildUpdates(1, 300, "score = 4.25"));
250+
db.Flush();
251+
Assert.Equal(0, table.BulkContiguousUpdateBatches); // ciphertext records → generic loop
225252

226253
var rows = db.ExecuteQuery("SELECT score FROM docs WHERE id = 300");
227254
Assert.Single(rows);

0 commit comments

Comments
 (0)