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
17 changes: 16 additions & 1 deletion src/SharpCoreDB/DatabaseOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="DatabaseOptions.cs" company="MPCoreDeveloper">
// src\SharpCoreDB\DatabaseOptions.cs
// Copyright (c) 2025-2026 MPCoreDeveloper and GitHub Copilot. All rights reserved.
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
Expand Down Expand Up @@ -82,6 +82,21 @@ public sealed class DatabaseOptions
/// </summary>
public byte[]? EncryptionKey { get; set; }

/// <summary>
/// Compression mode for block data in SingleFile storage.
/// Compression is applied before encryption on write, and removed after decryption on read.
/// No effect in Directory storage mode.
/// Default: None (backward compatible).
/// </summary>
public Storage.BlockCompressionMode BlockCompression { get; set; } = Storage.BlockCompressionMode.None;

/// <summary>
/// Minimum block size (in bytes) to attempt compression.
/// Blocks smaller than this are stored uncompressed to avoid overhead.
/// Default: 256 bytes.
/// </summary>
public int CompressionThreshold { get; set; } = 256;

/// <summary>
/// Gets or sets whether to enable memory-mapped I/O for reads.
/// Default: true (enables zero-copy reads).
Expand Down
59 changes: 59 additions & 0 deletions src/SharpCoreDB/Services/BlockCompressor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// src/SharpCoreDB/Services/BlockCompressor.cs
namespace SharpCoreDB.Services;

using System;
using System.IO;
using System.IO.Compression;
using SharpCoreDB.Storage;

/// <summary>
/// Compression/decompression for SingleFile block payloads.
/// AOT-safe: uses only BCL streams, no reflection.
/// </summary>
internal static class BlockCompressor
{
/// <summary>
/// Compresses data using the specified compression mode.
/// Returns the original data if mode is None.
/// </summary>
public static byte[] Compress(ReadOnlySpan<byte> data, BlockCompressionMode mode)
{
if (mode == BlockCompressionMode.None) return data.ToArray();

using var output = new MemoryStream(data.Length / 2);
using (var compressor = CreateCompressor(output, mode))
{
compressor.Write(data);
}
return output.ToArray();
}

/// <summary>
/// Decompresses data using the specified compression mode.
/// Returns the original data if mode is None.
/// </summary>
public static byte[] Decompress(ReadOnlySpan<byte> data, BlockCompressionMode mode)
{
if (mode == BlockCompressionMode.None) return data.ToArray();

using var input = new MemoryStream(data.ToArray());
using var decompressor = CreateDecompressor(input, mode);
using var output = new MemoryStream();
decompressor.CopyTo(output);
return output.ToArray();
}

private static Stream CreateCompressor(Stream output, BlockCompressionMode mode) => mode switch
{
BlockCompressionMode.Brotli => new BrotliStream(output, CompressionLevel.Fastest, leaveOpen: false),
BlockCompressionMode.GZip => new GZipStream(output, CompressionLevel.Fastest, leaveOpen: false),
_ => throw new ArgumentOutOfRangeException(nameof(mode))
};

private static Stream CreateDecompressor(Stream input, BlockCompressionMode mode) => mode switch
{
BlockCompressionMode.Brotli => new BrotliStream(input, CompressionMode.Decompress, leaveOpen: false),
BlockCompressionMode.GZip => new GZipStream(input, CompressionMode.Decompress, leaveOpen: false),
_ => throw new ArgumentOutOfRangeException(nameof(mode))
};
}
15 changes: 15 additions & 0 deletions src/SharpCoreDB/Storage/BlockCompressionMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// src\SharpCoreDB\Storage\BlockCompressionMode.cs
namespace SharpCoreDB.Storage;

/// <summary>
/// Compression algorithm for SingleFile block data.
/// </summary>
public enum BlockCompressionMode
{
/// <summary>No compression. Default for backward compatibility.</summary>
None = 0,
/// <summary>Brotli compression. Best ratio for text/JSON payloads.</summary>
Brotli = 1,
/// <summary>GZip compression. Faster decompression, slightly larger.</summary>
GZip = 2
}
2 changes: 1 addition & 1 deletion src/SharpCoreDB/Storage/Scdb/ScdbStructures.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="ScdbStructures.cs" company="MPCoreDeveloper">
// src\SharpCoreDB\Storage\Scdb\ScdbStructures.cs
// Copyright (c) 2025-2026 MPCoreDeveloper and GitHub Copilot. All rights reserved.
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
Expand Down
54 changes: 51 additions & 3 deletions src/SharpCoreDB/Storage/SingleFileStorageProvider.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// <copyright file="SingleFileStorageProvider.cs" company="MPCoreDeveloper">
// src\SharpCoreDB\Storage\SingleFileStorageProvider.cs
// Copyright (c) 2025-2026 MPCoreDeveloper and GitHub Copilot. All rights reserved.
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
Expand Down Expand Up @@ -110,6 +110,10 @@
// ReadBlockAsync/GetReadStream/GetReadSpan is decrypted (wrong keys throw).
private readonly AesGcmEncryption? _encryption;

