Skip to content

Commit 65bda1a

Browse files
refactor: reduce cognitive complexity (SonarCloud S3776) for 13 quick-win methods
1 parent 570116e commit 65bda1a

9 files changed

Lines changed: 467 additions & 307 deletions

File tree

‎src/SharpCoreDB/DataStructures/BTree.cs‎

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -366,60 +366,79 @@ private bool DeleteFromNode(Node node, TKey key)
366366
return true;
367367
}
368368

369-
// Internal (separator) node: values live in leaves, so this node only routes ranges.
370-
// Removing the separator outright would leave the child-pointer ↔ separator mapping
371-
// inconsistent (keys between the deleted separator and the next one would become
372-
// unreachable), so the separator is replaced by its in-order successor taken from the
373-
// right subtree's leftmost leaf, and that leaf entry is then deleted recursively.
374-
var successorChild = node.childrenArray[i + 1];
375-
while (!successorChild.IsLeaf)
376-
{
377-
successorChild = successorChild.childrenArray[0];
378-
}
369+
return DeleteSeparatorFromInternalNode(node, i);
370+
}
379371

380-
if (successorChild.keysCount > 0)
381-
{
382-
node.keysArray[i] = successorChild.keysArray[0];
383-
if (i < node.valuesCount)
384-
{
385-
node.valuesArray[i] = successorChild.valuesArray[0];
386-
}
372+
if (!node.IsLeaf)
373+
{
374+
return DeleteFromNode(node.childrenArray[i], key);
375+
}
387376

388-
return DeleteFromNode(node.childrenArray[i + 1], node.keysArray[i]);
389-
}
377+
return false;
378+
}
390379

