Skip to content

Commit 9d57ab8

Browse files
Merge pull request #356 from MPCoreDeveloper/sonar/cleanup-354
fix(sonar): resolve the 50 quality-gate findings on the backport
2 parents 1cf3427 + d0b67da commit 9d57ab8

11 files changed

Lines changed: 96 additions & 83 deletions

File tree

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

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public partial class Table
2020
/// <summary>Error message used by every write path when the table is opened read-only.</summary>
2121
private const string ReadOnlyInsertError = "Cannot insert in readonly mode";
2222

23+
/// <summary>Error message used by every DELETE path when the table is opened read-only.</summary>
24+
private const string ReadOnlyDeleteError = "Cannot delete in readonly mode";
25+
2326
/// <summary>
2427
/// Inserts a row into the table.
2528
/// Routes to columnar or page-based storage ENGINE based on StorageMode.
@@ -1615,6 +1618,29 @@ private void ValidateUpdatedRow(Dictionary<string, object> row)
16151618
}
16161619
}
16171620

1621+
/// <summary>
1622+
/// Patches a single row in place when the existing record bytes can be located and every
1623+
/// updated field fits its slot (fixed-size fields resolved at their actual offsets, or an
1624+
/// unchanged-size trailing variable field); otherwise falls back to a full serialization.
1625+
/// A same-length patch enables an in-place overwrite (Issue #6) so the file does not grow.
1626+
/// </summary>
1627+
private byte[] TryPatchOrSerializeRow(byte[]? existingData, Dictionary<string, object> updates, Dictionary<string, object> row)
1628+
{
1629+
if (existingData is { Length: > 0 })
1630+
{
1631+
var patched = _fixedWidthRecords
1632+
? TryOverwriteFixedWidthInPlace(existingData, updates)
1633+
: TryOverwriteFieldsInPlaceActual(existingData, updates);
1634+
1635+
if (patched is not null)
1636+
{
1637+
return patched;
1638+
}
1639+
}
1640+
1641+
return SerializeRowExact(row);
1642+
}
1643+
16181644
private void UpdateColumnarRow(Dictionary<string, object> row, IStorageEngine engine, Dictionary<string, object> updates, string? oldPkValue, Dictionary<string, object>? oldHashKeys, long rowPos)
16191645
{
16201646
// Fixed-width layout step: when the row's existing bytes can be located, patch
@@ -1627,12 +1653,7 @@ private void UpdateColumnarRow(Dictionary<string, object> row, IStorageEngine en
16271653
if (rowPos >= 0)
16281654
{
16291655
var existingData = engine.Read(Name, rowPos);
1630-
rowData = existingData is { Length: > 0 }
1631-
&& (_fixedWidthRecords
1632-
? TryOverwriteFixedWidthInPlace(existingData, updates)
1633-
: TryOverwriteFieldsInPlaceActual(existingData, updates)) is { } patched
1634-
? patched
1635-
: SerializeRowExact(row);
1656+
rowData = TryPatchOrSerializeRow(existingData, updates, row);
16361657
}
16371658
else
16381659
{
@@ -1771,7 +1792,7 @@ private void RepointPrimaryKeyIfChanged(Dictionary<string, object> row, string?
17711792
/// via the PK when present). The position lets the columnar write path patch fields in place
17721793
/// (fixed-width layout) instead of appending a new version.
17731794
/// </summary>
1774-
private List<(long Position, Dictionary<string, object> Row)> ResolveUpdateRows(string? where)
1795+
private List<(long Position, Dictionary<string, object> Row)> ResolveUpdateRows(string? where) // NOSONAR:S3776 - sequential guarded index-resolution steps (PK -> hash -> scan); extracting branches would re-read/duplicate shared fallbacks
17751796
{
17761797
var engine = GetOrCreateStorageEngine();
17771798
var result = new List<(long, Dictionary<string, object>)>();
@@ -1866,7 +1887,7 @@ private void RepointPrimaryKeyIfChanged(Dictionary<string, object> row, string?
18661887
/// <param name="newPosition">The storage position after relocation.</param>
18671888
/// <param name="oldPkValue">The PK value before the update (may be null if the table has no PK).</param>
18681889
/// <param name="newPkValue">The PK value after the update (may be null if the table has no PK).</param>
1869-
private void RepointIndexesAfterRelocation(long oldPosition, long newPosition, string? oldPkValue, string? newPkValue)
1890+
private void RepointIndexesAfterRelocation(long oldPosition, long newPosition, string? oldPkValue, string? newPkValue) // NOSONAR:S1172 - oldPosition retained for call-site symmetry with relocation-reporting engines (all callers already hold it)
18701891
{
18711892
if (this.PrimaryKeyIndex >= 0)
18721893
{
@@ -1933,7 +1954,7 @@ internal void UpdateMultiple(List<(string where, Dictionary<string, object> upda
19331954
bool touchesHashIndexedColumn = false;
19341955
if (this.hashIndexes.Count > 0)
19351956
{
1936-
foreach (var updateKey in updates.Keys)
1957+
foreach (var updateKey in updates.Keys) // NOSONAR:S3267 - updates.Keys is tiny; LINQ would add a closure + enumerator alloc per op in the batch-DML hot path
19371958
{
19381959
if (this.hashIndexes.ContainsKey(updateKey))
19391960
{
@@ -2167,12 +2188,7 @@ internal void UpdateMultiple(List<(string where, Dictionary<string, object> upda
21672188
if (oldPosition >= 0)
21682189
{
21692190
var existingData = engine.Read(Name, oldPosition);
2170-
rowData = existingData is { Length: > 0 }
2171-
&& (_fixedWidthRecords
2172-
? TryOverwriteFixedWidthInPlace(existingData, updates)
2173-
: TryOverwriteFieldsInPlaceActual(existingData, updates)) is { } patched
2174-
? patched
2175-
: SerializeRowExact(row);
2191+
rowData = TryPatchOrSerializeRow(existingData, updates, row);
21762192
}
21772193
else
21782194
{
@@ -2312,7 +2328,7 @@ private bool HasColumnCheckConstraints()
23122328
return false;
23132329
}
23142330

2315-
foreach (var expr in expressions)
2331+
foreach (var expr in expressions) // NOSONAR:S3267 - LINQ would allocate per call; this runs per UPDATE op in the batch-DML hot path
23162332
{
23172333
if (expr is not null)
23182334
{
@@ -2347,7 +2363,7 @@ private bool HasExplicitNamedIndex(string column)
23472363
/// cleanup, key-only hash-index cleanup (single lock per index) and row-count bookkeeping.
23482364
/// </summary>
23492365
/// <param name="recordsToDelete">The storage positions and their deserialized rows.</param>
2350-
private void DeleteRecordsCore(List<(long storagePosition, Dictionary<string, object> row)> recordsToDelete)
2366+
private void DeleteRecordsCore(List<(long storagePosition, Dictionary<string, object> row)> recordsToDelete) // NOSONAR:S3776 - per-engine physical delete, PK cleanup, key-only hash removal and compaction bookkeeping are distinct but share the batch; extraction would force intermediate lists
23512367
{
23522368
if (recordsToDelete.Count == 0)
23532369
return;
@@ -2438,7 +2454,7 @@ public void Delete(string? where)
24382454
/// </summary>
24392455
public int DeleteAffected(string? where)
24402456
{
2441-
if (this.isReadOnly) throw new InvalidOperationException("Cannot delete in readonly mode");
2457+
if (this.isReadOnly) throw new InvalidOperationException(ReadOnlyDeleteError);
24422458

24432459
this.rwLock.EnterWriteLock();
24442460
try
@@ -2462,7 +2478,7 @@ public int DeleteAffected(string? where)
24622478
/// </summary>
24632479
public List<Dictionary<string, object>> DeleteAffectedRows(string? where)
24642480
{
2465-
if (this.isReadOnly) throw new InvalidOperationException("Cannot delete in readonly mode");
2481+
if (this.isReadOnly) throw new InvalidOperationException(ReadOnlyDeleteError);
24662482

24672483
this.rwLock.EnterWriteLock();
24682484
try
@@ -2637,7 +2653,7 @@ public List<Dictionary<string, object>> DeleteAffectedRows(string? where)
26372653
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
26382654
internal void DeleteMultiple(List<string> whereConditions)
26392655
{
2640-
if (this.isReadOnly) throw new InvalidOperationException("Cannot delete in readonly mode");
2656+
if (this.isReadOnly) throw new InvalidOperationException(ReadOnlyDeleteError);
26412657
if (whereConditions.Count == 0) return;
26422658

26432659
this.rwLock.EnterWriteLock();
@@ -3102,7 +3118,7 @@ public bool UpdateByPrimaryKey(object key, Dictionary<string, object> updates)
31023118

31033119
// WP13: capture only what index maintenance needs instead of copying the whole row.
31043120
Dictionary<string, object>? oldHashKeys = null;
3105-
foreach (var kvp in this.hashIndexes)
3121+
foreach (var kvp in this.hashIndexes) // NOSONAR:S3267 - deliberate: LINQ Select/Where would allocate per point-update on the hot path
31063122
{
31073123
if (row.TryGetValue(kvp.Key, out var oldVal))
31083124
{
@@ -3202,7 +3218,7 @@ public bool DeleteByPrimaryKey(object key)
32023218
ArgumentNullException.ThrowIfNull(key);
32033219

32043220
if (this.isReadOnly)
3205-
throw new InvalidOperationException("Cannot delete in readonly mode");
3221+
throw new InvalidOperationException(ReadOnlyDeleteError);
32063222

32073223
if (this.PrimaryKeyIndex < 0)
32083224
return false;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ private int EstimateRowSize(Dictionary<string, object> row)
10211021
/// <param name="type">The data type of the value.</param>
10221022
/// <returns>Number of bytes written.</returns>
10231023
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
1024-
internal static int WriteTypedValueToSpan(Span<byte> buffer, object value, DataType type)
1024+
internal static int WriteTypedValueToSpan(Span<byte> buffer, object value, DataType type) // NOSONAR:S3776 - exhaustive per-DataType binary writer with size guards; splitting would add a dispatch layer on the row-serialization hot path
10251025
{
10261026
if (value == DBNull.Value || value == null)
10271027
{
@@ -1193,7 +1193,7 @@ internal static int WriteTypedValueToSpan(Span<byte> buffer, object value, DataT
11931193
/// <param name="bytesRead">Output: number of bytes consumed.</param>
11941194
/// <returns>The deserialized value.</returns>
11951195
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
1196-
internal static object ReadTypedValueFromSpan(ReadOnlySpan<byte> buffer, DataType type, out int bytesRead)
1196+
internal static object ReadTypedValueFromSpan(ReadOnlySpan<byte> buffer, DataType type, out int bytesRead) // NOSONAR:S3776 - exhaustive per-DataType binary reader with size guards; splitting would add a dispatch layer on the row-deserialization hot path
11971197
{
11981198
bytesRead = 1;
11991199

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ public partial class Table
5353
/// <returns>Zero-allocation enumerable of StructRow instances.</returns>
5454
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
5555
public IEnumerable<StructRow> ScanStructRows(bool enableCaching = false)
56+
{
57+
// ✅ FIX: Validate upfront, then delegate to the iterator so argument errors surface at
58+
// the call site instead of on first enumeration (iterator bodies defer all validation).
59+
ArgumentNullException.ThrowIfNull(this.storage);
60+
return ScanStructRowsCore(enableCaching);
61+
}
62+
63+
private IEnumerable<StructRow> ScanStructRowsCore(bool enableCaching)
5664
{
5765
// Fixed-width record layout (out-of-line overflow): the zero-alloc struct scan walks the
5866
// variable-length record format, so fixed-width tables fall back to the dictionary path
@@ -69,9 +77,6 @@ public IEnumerable<StructRow> ScanStructRows(bool enableCaching = false)
6977
yield break;
7078
}
7179

72-
// ✅ FIX: Validate upfront, then delegate to iterator methods
73-
ArgumentNullException.ThrowIfNull(this.storage);
74-
7580
// Build schema once for entire scan
7681
var schema = BuildVariableLengthSchema();
7782

@@ -191,7 +196,7 @@ private IEnumerable<StructRow> ScanStructRowsWhereCore(string? where, bool enabl
191196
return ScanStructRowsWhereCoreIterator(where, enableCaching);
192197
}
193198

194-
private IEnumerable<StructRow> ScanStructRowsWhereCoreIterator(string? where, bool enableCaching)
199+
private IEnumerable<StructRow> ScanStructRowsWhereCoreIterator(string? where, bool enableCaching) // NOSONAR:S3776 - ordered fast-path cascade (hash -> PK -> SIMD -> scan); each guard is a separate resolution strategy with early yield-break
195200
{
196201
// Fixed-width records: StructRow's variable-length schema can't walk the fixed-width
197202
// format, so matched records are materialized through the dictionary path. The numeric-SIMD
@@ -247,15 +252,15 @@ private IEnumerable<StructRow> ScanStructRowsWhereCoreIterator(string? where, bo
247252
{
248253
foreach (var row in Select(where))
249254
{
250-
yield return StructRow.FromDictionary(row, fixedColumns ?? [], fixedTypes ?? []);
255+
// fixedColumns/fixedTypes are non-null here (assigned when fixedWidth was resolved).
256+
yield return StructRow.FromDictionary(row, fixedColumns!, fixedTypes!);
251257
}
252258

253259
yield break;
254260
}
255261

256262
// Fallback: full scan with a simple equality predicate (scalar, allocation-free per row).
257-
// NOSONAR:S3267 - intentional: LINQ Where would allocate per row on the scan hot path.
258-
foreach (var row in ScanStructRows(enableCaching))
263+
foreach (var row in ScanStructRows(enableCaching)) // NOSONAR:S3267 - intentional: LINQ Where would allocate per row on the scan hot path
259264
{
260265
if (!hasSimpleWhere || simpleColumn is null || simpleValue is null ||
261266
MatchesSimpleWhere(row, schema, simpleColumn, simpleValue))
@@ -318,7 +323,7 @@ private IEnumerable<StructRow> ScanByPrimaryKeyPoint(
318323
/// (no deserialization, no boxing). Integer/Long use portable Vector&lt;T&gt;; Real uses
319324
/// direct per-record reads. Fixed-width tables materialize rows through the dictionary path.
320325
/// </summary>
321-
private IEnumerable<StructRow> ScanByNumericSimd(
326+
private IEnumerable<StructRow> ScanByNumericSimd( // NOSONAR - S3776 (SIMD kernel with per-type extraction passes) + S107 (layout parameters intentionally threaded; grouped further would add an allocation in the hot path)
322327
int numericOffset, DataType numericType, object numericExpected,
323328
VariableLengthSchema schema, IStorageEngine engine, bool enableCaching,
324329
bool fixedWidth, string[]? fixedColumns, DataType[]? fixedTypes)
@@ -510,7 +515,7 @@ public bool MoveNext()
510515
}
511516
}
512517

513-
private bool InitAndMoveNext()
518+
private bool InitAndMoveNext() // NOSONAR:S3776 - init resolves the WHERE via an ordered fast-path cascade and seeds the phase machine; each branch has distinct cleanup
514519
{
515520
_schema = _table.BuildVariableLengthSchema();
516521
_engine = _table.GetOrCreateStorageEngine();

‎src/SharpCoreDB/DataStructures/Table.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ private Dictionary<string, int> GetColumnIndexCache()
266266
/// Gets or sets whether this table uses the fixed-width record layout (out-of-line overflow).
267267
/// Persisted in table metadata so a database created with the flag reopens correctly.
268268
/// </summary>
269-
public bool IsFixedWidthRecords
269+
public bool IsFixedWidthRecords // NOSONAR:S2292 - backing field is read/written directly across the Table.* partial files and metadata round-trip; auto-property would not remove the field
270270
{
271271
get => _fixedWidthRecords;
272272
set => _fixedWidthRecords = value;

‎src/SharpCoreDB/Database/Execution/Database.Batch.cs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,7 @@ private static bool IsInsertStatement(string sql)
523523
/// back to the general regex path. Returns raw literal text (quotes included) so the caller
524524
/// still converts via <see cref="SqlParser.ParseValue"/>.
525525
/// </summary>
526-
private static bool TryScanCanonicalDml(
526+
private static bool TryScanCanonicalDml( // NOSONAR:S3776 - defensive canonical-shape scanner; each guard rejects a deviation to the regex fallback and must stay sequential to stay allocation-free
527527
string sql,
528528
out string table,
529529
out string setCol,
@@ -965,7 +965,7 @@ private bool TryParseDeleteForBatch(string sql, out string tableName, out string
965965
/// ✅ FIX: Always use outer transaction to prevent concurrent write corruption.
966966
/// </summary>
967967
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
968-
public void ExecuteBatchSQL(IEnumerable<string> sqlStatements)
968+
public void ExecuteBatchSQL(IEnumerable<string> sqlStatements) // NOSONAR:S3776 - grouped multi-table batch dispatcher (INSERT fast path / UPDATE / DELETE / fallback) under one transaction; extraction would fragment the single-lock contract
969969
{
970970
ArgumentNullException.ThrowIfNull(sqlStatements);
971971

‎src/SharpCoreDB/Services/SqlParser.Core.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ public List<Dictionary<string, object>> ExecuteQuery(CachedQueryPlan plan, Dicti
296296
/// Returns false (falling back to the full parser) for any condition that cannot be
297297
/// handled with exact parity to the legacy string-based path.
298298
/// </summary>
299-
private bool TryExecuteSimpleSelect(
299+
private bool TryExecuteSimpleSelect( // NOSONAR:S3776 - guarded fast-path cascade (indexed point lookup -> legacy WHERE-string fallback); each guard preserves exact parity with the parser and shares the fall-through
300300
SimpleSelectPlan simple,
301301
Dictionary<string, object?>? parameters,
302302
out List<Dictionary<string, object>> results)

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

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,6 @@ public bool OverwriteRecordAt(string path, long offset, byte[] data)
422422
{
423423
ArgumentNullException.ThrowIfNull(data);
424424

425-
bool inTransaction = IsInTransaction;
426-
427425
bool encryptWrites = ShouldEncryptWrites(path);
428426
byte[] record = EncryptRecord(data, encryptWrites);
429427
int recordLength = record.Length;
@@ -821,20 +819,17 @@ private void FlushBufferedOverwrites()
821819
if (!bufferedOverwrites.IsEmpty &&
822820
bufferedOverwrites.TryGetValue(path, out var buffered) &&
823821
buffered.TryGetValue(offset, out var newRecord) &&
824-
newRecord.Length > 0)
822+
newRecord.Length is > 0 and <= MaxRecordSize)
825823
{
826-
if (newRecord.Length <= MaxRecordSize)
827-
{
828-
byte[] bufferedPayload = new byte[newRecord.Length];
829-
Buffer.BlockCopy(newRecord, 0, bufferedPayload, 0, newRecord.Length);
830-
831-
if (UseRecordEncryption && FileHasEncryptedHeader(path))
832-
{
833-
return DecryptRecord(bufferedPayload);
834-
}
824+
byte[] bufferedPayload = new byte[newRecord.Length];
825+
Buffer.BlockCopy(newRecord, 0, bufferedPayload, 0, newRecord.Length);
835826

836-
return bufferedPayload;
827+
if (UseRecordEncryption && FileHasEncryptedHeader(path))
828+
{
829+
return DecryptRecord(bufferedPayload);
837830
}
831+
832+
return bufferedPayload;
838833
}
839834

840835
// PERF: Use cached SafeFileHandle + RandomAccess instead of opening a new

0 commit comments

Comments
 (0)