// ✅ Compression: block-level compression mode. Compression is applied before encryption
// on write, and removed after decryption on read. Per-block Compressed flag tracks state.
private readonly BlockCompressionMode _compressionMode;

/// <summary>
/// Initializes a new instance of the <see cref="SingleFileStorageProvider"/> class.
/// </summary>
Expand All @@ -136,6 +140,9 @@
"EncryptionKey is required when EnableEncryption is true."))
: null;

// ✅ Compression: store the compression mode for use in write/read paths.
_compressionMode = options.BlockCompression;

// Initialize subsystems
_blockRegistry = new BlockRegistry(this, header.BlockRegistryOffset, header.BlockRegistryLength);
_freeSpaceManager = new FreeSpaceManager(this, header.FsmOffset, header.FsmLength, header.PageSize);
Expand Down Expand Up @@ -439,10 +446,23 @@
/// ✅ Phase 1 Task 1.3: Queues write operations for batching (40-50% improvement).
/// Combined: Improves performance by ~60% by eliminating read-back + batching writes.
/// </remarks>
public async Task WriteBlockAsync(string blockName, ReadOnlyMemory<byte> data, CancellationToken cancellationToken = default)

Check failure on line 449 in src/SharpCoreDB/Storage/SingleFileStorageProvider.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 17 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=MPCoreDeveloper_SharpCoreDB&issues=AaBPrVA23f23caWmjq3T&open=AaBPrVA23f23caWmjq3T&pullRequest=344
{
ObjectDisposedException.ThrowIf(_disposed, this);

// ✅ Compression: compress before encrypt (ciphertext is incompressible).
// Only compress if above threshold and compression actually reduces size.
bool isCompressed = false;
if (_compressionMode != BlockCompressionMode.None && data.Length >= _options.CompressionThreshold)
{
var compressedData = BlockCompressor.Compress(data.Span, _compressionMode);
if (compressedData.Length < data.Length)
{
data = compressedData;
isCompressed = true;
}
}

// ✅ Issue #341: encrypt block data at rest before computing the checksum and
// queuing the write. The on-disk block is ciphertext (nonce, ciphertext, tag)
// and the checksum plus registry length describe that ciphertext, not the plaintext.
Expand Down Expand Up @@ -493,12 +513,19 @@
offset = registryEnd;
}

// ✅ Compression: set the Compressed flag if this block was compressed.
var flags = (uint)BlockFlags.Dirty;
if (isCompressed)
{
flags |= (uint)BlockFlags.Compressed;
}

entry = new BlockEntry
{
BlockType = (uint)Scdb.BlockType.TableData,
Offset = offset,
Length = (ulong)data.Length,
Flags = (uint)BlockFlags.Dirty
Flags = flags
};
}

Expand Down Expand Up @@ -819,6 +846,12 @@
result = _encryption.Decrypt(result);
}

// ✅ Compression: decompress after decrypt if the block was compressed.
if ((entry.Flags & (uint)BlockFlags.Compressed) != 0)
{
result = BlockCompressor.Decompress(result, _compressionMode);
}

return result;
}
finally
Expand Down Expand Up @@ -1479,6 +1512,9 @@
}
}

// ✅ Compression: set compression mode in header
header.CompressionMode = (byte)options.BlockCompression;

static ulong AlignToPage(ulong value, int pageSize)
{
var pageSizeUlong = (ulong)pageSize;
Expand Down Expand Up @@ -1648,6 +1684,16 @@
? "This SCDB file is encrypted; open it with EnableEncryption = true and the correct EncryptionKey."
: "This SCDB file is not encrypted; open it with EnableEncryption = false.");
}

// ✅ Compression: enforce compression-mode consistency. A file created with compression
// must be reopened with the same mode (decompression requires the matching algorithm).
if (header.CompressionMode != (byte)options.BlockCompression)
{
throw new InvalidOperationException(
header.CompressionMode != 0
? $"This SCDB file uses {(BlockCompressionMode)header.CompressionMode} compression; open it with BlockCompression = {(BlockCompressionMode)header.CompressionMode}."
: "This SCDB file is not compressed; open it with BlockCompression = None.");
}
}

private async Task WriteHeaderAsync(CancellationToken cancellationToken)
Expand Down Expand Up @@ -1778,7 +1824,9 @@
EnableEncryption = _options.EnableEncryption,
EncryptionKey = _options.EncryptionKey,
EnableMemoryMapping = false, // Don't use mmap for temp file
CreateImmediately = true
CreateImmediately = true,
BlockCompression = _options.BlockCompression,
CompressionThreshold = _options.CompressionThreshold
};

using (var tempProvider = SingleFileStorageProvider.Open(tempPath, tempOptions))
Expand Down
Loading
Loading