391-
// The right subtree is empty (fully drained) — fall back to the left subtree's maximum
392-
// when it still holds entries.
393-
var predecessorChild = node.childrenArray[i];
394-
while (!predecessorChild.IsLeaf)
380+
/// <summary>
381+
/// Deletes the separator at position <paramref name="i"/> of an internal (routing) node.
382+
/// Since values live in leaves, the separator is replaced by its in-order successor taken
383+
/// from the right subtree's leftmost leaf (which is then deleted recursively). When that
384+
/// subtree is fully drained, the left subtree's maximum is used instead. Only when both
385+
/// neighbour subtrees are exhausted is the separator dropped together with its empty right
386+
/// child, so the child-pointer ↔ separator mapping stays consistent.
387+
/// </summary>
388+
private bool DeleteSeparatorFromInternalNode(Node node, int i)
389+
{
390+
var successorChild = FindLeftmostLeaf(node.childrenArray[i + 1]);
391+
if (successorChild.keysCount > 0)
392+
{
393+
node.keysArray[i] = successorChild.keysArray[0];
394+
if (i < node.valuesCount)
395395
{
396-
predecessorChild = predecessorChild.childrenArray[predecessorChild.childrenCount - 1];
396+
node.valuesArray[i] = successorChild.valuesArray[0];
397397
}
398398

399-
if (predecessorChild.keysCount > 0)
400-
{
401-
int predPos = predecessorChild.keysCount - 1;
402-
node.keysArray[i] = predecessorChild.keysArray[predPos];
403-
if (i < node.valuesCount)
404-
{
405-
node.valuesArray[i] = predecessorChild.valuesArray[predPos];
406-
}
399+
return DeleteFromNode(node.childrenArray[i + 1], node.keysArray[i]);
400+
}
407401

408-
return DeleteFromNode(node.childrenArray[i], node.keysArray[i]);
402+
// The right subtree is empty (fully drained) — fall back to the left subtree's maximum
403+
// when it still holds entries.
404+
var predecessorChild = FindRightmostLeaf(node.childrenArray[i]);
405+
if (predecessorChild.keysCount > 0)
406+
{
407+
int predPos = predecessorChild.keysCount - 1;
408+
node.keysArray[i] = predecessorChild.keysArray[predPos];
409+
if (i < node.valuesCount)
410+
{
411+
node.valuesArray[i] = predecessorChild.valuesArray[predPos];
409412
}
410413

411-
// Both neighbour subtrees are drained — drop the separator together with its empty
412-
// right child so the child pointer count stays consistent with the key count.
413-
RemoveKeyAt(node, i);
414-
RemoveChildAt(node, i + 1);
415-
return true;
414+
return DeleteFromNode(node.childrenArray[i], node.keysArray[i]);
416415
}
417-
else if (!node.IsLeaf)
416+
417+
// Both neighbour subtrees are drained — drop the separator together with its empty
418+
// right child so the child pointer count stays consistent with the key count.
419+
RemoveKeyAt(node, i);
420+
RemoveChildAt(node, i + 1);
421+
return true;
422+
}
423+
424+
private static Node FindLeftmostLeaf(Node node)
425+
{
426+
while (!node.IsLeaf)
418427
{
419-
return DeleteFromNode(node.childrenArray[i], key);
428+
node = node.childrenArray[0];
420429
}
421430

422-
return false;
431+
return node;
432+
}
433+
434+
private static Node FindRightmostLeaf(Node node)
435+
{
436+
while (!node.IsLeaf)
437+
{
438+
node = node.childrenArray[node.childrenCount - 1];
439+
}
440+
441+
return node;
423442
}
424443

425444
private static void RemoveChildAt(Node node, int pos)

‎src/SharpCoreDB/DataStructures/HashIndex.cs‎

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -429,47 +429,57 @@ internal void AddBatchKeys(object?[] keys, long[] positions)
429429
_lock.EnterWriteLock();
430430
try
431431
{
432-
for (int i = 0; i < keys.Length; i++)
433-
{
434-
if (keys[i] is null)
435-
{
436-
continue;
437-
}
438-
439-
var normalizedKey = NormalizeKey(keys[i]);
432+
AddBatchKeysLockedCore(keys, positions);
433+
}
434+
finally
435+
{
436+
_lock.ExitWriteLock();
437+
}
438+
}
440439

441-
if (_useUnsafeEqualityIndex)
442-
{
443-
// Unique path: atomic check + add under outer lock.
444-
var keyBytes = BuildUnsafeKey(normalizedKey);
445-
if (HasUnsafeRowsForKey(keyBytes))
446-
{
447-
throw new InvalidOperationException(
448-
$"Duplicate key value '{keys[i]}' violates unique constraint on index '{_columnName}'");
449-
}
440+
/// <summary>
441+
/// Per-key insert loop for the batched add, executed under the outer write lock. Handles the
442+
/// unique (unsafe equality index) path via an atomic per-key check-and-add and the regular
443+
/// dictionary path via list append; duplicate keys on a unique index throw.
444+
/// </summary>
445+
private void AddBatchKeysLockedCore(object?[] keys, long[] positions)
446+
{
447+
for (int i = 0; i < keys.Length; i++)
448+
{
449+
if (keys[i] is null)
450+
{
451+
continue;
452+
}
450453

451-
_unsafeIndex.Add(keyBytes, positions[i]);
452-
_unsafeTotalRows++;
453-
continue;
454-
}
454+
var normalizedKey = NormalizeKey(keys[i]);
455455

456-
if (!_index.TryGetValue(normalizedKey, out var list))
457-
{
458-
list = [];
459-
_index[normalizedKey] = list;
460-
}
461-
else if (_isUnique && list.Count > 0)
456+
if (_useUnsafeEqualityIndex)
457+
{
458+
// Unique path: atomic check + add under outer lock.
459+
var keyBytes = BuildUnsafeKey(normalizedKey);
460+
if (HasUnsafeRowsForKey(keyBytes))
462461
{
463462
throw new InvalidOperationException(
464463
$"Duplicate key value '{keys[i]}' violates unique constraint on index '{_columnName}'");
465464
}
466465

467-
list.Add(positions[i]);
466+
_unsafeIndex.Add(keyBytes, positions[i]);
467+
_unsafeTotalRows++;
468+
continue;
468469
}
469-
}
470-
finally
471-
{
472-
_lock.ExitWriteLock();
470+
471+
if (!_index.TryGetValue(normalizedKey, out var list))
472+
{
473+
list = [];
474+
_index[normalizedKey] = list;
475+
}
476+
else if (_isUnique && list.Count > 0)
477+
{
478+
throw new InvalidOperationException(
479+
$"Duplicate key value '{keys[i]}' violates unique constraint on index '{_columnName}'");
480+
}
481+
482+
list.Add(positions[i]);
473483
}
474484
}
475485

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

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3420,37 +3420,50 @@ private bool IsFilePkOrderedUpTo(byte[] wholeFile, string pkCol, long firstSearc
34203420
long prev = long.MinValue;
34213421
while (walk + 4 <= wholeFile.Length && walk < firstSearch)
34223422
{
3423-
int len = BinaryPrimitives.ReadInt32LittleEndian(wholeFile.AsSpan((int)walk, 4));
3424-
if (len > 0 && walk + 4 + len <= wholeFile.Length)
3423+
if (!TryAdvancePkOrderedWalk(wholeFile, pkWantedPre, pkCol, ref walk, ref prev))
34253424
{
3426-
var r = DeserializeDeleteKeyRow(wholeFile.AsSpan((int)walk + 4, len), pkWantedPre);
3427-
if (r != null && r.TryGetValue(pkCol, out var v) && v is not null && v is not DBNull)
3428-
{
3429-
long pk = Convert.ToInt64(v, CultureInfo.InvariantCulture);
3430-
if (pk < prev)
3431-
{
3432-
return false; // physically unordered file -> per-row resolution
3433-
}
3425+
return false; // physically unordered or malformed file -> per-row resolution
3426+
}
3427+
}
34343428

3435-
prev = pk;
3436-
}
3429+
return true;
3430+
}
34373431

3438-
walk += 4 + len;
3439-
}
3440-
else
3432+
/// <summary>
3433+
/// Advances <paramref name="walk"/> over one physical record (or tombstone marker) of a
3434+
/// variable-length file, verifying the decoded PK keeps ascending order. Returns false when
3435+
/// the file is malformed (a zero length) or proves physically unordered.
3436+
/// </summary>
3437+
private bool TryAdvancePkOrderedWalk(byte[] wholeFile, int[] pkWanted, string pkCol, ref long walk, ref long prev)
3438+
{
3439+
int len = BinaryPrimitives.ReadInt32LittleEndian(wholeFile.AsSpan((int)walk, 4));
3440+
if (len > 0 && walk + 4 + len <= wholeFile.Length)
3441+
{
3442+
var r = DeserializeDeleteKeyRow(wholeFile.AsSpan((int)walk + 4, len), pkWanted);
3443+
if (r != null && r.TryGetValue(pkCol, out var v) && v is not null && v is not DBNull)
34413444
{
3442-
if (len == 0)
3445+
long pk = Convert.ToInt64(v, CultureInfo.InvariantCulture);
3446+
if (pk < prev)
34433447
{
3444-
return false;
3448+
return false; // physically unordered file -> per-row resolution
34453449
}
34463450

3447-
// Tombstone marker: the negative value already encodes the whole slot span
3448-
// (4-byte prefix + payload), so skipping by |len| lands exactly on the next
3449-
// record's prefix.
3450-
walk += Math.Abs(len);
3451+
prev = pk;
34513452
}
3453+
3454+
walk += 4 + len;
3455+
return true;
3456+
}
3457+
3458+
if (len == 0)
3459+
{
3460+
return false;
34523461
}
34533462

3463+
// Tombstone marker: the negative value already encodes the whole slot span
3464+
// (4-byte prefix + payload), so skipping by |len| lands exactly on the next
3465+
// record's prefix.
3466+
walk += Math.Abs(len);
34543467
return true;
34553468
}
34563469

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

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -96,34 +96,7 @@ public int MigrateToFixedWidth()
9696
// 5. Write the new records to a temp file and swap it in atomically (the same pattern
9797
// as AppendOnlyEngine.CompactTable, so encryption handling is identical).
9898
var tempPath = DataFile + ".fwmig.tmp";
99-
try
100-
{
101-
if (File.Exists(tempPath))
102-
{
103-
File.Delete(tempPath);
104-
}
105-
106-
if (records.Count > 0)
107-
{
108-
storage.AppendBytesMultiple(tempPath, records);
109-
}
110-
else
111-
{
112-
File.WriteAllBytes(tempPath, Array.Empty<byte>());
113-
}
114-
115-
File.Delete(DataFile);
116-
File.Move(tempPath, DataFile);
117-
}
118-
catch
119-
{
120-
if (File.Exists(tempPath))
121-
{
122-
try { File.Delete(tempPath); } catch { /* best-effort cleanup */ }
123-
}
124-
125-
throw;
126-
}
99+
WriteMigratedRecordsAndSwap(tempPath, records);
127100

128101
// 6. Rebuild the indexes against the new fixed-width records (DeserializeRow dispatches
129102
// to the fixed-width codec now) and fix the cached row count.
@@ -143,6 +116,43 @@ public int MigrateToFixedWidth()
143116
}
144117
}
145118

119+
/// <summary>
120+
/// Writes the migrated fixed-width records to <paramref name="tempPath"/> and atomically swaps
121+
/// it over <see cref="DataFile"/>. Any leftover temp file from an earlier failed migration is
122+
/// removed first; on failure the temp file is cleaned up and the exception re-thrown.
123+
/// </summary>
124+
private void WriteMigratedRecordsAndSwap(string tempPath, List<byte[]> records)
125+
{
126+
try
127+
{
128+
if (File.Exists(tempPath))
129+
{
130+
File.Delete(tempPath);
131+
}
132+
133+
if (records.Count > 0)
134+
{
135+
storage.AppendBytesMultiple(tempPath, records);
136+
}
137+
else
138+
{
139+
File.WriteAllBytes(tempPath, Array.Empty<byte>());
140+
}
141+
142+
File.Delete(DataFile);
143+
File.Move(tempPath, DataFile);
144+
}
145+
catch
146+
{
147+
if (File.Exists(tempPath))
148+
{
149+
try { File.Delete(tempPath); } catch { /* best-effort cleanup */ }
150+
}
151+
152+
throw;
153+
}
154+
}
155+
146156
/// <summary>
147157
/// Converts this table from page-based to columnar (append-only) storage in place: the
148158
/// page-based engine and its <c>.pages</c> files are dropped, <see cref="StorageMode"/> is set

0 commit comments

Comments
 (0)