diff --git a/CBDD.slnx b/CBDD.slnx index 81327a5..956425b 100755 --- a/CBDD.slnx +++ b/CBDD.slnx @@ -1,12 +1,12 @@ - - - - - - - - - - - + + + + + + + + + + + diff --git a/README.md b/README.md index b29ae75..4ebf379 100755 --- a/README.md +++ b/README.md @@ -1,16 +1,21 @@ # CBDD -CBDD is an embedded, document-oriented database engine for .NET 10. It targets internal platform teams that need predictable ACID behavior, low-latency local persistence, and typed access patterns without running an external database server. +CBDD is an embedded, document-oriented database engine for .NET 10. It targets internal platform teams that need +predictable ACID behavior, low-latency local persistence, and typed access patterns without running an external database +server. ## Purpose And Business Context -CBDD provides a local data layer for services and tools that need transactional durability, deterministic startup, and high-throughput reads/writes. The primary business outcome is reducing operational overhead for workloads that do not require a networked database cluster. +CBDD provides a local data layer for services and tools that need transactional durability, deterministic startup, and +high-throughput reads/writes. The primary business outcome is reducing operational overhead for workloads that do not +require a networked database cluster. ## Ownership And Support - Owning team: CBDD maintainers (repository owner: `@dohertj2`) - Primary support path: open a Gitea issue in this repository with labels `incident` or `bug` -- Escalation path: follow [`docs/runbook.md`](docs/runbook.md) and page the release maintainer listed in the active release PR +- Escalation path: follow [`docs/runbook.md`](docs/runbook.md) and page the release maintainer listed in the active + release PR ## Architecture Overview @@ -22,6 +27,7 @@ CBDD has four primary layers: 4. Source-generated mapping (`src/CBDD.SourceGenerators`) Detailed architecture material: + - [`docs/architecture.md`](docs/architecture.md) - [`RFC.md`](RFC.md) - [`C-BSON.md`](C-BSON.md) @@ -36,34 +42,44 @@ Detailed architecture material: ## Setup And Local Run 1. Clone the repository. + ```bash git clone https://gitea.dohertylan.com/dohertj2/CBDD.git cd CBDD ``` + Expected outcome: local repository checkout with `CBDD.slnx` present. 2. Restore dependencies. + ```bash dotnet restore ``` + Expected outcome: restore completes without package errors. 3. Build the solution. + ```bash dotnet build CBDD.slnx -c Release ``` + Expected outcome: solution builds without compiler errors. 4. Run tests. + ```bash dotnet test CBDD.slnx -c Release ``` + Expected outcome: all tests pass. 5. Run the full repository fitness check. + ```bash bash scripts/fitness-check.sh ``` + Expected outcome: format, build, tests, coverage threshold, and package checks complete. ## Configuration And Secrets @@ -135,9 +151,12 @@ if (!result.Executed) Common issues and remediation: -- Build/test environment failures: [`docs/troubleshooting.md#build-and-test-failures`](docs/troubleshooting.md#build-and-test-failures) -- Data-file recovery procedures: [`docs/troubleshooting.md#data-file-and-recovery-issues`](docs/troubleshooting.md#data-file-and-recovery-issues) -- Query/index behavior verification: [`docs/troubleshooting.md#query-and-index-issues`](docs/troubleshooting.md#query-and-index-issues) +- Build/test environment failures: [ + `docs/troubleshooting.md#build-and-test-failures`](docs/troubleshooting.md#build-and-test-failures) +- Data-file recovery procedures: [ + `docs/troubleshooting.md#data-file-and-recovery-issues`](docs/troubleshooting.md#data-file-and-recovery-issues) +- Query/index behavior verification: [ + `docs/troubleshooting.md#query-and-index-issues`](docs/troubleshooting.md#query-and-index-issues) ## Change Governance @@ -150,4 +169,5 @@ Common issues and remediation: - Documentation home: [`docs/README.md`](docs/README.md) - Major feature inventory: [`docs/features/README.md`](docs/features/README.md) -- Architecture decisions: [`docs/adr/0001-storage-engine-and-source-generation.md`](docs/adr/0001-storage-engine-and-source-generation.md) +- Architecture decisions: [ + `docs/adr/0001-storage-engine-and-source-generation.md`](docs/adr/0001-storage-engine-and-source-generation.md) diff --git a/src/CBDD.Bson/Document/BsonDocument.cs b/src/CBDD.Bson/Document/BsonDocument.cs index 831b40c..0605905 100755 --- a/src/CBDD.Bson/Document/BsonDocument.cs +++ b/src/CBDD.Bson/Document/BsonDocument.cs @@ -1,60 +1,64 @@ -using System; - -namespace ZB.MOM.WW.CBDD.Bson; - -/// -/// Represents an in-memory BSON document with lazy parsing. -/// Uses Memory<byte> to store raw BSON data for zero-copy operations. -/// -public sealed class BsonDocument -{ +using System.Collections.Concurrent; + +namespace ZB.MOM.WW.CBDD.Bson; + +/// +/// Represents an in-memory BSON document with lazy parsing. +/// Uses Memory<byte> to store raw BSON data for zero-copy operations. +/// +public sealed class BsonDocument +{ + private readonly ConcurrentDictionary? _keys; private readonly Memory _rawData; - private readonly System.Collections.Concurrent.ConcurrentDictionary? _keys; /// - /// Initializes a new instance of the class from raw BSON memory. + /// Initializes a new instance of the class from raw BSON memory. /// /// The raw BSON data. /// The optional key dictionary. - public BsonDocument(Memory rawBsonData, System.Collections.Concurrent.ConcurrentDictionary? keys = null) + public BsonDocument(Memory rawBsonData, ConcurrentDictionary? keys = null) { _rawData = rawBsonData; _keys = keys; } /// - /// Initializes a new instance of the class from raw BSON bytes. + /// Initializes a new instance of the class from raw BSON bytes. /// /// The raw BSON data. /// The optional key dictionary. - public BsonDocument(byte[] rawBsonData, System.Collections.Concurrent.ConcurrentDictionary? keys = null) + public BsonDocument(byte[] rawBsonData, ConcurrentDictionary? keys = null) { _rawData = rawBsonData; _keys = keys; - } - - /// - /// Gets the raw BSON bytes - /// - public ReadOnlySpan RawData => _rawData.Span; - - /// - /// Gets the document size in bytes - /// - public int Size => BitConverter.ToInt32(_rawData.Span[..4]); - - /// - /// Creates a reader for this document - /// - public BsonSpanReader GetReader() => new BsonSpanReader(_rawData.Span, _keys ?? new System.Collections.Concurrent.ConcurrentDictionary()); - + } + /// - /// Tries to get a field value by name. - /// Returns false if field not found. + /// Gets the raw BSON bytes + /// + public ReadOnlySpan RawData => _rawData.Span; + + /// + /// Gets the document size in bytes + /// + public int Size => BitConverter.ToInt32(_rawData.Span[..4]); + + /// + /// Creates a reader for this document + /// + public BsonSpanReader GetReader() + { + return new BsonSpanReader(_rawData.Span, + _keys ?? new ConcurrentDictionary()); + } + + /// + /// Tries to get a field value by name. + /// Returns false if field not found. /// /// The field name. - /// When this method returns, contains the field value if found; otherwise . - /// if the field is found; otherwise, . + /// When this method returns, contains the field value if found; otherwise . + /// if the field is found; otherwise, . public bool TryGetString(string fieldName, out string? value) { value = null; @@ -66,30 +70,30 @@ public sealed class BsonDocument fieldName = fieldName.ToLowerInvariant(); while (reader.Remaining > 1) { - var type = reader.ReadBsonType(); - if (type == BsonType.EndOfDocument) - break; - - var name = reader.ReadElementHeader(); + var type = reader.ReadBsonType(); + if (type == BsonType.EndOfDocument) + break; + + string name = reader.ReadElementHeader(); + + if (name == fieldName && type == BsonType.String) + { + value = reader.ReadString(); + return true; + } + + reader.SkipValue(type); + } + + return false; + } - if (name == fieldName && type == BsonType.String) - { - value = reader.ReadString(); - return true; - } - - reader.SkipValue(type); - } - - return false; - } - /// - /// Tries to get an Int32 field value by name. + /// Tries to get an Int32 field value by name. /// /// The field name. /// When this method returns, contains the field value if found; otherwise zero. - /// if the field is found; otherwise, . + /// if the field is found; otherwise, . public bool TryGetInt32(string fieldName, out int value) { value = 0; @@ -101,30 +105,30 @@ public sealed class BsonDocument fieldName = fieldName.ToLowerInvariant(); while (reader.Remaining > 1) { - var type = reader.ReadBsonType(); - if (type == BsonType.EndOfDocument) - break; - - var name = reader.ReadElementHeader(); + var type = reader.ReadBsonType(); + if (type == BsonType.EndOfDocument) + break; + + string name = reader.ReadElementHeader(); + + if (name == fieldName && type == BsonType.Int32) + { + value = reader.ReadInt32(); + return true; + } + + reader.SkipValue(type); + } + + return false; + } - if (name == fieldName && type == BsonType.Int32) - { - value = reader.ReadInt32(); - return true; - } - - reader.SkipValue(type); - } - - return false; - } - /// - /// Tries to get an ObjectId field value by name. + /// Tries to get an ObjectId field value by name. /// /// The field name. /// When this method returns, contains the field value if found; otherwise default. - /// if the field is found; otherwise, . + /// if the field is found; otherwise, . public bool TryGetObjectId(string fieldName, out ObjectId value) { value = default; @@ -136,52 +140,53 @@ public sealed class BsonDocument fieldName = fieldName.ToLowerInvariant(); while (reader.Remaining > 1) { - var type = reader.ReadBsonType(); - if (type == BsonType.EndOfDocument) - break; - - var name = reader.ReadElementHeader(); + var type = reader.ReadBsonType(); + if (type == BsonType.EndOfDocument) + break; + + string name = reader.ReadElementHeader(); + + if (name == fieldName && type == BsonType.ObjectId) + { + value = reader.ReadObjectId(); + return true; + } + + reader.SkipValue(type); + } + + return false; + } - if (name == fieldName && type == BsonType.ObjectId) - { - value = reader.ReadObjectId(); - return true; - } - - reader.SkipValue(type); - } - - return false; - } - /// - /// Creates a new BsonDocument from field values using a builder pattern + /// Creates a new BsonDocument from field values using a builder pattern /// /// The key map used for field name encoding. /// The action that populates the builder. /// The created BSON document. - public static BsonDocument Create(System.Collections.Concurrent.ConcurrentDictionary keyMap, Action buildAction) + public static BsonDocument Create(ConcurrentDictionary keyMap, + Action buildAction) { var builder = new BsonDocumentBuilder(keyMap); buildAction(builder); - return builder.Build(); - } -} - -/// -/// Builder for creating BSON documents -/// + return builder.Build(); + } +} + +/// +/// Builder for creating BSON documents +/// public sealed class BsonDocumentBuilder { - private byte[] _buffer = new byte[1024]; // Start with 1KB - private int _position; - private readonly System.Collections.Concurrent.ConcurrentDictionary _keyMap; - + private readonly ConcurrentDictionary _keyMap; + private byte[] _buffer = new byte[1024]; // Start with 1KB + private int _position; + /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The key map used for field name encoding. - public BsonDocumentBuilder(System.Collections.Concurrent.ConcurrentDictionary keyMap) + public BsonDocumentBuilder(ConcurrentDictionary keyMap) { _keyMap = keyMap; var writer = new BsonSpanWriter(_buffer, _keyMap); @@ -189,7 +194,7 @@ public sealed class BsonDocumentBuilder } /// - /// Adds a string field to the document. + /// Adds a string field to the document. /// /// The field name. /// The field value. @@ -204,7 +209,7 @@ public sealed class BsonDocumentBuilder } /// - /// Adds an Int32 field to the document. + /// Adds an Int32 field to the document. /// /// The field name. /// The field value. @@ -213,13 +218,13 @@ public sealed class BsonDocumentBuilder { EnsureCapacity(64); var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap); - writer.WriteInt32(name, value); + writer.WriteInt32(name, value); _position += writer.Position; return this; } /// - /// Adds an Int64 field to the document. + /// Adds an Int64 field to the document. /// /// The field name. /// The field value. @@ -228,13 +233,13 @@ public sealed class BsonDocumentBuilder { EnsureCapacity(64); var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap); - writer.WriteInt64(name, value); + writer.WriteInt64(name, value); _position += writer.Position; return this; } /// - /// Adds a Boolean field to the document. + /// Adds a Boolean field to the document. /// /// The field name. /// The field value. @@ -243,13 +248,13 @@ public sealed class BsonDocumentBuilder { EnsureCapacity(64); var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap); - writer.WriteBoolean(name, value); + writer.WriteBoolean(name, value); _position += writer.Position; return this; } /// - /// Adds an ObjectId field to the document. + /// Adds an ObjectId field to the document. /// /// The field name. /// The field value. @@ -258,19 +263,19 @@ public sealed class BsonDocumentBuilder { EnsureCapacity(64); var writer = new BsonSpanWriter(_buffer.AsSpan(_position..), _keyMap); - writer.WriteObjectId(name, value); + writer.WriteObjectId(name, value); _position += writer.Position; return this; } /// - /// Builds a BSON document from the accumulated fields. + /// Builds a BSON document from the accumulated fields. /// /// The constructed BSON document. public BsonDocument Build() { // Layout: [int32 size][field bytes...][0x00 terminator] - var totalSize = _position + 5; + int totalSize = _position + 5; var finalBuffer = new byte[totalSize]; BitConverter.TryWriteBytes(finalBuffer.AsSpan(0, 4), totalSize); @@ -279,14 +284,14 @@ public sealed class BsonDocumentBuilder return new BsonDocument(finalBuffer); } - - private void EnsureCapacity(int additional) - { - if (_position + additional > _buffer.Length) - { - var newBuffer = new byte[_buffer.Length * 2]; - _buffer.CopyTo(newBuffer, 0); - _buffer = newBuffer; - } - } -} + + private void EnsureCapacity(int additional) + { + if (_position + additional > _buffer.Length) + { + var newBuffer = new byte[_buffer.Length * 2]; + _buffer.CopyTo(newBuffer, 0); + _buffer = newBuffer; + } + } +} \ No newline at end of file diff --git a/src/CBDD.Bson/Document/BsonType.cs b/src/CBDD.Bson/Document/BsonType.cs index 17bbbd5..39d2fdd 100755 --- a/src/CBDD.Bson/Document/BsonType.cs +++ b/src/CBDD.Bson/Document/BsonType.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.CBDD.Bson; /// -/// BSON type codes as defined in BSON spec +/// BSON type codes as defined in BSON spec /// public enum BsonType : byte { @@ -27,4 +27,4 @@ public enum BsonType : byte Decimal128 = 0x13, MinKey = 0xFF, MaxKey = 0x7F -} +} \ No newline at end of file diff --git a/src/CBDD.Bson/IO/BsonBufferWriter.cs b/src/CBDD.Bson/IO/BsonBufferWriter.cs index c394f4d..599d5d5 100755 --- a/src/CBDD.Bson/IO/BsonBufferWriter.cs +++ b/src/CBDD.Bson/IO/BsonBufferWriter.cs @@ -1,262 +1,263 @@ -using System; -using System.Buffers; -using System.Buffers.Binary; -using System.Text; - -namespace ZB.MOM.WW.CBDD.Bson; - -/// -/// BSON writer that serializes to an IBufferWriter, enabling streaming serialization -/// without fixed buffer size limits. -/// +using System.Buffers; +using System.Buffers.Binary; +using System.Text; + +namespace ZB.MOM.WW.CBDD.Bson; + +/// +/// BSON writer that serializes to an IBufferWriter, enabling streaming serialization +/// without fixed buffer size limits. +/// public ref struct BsonBufferWriter { - private IBufferWriter _writer; - private int _totalBytesWritten; + private readonly IBufferWriter _writer; /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The buffer writer to write BSON bytes to. public BsonBufferWriter(IBufferWriter writer) { _writer = writer; - _totalBytesWritten = 0; + Position = 0; } /// - /// Gets the current write position in bytes. + /// Gets the current write position in bytes. /// - public int Position => _totalBytesWritten; + public int Position { get; private set; } - private void WriteBytes(ReadOnlySpan data) - { - var destination = _writer.GetSpan(data.Length); - data.CopyTo(destination); - _writer.Advance(data.Length); - _totalBytesWritten += data.Length; + private void WriteBytes(ReadOnlySpan data) + { + var destination = _writer.GetSpan(data.Length); + data.CopyTo(destination); + _writer.Advance(data.Length); + Position += data.Length; } - private void WriteByte(byte value) - { - var span = _writer.GetSpan(1); - span[0] = value; - _writer.Advance(1); - _totalBytesWritten++; + private void WriteByte(byte value) + { + var span = _writer.GetSpan(1); + span[0] = value; + _writer.Advance(1); + Position++; } /// - /// Writes a BSON date-time field. + /// Writes a BSON date-time field. /// /// The field name. /// The date-time value. public void WriteDateTime(string name, DateTime value) - { - WriteByte((byte)BsonType.DateTime); - WriteCString(name); - // BSON DateTime: milliseconds since Unix epoch (UTC) - var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var milliseconds = (long)(value.ToUniversalTime() - unixEpoch).TotalMilliseconds; - WriteInt64Internal(milliseconds); + { + WriteByte((byte)BsonType.DateTime); + WriteCString(name); + // BSON DateTime: milliseconds since Unix epoch (UTC) + var unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var milliseconds = (long)(value.ToUniversalTime() - unixEpoch).TotalMilliseconds; + WriteInt64Internal(milliseconds); } /// - /// Begins writing a BSON document. + /// Begins writing a BSON document. /// /// The position where the document size placeholder was written. public int BeginDocument() - { - // Write placeholder for size (4 bytes) - var sizePosition = _totalBytesWritten; - var span = _writer.GetSpan(4); - // Initialize with default value (will be patched later) - span[0] = 0; span[1] = 0; span[2] = 0; span[3] = 0; - _writer.Advance(4); - _totalBytesWritten += 4; - return sizePosition; + { + // Write placeholder for size (4 bytes) + int sizePosition = Position; + var span = _writer.GetSpan(4); + // Initialize with default value (will be patched later) + span[0] = 0; + span[1] = 0; + span[2] = 0; + span[3] = 0; + _writer.Advance(4); + Position += 4; + return sizePosition; } /// - /// Ends the current BSON document by writing the document terminator. + /// Ends the current BSON document by writing the document terminator. /// /// The position of the size placeholder for this document. public void EndDocument(int sizePosition) - { - // Write document terminator + { + // Write document terminator WriteByte(0); // Note: Size patching must be done by caller after accessing WrittenSpan // from ArrayBufferWriter (or equivalent) - } - + } + /// - /// Begins writing a nested BSON document field. + /// Begins writing a nested BSON document field. /// /// The field name. /// The position where the nested document size placeholder was written. public int BeginDocument(string name) - { - WriteByte((byte)BsonType.Document); - WriteCString(name); - return BeginDocument(); - } - + { + WriteByte((byte)BsonType.Document); + WriteCString(name); + return BeginDocument(); + } + /// - /// Begins writing a BSON array field. + /// Begins writing a BSON array field. /// /// The field name. /// The position where the array document size placeholder was written. public int BeginArray(string name) - { - WriteByte((byte)BsonType.Array); - WriteCString(name); - return BeginDocument(); - } - + { + WriteByte((byte)BsonType.Array); + WriteCString(name); + return BeginDocument(); + } + /// - /// Ends the current BSON array. + /// Ends the current BSON array. /// /// The position of the size placeholder for this array. public void EndArray(int sizePosition) - { - EndDocument(sizePosition); + { + EndDocument(sizePosition); } // Private helper methods - private void WriteInt32Internal(int value) - { - var span = _writer.GetSpan(4); - BinaryPrimitives.WriteInt32LittleEndian(span, value); - _writer.Advance(4); - _totalBytesWritten += 4; + private void WriteInt32Internal(int value) + { + var span = _writer.GetSpan(4); + BinaryPrimitives.WriteInt32LittleEndian(span, value); + _writer.Advance(4); + Position += 4; } - private void WriteInt64Internal(long value) - { - var span = _writer.GetSpan(8); - BinaryPrimitives.WriteInt64LittleEndian(span, value); - _writer.Advance(8); - _totalBytesWritten += 8; + private void WriteInt64Internal(long value) + { + var span = _writer.GetSpan(8); + BinaryPrimitives.WriteInt64LittleEndian(span, value); + _writer.Advance(8); + Position += 8; } /// - /// Writes a BSON ObjectId field. + /// Writes a BSON ObjectId field. /// /// The field name. /// The ObjectId value. public void WriteObjectId(string name, ObjectId value) - { - WriteByte((byte)BsonType.ObjectId); - WriteCString(name); - WriteBytes(value.ToByteArray()); - } - + { + WriteByte((byte)BsonType.ObjectId); + WriteCString(name); + WriteBytes(value.ToByteArray()); + } + /// - /// Writes a BSON string field. + /// Writes a BSON string field. /// /// The field name. /// The string value. public void WriteString(string name, string value) - { - WriteByte((byte)BsonType.String); - WriteCString(name); - WriteStringValue(value); + { + WriteByte((byte)BsonType.String); + WriteCString(name); + WriteStringValue(value); } /// - /// Writes a BSON boolean field. + /// Writes a BSON boolean field. /// /// The field name. /// The boolean value. public void WriteBoolean(string name, bool value) - { - WriteByte((byte)BsonType.Boolean); - WriteCString(name); - WriteByte((byte)(value ? 1 : 0)); + { + WriteByte((byte)BsonType.Boolean); + WriteCString(name); + WriteByte((byte)(value ? 1 : 0)); } /// - /// Writes a BSON null field. + /// Writes a BSON null field. /// /// The field name. public void WriteNull(string name) - { - WriteByte((byte)BsonType.Null); - WriteCString(name); - } - - private void WriteStringValue(string value) - { - // String: length (int32) + UTF8 bytes + null terminator - var bytes = Encoding.UTF8.GetBytes(value); - WriteInt32Internal(bytes.Length + 1); // +1 for null terminator - WriteBytes(bytes); - WriteByte(0); + { + WriteByte((byte)BsonType.Null); + WriteCString(name); } - private void WriteDoubleInternal(double value) - { - var span = _writer.GetSpan(8); - BinaryPrimitives.WriteDoubleLittleEndian(span, value); - _writer.Advance(8); - _totalBytesWritten += 8; + private void WriteStringValue(string value) + { + // String: length (int32) + UTF8 bytes + null terminator + byte[] bytes = Encoding.UTF8.GetBytes(value); + WriteInt32Internal(bytes.Length + 1); // +1 for null terminator + WriteBytes(bytes); + WriteByte(0); + } + + private void WriteDoubleInternal(double value) + { + var span = _writer.GetSpan(8); + BinaryPrimitives.WriteDoubleLittleEndian(span, value); + _writer.Advance(8); + Position += 8; } /// - /// Writes a BSON binary field. + /// Writes a BSON binary field. /// /// The field name. /// The binary data. public void WriteBinary(string name, ReadOnlySpan data) - { - WriteByte((byte)BsonType.Binary); - WriteCString(name); - WriteInt32Internal(data.Length); - WriteByte(0); // Binary subtype: Generic - WriteBytes(data); + { + WriteByte((byte)BsonType.Binary); + WriteCString(name); + WriteInt32Internal(data.Length); + WriteByte(0); // Binary subtype: Generic + WriteBytes(data); } /// - /// Writes a BSON 64-bit integer field. + /// Writes a BSON 64-bit integer field. /// /// The field name. /// The 64-bit integer value. public void WriteInt64(string name, long value) - { - WriteByte((byte)BsonType.Int64); - WriteCString(name); - WriteInt64Internal(value); + { + WriteByte((byte)BsonType.Int64); + WriteCString(name); + WriteInt64Internal(value); } /// - /// Writes a BSON double field. + /// Writes a BSON double field. /// /// The field name. /// The double value. public void WriteDouble(string name, double value) - { - WriteByte((byte)BsonType.Double); - WriteCString(name); - WriteDoubleInternal(value); + { + WriteByte((byte)BsonType.Double); + WriteCString(name); + WriteDoubleInternal(value); + } + + private void WriteCString(string value) + { + byte[] bytes = Encoding.UTF8.GetBytes(value); + WriteBytes(bytes); + WriteByte(0); // Null terminator } - private void WriteCString(string value) - { - var bytes = Encoding.UTF8.GetBytes(value); - WriteBytes(bytes); - WriteByte(0); // Null terminator - } - /// - /// Writes a BSON 32-bit integer field. + /// Writes a BSON 32-bit integer field. /// /// The field name. /// The 32-bit integer value. public void WriteInt32(string name, int value) - { - WriteByte((byte)BsonType.Int32); - WriteCString(name); - WriteInt32Internal(value); - } -} + { + WriteByte((byte)BsonType.Int32); + WriteCString(name); + WriteInt32Internal(value); + } +} \ No newline at end of file diff --git a/src/CBDD.Bson/IO/BsonSpanReader.cs b/src/CBDD.Bson/IO/BsonSpanReader.cs index aacdaed..a655d2d 100755 --- a/src/CBDD.Bson/IO/BsonSpanReader.cs +++ b/src/CBDD.Bson/IO/BsonSpanReader.cs @@ -1,344 +1,343 @@ -using System; using System.Buffers.Binary; +using System.Collections.Concurrent; using System.Text; namespace ZB.MOM.WW.CBDD.Bson; /// -/// Zero-allocation BSON reader using ReadOnlySpan<byte>. -/// Implemented as ref struct to ensure stack-only allocation. +/// Zero-allocation BSON reader using ReadOnlySpan<byte>. +/// Implemented as ref struct to ensure stack-only allocation. /// public ref struct BsonSpanReader { private ReadOnlySpan _buffer; - private int _position; - private readonly System.Collections.Concurrent.ConcurrentDictionary _keys; - - /// - /// Initializes a new instance of the struct. - /// - /// The BSON buffer to read. - /// The reverse key dictionary used for compressed element headers. - public BsonSpanReader(ReadOnlySpan buffer, System.Collections.Concurrent.ConcurrentDictionary keys) - { - _buffer = buffer; - _position = 0; - _keys = keys; - } - - /// - /// Gets the current read position in the buffer. - /// - public int Position => _position; - - /// - /// Gets the number of unread bytes remaining in the buffer. - /// - public int Remaining => _buffer.Length - _position; + private readonly ConcurrentDictionary _keys; /// - /// Reads the document size (first 4 bytes of a BSON document) + /// Initializes a new instance of the struct. + /// + /// The BSON buffer to read. + /// The reverse key dictionary used for compressed element headers. + public BsonSpanReader(ReadOnlySpan buffer, ConcurrentDictionary keys) + { + _buffer = buffer; + Position = 0; + _keys = keys; + } + + /// + /// Gets the current read position in the buffer. + /// + public int Position { get; private set; } + + /// + /// Gets the number of unread bytes remaining in the buffer. + /// + public int Remaining => _buffer.Length - Position; + + /// + /// Reads the document size (first 4 bytes of a BSON document) /// public int ReadDocumentSize() { if (Remaining < 4) throw new InvalidOperationException("Not enough bytes to read document size"); - var size = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4)); - _position += 4; + int size = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4)); + Position += 4; return size; } /// - /// Reads a BSON element type + /// Reads a BSON element type /// public BsonType ReadBsonType() { if (Remaining < 1) throw new InvalidOperationException("Not enough bytes to read BSON type"); - var type = (BsonType)_buffer[_position]; - _position++; + var type = (BsonType)_buffer[Position]; + Position++; return type; } /// - /// Reads a C-style null-terminated string (e-name in BSON spec) + /// Reads a C-style null-terminated string (e-name in BSON spec) /// public string ReadCString() { - var start = _position; - while (_position < _buffer.Length && _buffer[_position] != 0) - _position++; + int start = Position; + while (Position < _buffer.Length && _buffer[Position] != 0) + Position++; - if (_position >= _buffer.Length) + if (Position >= _buffer.Length) throw new InvalidOperationException("Unterminated C-string"); - var nameBytes = _buffer.Slice(start, _position - start); - _position++; // Skip null terminator + var nameBytes = _buffer.Slice(start, Position - start); + Position++; // Skip null terminator return Encoding.UTF8.GetString(nameBytes); } - /// - /// Reads a C-string into a destination span. Returns the number of bytes written. - /// - /// The destination character span. - public int ReadCString(Span destination) + /// + /// Reads a C-string into a destination span. Returns the number of bytes written. + /// + /// The destination character span. + public int ReadCString(Span destination) { - var start = _position; - while (_position < _buffer.Length && _buffer[_position] != 0) - _position++; + int start = Position; + while (Position < _buffer.Length && _buffer[Position] != 0) + Position++; - if (_position >= _buffer.Length) + if (Position >= _buffer.Length) throw new InvalidOperationException("Unterminated C-string"); - var nameBytes = _buffer.Slice(start, _position - start); - _position++; // Skip null terminator + var nameBytes = _buffer.Slice(start, Position - start); + Position++; // Skip null terminator return Encoding.UTF8.GetChars(nameBytes, destination); } /// - /// Reads a BSON string (4-byte length + UTF-8 bytes + null terminator) + /// Reads a BSON string (4-byte length + UTF-8 bytes + null terminator) /// public string ReadString() { - var length = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4)); - _position += 4; + int length = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4)); + Position += 4; if (length < 1) throw new InvalidOperationException("Invalid string length"); - var stringBytes = _buffer.Slice(_position, length - 1); // Exclude null terminator - _position += length; + var stringBytes = _buffer.Slice(Position, length - 1); // Exclude null terminator + Position += length; return Encoding.UTF8.GetString(stringBytes); } - /// - /// Reads a 32-bit integer. - /// - public int ReadInt32() + /// + /// Reads a 32-bit integer. + /// + public int ReadInt32() { if (Remaining < 4) throw new InvalidOperationException("Not enough bytes to read Int32"); - var value = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4)); - _position += 4; - return value; - } - - /// - /// Reads a 64-bit integer. - /// - public long ReadInt64() - { - if (Remaining < 8) - throw new InvalidOperationException("Not enough bytes to read Int64"); - - var value = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(_position, 8)); - _position += 8; - return value; - } - - /// - /// Reads a double-precision floating point value. - /// - public double ReadDouble() - { - if (Remaining < 8) - throw new InvalidOperationException("Not enough bytes to read Double"); - - var value = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(_position, 8)); - _position += 8; + int value = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4)); + Position += 4; return value; } /// - /// Reads spatial coordinates from a BSON array [X, Y]. - /// Returns a (double, double) tuple. + /// Reads a 64-bit integer. + /// + public long ReadInt64() + { + if (Remaining < 8) + throw new InvalidOperationException("Not enough bytes to read Int64"); + + long value = BinaryPrimitives.ReadInt64LittleEndian(_buffer.Slice(Position, 8)); + Position += 8; + return value; + } + + /// + /// Reads a double-precision floating point value. + /// + public double ReadDouble() + { + if (Remaining < 8) + throw new InvalidOperationException("Not enough bytes to read Double"); + + double value = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(Position, 8)); + Position += 8; + return value; + } + + /// + /// Reads spatial coordinates from a BSON array [X, Y]. + /// Returns a (double, double) tuple. /// public (double, double) ReadCoordinates() { // Skip array size (4 bytes) - _position += 4; + Position += 4; // Skip element 0 header: Type(1) + Name("0\0") (3 bytes) - _position += 3; - var x = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(_position, 8)); - _position += 8; + Position += 3; + double x = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(Position, 8)); + Position += 8; // Skip element 1 header: Type(1) + Name("1\0") (3 bytes) - _position += 3; - var y = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(_position, 8)); - _position += 8; + Position += 3; + double y = BinaryPrimitives.ReadDoubleLittleEndian(_buffer.Slice(Position, 8)); + Position += 8; // Skip end of array marker (1 byte) - _position++; + Position++; return (x, y); } - /// - /// Reads a Decimal128 value. - /// - public decimal ReadDecimal128() + /// + /// Reads a Decimal128 value. + /// + public decimal ReadDecimal128() { if (Remaining < 16) throw new InvalidOperationException("Not enough bytes to read Decimal128"); var bits = new int[4]; - bits[0] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4)); - bits[1] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position + 4, 4)); - bits[2] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position + 8, 4)); - bits[3] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position + 12, 4)); - _position += 16; + bits[0] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4)); + bits[1] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position + 4, 4)); + bits[2] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position + 8, 4)); + bits[3] = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position + 12, 4)); + Position += 16; return new decimal(bits); } - /// - /// Reads a boolean value. - /// - public bool ReadBoolean() + /// + /// Reads a boolean value. + /// + public bool ReadBoolean() { if (Remaining < 1) throw new InvalidOperationException("Not enough bytes to read Boolean"); - var value = _buffer[_position] != 0; - _position++; + bool value = _buffer[Position] != 0; + Position++; return value; } /// - /// Reads a BSON DateTime (UTC milliseconds since Unix epoch) + /// Reads a BSON DateTime (UTC milliseconds since Unix epoch) /// public DateTime ReadDateTime() { - var milliseconds = ReadInt64(); + long milliseconds = ReadInt64(); return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds).UtcDateTime; } /// - /// Reads a BSON DateTime as DateTimeOffset (UTC milliseconds since Unix epoch) + /// Reads a BSON DateTime as DateTimeOffset (UTC milliseconds since Unix epoch) /// public DateTimeOffset ReadDateTimeOffset() { - var milliseconds = ReadInt64(); + long milliseconds = ReadInt64(); return DateTimeOffset.FromUnixTimeMilliseconds(milliseconds); } /// - /// Reads a TimeSpan from BSON Int64 (ticks) + /// Reads a TimeSpan from BSON Int64 (ticks) /// public TimeSpan ReadTimeSpan() { - var ticks = ReadInt64(); + long ticks = ReadInt64(); return TimeSpan.FromTicks(ticks); } /// - /// Reads a DateOnly from BSON Int32 (day number) + /// Reads a DateOnly from BSON Int32 (day number) /// public DateOnly ReadDateOnly() { - var dayNumber = ReadInt32(); + int dayNumber = ReadInt32(); return DateOnly.FromDayNumber(dayNumber); } /// - /// Reads a TimeOnly from BSON Int64 (ticks) + /// Reads a TimeOnly from BSON Int64 (ticks) /// public TimeOnly ReadTimeOnly() { - var ticks = ReadInt64(); + long ticks = ReadInt64(); return new TimeOnly(ticks); } - /// - /// Reads a GUID value. - /// - public Guid ReadGuid() + /// + /// Reads a GUID value. + /// + public Guid ReadGuid() { return Guid.Parse(ReadString()); } /// - /// Reads a BSON ObjectId (12 bytes) + /// Reads a BSON ObjectId (12 bytes) /// public ObjectId ReadObjectId() { if (Remaining < 12) throw new InvalidOperationException("Not enough bytes to read ObjectId"); - var oidBytes = _buffer.Slice(_position, 12); - _position += 12; + var oidBytes = _buffer.Slice(Position, 12); + Position += 12; return new ObjectId(oidBytes); } - /// - /// Reads binary data (subtype + length + bytes) - /// - /// When this method returns, contains the BSON binary subtype. - public ReadOnlySpan ReadBinary(out byte subtype) + /// + /// Reads binary data (subtype + length + bytes) + /// + /// When this method returns, contains the BSON binary subtype. + public ReadOnlySpan ReadBinary(out byte subtype) { - var length = ReadInt32(); - + int length = ReadInt32(); + if (Remaining < 1) - throw new InvalidOperationException("Not enough bytes to read binary subtype"); - - subtype = _buffer[_position]; - _position++; + throw new InvalidOperationException("Not enough bytes to read binary subtype"); + + subtype = _buffer[Position]; + Position++; if (Remaining < length) throw new InvalidOperationException("Not enough bytes to read binary data"); - var data = _buffer.Slice(_position, length); - _position += length; + var data = _buffer.Slice(Position, length); + Position += length; return data; } - /// - /// Skips the current value based on type - /// - /// The BSON type of the value to skip. - public void SkipValue(BsonType type) + /// + /// Skips the current value based on type + /// + /// The BSON type of the value to skip. + public void SkipValue(BsonType type) { switch (type) { case BsonType.Double: - _position += 8; + Position += 8; break; case BsonType.String: - var stringLength = ReadInt32(); - _position += stringLength; + int stringLength = ReadInt32(); + Position += stringLength; break; case BsonType.Document: case BsonType.Array: - var docLength = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4)); - _position += docLength; + int docLength = BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4)); + Position += docLength; break; case BsonType.Binary: - var binaryLength = ReadInt32(); - _position += 1 + binaryLength; // subtype + data + int binaryLength = ReadInt32(); + Position += 1 + binaryLength; // subtype + data break; case BsonType.ObjectId: - _position += 12; + Position += 12; break; case BsonType.Boolean: - _position += 1; + Position += 1; break; case BsonType.DateTime: case BsonType.Int64: case BsonType.Timestamp: - _position += 8; + Position += 8; break; case BsonType.Decimal128: - _position += 16; + Position += 16; break; case BsonType.Int32: - _position += 4; + Position += 4; break; case BsonType.Null: // No data @@ -348,49 +347,50 @@ public ref struct BsonSpanReader } } - /// - /// Reads a single byte. - /// - public byte ReadByte() + /// + /// Reads a single byte. + /// + public byte ReadByte() { if (Remaining < 1) throw new InvalidOperationException("Not enough bytes to read byte"); - var value = _buffer[_position]; - _position++; + byte value = _buffer[Position]; + Position++; return value; } - /// - /// Peeks a 32-bit integer at the current position without advancing. - /// - public int PeekInt32() + /// + /// Peeks a 32-bit integer at the current position without advancing. + /// + public int PeekInt32() { if (Remaining < 4) throw new InvalidOperationException("Not enough bytes to peek Int32"); - return BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(_position, 4)); + return BinaryPrimitives.ReadInt32LittleEndian(_buffer.Slice(Position, 4)); } - /// - /// Reads an element header key identifier and resolves it to a key name. - /// - public string ReadElementHeader() + /// + /// Reads an element header key identifier and resolves it to a key name. + /// + public string ReadElementHeader() { if (Remaining < 2) throw new InvalidOperationException("Not enough bytes to read BSON element key ID"); - var id = BinaryPrimitives.ReadUInt16LittleEndian(_buffer.Slice(_position, 2)); - _position += 2; + ushort id = BinaryPrimitives.ReadUInt16LittleEndian(_buffer.Slice(Position, 2)); + Position += 2; - if (!_keys.TryGetValue(id, out var key)) - { + if (!_keys.TryGetValue(id, out string? key)) throw new InvalidOperationException($"BSON Key ID {id} not found in reverse key dictionary."); - } return key; } - /// - /// Returns a span containing all unread bytes. - /// - public ReadOnlySpan RemainingBytes() => _buffer[_position..]; -} + /// + /// Returns a span containing all unread bytes. + /// + public ReadOnlySpan RemainingBytes() + { + return _buffer[Position..]; + } +} \ No newline at end of file diff --git a/src/CBDD.Bson/IO/BsonSpanWriter.cs b/src/CBDD.Bson/IO/BsonSpanWriter.cs index fdee8d2..d499fe1 100755 --- a/src/CBDD.Bson/IO/BsonSpanWriter.cs +++ b/src/CBDD.Bson/IO/BsonSpanWriter.cs @@ -1,382 +1,380 @@ -using System; using System.Buffers.Binary; +using System.Collections.Concurrent; using System.Text; namespace ZB.MOM.WW.CBDD.Bson; /// -/// Zero-allocation BSON writer using Span<byte>. -/// Implemented as ref struct to ensure stack-only allocation. +/// Zero-allocation BSON writer using Span<byte>. +/// Implemented as ref struct to ensure stack-only allocation. /// public ref struct BsonSpanWriter { private Span _buffer; - private int _position; - private readonly System.Collections.Concurrent.ConcurrentDictionary _keyMap; + private readonly ConcurrentDictionary _keyMap; - /// - /// Initializes a new instance of the struct. - /// - /// The destination buffer to write BSON bytes into. - /// The cached key-name to key-id mapping. - public BsonSpanWriter(Span buffer, System.Collections.Concurrent.ConcurrentDictionary keyMap) + /// + /// Initializes a new instance of the struct. + /// + /// The destination buffer to write BSON bytes into. + /// The cached key-name to key-id mapping. + public BsonSpanWriter(Span buffer, ConcurrentDictionary keyMap) { _buffer = buffer; _keyMap = keyMap; - _position = 0; + Position = 0; } - /// - /// Gets the current write position in the buffer. - /// - public int Position => _position; - - /// - /// Gets the number of bytes remaining in the buffer. - /// - public int Remaining => _buffer.Length - _position; + /// + /// Gets the current write position in the buffer. + /// + public int Position { get; private set; } /// - /// Writes document size placeholder and returns the position to patch later + /// Gets the number of bytes remaining in the buffer. + /// + public int Remaining => _buffer.Length - Position; + + /// + /// Writes document size placeholder and returns the position to patch later /// public int WriteDocumentSizePlaceholder() { - var sizePosition = _position; - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), 0); - _position += 4; + int sizePosition = Position; + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position, 4), 0); + Position += 4; return sizePosition; } - /// - /// Patches the document size at the given position - /// - /// The position where the size placeholder was written. - public void PatchDocumentSize(int sizePosition) + /// + /// Patches the document size at the given position + /// + /// The position where the size placeholder was written. + public void PatchDocumentSize(int sizePosition) { - var size = _position - sizePosition; + int size = Position - sizePosition; BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(sizePosition, 4), size); } - /// - /// Writes a BSON element header (type + name) - /// - /// The BSON element type. - /// The field name. - public void WriteElementHeader(BsonType type, string name) + /// + /// Writes a BSON element header (type + name) + /// + /// The BSON element type. + /// The field name. + public void WriteElementHeader(BsonType type, string name) { - _buffer[_position] = (byte)type; - _position++; + _buffer[Position] = (byte)type; + Position++; - if (!_keyMap.TryGetValue(name, out var id)) - { - throw new InvalidOperationException($"BSON Key '{name}' not found in dictionary cache. Ensure all keys are registered before serialization."); - } + if (!_keyMap.TryGetValue(name, out ushort id)) + throw new InvalidOperationException( + $"BSON Key '{name}' not found in dictionary cache. Ensure all keys are registered before serialization."); - BinaryPrimitives.WriteUInt16LittleEndian(_buffer.Slice(_position, 2), id); - _position += 2; + BinaryPrimitives.WriteUInt16LittleEndian(_buffer.Slice(Position, 2), id); + Position += 2; } /// - /// Writes a C-style null-terminated string + /// Writes a C-style null-terminated string /// private void WriteCString(string value) { - var bytesWritten = Encoding.UTF8.GetBytes(value, _buffer[_position..]); - _position += bytesWritten; - _buffer[_position] = 0; // Null terminator - _position++; + int bytesWritten = Encoding.UTF8.GetBytes(value, _buffer[Position..]); + Position += bytesWritten; + _buffer[Position] = 0; // Null terminator + Position++; } /// - /// Writes end-of-document marker + /// Writes end-of-document marker /// public void WriteEndOfDocument() { - _buffer[_position] = 0; - _position++; + _buffer[Position] = 0; + Position++; } - /// - /// Writes a BSON string element - /// - /// The field name. - /// The string value. - public void WriteString(string name, string value) + /// + /// Writes a BSON string element + /// + /// The field name. + /// The string value. + public void WriteString(string name, string value) { WriteElementHeader(BsonType.String, name); - var valueBytes = Encoding.UTF8.GetByteCount(value); - var stringLength = valueBytes + 1; // Include null terminator + int valueBytes = Encoding.UTF8.GetByteCount(value); + int stringLength = valueBytes + 1; // Include null terminator - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), stringLength); - _position += 4; + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position, 4), stringLength); + Position += 4; - Encoding.UTF8.GetBytes(value, _buffer[_position..]); - _position += valueBytes; + Encoding.UTF8.GetBytes(value, _buffer[Position..]); + Position += valueBytes; - _buffer[_position] = 0; // Null terminator - _position++; + _buffer[Position] = 0; // Null terminator + Position++; } - /// - /// Writes a BSON int32 element. - /// - /// The field name. - /// The 32-bit integer value. - public void WriteInt32(string name, int value) + /// + /// Writes a BSON int32 element. + /// + /// The field name. + /// The 32-bit integer value. + public void WriteInt32(string name, int value) { WriteElementHeader(BsonType.Int32, name); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), value); - _position += 4; + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position, 4), value); + Position += 4; } - /// - /// Writes a BSON int64 element. - /// - /// The field name. - /// The 64-bit integer value. - public void WriteInt64(string name, long value) + /// + /// Writes a BSON int64 element. + /// + /// The field name. + /// The 64-bit integer value. + public void WriteInt64(string name, long value) { WriteElementHeader(BsonType.Int64, name); - BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), value); - _position += 8; + BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(Position, 8), value); + Position += 8; } - /// - /// Writes a BSON double element. - /// - /// The field name. - /// The double-precision value. - public void WriteDouble(string name, double value) + /// + /// Writes a BSON double element. + /// + /// The field name. + /// The double-precision value. + public void WriteDouble(string name, double value) { WriteElementHeader(BsonType.Double, name); - BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(_position, 8), value); - _position += 8; + BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(Position, 8), value); + Position += 8; } - /// - /// Writes spatial coordinates as a BSON array [X, Y]. - /// Optimized for (double, double) tuples. - /// - /// The field name. - /// The coordinate tuple as (X, Y). - public void WriteCoordinates(string name, (double, double) coordinates) + /// + /// Writes spatial coordinates as a BSON array [X, Y]. + /// Optimized for (double, double) tuples. + /// + /// The field name. + /// The coordinate tuple as (X, Y). + public void WriteCoordinates(string name, (double, double) coordinates) { - WriteElementHeader(BsonType.Array, name); - - var startPos = _position; - _position += 4; // Placeholder for array size + WriteElementHeader(BsonType.Array, name); + + int startPos = Position; + Position += 4; // Placeholder for array size // Element 0: X - _buffer[_position++] = (byte)BsonType.Double; - _buffer[_position++] = 0x30; // '0' - _buffer[_position++] = 0x00; // Null - BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(_position, 8), coordinates.Item1); - _position += 8; + _buffer[Position++] = (byte)BsonType.Double; + _buffer[Position++] = 0x30; // '0' + _buffer[Position++] = 0x00; // Null + BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(Position, 8), coordinates.Item1); + Position += 8; // Element 1: Y - _buffer[_position++] = (byte)BsonType.Double; - _buffer[_position++] = 0x31; // '1' - _buffer[_position++] = 0x00; // Null - BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(_position, 8), coordinates.Item2); - _position += 8; + _buffer[Position++] = (byte)BsonType.Double; + _buffer[Position++] = 0x31; // '1' + _buffer[Position++] = 0x00; // Null + BinaryPrimitives.WriteDoubleLittleEndian(_buffer.Slice(Position, 8), coordinates.Item2); + Position += 8; - _buffer[_position++] = 0x00; // End of array marker + _buffer[Position++] = 0x00; // End of array marker // Patch array size - var size = _position - startPos; + int size = Position - startPos; BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(startPos, 4), size); } - /// - /// Writes a BSON Decimal128 element from a value. - /// - /// The field name. - /// The decimal value. - public void WriteDecimal128(string name, decimal value) + /// + /// Writes a BSON Decimal128 element from a value. + /// + /// The field name. + /// The decimal value. + public void WriteDecimal128(string name, decimal value) { WriteElementHeader(BsonType.Decimal128, name); // Note: usage of C# decimal bits for round-trip fidelity within ZB.MOM.WW.CBDD. // This makes it compatible with CBDD Reader but strictly speaking not standard IEEE 754-2008 Decimal128. - var bits = decimal.GetBits(value); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), bits[0]); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position + 4, 4), bits[1]); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position + 8, 4), bits[2]); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position + 12, 4), bits[3]); - _position += 16; + int[] bits = decimal.GetBits(value); + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position, 4), bits[0]); + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position + 4, 4), bits[1]); + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position + 8, 4), bits[2]); + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position + 12, 4), bits[3]); + Position += 16; } - /// - /// Writes a BSON boolean element. - /// - /// The field name. - /// The boolean value. - public void WriteBoolean(string name, bool value) + /// + /// Writes a BSON boolean element. + /// + /// The field name. + /// The boolean value. + public void WriteBoolean(string name, bool value) { WriteElementHeader(BsonType.Boolean, name); - _buffer[_position] = (byte)(value ? 1 : 0); - _position++; + _buffer[Position] = (byte)(value ? 1 : 0); + Position++; } - /// - /// Writes a BSON UTC datetime element. - /// - /// The field name. - /// The date and time value. - public void WriteDateTime(string name, DateTime value) + /// + /// Writes a BSON UTC datetime element. + /// + /// The field name. + /// The date and time value. + public void WriteDateTime(string name, DateTime value) { WriteElementHeader(BsonType.DateTime, name); - var milliseconds = new DateTimeOffset(value.ToUniversalTime()).ToUnixTimeMilliseconds(); - BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), milliseconds); - _position += 8; + long milliseconds = new DateTimeOffset(value.ToUniversalTime()).ToUnixTimeMilliseconds(); + BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(Position, 8), milliseconds); + Position += 8; } - /// - /// Writes a BSON UTC datetime element from a value. - /// - /// The field name. - /// The date and time offset value. - public void WriteDateTimeOffset(string name, DateTimeOffset value) + /// + /// Writes a BSON UTC datetime element from a value. + /// + /// The field name. + /// The date and time offset value. + public void WriteDateTimeOffset(string name, DateTimeOffset value) { WriteElementHeader(BsonType.DateTime, name); - var milliseconds = value.ToUnixTimeMilliseconds(); - BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), milliseconds); - _position += 8; + long milliseconds = value.ToUnixTimeMilliseconds(); + BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(Position, 8), milliseconds); + Position += 8; } - /// - /// Writes a BSON int64 element containing ticks from a . - /// - /// The field name. - /// The time span value. - public void WriteTimeSpan(string name, TimeSpan value) + /// + /// Writes a BSON int64 element containing ticks from a . + /// + /// The field name. + /// The time span value. + public void WriteTimeSpan(string name, TimeSpan value) { WriteElementHeader(BsonType.Int64, name); - BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), value.Ticks); - _position += 8; + BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(Position, 8), value.Ticks); + Position += 8; } - /// - /// Writes a BSON int32 element containing the . - /// - /// The field name. - /// The date-only value. - public void WriteDateOnly(string name, DateOnly value) + /// + /// Writes a BSON int32 element containing the . + /// + /// The field name. + /// The date-only value. + public void WriteDateOnly(string name, DateOnly value) { WriteElementHeader(BsonType.Int32, name); - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), value.DayNumber); - _position += 4; + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position, 4), value.DayNumber); + Position += 4; } - /// - /// Writes a BSON int64 element containing ticks from a . - /// - /// The field name. - /// The time-only value. - public void WriteTimeOnly(string name, TimeOnly value) + /// + /// Writes a BSON int64 element containing ticks from a . + /// + /// The field name. + /// The time-only value. + public void WriteTimeOnly(string name, TimeOnly value) { WriteElementHeader(BsonType.Int64, name); - BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(_position, 8), value.Ticks); - _position += 8; + BinaryPrimitives.WriteInt64LittleEndian(_buffer.Slice(Position, 8), value.Ticks); + Position += 8; } - /// - /// Writes a GUID as a BSON string element. - /// - /// The field name. - /// The GUID value. - public void WriteGuid(string name, Guid value) + /// + /// Writes a GUID as a BSON string element. + /// + /// The field name. + /// The GUID value. + public void WriteGuid(string name, Guid value) { WriteString(name, value.ToString()); } - /// - /// Writes a BSON ObjectId element. - /// - /// The field name. - /// The ObjectId value. - public void WriteObjectId(string name, ObjectId value) + /// + /// Writes a BSON ObjectId element. + /// + /// The field name. + /// The ObjectId value. + public void WriteObjectId(string name, ObjectId value) { WriteElementHeader(BsonType.ObjectId, name); - value.WriteTo(_buffer.Slice(_position, 12)); - _position += 12; + value.WriteTo(_buffer.Slice(Position, 12)); + Position += 12; } - /// - /// Writes a BSON null element. - /// - /// The field name. - public void WriteNull(string name) + /// + /// Writes a BSON null element. + /// + /// The field name. + public void WriteNull(string name) { WriteElementHeader(BsonType.Null, name); // No value to write for null } - /// - /// Writes binary data - /// - /// The field name. - /// The binary payload. - /// The BSON binary subtype. - public void WriteBinary(string name, ReadOnlySpan data, byte subtype = 0) + /// + /// Writes binary data + /// + /// The field name. + /// The binary payload. + /// The BSON binary subtype. + public void WriteBinary(string name, ReadOnlySpan data, byte subtype = 0) { - WriteElementHeader(BsonType.Binary, name); - - BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(_position, 4), data.Length); - _position += 4; - - _buffer[_position] = subtype; - _position++; - - data.CopyTo(_buffer[_position..]); - _position += data.Length; + WriteElementHeader(BsonType.Binary, name); + + BinaryPrimitives.WriteInt32LittleEndian(_buffer.Slice(Position, 4), data.Length); + Position += 4; + + _buffer[Position] = subtype; + Position++; + + data.CopyTo(_buffer[Position..]); + Position += data.Length; } - /// - /// Begins writing a subdocument and returns the size position to patch later - /// - /// The field name for the subdocument. - public int BeginDocument(string name) + /// + /// Begins writing a subdocument and returns the size position to patch later + /// + /// The field name for the subdocument. + public int BeginDocument(string name) { WriteElementHeader(BsonType.Document, name); return WriteDocumentSizePlaceholder(); } /// - /// Begins writing the root document and returns the size position to patch later + /// Begins writing the root document and returns the size position to patch later /// public int BeginDocument() { return WriteDocumentSizePlaceholder(); } - /// - /// Ends the current document - /// - /// The position returned by . - public void EndDocument(int sizePosition) + /// + /// Ends the current document + /// + /// The position returned by . + public void EndDocument(int sizePosition) { WriteEndOfDocument(); PatchDocumentSize(sizePosition); } - /// - /// Begins writing a BSON array and returns the size position to patch later - /// - /// The field name for the array. - public int BeginArray(string name) + /// + /// Begins writing a BSON array and returns the size position to patch later + /// + /// The field name for the array. + public int BeginArray(string name) { WriteElementHeader(BsonType.Array, name); return WriteDocumentSizePlaceholder(); } - /// - /// Ends the current BSON array - /// - /// The position returned by . - public void EndArray(int sizePosition) + /// + /// Ends the current BSON array + /// + /// The position returned by . + public void EndArray(int sizePosition) { WriteEndOfDocument(); PatchDocumentSize(sizePosition); } -} +} \ No newline at end of file diff --git a/src/CBDD.Bson/Metadata/Attributes.cs b/src/CBDD.Bson/Metadata/Attributes.cs index 4eaf853..e0ae2f9 100755 --- a/src/CBDD.Bson/Metadata/Attributes.cs +++ b/src/CBDD.Bson/Metadata/Attributes.cs @@ -1,14 +1,11 @@ -using System; +namespace ZB.MOM.WW.CBDD.Bson; -namespace ZB.MOM.WW.CBDD.Bson +[AttributeUsage(AttributeTargets.Property)] +public class BsonIdAttribute : Attribute { - [AttributeUsage(AttributeTargets.Property)] - public class BsonIdAttribute : Attribute - { - } - - [AttributeUsage(AttributeTargets.Property)] - public class BsonIgnoreAttribute : Attribute - { - } } + +[AttributeUsage(AttributeTargets.Property)] +public class BsonIgnoreAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/CBDD.Bson/Schema/BsonField.cs b/src/CBDD.Bson/Schema/BsonField.cs index 6c8488a..4427174 100755 --- a/src/CBDD.Bson/Schema/BsonField.cs +++ b/src/CBDD.Bson/Schema/BsonField.cs @@ -1,59 +1,56 @@ -namespace ZB.MOM.WW.CBDD.Bson.Schema; - -public partial class BsonField +namespace ZB.MOM.WW.CBDD.Bson.Schema; + +public class BsonField { /// - /// Gets the field name. + /// Gets the field name. /// public required string Name { get; init; } /// - /// Gets the field BSON type. + /// Gets the field BSON type. /// public BsonType Type { get; init; } /// - /// Gets a value indicating whether the field is nullable. + /// Gets a value indicating whether the field is nullable. /// public bool IsNullable { get; init; } /// - /// Gets the nested schema when this field is a document. + /// Gets the nested schema when this field is a document. /// public BsonSchema? NestedSchema { get; init; } /// - /// Gets the array item type when this field is an array. + /// Gets the array item type when this field is an array. /// public BsonType? ArrayItemType { get; init; } /// - /// Writes this field definition to BSON. + /// Writes this field definition to BSON. /// /// The BSON writer. public void ToBson(ref BsonSpanWriter writer) { - var size = writer.BeginDocument(); - writer.WriteString("n", Name); - writer.WriteInt32("t", (int)Type); + int size = writer.BeginDocument(); + writer.WriteString("n", Name); + writer.WriteInt32("t", (int)Type); writer.WriteBoolean("b", IsNullable); - if (NestedSchema != null) - { - writer.WriteElementHeader(BsonType.Document, "s"); - NestedSchema.ToBson(ref writer); + if (NestedSchema != null) + { + writer.WriteElementHeader(BsonType.Document, "s"); + NestedSchema.ToBson(ref writer); } - if (ArrayItemType != null) - { - writer.WriteInt32("a", (int)ArrayItemType.Value); - } + if (ArrayItemType != null) writer.WriteInt32("a", (int)ArrayItemType.Value); writer.EndDocument(size); } /// - /// Reads a field definition from BSON. + /// Reads a field definition from BSON. /// /// The BSON reader. /// The deserialized field. @@ -61,59 +58,59 @@ public partial class BsonField { reader.ReadInt32(); // Read doc size - string name = ""; - BsonType type = BsonType.Null; - bool isNullable = false; - BsonSchema? nestedSchema = null; - BsonType? arrayItemType = null; - - while (reader.Remaining > 1) - { - var btype = reader.ReadBsonType(); + var name = ""; + var type = BsonType.Null; + var isNullable = false; + BsonSchema? nestedSchema = null; + BsonType? arrayItemType = null; + + while (reader.Remaining > 1) + { + var btype = reader.ReadBsonType(); if (btype == BsonType.EndOfDocument) break; - var key = reader.ReadElementHeader(); - switch (key) - { - case "n": name = reader.ReadString(); break; - case "t": type = (BsonType)reader.ReadInt32(); break; - case "b": isNullable = reader.ReadBoolean(); break; - case "s": nestedSchema = BsonSchema.FromBson(ref reader); break; - case "a": arrayItemType = (BsonType)reader.ReadInt32(); break; - default: reader.SkipValue(btype); break; - } + string key = reader.ReadElementHeader(); + switch (key) + { + case "n": name = reader.ReadString(); break; + case "t": type = (BsonType)reader.ReadInt32(); break; + case "b": isNullable = reader.ReadBoolean(); break; + case "s": nestedSchema = BsonSchema.FromBson(ref reader); break; + case "a": arrayItemType = (BsonType)reader.ReadInt32(); break; + default: reader.SkipValue(btype); break; + } } - return new BsonField - { - Name = name, - Type = type, - IsNullable = isNullable, - NestedSchema = nestedSchema, + return new BsonField + { + Name = name, + Type = type, + IsNullable = isNullable, + NestedSchema = nestedSchema, ArrayItemType = arrayItemType }; } /// - /// Computes a hash representing the field definition. + /// Computes a hash representing the field definition. /// /// The computed hash value. public long GetHash() { var hash = new HashCode(); - hash.Add(Name); - hash.Add((int)Type); - hash.Add(IsNullable); - hash.Add(ArrayItemType); + hash.Add(Name); + hash.Add((int)Type); + hash.Add(IsNullable); + hash.Add(ArrayItemType); if (NestedSchema != null) hash.Add(NestedSchema.GetHash()); return hash.ToHashCode(); } /// - /// Determines whether this field is equal to another field. + /// Determines whether this field is equal to another field. /// /// The other field. - /// if the fields are equal; otherwise, . + /// if the fields are equal; otherwise, . public bool Equals(BsonField? other) { if (other == null) return false; @@ -121,8 +118,14 @@ public partial class BsonField } /// - public override bool Equals(object? obj) => Equals(obj as BsonField); + public override bool Equals(object? obj) + { + return Equals(obj as BsonField); + } /// - public override int GetHashCode() => (int)GetHash(); -} + public override int GetHashCode() + { + return (int)GetHash(); + } +} \ No newline at end of file diff --git a/src/CBDD.Bson/Schema/BsonSchema.cs b/src/CBDD.Bson/Schema/BsonSchema.cs index adab46b..ae2353e 100755 --- a/src/CBDD.Bson/Schema/BsonSchema.cs +++ b/src/CBDD.Bson/Schema/BsonSchema.cs @@ -1,45 +1,46 @@ -namespace ZB.MOM.WW.CBDD.Bson.Schema; - -public partial class BsonSchema +namespace ZB.MOM.WW.CBDD.Bson.Schema; + +public class BsonSchema { /// - /// Gets or sets the schema title. + /// Gets or sets the schema title. /// public string? Title { get; set; } /// - /// Gets or sets the schema version. + /// Gets or sets the schema version. /// public int? Version { get; set; } /// - /// Gets the schema fields. + /// Gets the schema fields. /// public List Fields { get; } = new(); /// - /// Serializes this schema instance to BSON. + /// Serializes this schema instance to BSON. /// /// The BSON writer to write into. public void ToBson(ref BsonSpanWriter writer) { - var size = writer.BeginDocument(); - if (Title != null) writer.WriteString("t", Title); + int size = writer.BeginDocument(); + if (Title != null) writer.WriteString("t", Title); if (Version != null) writer.WriteInt32("_v", Version.Value); - var fieldsSize = writer.BeginArray("f"); - for (int i = 0; i < Fields.Count; i++) - { - writer.WriteElementHeader(BsonType.Document, i.ToString()); - Fields[i].ToBson(ref writer); - } + int fieldsSize = writer.BeginArray("f"); + for (var i = 0; i < Fields.Count; i++) + { + writer.WriteElementHeader(BsonType.Document, i.ToString()); + Fields[i].ToBson(ref writer); + } + writer.EndArray(fieldsSize); writer.EndDocument(size); } /// - /// Deserializes a schema instance from BSON. + /// Deserializes a schema instance from BSON. /// /// The BSON reader to read from. /// The deserialized schema. @@ -47,55 +48,53 @@ public partial class BsonSchema { reader.ReadInt32(); // Read doc size - var schema = new BsonSchema(); - - while (reader.Remaining > 1) - { - var btype = reader.ReadBsonType(); + var schema = new BsonSchema(); + + while (reader.Remaining > 1) + { + var btype = reader.ReadBsonType(); if (btype == BsonType.EndOfDocument) break; - var key = reader.ReadElementHeader(); - switch (key) - { - case "t": schema.Title = reader.ReadString(); break; - case "_v": schema.Version = reader.ReadInt32(); break; - case "f": - reader.ReadInt32(); // array size - while (reader.Remaining > 1) - { - var itemType = reader.ReadBsonType(); - if (itemType == BsonType.EndOfDocument) break; - reader.ReadElementHeader(); // index - schema.Fields.Add(BsonField.FromBson(ref reader)); - } - break; - default: reader.SkipValue(btype); break; - } + string key = reader.ReadElementHeader(); + switch (key) + { + case "t": schema.Title = reader.ReadString(); break; + case "_v": schema.Version = reader.ReadInt32(); break; + case "f": + reader.ReadInt32(); // array size + while (reader.Remaining > 1) + { + var itemType = reader.ReadBsonType(); + if (itemType == BsonType.EndOfDocument) break; + reader.ReadElementHeader(); // index + schema.Fields.Add(BsonField.FromBson(ref reader)); + } + + break; + default: reader.SkipValue(btype); break; + } } return schema; } /// - /// Computes a hash value for this schema based on its contents. + /// Computes a hash value for this schema based on its contents. /// /// The computed hash value. public long GetHash() { - var hash = new HashCode(); - hash.Add(Title); - foreach (var field in Fields) - { - hash.Add(field.GetHash()); - } + var hash = new HashCode(); + hash.Add(Title); + foreach (var field in Fields) hash.Add(field.GetHash()); return hash.ToHashCode(); } /// - /// Determines whether this schema is equal to another schema. + /// Determines whether this schema is equal to another schema. /// /// The schema to compare with. - /// when schemas are equal; otherwise, . + /// when schemas are equal; otherwise, . public bool Equals(BsonSchema? other) { if (other == null) return false; @@ -103,27 +102,29 @@ public partial class BsonSchema } /// - public override bool Equals(object? obj) => Equals(obj as BsonSchema); + public override bool Equals(object? obj) + { + return Equals(obj as BsonSchema); + } /// - public override int GetHashCode() => (int)GetHash(); + public override int GetHashCode() + { + return (int)GetHash(); + } /// - /// Enumerates all field keys in this schema, including nested schema keys. + /// Enumerates all field keys in this schema, including nested schema keys. /// /// An enumerable of field keys. public IEnumerable GetAllKeys() { - foreach (var field in Fields) - { - yield return field.Name; - if (field.NestedSchema != null) - { - foreach (var nestedKey in field.NestedSchema.GetAllKeys()) - { - yield return nestedKey; - } - } - } - } -} + foreach (var field in Fields) + { + yield return field.Name; + if (field.NestedSchema != null) + foreach (string nestedKey in field.NestedSchema.GetAllKeys()) + yield return nestedKey; + } + } +} \ No newline at end of file diff --git a/src/CBDD.Bson/Types/ObjectId.cs b/src/CBDD.Bson/Types/ObjectId.cs index c2ece9a..b5d551b 100755 --- a/src/CBDD.Bson/Types/ObjectId.cs +++ b/src/CBDD.Bson/Types/ObjectId.cs @@ -1,11 +1,10 @@ -using System; using System.Runtime.InteropServices; namespace ZB.MOM.WW.CBDD.Bson; /// -/// 12-byte ObjectId compatible with MongoDB ObjectId. -/// Implemented as readonly struct for zero allocation. +/// 12-byte ObjectId compatible with MongoDB ObjectId. +/// Implemented as readonly struct for zero allocation. /// [StructLayout(LayoutKind.Explicit, Size = 12)] public readonly struct ObjectId : IEquatable @@ -14,20 +13,20 @@ public readonly struct ObjectId : IEquatable [FieldOffset(4)] private readonly long _randomAndCounter; /// - /// Empty ObjectId (all zeros) + /// Empty ObjectId (all zeros) /// - public static readonly ObjectId Empty = new ObjectId(0, 0); + public static readonly ObjectId Empty = new(0, 0); /// - /// Maximum ObjectId (all 0xFF bytes) - useful for range queries + /// Maximum ObjectId (all 0xFF bytes) - useful for range queries /// - public static readonly ObjectId MaxValue = new ObjectId(int.MaxValue, long.MaxValue); + public static readonly ObjectId MaxValue = new(int.MaxValue, long.MaxValue); - /// - /// Initializes a new instance of the struct from raw bytes. - /// - /// The 12-byte ObjectId value. - public ObjectId(ReadOnlySpan bytes) + /// + /// Initializes a new instance of the struct from raw bytes. + /// + /// The 12-byte ObjectId value. + public ObjectId(ReadOnlySpan bytes) { if (bytes.Length != 12) throw new ArgumentException("ObjectId must be exactly 12 bytes", nameof(bytes)); @@ -36,32 +35,32 @@ public readonly struct ObjectId : IEquatable _randomAndCounter = BitConverter.ToInt64(bytes[4..12]); } - /// - /// Initializes a new instance of the struct from its components. - /// - /// The Unix timestamp portion. - /// The random and counter portion. - public ObjectId(int timestamp, long randomAndCounter) + /// + /// Initializes a new instance of the struct from its components. + /// + /// The Unix timestamp portion. + /// The random and counter portion. + public ObjectId(int timestamp, long randomAndCounter) { _timestamp = timestamp; _randomAndCounter = randomAndCounter; } /// - /// Creates a new ObjectId with current timestamp + /// Creates a new ObjectId with current timestamp /// public static ObjectId NewObjectId() { var timestamp = (int)DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var random = Random.Shared.NextInt64(); + long random = Random.Shared.NextInt64(); return new ObjectId(timestamp, random); } - /// - /// Writes the ObjectId to the destination span (must be 12 bytes) - /// - /// The destination span to write into. - public void WriteTo(Span destination) + /// + /// Writes the ObjectId to the destination span (must be 12 bytes) + /// + /// The destination span to write into. + public void WriteTo(Span destination) { if (destination.Length < 12) throw new ArgumentException("Destination must be at least 12 bytes", nameof(destination)); @@ -71,7 +70,7 @@ public readonly struct ObjectId : IEquatable } /// - /// Converts ObjectId to byte array + /// Converts ObjectId to byte array /// public byte[] ToByteArray() { @@ -81,32 +80,47 @@ public readonly struct ObjectId : IEquatable } /// - /// Gets timestamp portion as UTC DateTime + /// Gets timestamp portion as UTC DateTime /// public DateTime Timestamp => DateTimeOffset.FromUnixTimeSeconds(_timestamp).UtcDateTime; - /// - /// Determines whether this instance and another have the same value. - /// - /// The object to compare with this instance. - /// if the values are equal; otherwise, . - public bool Equals(ObjectId other) => - _timestamp == other._timestamp && _randomAndCounter == other._randomAndCounter; - - /// - public override bool Equals(object? obj) => obj is ObjectId other && Equals(other); - - /// - public override int GetHashCode() => HashCode.Combine(_timestamp, _randomAndCounter); + /// + /// Determines whether this instance and another have the same value. + /// + /// The object to compare with this instance. + /// if the values are equal; otherwise, . + public bool Equals(ObjectId other) + { + return _timestamp == other._timestamp && _randomAndCounter == other._randomAndCounter; + } - public static bool operator ==(ObjectId left, ObjectId right) => left.Equals(right); - public static bool operator !=(ObjectId left, ObjectId right) => !left.Equals(right); + /// + public override bool Equals(object? obj) + { + return obj is ObjectId other && Equals(other); + } - /// - public override string ToString() + /// + public override int GetHashCode() + { + return HashCode.Combine(_timestamp, _randomAndCounter); + } + + public static bool operator ==(ObjectId left, ObjectId right) + { + return left.Equals(right); + } + + public static bool operator !=(ObjectId left, ObjectId right) + { + return !left.Equals(right); + } + + /// + public override string ToString() { Span bytes = stackalloc byte[12]; WriteTo(bytes); return Convert.ToHexString(bytes).ToLowerInvariant(); } -} +} \ No newline at end of file diff --git a/src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj b/src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj index a78bed0..a47bc82 100755 --- a/src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj +++ b/src/CBDD.Bson/ZB.MOM.WW.CBDD.Bson.csproj @@ -1,28 +1,28 @@ - - net10.0 - ZB.MOM.WW.CBDD.Bson - ZB.MOM.WW.CBDD.Bson - latest - enable - enable - true - true - - ZB.MOM.WW.CBDD.Bson - 1.3.1 - CBDD Team - BSON Serialization Library for High-Performance Database Engine - MIT - README.md - https://github.com/EntglDb/CBDD - database;embedded;bson;nosql;net10;zero-allocation - True - + + net10.0 + ZB.MOM.WW.CBDD.Bson + ZB.MOM.WW.CBDD.Bson + latest + enable + enable + true + true - - - + ZB.MOM.WW.CBDD.Bson + 1.3.1 + CBDD Team + BSON Serialization Library for High-Performance Database Engine + MIT + README.md + https://github.com/EntglDb/CBDD + database;embedded;bson;nosql;net10;zero-allocation + True + + + + + diff --git a/src/CBDD.Core/CDC/ChangeStreamDispatcher.cs b/src/CBDD.Core/CDC/ChangeStreamDispatcher.cs index dc3b841..2de8389 100755 --- a/src/CBDD.Core/CDC/ChangeStreamDispatcher.cs +++ b/src/CBDD.Core/CDC/ChangeStreamDispatcher.cs @@ -1,23 +1,21 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Threading; using System.Threading.Channels; -using System.Threading.Tasks; namespace ZB.MOM.WW.CBDD.Core.CDC; -internal sealed class ChangeStreamDispatcher : IDisposable -{ +internal sealed class ChangeStreamDispatcher : IDisposable +{ private readonly Channel _channel; - private readonly ConcurrentDictionary, byte>> _subscriptions = new(); - private readonly ConcurrentDictionary _payloadWatcherCounts = new(); private readonly CancellationTokenSource _cts = new(); + private readonly ConcurrentDictionary _payloadWatcherCounts = new(); - /// - /// Initializes a new change stream dispatcher. - /// - public ChangeStreamDispatcher() + private readonly ConcurrentDictionary, byte>> + _subscriptions = new(); + + /// + /// Initializes a new change stream dispatcher. + /// + public ChangeStreamDispatcher() { _channel = Channel.CreateUnbounded(new UnboundedChannelOptions { @@ -28,50 +26,57 @@ internal sealed class ChangeStreamDispatcher : IDisposable Task.Run(ProcessEventsAsync); } - /// - /// Publishes a change event to subscribers. - /// - /// The change event to publish. - public void Publish(InternalChangeEvent change) + /// + /// Releases dispatcher resources. + /// + public void Dispose() + { + _cts.Cancel(); + _cts.Dispose(); + } + + /// + /// Publishes a change event to subscribers. + /// + /// The change event to publish. + public void Publish(InternalChangeEvent change) { _channel.Writer.TryWrite(change); } - /// - /// Determines whether a collection has subscribers that require payloads. - /// - /// The collection name. - /// if payload watchers exist; otherwise, . - public bool HasPayloadWatchers(string collectionName) + /// + /// Determines whether a collection has subscribers that require payloads. + /// + /// The collection name. + /// if payload watchers exist; otherwise, . + public bool HasPayloadWatchers(string collectionName) { - return _payloadWatcherCounts.TryGetValue(collectionName, out var count) && count > 0; + return _payloadWatcherCounts.TryGetValue(collectionName, out int count) && count > 0; } - /// - /// Determines whether a collection has any subscribers. - /// - /// The collection name. - /// if subscribers exist; otherwise, . - public bool HasAnyWatchers(string collectionName) + /// + /// Determines whether a collection has any subscribers. + /// + /// The collection name. + /// if subscribers exist; otherwise, . + public bool HasAnyWatchers(string collectionName) { return _subscriptions.TryGetValue(collectionName, out var subs) && !subs.IsEmpty; } - /// - /// Subscribes a channel writer to collection change events. - /// - /// The collection name to subscribe to. - /// Whether this subscriber requires event payloads. - /// The destination channel writer. - /// An that removes the subscription when disposed. - public IDisposable Subscribe(string collectionName, bool capturePayload, ChannelWriter writer) + /// + /// Subscribes a channel writer to collection change events. + /// + /// The collection name to subscribe to. + /// Whether this subscriber requires event payloads. + /// The destination channel writer. + /// An that removes the subscription when disposed. + public IDisposable Subscribe(string collectionName, bool capturePayload, ChannelWriter writer) { - if (capturePayload) - { - _payloadWatcherCounts.AddOrUpdate(collectionName, 1, (_, count) => count + 1); - } + if (capturePayload) _payloadWatcherCounts.AddOrUpdate(collectionName, 1, (_, count) => count + 1); - var collectionSubs = _subscriptions.GetOrAdd(collectionName, _ => new ConcurrentDictionary, byte>()); + var collectionSubs = _subscriptions.GetOrAdd(collectionName, + _ => new ConcurrentDictionary, byte>()); collectionSubs.TryAdd(writer, 0); return new Subscription(() => Unsubscribe(collectionName, capturePayload, writer)); @@ -79,15 +84,9 @@ internal sealed class ChangeStreamDispatcher : IDisposable private void Unsubscribe(string collectionName, bool capturePayload, ChannelWriter writer) { - if (_subscriptions.TryGetValue(collectionName, out var collectionSubs)) - { - collectionSubs.TryRemove(writer, out _); - } + if (_subscriptions.TryGetValue(collectionName, out var collectionSubs)) collectionSubs.TryRemove(writer, out _); - if (capturePayload) - { - _payloadWatcherCounts.AddOrUpdate(collectionName, 0, (_, count) => Math.Max(0, count - 1)); - } + if (capturePayload) _payloadWatcherCounts.AddOrUpdate(collectionName, 0, (_, count) => Math.Max(0, count - 1)); } private async Task ProcessEventsAsync() @@ -96,60 +95,45 @@ internal sealed class ChangeStreamDispatcher : IDisposable { var reader = _channel.Reader; while (await reader.WaitToReadAsync(_cts.Token)) - { - while (reader.TryRead(out var @event)) - { - if (_subscriptions.TryGetValue(@event.CollectionName, out var collectionSubs)) - { - foreach (var writer in collectionSubs.Keys) - { - // Optimized fan-out: non-blocking TryWrite. - // If a subscriber channel is full (unlikely with Unbounded), - // we skip or drop. Usually, subscribers will also use Unbounded. - writer.TryWrite(@event); - } - } - } - } + while (reader.TryRead(out var @event)) + if (_subscriptions.TryGetValue(@event.CollectionName, out var collectionSubs)) + foreach (var writer in collectionSubs.Keys) + // Optimized fan-out: non-blocking TryWrite. + // If a subscriber channel is full (unlikely with Unbounded), + // we skip or drop. Usually, subscribers will also use Unbounded. + writer.TryWrite(@event); + } + catch (OperationCanceledException) + { } - catch (OperationCanceledException) { } catch (Exception) { // Internal error logging could go here } } - /// - /// Releases dispatcher resources. - /// - public void Dispose() - { - _cts.Cancel(); - _cts.Dispose(); - } - private sealed class Subscription : IDisposable { private readonly Action _onDispose; private bool _disposed; - /// - /// Initializes a new subscription token. - /// - /// Callback executed when the subscription is disposed. - public Subscription(Action onDispose) + /// + /// Initializes a new subscription token. + /// + /// Callback executed when the subscription is disposed. + public Subscription(Action onDispose) { _onDispose = onDispose; } - /// - /// Disposes the subscription and unregisters the subscriber. - /// - public void Dispose() + /// + /// Disposes the subscription and unregisters the subscriber. + /// + public void Dispose() { if (_disposed) return; _onDispose(); _disposed = true; } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/CDC/ChangeStreamEvent.cs b/src/CBDD.Core/CDC/ChangeStreamEvent.cs index 4e87411..b2b27fd 100755 --- a/src/CBDD.Core/CDC/ChangeStreamEvent.cs +++ b/src/CBDD.Core/CDC/ChangeStreamEvent.cs @@ -1,76 +1,75 @@ -using System; -using ZB.MOM.WW.CBDD.Core.Transactions; - -namespace ZB.MOM.WW.CBDD.Core.CDC; - -/// -/// A generic, immutable struct representing a data change in a collection. -/// +using ZB.MOM.WW.CBDD.Core.Transactions; + +namespace ZB.MOM.WW.CBDD.Core.CDC; + +/// +/// A generic, immutable struct representing a data change in a collection. +/// public readonly struct ChangeStreamEvent where T : class { /// - /// Gets the UTC timestamp when the change was recorded. + /// Gets the UTC timestamp when the change was recorded. /// public long Timestamp { get; init; } /// - /// Gets the transaction identifier that produced the change. + /// Gets the transaction identifier that produced the change. /// public ulong TransactionId { get; init; } /// - /// Gets the collection name where the change occurred. + /// Gets the collection name where the change occurred. /// public string CollectionName { get; init; } /// - /// Gets the operation type associated with the change. + /// Gets the operation type associated with the change. /// public OperationType Type { get; init; } /// - /// Gets the changed document identifier. + /// Gets the changed document identifier. /// public TId DocumentId { get; init; } - /// - /// The deserialized entity. Null if capturePayload was false during Watch(). - /// - public T? Entity { get; init; } -} - -/// -/// Low-level event structure used internally to transport changes before deserialization. -/// + /// + /// The deserialized entity. Null if capturePayload was false during Watch(). + /// + public T? Entity { get; init; } +} + +/// +/// Low-level event structure used internally to transport changes before deserialization. +/// internal readonly struct InternalChangeEvent { /// - /// Gets the UTC timestamp when the change was recorded. + /// Gets the UTC timestamp when the change was recorded. /// public long Timestamp { get; init; } /// - /// Gets the transaction identifier that produced the change. + /// Gets the transaction identifier that produced the change. /// public ulong TransactionId { get; init; } /// - /// Gets the collection name where the change occurred. + /// Gets the collection name where the change occurred. /// public string CollectionName { get; init; } /// - /// Gets the operation type associated with the change. + /// Gets the operation type associated with the change. /// public OperationType Type { get; init; } - /// - /// Raw BSON of the Document ID. - /// + /// + /// Raw BSON of the Document ID. + /// public ReadOnlyMemory IdBytes { get; init; } - /// - /// Raw BSON of the Entity. Null if payload not captured. - /// - public ReadOnlyMemory? PayloadBytes { get; init; } -} + /// + /// Raw BSON of the Entity. Null if payload not captured. + /// + public ReadOnlyMemory? PayloadBytes { get; init; } +} \ No newline at end of file diff --git a/src/CBDD.Core/CDC/ChangeStreamObservable.cs b/src/CBDD.Core/CDC/ChangeStreamObservable.cs index 10c7561..d9ca0ba 100755 --- a/src/CBDD.Core/CDC/ChangeStreamObservable.cs +++ b/src/CBDD.Core/CDC/ChangeStreamObservable.cs @@ -1,49 +1,45 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Threading.Channels; -using System.Threading.Tasks; -using System.Threading; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Indexing; namespace ZB.MOM.WW.CBDD.Core.CDC; -internal sealed class ChangeStreamObservable : IObservable> where T : class -{ - private readonly ChangeStreamDispatcher _dispatcher; - private readonly string _collectionName; +internal sealed class ChangeStreamObservable : IObservable> where T : class +{ private readonly bool _capturePayload; - private readonly IDocumentMapper _mapper; + private readonly string _collectionName; + private readonly ChangeStreamDispatcher _dispatcher; private readonly ConcurrentDictionary _keyReverseMap; + private readonly IDocumentMapper _mapper; - /// - /// Initializes a new observable wrapper for collection change events. - /// - /// The dispatcher producing internal change events. - /// The collection to subscribe to. - /// Whether full entity payloads should be included. - /// The document mapper used for ID and payload deserialization. - /// The key reverse map used by BSON readers. - public ChangeStreamObservable( - ChangeStreamDispatcher dispatcher, - string collectionName, - bool capturePayload, - IDocumentMapper mapper, + /// + /// Initializes a new observable wrapper for collection change events. + /// + /// The dispatcher producing internal change events. + /// The collection to subscribe to. + /// Whether full entity payloads should be included. + /// The document mapper used for ID and payload deserialization. + /// The key reverse map used by BSON readers. + public ChangeStreamObservable( + ChangeStreamDispatcher dispatcher, + string collectionName, + bool capturePayload, + IDocumentMapper mapper, ConcurrentDictionary keyReverseMap) { _dispatcher = dispatcher; _collectionName = collectionName; _capturePayload = capturePayload; - _mapper = mapper; - _keyReverseMap = keyReverseMap; - } - - /// - public IDisposable Subscribe(IObserver> observer) - { - if (observer == null) throw new ArgumentNullException(nameof(observer)); + _mapper = mapper; + _keyReverseMap = keyReverseMap; + } + + /// + public IDisposable Subscribe(IObserver> observer) + { + if (observer == null) throw new ArgumentNullException(nameof(observer)); var cts = new CancellationTokenSource(); var channel = Channel.CreateUnbounded(new UnboundedChannelOptions @@ -60,46 +56,43 @@ internal sealed class ChangeStreamObservable : IObservable reader, IObserver> observer, CancellationToken ct) + private async Task BridgeChannelToObserverAsync(ChannelReader reader, + IObserver> observer, CancellationToken ct) { try { while (await reader.WaitToReadAsync(ct)) - { - while (reader.TryRead(out var internalEvent)) + while (reader.TryRead(out var internalEvent)) + try { - try - { - // Deserializza ID - var eventId = _mapper.FromIndexKey(new IndexKey(internalEvent.IdBytes.ToArray())); - - // Deserializza Payload (se presente) - T? entity = default; - if (internalEvent.PayloadBytes.HasValue) - { - entity = _mapper.Deserialize(new BsonSpanReader(internalEvent.PayloadBytes.Value.Span, _keyReverseMap)); - } + // Deserializza ID + var eventId = _mapper.FromIndexKey(new IndexKey(internalEvent.IdBytes.ToArray())); - var externalEvent = new ChangeStreamEvent - { - Timestamp = internalEvent.Timestamp, - TransactionId = internalEvent.TransactionId, - CollectionName = internalEvent.CollectionName, - Type = internalEvent.Type, - DocumentId = eventId, - Entity = entity - }; + // Deserializza Payload (se presente) + T? entity = default; + if (internalEvent.PayloadBytes.HasValue) + entity = _mapper.Deserialize(new BsonSpanReader(internalEvent.PayloadBytes.Value.Span, + _keyReverseMap)); - observer.OnNext(externalEvent); - } - catch (Exception ex) + var externalEvent = new ChangeStreamEvent { - // In case of deserialization error, we notify and continue if possible - // Or we can stop the observer. - observer.OnError(ex); - } + Timestamp = internalEvent.Timestamp, + TransactionId = internalEvent.TransactionId, + CollectionName = internalEvent.CollectionName, + Type = internalEvent.Type, + DocumentId = eventId, + Entity = entity + }; + + observer.OnNext(externalEvent); } - } + catch (Exception ex) + { + // In case of deserialization error, we notify and continue if possible + // Or we can stop the observer. + observer.OnError(ex); + } + observer.OnCompleted(); } catch (OperationCanceledException) @@ -112,33 +105,34 @@ internal sealed class ChangeStreamObservable : IObservable _writer; + private sealed class CompositeDisposable : IDisposable + { private readonly Task _bridgeTask; + private readonly CancellationTokenSource _cts; + private readonly IDisposable _dispatcherSubscription; + private readonly ChannelWriter _writer; private bool _disposed; - /// - /// Initializes a new disposable wrapper for change stream resources. - /// - /// The dispatcher subscription handle. - /// The cancellation source controlling the bridge task. - /// The channel writer for internal change events. - /// The running bridge task. - public CompositeDisposable(IDisposable dispatcherSubscription, CancellationTokenSource cts, ChannelWriter writer, Task bridgeTask) - { - _dispatcherSubscription = dispatcherSubscription; - _cts = cts; - _writer = writer; - _bridgeTask = bridgeTask; - } - - /// - public void Dispose() - { - if (_disposed) return; + /// + /// Initializes a new disposable wrapper for change stream resources. + /// + /// The dispatcher subscription handle. + /// The cancellation source controlling the bridge task. + /// The channel writer for internal change events. + /// The running bridge task. + public CompositeDisposable(IDisposable dispatcherSubscription, CancellationTokenSource cts, + ChannelWriter writer, Task bridgeTask) + { + _dispatcherSubscription = dispatcherSubscription; + _cts = cts; + _writer = writer; + _bridgeTask = bridgeTask; + } + + /// + public void Dispose() + { + if (_disposed) return; _disposed = true; _dispatcherSubscription.Dispose(); @@ -147,4 +141,4 @@ internal sealed class ChangeStreamObservable : IObservable -/// Handles CDC watch/notify behavior for a single collection. -/// Extracted from DocumentCollection to keep storage/query concerns separated from event plumbing. +/// Handles CDC watch/notify behavior for a single collection. +/// Extracted from DocumentCollection to keep storage/query concerns separated from event plumbing. /// /// Document identifier type. /// Document type. internal sealed class CollectionCdcPublisher where T : class { - private readonly ITransactionHolder _transactionHolder; private readonly string _collectionName; - private readonly IDocumentMapper _mapper; private readonly ChangeStreamDispatcher? _dispatcher; private readonly ConcurrentDictionary _keyReverseMap; + private readonly IDocumentMapper _mapper; + private readonly ITransactionHolder _transactionHolder; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The transaction holder. /// The collection name. @@ -42,7 +41,7 @@ internal sealed class CollectionCdcPublisher where T : class } /// - /// Executes Watch. + /// Executes Watch. /// /// Whether to include payload data. public IObservable> Watch(bool capturePayload = false) @@ -59,7 +58,7 @@ internal sealed class CollectionCdcPublisher where T : class } /// - /// Executes Notify. + /// Executes Notify. /// /// The operation type. /// The document identifier. @@ -74,15 +73,11 @@ internal sealed class CollectionCdcPublisher where T : class return; ReadOnlyMemory? payload = null; - if (!docData.IsEmpty && _dispatcher.HasPayloadWatchers(_collectionName)) - { - payload = docData.ToArray(); - } + if (!docData.IsEmpty && _dispatcher.HasPayloadWatchers(_collectionName)) payload = docData.ToArray(); - var idBytes = _mapper.ToIndexKey(id).Data.ToArray(); + byte[] idBytes = _mapper.ToIndexKey(id).Data.ToArray(); if (transaction is Transaction t) - { t.AddChange(new InternalChangeEvent { Timestamp = DateTime.UtcNow.Ticks, @@ -92,6 +87,5 @@ internal sealed class CollectionCdcPublisher where T : class IdBytes = idBytes, PayloadBytes = payload }); - } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Collections/BaseMappers.cs b/src/CBDD.Core/Collections/BaseMappers.cs index e6d5e26..9d6f96e 100755 --- a/src/CBDD.Core/Collections/BaseMappers.cs +++ b/src/CBDD.Core/Collections/BaseMappers.cs @@ -1,25 +1,21 @@ -using System; -using System.Buffers; -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Indexing; -using System.Linq; -using System.Collections.Generic; -using ZB.MOM.WW.CBDD.Bson.Schema; - -namespace ZB.MOM.WW.CBDD.Core.Collections; - -/// -/// Base class for custom mappers that provides bidirectional IndexKey mapping for standard types. -/// +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Bson.Schema; +using ZB.MOM.WW.CBDD.Core.Indexing; + +namespace ZB.MOM.WW.CBDD.Core.Collections; + +/// +/// Base class for custom mappers that provides bidirectional IndexKey mapping for standard types. +/// public abstract class DocumentMapperBase : IDocumentMapper where T : class { /// - /// Gets the target collection name for the mapped entity type. + /// Gets the target collection name for the mapped entity type. /// public abstract string CollectionName { get; } /// - /// Serializes an entity instance into BSON. + /// Serializes an entity instance into BSON. /// /// The entity to serialize. /// The BSON writer to write into. @@ -27,96 +23,129 @@ public abstract class DocumentMapperBase : IDocumentMapper where public abstract int Serialize(T entity, BsonSpanWriter writer); /// - /// Deserializes an entity instance from BSON. + /// Deserializes an entity instance from BSON. /// /// The BSON reader to read from. /// The deserialized entity. public abstract T Deserialize(BsonSpanReader reader); /// - /// Gets the identifier value from an entity. + /// Gets the identifier value from an entity. /// /// The entity to read the identifier from. /// The identifier value. public abstract TId GetId(T entity); /// - /// Sets the identifier value on an entity. + /// Sets the identifier value on an entity. /// /// The entity to update. /// The identifier value to assign. public abstract void SetId(T entity, TId id); /// - /// Converts a typed identifier value into an index key. + /// Converts a typed identifier value into an index key. /// /// The identifier value. /// The index key representation of the identifier. - public virtual IndexKey ToIndexKey(TId id) => IndexKey.Create(id); + public virtual IndexKey ToIndexKey(TId id) + { + return IndexKey.Create(id); + } /// - /// Converts an index key back into a typed identifier value. + /// Converts an index key back into a typed identifier value. /// /// The index key to convert. /// The typed identifier value. - public virtual TId FromIndexKey(IndexKey key) => key.As(); + public virtual TId FromIndexKey(IndexKey key) + { + return key.As(); + } /// - /// Gets all mapped field keys used by this mapper. + /// Gets all mapped field keys used by this mapper. /// public virtual IEnumerable UsedKeys => GetSchema().GetAllKeys(); /// - /// Builds the BSON schema for the mapped entity type. + /// Builds the BSON schema for the mapped entity type. /// /// The generated BSON schema. - public virtual BsonSchema GetSchema() => BsonSchemaGenerator.FromType(); + public virtual BsonSchema GetSchema() + { + return BsonSchemaGenerator.FromType(); + } } - -/// -/// Base class for mappers using ObjectId as primary key. -/// + +/// +/// Base class for mappers using ObjectId as primary key. +/// public abstract class ObjectIdMapperBase : DocumentMapperBase, IDocumentMapper where T : class { /// - public override IndexKey ToIndexKey(ObjectId id) => IndexKey.Create(id); + public override IndexKey ToIndexKey(ObjectId id) + { + return IndexKey.Create(id); + } /// - public override ObjectId FromIndexKey(IndexKey key) => key.As(); + public override ObjectId FromIndexKey(IndexKey key) + { + return key.As(); + } } - -/// -/// Base class for mappers using Int32 as primary key. -/// + +/// +/// Base class for mappers using Int32 as primary key. +/// public abstract class Int32MapperBase : DocumentMapperBase where T : class { /// - public override IndexKey ToIndexKey(int id) => IndexKey.Create(id); + public override IndexKey ToIndexKey(int id) + { + return IndexKey.Create(id); + } /// - public override int FromIndexKey(IndexKey key) => key.As(); + public override int FromIndexKey(IndexKey key) + { + return key.As(); + } } - -/// -/// Base class for mappers using String as primary key. -/// + +/// +/// Base class for mappers using String as primary key. +/// public abstract class StringMapperBase : DocumentMapperBase where T : class { /// - public override IndexKey ToIndexKey(string id) => IndexKey.Create(id); + public override IndexKey ToIndexKey(string id) + { + return IndexKey.Create(id); + } /// - public override string FromIndexKey(IndexKey key) => key.As(); + public override string FromIndexKey(IndexKey key) + { + return key.As(); + } } - -/// -/// Base class for mappers using Guid as primary key. -/// + +/// +/// Base class for mappers using Guid as primary key. +/// public abstract class GuidMapperBase : DocumentMapperBase where T : class { /// - public override IndexKey ToIndexKey(Guid id) => IndexKey.Create(id); + public override IndexKey ToIndexKey(Guid id) + { + return IndexKey.Create(id); + } /// - public override Guid FromIndexKey(IndexKey key) => key.As(); -} + public override Guid FromIndexKey(IndexKey key) + { + return key.As(); + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Collections/BsonSchemaGenerator.cs b/src/CBDD.Core/Collections/BsonSchemaGenerator.cs index 8a58cd3..7fa6a03 100755 --- a/src/CBDD.Core/Collections/BsonSchemaGenerator.cs +++ b/src/CBDD.Core/Collections/BsonSchemaGenerator.cs @@ -1,36 +1,33 @@ -using System.Reflection; -using System.Linq; using System.Collections; -using System.Collections.Generic; using System.Collections.Concurrent; +using System.Reflection; using ZB.MOM.WW.CBDD.Bson; -using System; using ZB.MOM.WW.CBDD.Bson.Schema; namespace ZB.MOM.WW.CBDD.Core.Collections; -public static class BsonSchemaGenerator -{ - /// - /// Generates a BSON schema for the specified CLR type. - /// - /// The CLR type to inspect. - /// The generated BSON schema. - public static BsonSchema FromType() - { - return FromType(typeof(T)); - } +public static class BsonSchemaGenerator +{ + private static readonly ConcurrentDictionary _cache = new(); - private static readonly ConcurrentDictionary _cache = new(); - - /// - /// Generates a BSON schema for the specified CLR type. - /// - /// The CLR type to inspect. - /// The generated BSON schema. - public static BsonSchema FromType(Type type) - { - return _cache.GetOrAdd(type, GenerateSchema); + /// + /// Generates a BSON schema for the specified CLR type. + /// + /// The CLR type to inspect. + /// The generated BSON schema. + public static BsonSchema FromType() + { + return FromType(typeof(T)); + } + + /// + /// Generates a BSON schema for the specified CLR type. + /// + /// The CLR type to inspect. + /// The generated BSON schema. + public static BsonSchema FromType(Type type) + { + return _cache.GetOrAdd(type, GenerateSchema); } private static BsonSchema GenerateSchema(Type type) @@ -47,10 +44,7 @@ public static class BsonSchemaGenerator AddField(schema, prop.Name, prop.PropertyType); } - foreach (var field in fields) - { - AddField(schema, field.Name, field.FieldType); - } + foreach (var field in fields) AddField(schema, field.Name, field.FieldType); return schema; } @@ -60,10 +54,7 @@ public static class BsonSchemaGenerator name = name.ToLowerInvariant(); // Convention: id -> _id for root document - if (name.Equals("id", StringComparison.OrdinalIgnoreCase)) - { - name = "_id"; - } + if (name.Equals("id", StringComparison.OrdinalIgnoreCase)) name = "_id"; var (bsonType, nestedSchema, itemType) = GetBsonType(type); @@ -97,20 +88,18 @@ public static class BsonSchemaGenerator if (type != typeof(string) && typeof(IEnumerable).IsAssignableFrom(type)) { var itemType = GetCollectionItemType(type); - var (itemBsonType, itemNested, _) = GetBsonType(itemType); - - // For arrays, if item is Document, we use NestedSchema to describe the item + var (itemBsonType, itemNested, _) = GetBsonType(itemType); + + // For arrays, if item is Document, we use NestedSchema to describe the item return (BsonType.Array, itemNested, itemBsonType); } // Nested Objects / Structs // If it's not a string, not a primitive, and not an array/list, treat as Document if (type != typeof(string) && !type.IsPrimitive && !type.IsEnum) - { // Avoid infinite recursion? // Simple approach: generating nested schema return (BsonType.Document, FromType(type), null); - } return (BsonType.Undefined, null, null); } @@ -122,17 +111,15 @@ public static class BsonSchemaGenerator private static Type GetCollectionItemType(Type type) { - if (type.IsArray) return type.GetElementType()!; - - // If type itself is IEnumerable + if (type.IsArray) return type.GetElementType()!; + + // If type itself is IEnumerable if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { return type.GetGenericArguments()[0]; - } var enumerableType = type.GetInterfaces() - .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); - + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)); + return enumerableType?.GetGenericArguments()[0] ?? typeof(object); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Collections/DocumentCollection.Scan.cs b/src/CBDD.Core/Collections/DocumentCollection.Scan.cs index ffc79aa..5728809 100644 --- a/src/CBDD.Core/Collections/DocumentCollection.Scan.cs +++ b/src/CBDD.Core/Collections/DocumentCollection.Scan.cs @@ -8,8 +8,8 @@ namespace ZB.MOM.WW.CBDD.Core.Collections; public partial class DocumentCollection where T : class { /// - /// Scans the entire collection using a raw BSON predicate. - /// This avoids deserializing documents that don't match the criteria. + /// Scans the entire collection using a raw BSON predicate. + /// This avoids deserializing documents that don't match the criteria. /// /// Function to evaluate raw BSON data /// Matching documents @@ -18,8 +18,8 @@ public partial class DocumentCollection where T : class if (predicate == null) throw new ArgumentNullException(nameof(predicate)); var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var txnId = transaction.TransactionId; - var pageCount = _storage.PageCount; + ulong txnId = transaction.TransactionId; + uint pageCount = _storage.PageCount; var buffer = new byte[_storage.PageSize]; var pageResults = new List(); @@ -28,16 +28,13 @@ public partial class DocumentCollection where T : class pageResults.Clear(); ScanPage(pageId, txnId, buffer, predicate, pageResults); - foreach (var doc in pageResults) - { - yield return doc; - } + foreach (var doc in pageResults) yield return doc; } } /// - /// Scans the collection in parallel using multiple threads. - /// Useful for large collections on multi-core machines. + /// Scans the collection in parallel using multiple threads. + /// Useful for large collections on multi-core machines. /// /// Function to evaluate raw BSON data /// Number of threads to use (default: -1 = ProcessorCount) @@ -46,7 +43,7 @@ public partial class DocumentCollection where T : class if (predicate == null) throw new ArgumentNullException(nameof(predicate)); var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var txnId = transaction.TransactionId; + ulong txnId = transaction.TransactionId; var pageCount = (int)_storage.PageCount; if (degreeOfParallelism <= 0) @@ -61,15 +58,14 @@ public partial class DocumentCollection where T : class var localResults = new List(); for (int i = range.Item1; i < range.Item2; i++) - { ScanPage((uint)i, txnId, localBuffer, predicate, localResults); - } return localResults; }); } - private void ScanPage(uint pageId, ulong txnId, byte[] buffer, Func predicate, List results) + private void ScanPage(uint pageId, ulong txnId, byte[] buffer, Func predicate, + List results) { _storage.ReadPage(pageId, txnId, buffer); var header = SlottedPageHeader.ReadFrom(buffer); @@ -80,7 +76,7 @@ public partial class DocumentCollection where T : class var slots = MemoryMarshal.Cast( buffer.AsSpan(SlottedPageHeader.Size, header.SlotCount * SlotEntry.Size)); - for (int i = 0; i < header.SlotCount; i++) + for (var i = 0; i < header.SlotCount; i++) { var slot = slots[i]; @@ -98,4 +94,4 @@ public partial class DocumentCollection where T : class } } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Collections/DocumentCollection.cs b/src/CBDD.Core/Collections/DocumentCollection.cs index cf1d85d..02630ea 100755 --- a/src/CBDD.Core/Collections/DocumentCollection.cs +++ b/src/CBDD.Core/Collections/DocumentCollection.cs @@ -1,121 +1,114 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.Linq.Expressions; +using System.Runtime.CompilerServices; +using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.CDC; -using System.Buffers; -using System.Buffers.Binary; -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Core.Compression; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; -using System.Runtime.CompilerServices; -using System.Threading.Tasks; -using System.Linq; -using System.Linq.Expressions; -using ZB.MOM.WW.CBDD.Core.Query; -using System.Collections.Generic; -using System; -using System.IO; -using System.Diagnostics; -using System.Threading; -using ZB.MOM.WW.CBDD.Core; +using ZB.MOM.WW.CBDD.Core.Compression; +using ZB.MOM.WW.CBDD.Core.Indexing; +using ZB.MOM.WW.CBDD.Core.Metadata; +using ZB.MOM.WW.CBDD.Core.Query; +using ZB.MOM.WW.CBDD.Core.Storage; +using ZB.MOM.WW.CBDD.Core.Transactions; -[assembly: InternalsVisibleTo("ZB.MOM.WW.CBDD.Tests")] +[assembly: InternalsVisibleTo("ZB.MOM.WW.CBDD.Tests")] namespace ZB.MOM.WW.CBDD.Core.Collections; -public class DocumentCollection : DocumentCollection where T : class -{ - /// - /// Initializes a new document collection that uses as the primary key. - /// - /// The storage engine used for persistence. - /// The transaction context holder. - /// The document mapper for . - /// Optional collection name override. - public DocumentCollection(StorageEngine storage, ITransactionHolder transactionHolder, IDocumentMapper mapper, string? collectionName = null) - : this((IStorageEngine)storage, transactionHolder, mapper, collectionName) - { - } - - /// - /// Initializes a new document collection that uses as the primary key. - /// - /// The storage engine used for persistence. - /// The transaction context holder. - /// The document mapper for . - /// Optional collection name override. - internal DocumentCollection(IStorageEngine storage, ITransactionHolder transactionHolder, IDocumentMapper mapper, string? collectionName = null) - : base(storage, transactionHolder, mapper, collectionName) - { - } -} +public class DocumentCollection : DocumentCollection where T : class +{ + /// + /// Initializes a new document collection that uses as the primary key. + /// + /// The storage engine used for persistence. + /// The transaction context holder. + /// The document mapper for . + /// Optional collection name override. + public DocumentCollection(StorageEngine storage, ITransactionHolder transactionHolder, IDocumentMapper mapper, + string? collectionName = null) + : this((IStorageEngine)storage, transactionHolder, mapper, collectionName) + { + } -/// -/// Production-ready document collection with slotted page architecture. -/// Supports multiple documents per page, overflow chains, and efficient space utilization. -/// Represents a collection of documents of type T with an ID of type TId. -/// -/// Type of the primary key -/// Type of the entity -public partial class DocumentCollection : IDisposable, ICompactionAwareCollection where T : class -{ - private readonly ITransactionHolder _transactionHolder; - private readonly IStorageEngine _storage; - private readonly IDocumentMapper _mapper; - internal readonly BTreeIndex _primaryIndex; - private readonly CollectionIndexManager _indexManager; - private readonly CollectionCdcPublisher _cdcPublisher; - private readonly string _collectionName; + /// + /// Initializes a new document collection that uses as the primary key. + /// + /// The storage engine used for persistence. + /// The transaction context holder. + /// The document mapper for . + /// Optional collection name override. + internal DocumentCollection(IStorageEngine storage, ITransactionHolder transactionHolder, IDocumentMapper mapper, + string? collectionName = null) + : base(storage, transactionHolder, mapper, collectionName) + { + } +} + +/// +/// Production-ready document collection with slotted page architecture. +/// Supports multiple documents per page, overflow chains, and efficient space utilization. +/// Represents a collection of documents of type T with an ID of type TId. +/// +/// Type of the primary key +/// Type of the entity +public partial class DocumentCollection : IDisposable, ICompactionAwareCollection where T : class +{ + private const int OverflowMetadataSize = 8; + private const int MaxLogicalDocumentSizeBytes = 16 * 1024 * 1024; + private const int MaxStoredPayloadSizeBytes = MaxLogicalDocumentSizeBytes + CompressedPayloadHeader.Size; + private readonly CollectionCdcPublisher _cdcPublisher; + + // Concurrency control for write operations (B-Tree and Page modifications) + private readonly SemaphoreSlim _collectionLock = new(1, 1); + private readonly string _collectionName; // Free space tracking: PageId → Free bytes private readonly Dictionary _freeSpaceMap; + private readonly CollectionIndexManager _indexManager; + private readonly IDocumentMapper _mapper; + + private readonly int _maxDocumentSizeForSinglePage; + internal readonly BTreeIndex _primaryIndex; + private readonly IStorageEngine _storage; + private readonly ITransactionHolder _transactionHolder; // Current page for inserts (optimization) private uint _currentDataPage; - /// - /// Gets the current persisted schema version for the collection. - /// - public SchemaVersion? CurrentSchemaVersion { get; private set; } + /// + /// Initializes a new instance of the document collection. + /// + /// The storage engine used for persistence. + /// The transaction context holder. + /// The mapper used to serialize and deserialize documents. + /// Optional collection name override. + public DocumentCollection(StorageEngine storage, ITransactionHolder transactionHolder, + IDocumentMapper mapper, string? collectionName = null) + : this((IStorageEngine)storage, transactionHolder, mapper, collectionName) + { + } - // Concurrency control for write operations (B-Tree and Page modifications) - private readonly SemaphoreSlim _collectionLock = new(1, 1); - - private readonly int _maxDocumentSizeForSinglePage; - private const int OverflowMetadataSize = 8; - private const int MaxLogicalDocumentSizeBytes = 16 * 1024 * 1024; - private const int MaxStoredPayloadSizeBytes = MaxLogicalDocumentSizeBytes + CompressedPayloadHeader.Size; - - /// - /// Initializes a new instance of the document collection. - /// - /// The storage engine used for persistence. - /// The transaction context holder. - /// The mapper used to serialize and deserialize documents. - /// Optional collection name override. - public DocumentCollection(StorageEngine storage, ITransactionHolder transactionHolder, IDocumentMapper mapper, string? collectionName = null) - : this((IStorageEngine)storage, transactionHolder, mapper, collectionName) - { - } - - /// - /// Initializes a new instance of the document collection. - /// - /// The storage engine used for persistence. - /// The transaction context holder. - /// The mapper used to serialize and deserialize documents. - /// Optional collection name override. - internal DocumentCollection(IStorageEngine storage, ITransactionHolder transactionHolder, IDocumentMapper mapper, string? collectionName = null) - { - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - _transactionHolder = transactionHolder ?? throw new ArgumentNullException(nameof(transactionHolder)); - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - _collectionName = collectionName ?? _mapper.CollectionName; - _cdcPublisher = new CollectionCdcPublisher( - _transactionHolder, - _collectionName, - _mapper, - _storage.Cdc, - _storage.GetKeyReverseMap()); + /// + /// Initializes a new instance of the document collection. + /// + /// The storage engine used for persistence. + /// The transaction context holder. + /// The mapper used to serialize and deserialize documents. + /// Optional collection name override. + internal DocumentCollection(IStorageEngine storage, ITransactionHolder transactionHolder, + IDocumentMapper mapper, string? collectionName = null) + { + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _transactionHolder = transactionHolder ?? throw new ArgumentNullException(nameof(transactionHolder)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _collectionName = collectionName ?? _mapper.CollectionName; + _cdcPublisher = new CollectionCdcPublisher( + _transactionHolder, + _collectionName, + _mapper, + _storage.Cdc, + _storage.GetKeyReverseMap()); // Initialize secondary index manager first (loads metadata including Primary Root Page ID) _indexManager = new CollectionIndexManager(_storage, _mapper, _collectionName); @@ -135,45 +128,57 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // If a new root page was allocated, persist it if (_indexManager.PrimaryRootPageId != _primaryIndex.RootPageId) - { _indexManager.SetPrimaryRootPageId(_primaryIndex.RootPageId); - } - // Register keys used by the mapper to ensure they are available for compression - _storage.RegisterKeys(_mapper.UsedKeys); - } - - private void RefreshPrimaryIndexRootFromMetadata() - { - var metadata = _storage.GetCollectionMetadata(_collectionName); - if (metadata == null || metadata.PrimaryRootPageId == 0) - return; - - if (metadata.PrimaryRootPageId != _primaryIndex.RootPageId) - { - _primaryIndex.SetRootPageId(metadata.PrimaryRootPageId); - } - } - - /// - void ICompactionAwareCollection.RefreshIndexBindingsAfterCompaction() - { - var metadata = _storage.GetCollectionMetadata(_collectionName); - if (metadata == null) - return; - - _indexManager.RebindFromMetadata(metadata); - - if (metadata.PrimaryRootPageId != 0 && metadata.PrimaryRootPageId != _primaryIndex.RootPageId) - { - _primaryIndex.SetRootPageId(metadata.PrimaryRootPageId); - } - } - - private void EnsureSchema() - { - var currentSchema = _mapper.GetSchema(); - var metadata = _indexManager.GetMetadata(); + // Register keys used by the mapper to ensure they are available for compression + _storage.RegisterKeys(_mapper.UsedKeys); + } + + /// + /// Gets the current persisted schema version for the collection. + /// + public SchemaVersion? CurrentSchemaVersion { get; private set; } + + /// + void ICompactionAwareCollection.RefreshIndexBindingsAfterCompaction() + { + var metadata = _storage.GetCollectionMetadata(_collectionName); + if (metadata == null) + return; + + _indexManager.RebindFromMetadata(metadata); + + if (metadata.PrimaryRootPageId != 0 && metadata.PrimaryRootPageId != _primaryIndex.RootPageId) + _primaryIndex.SetRootPageId(metadata.PrimaryRootPageId); + } + + /// + /// Releases all resources used by the current instance of the class. + /// + /// + /// Call this method when you are finished using the object to free unmanaged resources + /// immediately. After calling Dispose, the object should not be used. + /// + public void Dispose() + { + _indexManager.Dispose(); + GC.SuppressFinalize(this); + } + + private void RefreshPrimaryIndexRootFromMetadata() + { + var metadata = _storage.GetCollectionMetadata(_collectionName); + if (metadata == null || metadata.PrimaryRootPageId == 0) + return; + + if (metadata.PrimaryRootPageId != _primaryIndex.RootPageId) + _primaryIndex.SetRootPageId(metadata.PrimaryRootPageId); + } + + private void EnsureSchema() + { + var currentSchema = _mapper.GetSchema(); + var metadata = _indexManager.GetMetadata(); var persistedSchemas = _storage.GetSchemas(metadata.SchemaRootPageId); var latestPersisted = persistedSchemas.Count > 0 ? persistedSchemas[persistedSchemas.Count - 1] : null; @@ -184,7 +189,7 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC int nextVersion = persistedSchemas.Count + 1; currentSchema.Version = nextVersion; - var newRootId = _storage.AppendSchema(metadata.SchemaRootPageId, currentSchema); + uint newRootId = _storage.AppendSchema(metadata.SchemaRootPageId, currentSchema); if (newRootId != metadata.SchemaRootPageId) { metadata.SchemaRootPageId = newRootId; @@ -196,15 +201,447 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC else { // Latest persisted is same as current structure - CurrentSchemaVersion = new SchemaVersion(latestPersisted.Version ?? persistedSchemas.Count, latestPersisted.GetHash()); + CurrentSchemaVersion = new SchemaVersion(latestPersisted.Version ?? persistedSchemas.Count, + latestPersisted.GetHash()); } } + private (byte[]? storedPayloadOverride, SlotFlags slotFlags) PreparePayloadForStorage( + ReadOnlySpan logicalPayload) + { + if (TryCreateCompressedPayload(logicalPayload, out byte[]? compressedPayload)) + return (compressedPayload, SlotFlags.Compressed); + + return (null, SlotFlags.None); + } + + private bool TryCreateCompressedPayload(ReadOnlySpan logicalPayload, out byte[]? storedPayload) + { + storedPayload = null; + + var options = _storage.CompressionOptions; + var telemetry = _storage.CompressionTelemetry; + if (!options.EnableCompression) + return false; + + if (logicalPayload.Length < options.MinSizeBytes) + { + telemetry.RecordCompressionSkippedTooSmall(); + return false; + } + + if (options.MaxCompressionInputBytes.HasValue && logicalPayload.Length > options.MaxCompressionInputBytes.Value) + { + telemetry.RecordSafetyLimitRejection(); + return false; + } + + telemetry.RecordCompressionAttempt(logicalPayload.Length); + try + { + long startedAt = Stopwatch.GetTimestamp(); + byte[] compressedPayload = + _storage.CompressionService.Compress(logicalPayload, options.Codec, options.Level); + long elapsedTicks = Stopwatch.GetTimestamp() - startedAt; + telemetry.RecordCompressionCpuTicks(elapsedTicks); + int compressedStorageLength = CompressedPayloadHeader.Size + compressedPayload.Length; + + if (!MeetsMinSavingsPercent(logicalPayload.Length, compressedStorageLength, options.MinSavingsPercent)) + { + telemetry.RecordCompressionSkippedInsufficientSavings(); + return false; + } + + var output = new byte[compressedStorageLength]; + var header = CompressedPayloadHeader.Create(options.Codec, logicalPayload.Length, compressedPayload); + header.WriteTo(output.AsSpan(0, CompressedPayloadHeader.Size)); + compressedPayload.CopyTo(output.AsSpan(CompressedPayloadHeader.Size)); + + telemetry.RecordCompressionSuccess(output.Length); + storedPayload = output; + return true; + } + catch + { + telemetry.RecordCompressionFailure(); + return false; + } + } + + private static bool MeetsMinSavingsPercent(int originalLength, int compressedStorageLength, int minSavingsPercent) + { + if (originalLength <= 0) + return false; + + int savedBytes = originalLength - compressedStorageLength; + if (savedBytes <= 0) + return false; + + var savingsPercent = (int)(savedBytes * 100L / originalLength); + return savingsPercent >= minSavingsPercent; + } + + private static void ValidateSlotBounds(in SlotEntry slot, int bufferLength, in DocumentLocation location) + { + int endOffset = slot.Offset + slot.Length; + if (slot.Offset < SlottedPageHeader.Size || endOffset > bufferLength) + throw new InvalidDataException( + $"Corrupted slot bounds: Offset={slot.Offset}, Length={slot.Length}, Buffer={bufferLength}, SlotIndex={location.SlotIndex}, PageId={location.PageId}, Flags={slot.Flags}"); + } + + private byte[] ReassembleOverflowPayload(ReadOnlySpan primaryPayload, ulong transactionId, byte[] pageBuffer, + in DocumentLocation location) + { + if (primaryPayload.Length < OverflowMetadataSize) + throw new InvalidDataException( + $"Corrupted overflow metadata: primary slot too small ({primaryPayload.Length} bytes) at {location.PageId}:{location.SlotIndex}."); + + int totalLength = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); + if (totalLength < 0 || totalLength > MaxStoredPayloadSizeBytes) + { + _storage.CompressionTelemetry.RecordSafetyLimitRejection(); + throw new InvalidDataException( + $"Corrupted overflow metadata: invalid total length {totalLength} at {location.PageId}:{location.SlotIndex}."); + } + + uint currentOverflowPageId = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); + int primaryChunkSize = primaryPayload.Length - OverflowMetadataSize; + if (totalLength < primaryChunkSize) + throw new InvalidDataException( + $"Corrupted overflow metadata: total length {totalLength} is smaller than primary chunk {primaryChunkSize} at {location.PageId}:{location.SlotIndex}."); + + var fullPayload = new byte[totalLength]; + primaryPayload.Slice(OverflowMetadataSize, primaryChunkSize).CopyTo(fullPayload); + + int offset = primaryChunkSize; + int maxChunkSize = _storage.PageSize - SlottedPageHeader.Size; + + while (currentOverflowPageId != 0 && offset < totalLength) + { + _storage.ReadPage(currentOverflowPageId, transactionId, pageBuffer); + var overflowHeader = SlottedPageHeader.ReadFrom(pageBuffer); + if (overflowHeader.PageType != PageType.Overflow) + throw new InvalidDataException( + $"Corrupted overflow chain: page {currentOverflowPageId} is not an overflow page."); + + int remaining = totalLength - offset; + int chunkSize = Math.Min(maxChunkSize, remaining); + pageBuffer.AsSpan(SlottedPageHeader.Size, chunkSize).CopyTo(fullPayload.AsSpan(offset)); + offset += chunkSize; + currentOverflowPageId = overflowHeader.NextOverflowPage; + } + + if (offset != totalLength) + throw new InvalidDataException( + $"Corrupted overflow chain: expected {totalLength} bytes but reconstructed {offset} bytes at {location.PageId}:{location.SlotIndex}."); + + if (currentOverflowPageId != 0) + throw new InvalidDataException( + $"Corrupted overflow chain: extra overflow pages remain after reconstruction at {location.PageId}:{location.SlotIndex}."); + + return fullPayload; + } + + private byte[] DecompressStoredPayload(ReadOnlySpan storedPayload, in DocumentLocation location) + { + var telemetry = _storage.CompressionTelemetry; + telemetry.RecordDecompressionAttempt(); + + try + { + if (storedPayload.Length < CompressedPayloadHeader.Size) + throw new InvalidDataException( + $"Corrupted compressed payload: missing header at {location.PageId}:{location.SlotIndex}."); + + var header = CompressedPayloadHeader.ReadFrom(storedPayload.Slice(0, CompressedPayloadHeader.Size)); + if (!Enum.IsDefined(typeof(CompressionCodec), header.Codec) || header.Codec == CompressionCodec.None) + throw new InvalidDataException( + $"Corrupted compressed payload: invalid codec '{header.Codec}' at {location.PageId}:{location.SlotIndex}."); + + if (header.OriginalLength < 0 || + header.OriginalLength > _storage.CompressionOptions.MaxDecompressedSizeBytes) + { + telemetry.RecordSafetyLimitRejection(); + throw new InvalidDataException( + $"Corrupted compressed payload: invalid decompressed length {header.OriginalLength} at {location.PageId}:{location.SlotIndex}."); + } + + int compressedLength = storedPayload.Length - CompressedPayloadHeader.Size; + if (header.CompressedLength < 0 || header.CompressedLength != compressedLength) + throw new InvalidDataException( + $"Corrupted compressed payload: invalid compressed length {header.CompressedLength} (actual {compressedLength}) at {location.PageId}:{location.SlotIndex}."); + + var compressedPayload = storedPayload.Slice(CompressedPayloadHeader.Size, header.CompressedLength); + if (!header.ValidateChecksum(compressedPayload)) + { + telemetry.RecordChecksumFailure(); + throw new InvalidDataException( + $"Corrupted compressed payload: checksum mismatch at {location.PageId}:{location.SlotIndex}."); + } + + if (!_storage.CompressionService.TryGetCodec(header.Codec, out _)) + throw new InvalidDataException( + $"Corrupted compressed payload: codec '{header.Codec}' is not registered at {location.PageId}:{location.SlotIndex}."); + + long startedAt = Stopwatch.GetTimestamp(); + byte[] decompressed = _storage.CompressionService.Decompress( + compressedPayload, + header.Codec, + header.OriginalLength, + _storage.CompressionOptions.MaxDecompressedSizeBytes); + long elapsedTicks = Stopwatch.GetTimestamp() - startedAt; + telemetry.RecordDecompressionCpuTicks(elapsedTicks); + + if (decompressed.Length != header.OriginalLength) + throw new InvalidDataException( + $"Corrupted compressed payload: decompressed length {decompressed.Length} does not match expected {header.OriginalLength} at {location.PageId}:{location.SlotIndex}."); + + telemetry.RecordDecompressionSuccess(decompressed.Length); + return decompressed; + } + catch (InvalidDataException) + { + telemetry.RecordDecompressionFailure(); + throw; + } + catch (Exception ex) + { + telemetry.RecordDecompressionFailure(); + throw new InvalidDataException( + $"Failed to decompress payload at {location.PageId}:{location.SlotIndex}.", ex); + } + } + + /// + /// Serializes an entity with adaptive buffer sizing (Stepped Retry). + /// Strategies: + /// 1. 64KB (Covers 99% of docs, small overhead) + /// 2. 2MB (Covers large docs) + /// 3. 16MB (Max limit) + /// + private int SerializeWithRetry(T entity, out byte[] rentedBuffer) + { + // 64KB, 2MB, 16MB + int[] steps = { 65536, 2097152, 16777216 }; + + for (var i = 0; i < steps.Length; i++) + { + int size = steps[i]; + + // Ensure we at least cover PageSize (unlikely to be > 64KB but safe) + if (size < _storage.PageSize) size = _storage.PageSize; + + byte[] buffer = ArrayPool.Shared.Rent(size); + try + { + int bytesWritten = _mapper.Serialize(entity, new BsonSpanWriter(buffer, _storage.GetKeyMap())); + + // Inject schema version if available + if (CurrentSchemaVersion != null) + { + if (bytesWritten + 8 > buffer.Length) + throw new IndexOutOfRangeException("Not enough space for version field"); + AppendVersionField(buffer, ref bytesWritten); + } + + rentedBuffer = buffer; + return bytesWritten; + } + catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || + ex is ArgumentOutOfRangeException) + { + ArrayPool.Shared.Return(buffer); + // Continue to next step + } + catch + { + ArrayPool.Shared.Return(buffer); + throw; + } + } + + rentedBuffer = null!; // specific compiler satisfaction, though we throw + throw new InvalidOperationException("Document too large. Maximum size allowed is 16MB."); + } + + /// + /// Appends a version field to the specified BSON buffer if a current schema version is set. + /// + /// + /// The version field is only appended if a current schema version is available. The method + /// updates the BSON document's size and ensures the buffer remains in a valid BSON format. + /// + /// + /// The byte array buffer to which the version field is appended. Must be large enough to accommodate the additional + /// bytes. + /// + /// + /// A reference to the number of bytes written to the buffer. Updated to reflect the new total after the version + /// field is appended. + /// + private void AppendVersionField(byte[] buffer, ref int bytesWritten) + { + if (CurrentSchemaVersion == null) return; + + int version = CurrentSchemaVersion.Value.Version; + + // BSON element for _v (Int32) with Compressed Key: + // Type (1 byte: 0x10) + // Key ID (2 bytes, little-endian) + // Value (4 bytes: int32) + // Total = 7 bytes + + int pos = bytesWritten - 1; // Position of old 0x00 terminator + buffer[pos++] = 0x10; // Int32 + + ushort versionKeyId = _storage.GetOrAddDictionaryEntry("_v"); + BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(pos, 2), versionKeyId); + pos += 2; + + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(pos, 4), version); + pos += 4; + + buffer[pos++] = 0x00; // new document terminator + + bytesWritten = pos; + + // Update total size (first 4 bytes) + BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), bytesWritten); + } + + /// + /// Performs a vector similarity search on the specified index and returns up to the top-k matching documents of + /// type T. + /// + /// + /// The search uses approximate nearest neighbor algorithms for efficient retrieval. The efSearch + /// parameter can be tuned to balance search speed and accuracy. Results are filtered to include only documents that + /// can be successfully retrieved from storage. + /// + /// The name of the index to search. Cannot be null or empty. + /// + /// The query vector used to find similar documents. The array length must match the dimensionality of + /// the index. + /// + /// The maximum number of nearest neighbors to return. Must be greater than zero. + /// + /// The size of the dynamic candidate list during search. Higher values may improve recall at the cost of + /// performance. Must be greater than zero. The default is 100. + /// + /// + /// An optional transaction context to use for the search. If null, the operation is performed without a + /// transaction. + /// + /// + /// An enumerable collection of up to k documents of type T that are most similar to the query vector. The + /// collection may be empty if no matches are found. + /// + /// Thrown if indexName is null, empty, or does not correspond to an existing index. + public IEnumerable VectorSearch(string indexName, float[] query, int k, int efSearch = 100) + { + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var index = _indexManager.GetIndex(indexName); + if (index == null) + throw new ArgumentException($"Index '{indexName}' not found.", nameof(indexName)); + + foreach (var result in index.VectorSearch(query, k, efSearch, transaction)) + { + var doc = FindByLocation(result.Location); + if (doc != null) yield return doc; + } + } + + /// + /// Finds all documents located within a specified radius of a geographic center point using a spatial index. + /// + /// The name of the spatial index to use for the search. Cannot be null or empty. + /// A tuple representing the latitude and longitude of the center point, in decimal degrees. + /// The search radius, in kilometers. Must be greater than zero. + /// + /// An optional transaction context to use for the operation. If null, the default transaction is + /// used. + /// + /// + /// An enumerable collection of documents of type T that are located within the specified radius of the center + /// point. The collection is empty if no documents are found. + /// + /// Thrown if indexName is null, empty, or does not correspond to an existing index. + public IEnumerable Near(string indexName, (double Latitude, double Longitude) center, double radiusKm) + { + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var index = _indexManager.GetIndex(indexName); + if (index == null) + throw new ArgumentException($"Index '{indexName}' not found.", nameof(indexName)); + + foreach (var loc in index.Near(center, radiusKm, transaction)) + { + var doc = FindByLocation(loc); + if (doc != null) yield return doc; + } + } + + /// + /// Returns all documents within the specified rectangular geographic area from the given spatial index. + /// + /// The name of the spatial index to search within. Cannot be null or empty. + /// The minimum latitude and longitude coordinates defining one corner of the search rectangle. + /// The maximum latitude and longitude coordinates defining the opposite corner of the search rectangle. + /// + /// An enumerable collection of documents of type T that are located within the specified geographic bounds. The + /// collection is empty if no documents are found. + /// + /// + /// Transactions are managed implicitly through the collection's ; callers do not + /// supply a transaction parameter. + /// + /// Thrown if indexName is null, empty, or does not correspond to an existing index. + public IEnumerable Within(string indexName, (double Latitude, double Longitude) min, + (double Latitude, double Longitude) max) + { + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var index = _indexManager.GetIndex(indexName); + if (index == null) + throw new ArgumentException($"Index '{indexName}' not found.", nameof(indexName)); + + foreach (var loc in index.Within(min, max, transaction)) + { + var doc = FindByLocation(loc); + if (doc != null) yield return doc; + } + } + + /// + /// Subscribes to a change stream that notifies observers of changes to the collection. + /// + /// + /// The returned observable emits events as changes are detected in the collection. Observers can + /// subscribe to receive real-time updates. The behavior of the event payload depends on the value of the + /// capturePayload parameter. + /// + /// + /// true to include the full payload of changed documents in each event; otherwise, false to include only metadata + /// about the change. The default is false. + /// + /// + /// An observable sequence of change stream events for the collection. Subscribers receive notifications as changes + /// occur. + /// + /// Thrown if change data capture (CDC) is not initialized for the storage. + public IObservable> Watch(bool capturePayload = false) + { + return _cdcPublisher.Watch(capturePayload); + } + + private void NotifyCdc(OperationType type, TId id, ReadOnlySpan docData = default) + { + _cdcPublisher.Notify(type, id, docData); + } + #region Index Management API /// - /// Creates a secondary index on a property for fast lookups. - /// The index is automatically maintained on insert/update/delete operations. + /// Creates a secondary index on a property for fast lookups. + /// The index is automatically maintained on insert/update/delete operations. /// /// Property type /// Expression to extract the indexed property (e.g., p => p.Age) @@ -212,17 +649,15 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC /// If true, enforces uniqueness constraint on the indexed values /// The created secondary index /// - /// // Simple index on Age - /// collection.CreateIndex(p => p.Age); - /// - /// // Unique index on Email - /// collection.CreateIndex(p => p.Email, unique: true); - /// - /// // Custom name - /// collection.CreateIndex(p => p.LastName, name: "idx_lastname"); + /// // Simple index on Age + /// collection.CreateIndex(p => p.Age); + /// // Unique index on Email + /// collection.CreateIndex(p => p.Email, unique: true); + /// // Custom name + /// collection.CreateIndex(p => p.LastName, name: "idx_lastname"); /// public CollectionSecondaryIndex CreateIndex( - System.Linq.Expressions.Expression> keySelector, + Expression> keySelector, string? name = null, bool unique = false) { @@ -242,18 +677,18 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Creates a vector (HNSW) index for similarity search. - /// - /// The type of the indexed vector property. - /// Expression selecting the property to index. - /// The number of vector dimensions. - /// The similarity metric used for distance calculations. - /// Optional index name. - /// The created secondary index. - public CollectionSecondaryIndex CreateVectorIndex( - System.Linq.Expressions.Expression> keySelector, - int dimensions, + /// + /// Creates a vector (HNSW) index for similarity search. + /// + /// The type of the indexed vector property. + /// Expression selecting the property to index. + /// The number of vector dimensions. + /// The similarity metric used for distance calculations. + /// Optional index name. + /// The created secondary index. + public CollectionSecondaryIndex CreateVectorIndex( + Expression> keySelector, + int dimensions, VectorMetric metric = VectorMetric.Cosine, string? name = null) { @@ -269,39 +704,36 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } /// - /// Ensures that an index exists on the specified property. - /// If the index already exists, it is returned without modification (idempotent). - /// If it doesn't exist, it is created and populated. - /// - /// The type of the indexed property. - /// Expression selecting the property to index. - /// Optional index name. - /// Whether the index enforces unique values. - /// An existing or newly created secondary index. - public CollectionSecondaryIndex EnsureIndex( - System.Linq.Expressions.Expression> keySelector, - string? name = null, + /// Ensures that an index exists on the specified property. + /// If the index already exists, it is returned without modification (idempotent). + /// If it doesn't exist, it is created and populated. + /// + /// The type of the indexed property. + /// Expression selecting the property to index. + /// Optional index name. + /// Whether the index enforces unique values. + /// An existing or newly created secondary index. + public CollectionSecondaryIndex EnsureIndex( + Expression> keySelector, + string? name = null, bool unique = false) { if (keySelector == null) throw new ArgumentNullException(nameof(keySelector)); // 1. Check if index already exists (fast path) - var propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); - var indexName = name ?? $"idx_{string.Join("_", propertyPaths)}"; + string[] propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); + string indexName = name ?? $"idx_{string.Join("_", propertyPaths)}"; var existingIndex = GetIndex(indexName); - if (existingIndex != null) - { - return existingIndex; - } + if (existingIndex != null) return existingIndex; // 2. Create if missing (slow path: rebuilds index) return CreateIndex(keySelector, name, unique); } /// - /// Drops (removes) an existing secondary index by name. - /// The primary index (_id) cannot be dropped. + /// Drops (removes) an existing secondary index by name. + /// The primary index (_id) cannot be dropped. /// /// Name of the index to drop /// True if the index was found and dropped, false otherwise @@ -318,8 +750,8 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } /// - /// Gets metadata about all secondary indexes in this collection. - /// Does not include the primary index (_id). + /// Gets metadata about all secondary indexes in this collection. + /// Does not include the primary index (_id). /// /// Collection of index metadata public IEnumerable GetIndexes() @@ -327,11 +759,11 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC return _indexManager.GetIndexInfo(); } - /// - /// Applies an index builder definition to the collection metadata and index store. - /// - /// The index builder definition to apply. - internal void ApplyIndexBuilder(Metadata.IndexBuilder builder) + /// + /// Applies an index builder definition to the collection metadata and index store. + /// + /// The index builder definition to apply. + internal void ApplyIndexBuilder(IndexBuilder builder) { // Use the IndexManager directly to ensure the index exists // We need to convert the LambdaExpression to a typed expression if possible, @@ -339,54 +771,47 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // For now, let's use a dynamic approach or cast if we know it's Func if (builder.Type == IndexType.Vector) - { - _indexManager.CreateVectorIndexUntyped(builder.KeySelector, builder.Dimensions, builder.Metric, builder.Name); - } + _indexManager.CreateVectorIndexUntyped(builder.KeySelector, builder.Dimensions, builder.Metric, + builder.Name); else if (builder.Type == IndexType.Spatial) - { _indexManager.CreateSpatialIndexUntyped(builder.KeySelector, builder.Name); - } - else if (builder.KeySelector is System.Linq.Expressions.Expression> selector) - { + else if (builder.KeySelector is Expression> selector) _indexManager.EnsureIndex(selector, builder.Name, builder.IsUnique); - } else - { // Try to rebuild the expression or use untyped version _indexManager.EnsureIndexUntyped(builder.KeySelector, builder.Name, builder.IsUnique); - } } /// - /// Gets a queryable interface for this collection. - /// Supports LINQ queries that are translated to optimized BTree scans or index lookups. + /// Gets a queryable interface for this collection. + /// Supports LINQ queries that are translated to optimized BTree scans or index lookups. /// public IQueryable AsQueryable() { return new BTreeQueryable(new BTreeQueryProvider(this)); } - /// - /// Gets a specific secondary index by name for advanced querying. - /// Returns null if the index doesn't exist. - /// - /// Name of the index. - /// The matching secondary index, or null when not found. - public CollectionSecondaryIndex? GetIndex(string name) + /// + /// Gets a specific secondary index by name for advanced querying. + /// Returns null if the index doesn't exist. + /// + /// Name of the index. + /// The matching secondary index, or null when not found. + public CollectionSecondaryIndex? GetIndex(string name) { return _indexManager.GetIndex(name); } - /// - /// Queries a specific index for a range of values. - /// Returns matching documents using the index for efficient retrieval. - /// - /// Name of the index to query. - /// Inclusive lower bound key. - /// Inclusive upper bound key. - /// True to iterate ascending; false for descending. - /// Documents that match the requested index range. - public IEnumerable QueryIndex(string indexName, object? minKey, object? maxKey, bool ascending = true) + /// + /// Queries a specific index for a range of values. + /// Returns matching documents using the index for efficient retrieval. + /// + /// Name of the index to query. + /// Inclusive lower bound key. + /// Inclusive upper bound key. + /// True to iterate ascending; false for descending. + /// Documents that match the requested index range. + public IEnumerable QueryIndex(string indexName, object? minKey, object? maxKey, bool ascending = true) { var index = GetIndex(indexName); if (index == null) throw new ArgumentException($"Index {indexName} not found"); @@ -402,33 +827,28 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } /// - /// Rebuilds an index by scanning all existing documents and re-inserting them. - /// Called automatically when creating a new index. + /// Rebuilds an index by scanning all existing documents and re-inserting them. + /// Called automatically when creating a new index. /// - private void RebuildIndex(CollectionSecondaryIndex index) - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - // Iterate all documents in the collection via primary index - var minKey = new IndexKey(Array.Empty()); - var maxKey = new IndexKey(Enumerable.Repeat((byte)0xFF, 32).ToArray()); + private void RebuildIndex(CollectionSecondaryIndex index) + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + // Iterate all documents in the collection via primary index + var minKey = new IndexKey(Array.Empty()); + var maxKey = new IndexKey(Enumerable.Repeat((byte)0xFF, 32).ToArray()); foreach (var entry in _primaryIndex.Range(minKey, maxKey, IndexDirection.Forward, transaction.TransactionId)) - { try { var document = FindByLocation(entry.Location); - if (document != null) - { - index.Insert(document, entry.Location, transaction); - } + if (document != null) index.Insert(document, entry.Location, transaction); } catch { // Skip documents that fail to load or index // Production: should log errors } - } } #endregion @@ -438,17 +858,15 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC private uint FindPageWithSpace(int requiredBytes) { var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var txnId = transaction.TransactionId; + ulong txnId = transaction.TransactionId; // Try current page first if (_currentDataPage != 0) { - if (_freeSpaceMap.TryGetValue(_currentDataPage, out var freeBytes)) + if (_freeSpaceMap.TryGetValue(_currentDataPage, out ushort freeBytes)) { if (freeBytes >= requiredBytes && !_storage.IsPageLocked(_currentDataPage, txnId)) - { return _currentDataPage; - } } else { @@ -467,16 +885,10 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } // Search free space map - foreach (var (pageId, freeBytes) in _freeSpaceMap) - { + foreach ((uint pageId, ushort freeBytes) in _freeSpaceMap) if (freeBytes >= requiredBytes) - { if (!_storage.IsPageLocked(pageId, txnId)) - { return pageId; - } - } - } return 0; // No suitable page } @@ -485,10 +897,10 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC { var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var pageId = _storage.AllocatePage(); + uint pageId = _storage.AllocatePage(); // Initialize slotted page header - var buffer = ArrayPool.Shared.Rent(_storage.PageSize); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { buffer.AsSpan().Clear(); @@ -510,7 +922,8 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC if (transaction is Transaction t) { // OPTIMIZATION: Pass ReadOnlyMemory to avoid ToArray() allocation - var writeOp = new WriteOperation(ObjectId.Empty, buffer.AsMemory(0, _storage.PageSize), pageId, OperationType.AllocatePage); + var writeOp = new WriteOperation(ObjectId.Empty, buffer.AsMemory(0, _storage.PageSize), pageId, + OperationType.AllocatePage); t.AddWrite(writeOp); } else @@ -530,10 +943,10 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC return pageId; } - private ushort InsertIntoPage(uint pageId, ReadOnlySpan data, SlotFlags slotFlags = SlotFlags.None) + private ushort InsertIntoPage(uint pageId, ReadOnlySpan data, SlotFlags slotFlags = SlotFlags.None) { var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var buffer = ArrayPool.Shared.Rent(_storage.PageSize); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { @@ -558,34 +971,35 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } // Check free space - var freeSpace = header.AvailableFreeSpace; - var requiredSpace = data.Length + SlotEntry.Size; + int freeSpace = header.AvailableFreeSpace; + int requiredSpace = data.Length + SlotEntry.Size; if (freeSpace < requiredSpace) - throw new InvalidOperationException($"Not enough space: need {requiredSpace}, have {freeSpace} | PageId={pageId} | SlotCount={header.SlotCount} | Start={header.FreeSpaceStart} | End={header.FreeSpaceEnd} | Map={_freeSpaceMap.GetValueOrDefault(pageId)}"); + throw new InvalidOperationException( + $"Not enough space: need {requiredSpace}, have {freeSpace} | PageId={pageId} | SlotCount={header.SlotCount} | Start={header.FreeSpaceStart} | End={header.FreeSpaceEnd} | Map={_freeSpaceMap.GetValueOrDefault(pageId)}"); // Find free slot (reuse deleted or create new) ushort slotIndex = FindFreeSlot(buffer, ref header); // Write document at end of used space (grows up) - var docOffset = header.FreeSpaceEnd - data.Length; + int docOffset = header.FreeSpaceEnd - data.Length; data.CopyTo(buffer.AsSpan(docOffset, data.Length)); // Write slot entry - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); - var slot = new SlotEntry - { - Offset = (ushort)docOffset, - Length = (ushort)data.Length, - Flags = slotFlags - }; + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; + var slot = new SlotEntry + { + Offset = (ushort)docOffset, + Length = (ushort)data.Length, + Flags = slotFlags + }; slot.WriteTo(buffer.AsSpan(slotOffset)); // Update header if (slotIndex >= header.SlotCount) header.SlotCount = (ushort)(slotIndex + 1); - header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + (header.SlotCount * SlotEntry.Size)); + header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size); header.FreeSpaceEnd = (ushort)docOffset; header.WriteTo(buffer); @@ -594,10 +1008,10 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC { // OPTIMIZATION: Pass ReadOnlyMemory to avoid ToArray() allocation var writeOp = new WriteOperation( - documentId: ObjectId.Empty, - newValue: buffer.AsMemory(0, _storage.PageSize), - pageId: pageId, - type: OperationType.Insert + ObjectId.Empty, + buffer.AsMemory(0, _storage.PageSize), + pageId, + OperationType.Insert ); t.AddWrite(writeOp); } @@ -622,7 +1036,7 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // Scan existing slots for deleted ones for (ushort i = 0; i < header.SlotCount; i++) { - var slotOffset = SlottedPageHeader.Size + (i * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + i * SlotEntry.Size; var slot = SlotEntry.ReadFrom(page.Slice(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) @@ -635,8 +1049,8 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC private uint AllocateOverflowPage(ReadOnlySpan data, uint nextOverflowPageId, ITransaction transaction) { - var pageId = _storage.AllocatePage(); - var buffer = ArrayPool.Shared.Rent(_storage.PageSize); + uint pageId = _storage.AllocatePage(); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { @@ -660,10 +1074,10 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // NEW: Buffer write in transaction or write immediately var writeOp = new WriteOperation( - documentId: ObjectId.Empty, - newValue: buffer.AsSpan(0, _storage.PageSize).ToArray(), - pageId: pageId, - type: OperationType.Insert + ObjectId.Empty, + buffer.AsSpan(0, _storage.PageSize).ToArray(), + pageId, + OperationType.Insert ); ((Transaction)transaction).AddWrite(writeOp); @@ -675,12 +1089,13 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - private (uint pageId, ushort slotIndex) InsertWithOverflow(ReadOnlySpan data, SlotFlags slotFlags = SlotFlags.None) - { - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - // 1. Calculate Primary Chunk Size - // We need 8 bytes for metadata (TotalLength: 4, NextOverflowPage: 4) - int maxPrimaryPayload = _maxDocumentSizeForSinglePage - OverflowMetadataSize; + private (uint pageId, ushort slotIndex) InsertWithOverflow(ReadOnlySpan data, + SlotFlags slotFlags = SlotFlags.None) + { + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + // 1. Calculate Primary Chunk Size + // We need 8 bytes for metadata (TotalLength: 4, NextOverflowPage: 4) + int maxPrimaryPayload = _maxDocumentSizeForSinglePage - OverflowMetadataSize; // 2. Build Overflow Chain (Reverse Order) // We must ensure that pages closer to Primary are FULL (PageSize-Header), @@ -698,8 +1113,8 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // 2a. Handle Tail (if any) - This is the highest offset if (tailSize > 0) { - int tailOffset = maxPrimaryPayload + (fullPages * overflowChunkSize); - var overflowPageId = AllocateOverflowPage( + int tailOffset = maxPrimaryPayload + fullPages * overflowChunkSize; + uint overflowPageId = AllocateOverflowPage( data.Slice(tailOffset, tailSize), nextOverflowPageId, // Points to 0 (or previous tail if we had one? No, 0) transaction @@ -715,8 +1130,8 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // Iterate from last full page down to first full page for (int i = fullPages - 1; i >= 0; i--) { - int chunkOffset = maxPrimaryPayload + (i * overflowChunkSize); - var overflowPageId = AllocateOverflowPage( + int chunkOffset = maxPrimaryPayload + i * overflowChunkSize; + uint overflowPageId = AllocateOverflowPage( data.Slice(chunkOffset, overflowChunkSize), nextOverflowPageId, transaction @@ -729,15 +1144,15 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // Layout: [TotalLength (4)] [NextOverflowPage (4)] [DataChunk (...)] // Since we are in InsertWithOverflow, we know data.Length > maxPrimaryPayload int primaryPayloadSize = maxPrimaryPayload; - int totalSlotSize = OverflowMetadataSize + primaryPayloadSize; + int totalSlotSize = OverflowMetadataSize + primaryPayloadSize; // Allocate primary page - var primaryPageId = FindPageWithSpace(totalSlotSize + SlotEntry.Size); + uint primaryPageId = FindPageWithSpace(totalSlotSize + SlotEntry.Size); if (primaryPageId == 0) primaryPageId = AllocateNewDataPage(); // 4. Write to Primary Page - var buffer = ArrayPool.Shared.Rent(_storage.PageSize); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { _storage.ReadPage(primaryPageId, transaction.TransactionId, buffer); @@ -748,12 +1163,13 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC ushort slotIndex = FindFreeSlot(buffer, ref header); // Write payload at end of used space - var docOffset = header.FreeSpaceEnd - totalSlotSize; + int docOffset = header.FreeSpaceEnd - totalSlotSize; var payloadSpan = buffer.AsSpan(docOffset, totalSlotSize); // Write Metadata - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(payloadSpan.Slice(0, 4), data.Length); // Total Length - System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(payloadSpan.Slice(4, 4), nextOverflowPageId); // First Overflow Page + BinaryPrimitives.WriteInt32LittleEndian(payloadSpan.Slice(0, 4), data.Length); // Total Length + BinaryPrimitives.WriteUInt32LittleEndian(payloadSpan.Slice(4, 4), + nextOverflowPageId); // First Overflow Page // Write Data Chunk data.Slice(0, primaryPayloadSize).CopyTo(payloadSpan.Slice(8)); @@ -763,29 +1179,29 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // LENGTH: Length of data *in this slot* (Metadata + Chunk) // This avoids the 65KB limit issue for the SlotEntry.Length field itself, // as specific slots are bounded by Page Size (16KB). - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; var slot = new SlotEntry { - Offset = (ushort)docOffset, - Length = (ushort)totalSlotSize, - Flags = slotFlags | SlotFlags.HasOverflow - }; + Offset = (ushort)docOffset, + Length = (ushort)totalSlotSize, + Flags = slotFlags | SlotFlags.HasOverflow + }; slot.WriteTo(buffer.AsSpan(slotOffset)); // Update header if (slotIndex >= header.SlotCount) header.SlotCount = (ushort)(slotIndex + 1); - header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + (header.SlotCount * SlotEntry.Size)); + header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size); header.FreeSpaceEnd = (ushort)docOffset; header.WriteTo(buffer); // NEW: Buffer write in transaction or write immediately var writeOp = new WriteOperation( - documentId: ObjectId.Empty, - newValue: buffer.AsMemory(0, _storage.PageSize), - pageId: primaryPageId, - type: OperationType.Insert + ObjectId.Empty, + buffer.AsMemory(0, _storage.PageSize), + primaryPageId, + OperationType.Insert ); ((Transaction)transaction).AddWrite(writeOp); @@ -801,7 +1217,7 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } /// - /// Inserts a new document into the collection + /// Inserts a new document into the collection /// /// Entity to insert /// Optional transaction to batch multiple operations. If null, auto-commits. @@ -831,12 +1247,12 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Asynchronously inserts a new document into the collection - /// - /// The entity to insert. - /// The identifier of the inserted entity. - public async Task InsertAsync(T entity) + /// + /// Asynchronously inserts a new document into the collection + /// + /// The entity to insert. + /// The identifier of the inserted entity. + public async Task InsertAsync(T entity) { var transaction = await _transactionHolder.GetCurrentTransactionOrStartAsync(); if (entity == null) throw new ArgumentNullException(nameof(entity)); @@ -862,15 +1278,15 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } /// - /// Inserts multiple documents in a single transaction for optimal performance. - /// This is the recommended way to insert many documents at once. - /// Uses micro-batched parallel serialization for optimal CPU utilization without excessive memory overhead. + /// Inserts multiple documents in a single transaction for optimal performance. + /// This is the recommended way to insert many documents at once. + /// Uses micro-batched parallel serialization for optimal CPU utilization without excessive memory overhead. /// /// Collection of entities to insert /// List of ObjectIds for the inserted documents /// - /// var people = new List<Person> { person1, person2, person3 }; - /// var ids = collection.InsertBulk(people); + /// var people = new List<Person> { person1, person2, person3 }; + /// var ids = collection.InsertBulk(people); /// public List InsertBulk(IEnumerable entities) { @@ -900,12 +1316,12 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Asynchronously inserts multiple documents in a single transaction. - /// - /// Collection of entities to insert. - /// List of identifiers for the inserted entities. - public async Task> InsertBulkAsync(IEnumerable entities) + /// + /// Asynchronously inserts multiple documents in a single transaction. + /// + /// Collection of entities to insert. + /// List of identifiers for the inserted entities. + public async Task> InsertBulkAsync(IEnumerable entities) { var transaction = await _transactionHolder.GetCurrentTransactionOrStartAsync(); if (entities == null) throw new ArgumentNullException(nameof(entities)); @@ -939,7 +1355,7 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC const int BATCH_SIZE = 50; - for (int batchStart = 0; batchStart < entityList.Count; batchStart += BATCH_SIZE) + for (var batchStart = 0; batchStart < entityList.Count; batchStart += BATCH_SIZE) { int batchEnd = Math.Min(batchStart + BATCH_SIZE, entityList.Count); int batchCount = batchEnd - batchStart; @@ -947,18 +1363,18 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // PHASE 1: Parallel serialize this batch var serializedBatch = new (TId id, byte[] data, int length)[batchCount]; - System.Threading.Tasks.Parallel.For(0, batchCount, i => + Parallel.For(0, batchCount, i => { var entity = entityList[batchStart + i]; var id = EnsureId(entity); - var length = SerializeWithRetry(entity, out var buffer); + int length = SerializeWithRetry(entity, out byte[] buffer); serializedBatch[i] = (id, buffer, length); }); // PHASE 2: Sequential insert this batch - for (int i = 0; i < batchCount; i++) + for (var i = 0; i < batchCount; i++) { - var (id, buffer, length) = serializedBatch[i]; + (var id, byte[] buffer, int length) = serializedBatch[i]; var entity = entityList[batchStart + i]; try @@ -990,13 +1406,14 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC _mapper.SetId(entity, id); } } + return id; } private TId InsertCore(T entity) { var id = EnsureId(entity); - var length = SerializeWithRetry(entity, out var buffer); + int length = SerializeWithRetry(entity, out byte[] buffer); try { InsertDataCore(id, entity, buffer.AsSpan(0, length)); @@ -1008,26 +1425,26 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - private void InsertDataCore(TId id, T entity, ReadOnlySpan docData) - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var (storedPayloadOverride, storedPayloadFlags) = PreparePayloadForStorage(docData); - ReadOnlySpan storedPayload = storedPayloadOverride is null ? docData : storedPayloadOverride; - - DocumentLocation location; - if (storedPayload.Length + SlotEntry.Size <= _maxDocumentSizeForSinglePage) - { - var pageId = FindPageWithSpace(storedPayload.Length + SlotEntry.Size); - if (pageId == 0) pageId = AllocateNewDataPage(); - var slotIndex = InsertIntoPage(pageId, storedPayload, storedPayloadFlags); - location = new DocumentLocation(pageId, slotIndex); - } - else - { - var (pageId, slotIndex) = InsertWithOverflow(storedPayload, storedPayloadFlags); - location = new DocumentLocation(pageId, slotIndex); - } + private void InsertDataCore(TId id, T entity, ReadOnlySpan docData) + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + (byte[]? storedPayloadOverride, var storedPayloadFlags) = PreparePayloadForStorage(docData); + var storedPayload = storedPayloadOverride is null ? docData : storedPayloadOverride; + + DocumentLocation location; + if (storedPayload.Length + SlotEntry.Size <= _maxDocumentSizeForSinglePage) + { + uint pageId = FindPageWithSpace(storedPayload.Length + SlotEntry.Size); + if (pageId == 0) pageId = AllocateNewDataPage(); + ushort slotIndex = InsertIntoPage(pageId, storedPayload, storedPayloadFlags); + location = new DocumentLocation(pageId, slotIndex); + } + else + { + (uint pageId, ushort slotIndex) = InsertWithOverflow(storedPayload, storedPayloadFlags); + location = new DocumentLocation(pageId, slotIndex); + } var key = _mapper.ToIndexKey(id); _primaryIndex.Insert(key, location, transaction.TransactionId); @@ -1042,46 +1459,39 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC #region Find /// - /// Finds a document by its ObjectId. - /// If called within a transaction, will see uncommitted changes ("Read Your Own Writes"). - /// Otherwise creates a read-only snapshot transaction. + /// Finds a document by its ObjectId. + /// If called within a transaction, will see uncommitted changes ("Read Your Own Writes"). + /// Otherwise creates a read-only snapshot transaction. /// /// ObjectId of the document /// Optional transaction for isolation (supports Read Your Own Writes) /// The document, or null if not found - public T? FindById(TId id) - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - try - { - var key = _mapper.ToIndexKey(id); + public T? FindById(TId id) + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var key = _mapper.ToIndexKey(id); - if (!_primaryIndex.TryFind(key, out var location, transaction.TransactionId)) - return null; + if (!_primaryIndex.TryFind(key, out var location, transaction.TransactionId)) + return null; - return FindByLocation(location); - } - finally - { - } + return FindByLocation(location); } - /// - /// Returns all documents in the collection. - /// WARNING: This method requires an external transaction for proper isolation! - /// If no transaction is provided, reads committed snapshot only (may see partial updates). + /// Returns all documents in the collection. + /// WARNING: This method requires an external transaction for proper isolation! + /// If no transaction is provided, reads committed snapshot only (may see partial updates). /// /// Transaction for isolation (REQUIRED for consistent reads during concurrent writes) /// Enumerable of all documents - public IEnumerable FindAll() - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var txnId = transaction?.TransactionId ?? 0; - var minKey = new IndexKey(Array.Empty()); + public IEnumerable FindAll() + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + ulong txnId = transaction?.TransactionId ?? 0; + var minKey = new IndexKey(Array.Empty()); var maxKey = new IndexKey(Enumerable.Repeat((byte)0xFF, 32).ToArray()); foreach (var entry in _primaryIndex.Range(minKey, maxKey, IndexDirection.Forward, txnId)) @@ -1092,16 +1502,16 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Finds a document by its physical storage location. - /// - /// The page and slot location of the document. - /// The document if found; otherwise, null. - internal T? FindByLocation(DocumentLocation location) + /// + /// Finds a document by its physical storage location. + /// + /// The page and slot location of the document. + /// The document if found; otherwise, null. + internal T? FindByLocation(DocumentLocation location) { var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var txnId = transaction?.TransactionId ?? 0; - var buffer = ArrayPool.Shared.Rent(_storage.PageSize); + ulong txnId = transaction?.TransactionId ?? 0; + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { // Read from StorageEngine with transaction isolation @@ -1112,34 +1522,35 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC if (location.SlotIndex >= header.SlotCount) return null; - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); - if ((slot.Flags & SlotFlags.Deleted) != 0) - return null; - - ValidateSlotBounds(slot, buffer.Length, location); - - if ((slot.Flags & SlotFlags.HasOverflow) != 0) - { - var storedPayload = ReassembleOverflowPayload(buffer.AsSpan(slot.Offset, slot.Length), txnId, buffer, location); - var logicalPayload = (slot.Flags & SlotFlags.Compressed) != 0 - ? DecompressStoredPayload(storedPayload, location) - : storedPayload; - - return _mapper.Deserialize(new BsonSpanReader(logicalPayload, _storage.GetKeyReverseMap())); - } - - var docData = buffer.AsSpan(slot.Offset, slot.Length); - if ((slot.Flags & SlotFlags.Compressed) != 0) - { - var logicalPayload = DecompressStoredPayload(docData, location); - return _mapper.Deserialize(new BsonSpanReader(logicalPayload, _storage.GetKeyReverseMap())); - } - - return _mapper.Deserialize(new BsonSpanReader(docData, _storage.GetKeyReverseMap())); - } - finally + if ((slot.Flags & SlotFlags.Deleted) != 0) + return null; + + ValidateSlotBounds(slot, buffer.Length, location); + + if ((slot.Flags & SlotFlags.HasOverflow) != 0) + { + byte[] storedPayload = + ReassembleOverflowPayload(buffer.AsSpan(slot.Offset, slot.Length), txnId, buffer, location); + byte[] logicalPayload = (slot.Flags & SlotFlags.Compressed) != 0 + ? DecompressStoredPayload(storedPayload, location) + : storedPayload; + + return _mapper.Deserialize(new BsonSpanReader(logicalPayload, _storage.GetKeyReverseMap())); + } + + var docData = buffer.AsSpan(slot.Offset, slot.Length); + if ((slot.Flags & SlotFlags.Compressed) != 0) + { + byte[] logicalPayload = DecompressStoredPayload(docData, location); + return _mapper.Deserialize(new BsonSpanReader(logicalPayload, _storage.GetKeyReverseMap())); + } + + return _mapper.Deserialize(new BsonSpanReader(docData, _storage.GetKeyReverseMap())); + } + finally { ArrayPool.Shared.Return(buffer); } @@ -1149,12 +1560,12 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC #region Update & Delete - /// - /// Updates an existing document in the collection - /// - /// The entity containing updated values. - /// True if the document was updated; otherwise, false. - public bool Update(T entity) + /// + /// Updates an existing document in the collection + /// + /// The entity containing updated values. + /// True if the document was updated; otherwise, false. + public bool Update(T entity) { var transaction = _transactionHolder.GetCurrentTransactionOrStart(); if (entity == null) throw new ArgumentNullException(nameof(entity)); @@ -1164,7 +1575,7 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC { try { - var result = UpdateCore(entity); + bool result = UpdateCore(entity); return result; } catch @@ -1179,19 +1590,19 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Asynchronously updates an existing document in the collection - /// - /// The entity containing updated values. - /// True if the document was updated; otherwise, false. - public async Task UpdateAsync(T entity) + /// + /// Asynchronously updates an existing document in the collection + /// + /// The entity containing updated values. + /// True if the document was updated; otherwise, false. + public async Task UpdateAsync(T entity) { if (entity == null) throw new ArgumentNullException(nameof(entity)); await _collectionLock.WaitAsync(); try { - var result = UpdateCore(entity); + bool result = UpdateCore(entity); return result; } finally @@ -1200,17 +1611,17 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Updates multiple documents in a single operation. - /// - /// The entities to update. - /// The number of updated documents. - public int UpdateBulk(IEnumerable entities) + /// + /// Updates multiple documents in a single operation. + /// + /// The entities to update. + /// The number of updated documents. + public int UpdateBulk(IEnumerable entities) { if (entities == null) throw new ArgumentNullException(nameof(entities)); var entityList = entities.ToList(); - int updateCount = 0; + var updateCount = 0; _collectionLock.Wait(); try @@ -1224,17 +1635,17 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Asynchronously updates multiple documents in a single transaction. - /// - /// The entities to update. - /// The number of updated documents. - public async Task UpdateBulkAsync(IEnumerable entities) + /// + /// Asynchronously updates multiple documents in a single transaction. + /// + /// The entities to update. + /// The number of updated documents. + public async Task UpdateBulkAsync(IEnumerable entities) { if (entities == null) throw new ArgumentNullException(nameof(entities)); var entityList = entities.ToList(); - int updateCount = 0; + var updateCount = 0; await _collectionLock.WaitAsync(); try @@ -1248,14 +1659,14 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - private int UpdateBulkInternal(List entityList) - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - int updateCount = 0; - const int BATCH_SIZE = 50; + private int UpdateBulkInternal(List entityList) + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var updateCount = 0; + const int BATCH_SIZE = 50; - for (int batchStart = 0; batchStart < entityList.Count; batchStart += BATCH_SIZE) + for (var batchStart = 0; batchStart < entityList.Count; batchStart += BATCH_SIZE) { int batchEnd = Math.Min(batchStart + BATCH_SIZE, entityList.Count); int batchCount = batchEnd - batchStart; @@ -1263,7 +1674,7 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // PHASE 1: Parallel Serialization var serializedBatch = new (TId id, byte[] data, int length, bool found)[batchCount]; - for (int i = 0; i < batchCount; i++) + for (var i = 0; i < batchCount; i++) { var entity = entityList[batchStart + i]; var id = _mapper.GetId(entity); @@ -1271,9 +1682,9 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC // Check if entity exists // We do this sequentially to avoid ThreadPool exhaustion or IO-related deadlocks - if (_primaryIndex.TryFind(key, out var _, transaction.TransactionId)) + if (_primaryIndex.TryFind(key, out _, transaction.TransactionId)) { - var length = SerializeWithRetry(entity, out var buffer); + int length = SerializeWithRetry(entity, out byte[] buffer); serializedBatch[i] = (id, buffer, length, true); } else @@ -1283,9 +1694,9 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } // PHASE 2: Sequential Update - for (int i = 0; i < batchCount; i++) + for (var i = 0; i < batchCount; i++) { - var (id, docData, length, found) = serializedBatch[i]; + (var id, byte[] docData, int length, bool found) = serializedBatch[i]; if (!found) continue; var entity = entityList[batchStart + i]; @@ -1300,13 +1711,14 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } } + return updateCount; } private bool UpdateCore(T entity) { var id = _mapper.GetId(entity); - var length = SerializeWithRetry(entity, out var buffer); + int length = SerializeWithRetry(entity, out byte[] buffer); try { return UpdateDataCore(id, entity, buffer.AsSpan(0, length)); @@ -1317,14 +1729,14 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - private bool UpdateDataCore(TId id, T entity, ReadOnlySpan docData) - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var key = _mapper.ToIndexKey(id); - var (storedPayloadOverride, storedPayloadFlags) = PreparePayloadForStorage(docData); - ReadOnlySpan storedPayload = storedPayloadOverride is null ? docData : storedPayloadOverride; - var bytesWritten = storedPayload.Length; + private bool UpdateDataCore(TId id, T entity, ReadOnlySpan docData) + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var key = _mapper.ToIndexKey(id); + (byte[]? storedPayloadOverride, var storedPayloadFlags) = PreparePayloadForStorage(docData); + var storedPayload = storedPayloadOverride is null ? docData : storedPayloadOverride; + int bytesWritten = storedPayload.Length; if (!_primaryIndex.TryFind(key, out var oldLocation, transaction.TransactionId)) return false; @@ -1334,23 +1746,23 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC if (oldEntity == null) return false; // Read old page - var pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { _storage.ReadPage(oldLocation.PageId, transaction.TransactionId, pageBuffer); - var slotOffset = SlottedPageHeader.Size + (oldLocation.SlotIndex * SlotEntry.Size); - var oldSlot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset)); - - if (bytesWritten <= oldSlot.Length && (oldSlot.Flags & SlotFlags.HasOverflow) == 0) - { - // In-place update - storedPayload.CopyTo(pageBuffer.AsSpan(oldSlot.Offset, bytesWritten)); - var newSlot = oldSlot; - newSlot.Length = (ushort)bytesWritten; - newSlot.Flags = storedPayloadFlags; - newSlot.WriteTo(pageBuffer.AsSpan(slotOffset)); - _storage.WritePage(oldLocation.PageId, transaction.TransactionId, pageBuffer); + int slotOffset = SlottedPageHeader.Size + oldLocation.SlotIndex * SlotEntry.Size; + var oldSlot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset)); + + if (bytesWritten <= oldSlot.Length && (oldSlot.Flags & SlotFlags.HasOverflow) == 0) + { + // In-place update + storedPayload.CopyTo(pageBuffer.AsSpan(oldSlot.Offset, bytesWritten)); + var newSlot = oldSlot; + newSlot.Length = (ushort)bytesWritten; + newSlot.Flags = storedPayloadFlags; + newSlot.WriteTo(pageBuffer.AsSpan(slotOffset)); + _storage.WritePage(oldLocation.PageId, transaction.TransactionId, pageBuffer); // Notify secondary indexes (primary index unchanged) _indexManager.UpdateInAll(oldEntity, entity, oldLocation, oldLocation, transaction); @@ -1362,21 +1774,21 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC else { // Delete old + insert new - DeleteCore(id, notifyCdc: false); + DeleteCore(id, false); DocumentLocation newLocation; - if (bytesWritten + SlotEntry.Size <= _maxDocumentSizeForSinglePage) - { - var newPageId = FindPageWithSpace(bytesWritten + SlotEntry.Size); - if (newPageId == 0) newPageId = AllocateNewDataPage(); - var newSlotIndex = InsertIntoPage(newPageId, storedPayload, storedPayloadFlags); - newLocation = new DocumentLocation(newPageId, newSlotIndex); - } - else - { - var (newPageId, newSlotIndex) = InsertWithOverflow(storedPayload, storedPayloadFlags); - newLocation = new DocumentLocation(newPageId, newSlotIndex); - } + if (bytesWritten + SlotEntry.Size <= _maxDocumentSizeForSinglePage) + { + uint newPageId = FindPageWithSpace(bytesWritten + SlotEntry.Size); + if (newPageId == 0) newPageId = AllocateNewDataPage(); + ushort newSlotIndex = InsertIntoPage(newPageId, storedPayload, storedPayloadFlags); + newLocation = new DocumentLocation(newPageId, newSlotIndex); + } + else + { + (uint newPageId, ushort newSlotIndex) = InsertWithOverflow(storedPayload, storedPayloadFlags); + newLocation = new DocumentLocation(newPageId, newSlotIndex); + } _primaryIndex.Insert(key, newLocation, transaction.TransactionId); _indexManager.UpdateInAll(oldEntity, entity, oldLocation, newLocation, transaction); @@ -1392,17 +1804,17 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Deletes a document by its primary key. - /// - /// The identifier of the document to delete. - /// True if a document was deleted; otherwise, false. - public bool Delete(TId id) + /// + /// Deletes a document by its primary key. + /// + /// The identifier of the document to delete. + /// True if a document was deleted; otherwise, false. + public bool Delete(TId id) { _collectionLock.Wait(); try { - var result = DeleteCore(id); + bool result = DeleteCore(id); return result; } finally @@ -1411,17 +1823,17 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Asynchronously deletes a document by its primary key. - /// - /// The identifier of the document to delete. - /// True if a document was deleted; otherwise, false. - public async Task DeleteAsync(TId id) + /// + /// Asynchronously deletes a document by its primary key. + /// + /// The identifier of the document to delete. + /// True if a document was deleted; otherwise, false. + public async Task DeleteAsync(TId id) { await _collectionLock.WaitAsync(); try { - var result = DeleteCore(id); + bool result = DeleteCore(id); return result; } finally @@ -1430,17 +1842,17 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Deletes multiple documents in a single transaction. - /// Efficiently updates storage and index. - /// - /// The identifiers of documents to delete. - /// The number of deleted documents. - public int DeleteBulk(IEnumerable ids) + /// + /// Deletes multiple documents in a single transaction. + /// Efficiently updates storage and index. + /// + /// The identifiers of documents to delete. + /// The number of deleted documents. + public int DeleteBulk(IEnumerable ids) { if (ids == null) throw new ArgumentNullException(nameof(ids)); - int deleteCount = 0; + var deleteCount = 0; _collectionLock.Wait(); try { @@ -1453,16 +1865,16 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - /// - /// Asynchronously deletes multiple documents in a single transaction. - /// - /// The identifiers of documents to delete. - /// The number of deleted documents. - public async Task DeleteBulkAsync(IEnumerable ids) + /// + /// Asynchronously deletes multiple documents in a single transaction. + /// + /// The identifiers of documents to delete. + /// The number of deleted documents. + public async Task DeleteBulkAsync(IEnumerable ids) { if (ids == null) throw new ArgumentNullException(nameof(ids)); - int deleteCount = 0; + var deleteCount = 0; await _collectionLock.WaitAsync(); try { @@ -1477,43 +1889,38 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC private int DeleteBulkInternal(IEnumerable ids) { - int deleteCount = 0; + var deleteCount = 0; foreach (var id in ids) - { if (DeleteCore(id)) deleteCount++; - } return deleteCount; } - private bool DeleteCore(TId id, bool notifyCdc = true) - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var key = _mapper.ToIndexKey(id); - if (!_primaryIndex.TryFind(key, out var location, transaction.TransactionId)) + private bool DeleteCore(TId id, bool notifyCdc = true) + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + var key = _mapper.ToIndexKey(id); + if (!_primaryIndex.TryFind(key, out var location, transaction.TransactionId)) return false; // Notify secondary indexes BEFORE deleting document from storage var entity = FindByLocation(location); - if (entity != null) - { - _indexManager.DeleteFromAll(entity, location, transaction); - } + if (entity != null) _indexManager.DeleteFromAll(entity, location, transaction); // Read page - var buffer = ArrayPool.Shared.Rent(_storage.PageSize); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { _storage.ReadPage(location.PageId, transaction.TransactionId, buffer); - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); // Check if slot has overflow and free it if ((slot.Flags & SlotFlags.HasOverflow) != 0) { - var nextOverflowPage = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian( + uint nextOverflowPage = BinaryPrimitives.ReadUInt32LittleEndian( buffer.AsSpan(slot.Offset + 4, 4)); FreeOverflowChain(nextOverflowPage); } @@ -1542,14 +1949,14 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC private void FreeOverflowChain(uint overflowPageId) { var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var tempBuffer = ArrayPool.Shared.Rent(_storage.PageSize); + byte[] tempBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { while (overflowPageId != 0) { _storage.ReadPage(overflowPageId, transaction.TransactionId, tempBuffer); var header = SlottedPageHeader.ReadFrom(tempBuffer); - var nextPage = header.NextOverflowPage; + uint nextPage = header.NextOverflowPage; // Recycle this page _storage.FreePage(overflowPageId); @@ -1563,480 +1970,49 @@ public partial class DocumentCollection : IDisposable, ICompactionAwareC } } - #endregion - - #region Query Helpers + #endregion + + #region Query Helpers /// - /// Counts all documents in the collection. - /// If called within a transaction, will count uncommitted changes. + /// Counts all documents in the collection. + /// If called within a transaction, will count uncommitted changes. /// /// Optional transaction for isolation /// Number of documents - public int Count() - { - RefreshPrimaryIndexRootFromMetadata(); - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - // Count all entries in primary index - // Use generic min/max keys for the index + public int Count() + { + RefreshPrimaryIndexRootFromMetadata(); + var transaction = _transactionHolder.GetCurrentTransactionOrStart(); + // Count all entries in primary index + // Use generic min/max keys for the index var minKey = IndexKey.MinKey; var maxKey = IndexKey.MaxKey; return _primaryIndex.Range(minKey, maxKey, IndexDirection.Forward, transaction.TransactionId).Count(); } - /// - /// Finds all documents matching the predicate. - /// If transaction is provided, will see uncommitted changes. - /// - /// Predicate used to filter documents. - /// Documents that match the predicate. - public IEnumerable FindAll(Func predicate) + /// + /// Finds all documents matching the predicate. + /// If transaction is provided, will see uncommitted changes. + /// + /// Predicate used to filter documents. + /// Documents that match the predicate. + public IEnumerable FindAll(Func predicate) { foreach (var entity in FindAll()) - { if (predicate(entity)) yield return entity; - } - } - - /// - /// Find entities matching predicate (alias for FindAll with predicate) - /// - /// Predicate used to filter documents. - /// Documents that match the predicate. - public IEnumerable Find(Func predicate) - => FindAll(predicate); - - #endregion - - private (byte[]? storedPayloadOverride, SlotFlags slotFlags) PreparePayloadForStorage(ReadOnlySpan logicalPayload) - { - if (TryCreateCompressedPayload(logicalPayload, out var compressedPayload)) - { - return (compressedPayload, SlotFlags.Compressed); - } - - return (null, SlotFlags.None); - } - - private bool TryCreateCompressedPayload(ReadOnlySpan logicalPayload, out byte[]? storedPayload) - { - storedPayload = null; - - var options = _storage.CompressionOptions; - var telemetry = _storage.CompressionTelemetry; - if (!options.EnableCompression) - return false; - - if (logicalPayload.Length < options.MinSizeBytes) - { - telemetry.RecordCompressionSkippedTooSmall(); - return false; - } - - if (options.MaxCompressionInputBytes.HasValue && logicalPayload.Length > options.MaxCompressionInputBytes.Value) - { - telemetry.RecordSafetyLimitRejection(); - return false; - } - - telemetry.RecordCompressionAttempt(logicalPayload.Length); - try - { - long startedAt = Stopwatch.GetTimestamp(); - var compressedPayload = _storage.CompressionService.Compress(logicalPayload, options.Codec, options.Level); - long elapsedTicks = Stopwatch.GetTimestamp() - startedAt; - telemetry.RecordCompressionCpuTicks(elapsedTicks); - int compressedStorageLength = CompressedPayloadHeader.Size + compressedPayload.Length; - - if (!MeetsMinSavingsPercent(logicalPayload.Length, compressedStorageLength, options.MinSavingsPercent)) - { - telemetry.RecordCompressionSkippedInsufficientSavings(); - return false; - } - - var output = new byte[compressedStorageLength]; - var header = CompressedPayloadHeader.Create(options.Codec, logicalPayload.Length, compressedPayload); - header.WriteTo(output.AsSpan(0, CompressedPayloadHeader.Size)); - compressedPayload.CopyTo(output.AsSpan(CompressedPayloadHeader.Size)); - - telemetry.RecordCompressionSuccess(output.Length); - storedPayload = output; - return true; - } - catch - { - telemetry.RecordCompressionFailure(); - return false; - } - } - - private static bool MeetsMinSavingsPercent(int originalLength, int compressedStorageLength, int minSavingsPercent) - { - if (originalLength <= 0) - return false; - - int savedBytes = originalLength - compressedStorageLength; - if (savedBytes <= 0) - return false; - - int savingsPercent = (int)((savedBytes * 100L) / originalLength); - return savingsPercent >= minSavingsPercent; - } - - private static void ValidateSlotBounds(in SlotEntry slot, int bufferLength, in DocumentLocation location) - { - int endOffset = slot.Offset + slot.Length; - if (slot.Offset < SlottedPageHeader.Size || endOffset > bufferLength) - { - throw new InvalidDataException( - $"Corrupted slot bounds: Offset={slot.Offset}, Length={slot.Length}, Buffer={bufferLength}, SlotIndex={location.SlotIndex}, PageId={location.PageId}, Flags={slot.Flags}"); - } - } - - private byte[] ReassembleOverflowPayload(ReadOnlySpan primaryPayload, ulong transactionId, byte[] pageBuffer, in DocumentLocation location) - { - if (primaryPayload.Length < OverflowMetadataSize) - { - throw new InvalidDataException( - $"Corrupted overflow metadata: primary slot too small ({primaryPayload.Length} bytes) at {location.PageId}:{location.SlotIndex}."); - } - - int totalLength = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); - if (totalLength < 0 || totalLength > MaxStoredPayloadSizeBytes) - { - _storage.CompressionTelemetry.RecordSafetyLimitRejection(); - throw new InvalidDataException( - $"Corrupted overflow metadata: invalid total length {totalLength} at {location.PageId}:{location.SlotIndex}."); - } - - uint currentOverflowPageId = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); - int primaryChunkSize = primaryPayload.Length - OverflowMetadataSize; - if (totalLength < primaryChunkSize) - { - throw new InvalidDataException( - $"Corrupted overflow metadata: total length {totalLength} is smaller than primary chunk {primaryChunkSize} at {location.PageId}:{location.SlotIndex}."); - } - - var fullPayload = new byte[totalLength]; - primaryPayload.Slice(OverflowMetadataSize, primaryChunkSize).CopyTo(fullPayload); - - int offset = primaryChunkSize; - int maxChunkSize = _storage.PageSize - SlottedPageHeader.Size; - - while (currentOverflowPageId != 0 && offset < totalLength) - { - _storage.ReadPage(currentOverflowPageId, transactionId, pageBuffer); - var overflowHeader = SlottedPageHeader.ReadFrom(pageBuffer); - if (overflowHeader.PageType != PageType.Overflow) - { - throw new InvalidDataException( - $"Corrupted overflow chain: page {currentOverflowPageId} is not an overflow page."); - } - - int remaining = totalLength - offset; - int chunkSize = Math.Min(maxChunkSize, remaining); - pageBuffer.AsSpan(SlottedPageHeader.Size, chunkSize).CopyTo(fullPayload.AsSpan(offset)); - offset += chunkSize; - currentOverflowPageId = overflowHeader.NextOverflowPage; - } - - if (offset != totalLength) - { - throw new InvalidDataException( - $"Corrupted overflow chain: expected {totalLength} bytes but reconstructed {offset} bytes at {location.PageId}:{location.SlotIndex}."); - } - - if (currentOverflowPageId != 0) - { - throw new InvalidDataException( - $"Corrupted overflow chain: extra overflow pages remain after reconstruction at {location.PageId}:{location.SlotIndex}."); - } - - return fullPayload; - } - - private byte[] DecompressStoredPayload(ReadOnlySpan storedPayload, in DocumentLocation location) - { - var telemetry = _storage.CompressionTelemetry; - telemetry.RecordDecompressionAttempt(); - - try - { - if (storedPayload.Length < CompressedPayloadHeader.Size) - { - throw new InvalidDataException( - $"Corrupted compressed payload: missing header at {location.PageId}:{location.SlotIndex}."); - } - - var header = CompressedPayloadHeader.ReadFrom(storedPayload.Slice(0, CompressedPayloadHeader.Size)); - if (!Enum.IsDefined(typeof(CompressionCodec), header.Codec) || header.Codec == CompressionCodec.None) - { - throw new InvalidDataException( - $"Corrupted compressed payload: invalid codec '{header.Codec}' at {location.PageId}:{location.SlotIndex}."); - } - - if (header.OriginalLength < 0 || header.OriginalLength > _storage.CompressionOptions.MaxDecompressedSizeBytes) - { - telemetry.RecordSafetyLimitRejection(); - throw new InvalidDataException( - $"Corrupted compressed payload: invalid decompressed length {header.OriginalLength} at {location.PageId}:{location.SlotIndex}."); - } - - int compressedLength = storedPayload.Length - CompressedPayloadHeader.Size; - if (header.CompressedLength < 0 || header.CompressedLength != compressedLength) - { - throw new InvalidDataException( - $"Corrupted compressed payload: invalid compressed length {header.CompressedLength} (actual {compressedLength}) at {location.PageId}:{location.SlotIndex}."); - } - - var compressedPayload = storedPayload.Slice(CompressedPayloadHeader.Size, header.CompressedLength); - if (!header.ValidateChecksum(compressedPayload)) - { - telemetry.RecordChecksumFailure(); - throw new InvalidDataException( - $"Corrupted compressed payload: checksum mismatch at {location.PageId}:{location.SlotIndex}."); - } - - if (!_storage.CompressionService.TryGetCodec(header.Codec, out _)) - { - throw new InvalidDataException( - $"Corrupted compressed payload: codec '{header.Codec}' is not registered at {location.PageId}:{location.SlotIndex}."); - } - - long startedAt = Stopwatch.GetTimestamp(); - var decompressed = _storage.CompressionService.Decompress( - compressedPayload, - header.Codec, - header.OriginalLength, - _storage.CompressionOptions.MaxDecompressedSizeBytes); - long elapsedTicks = Stopwatch.GetTimestamp() - startedAt; - telemetry.RecordDecompressionCpuTicks(elapsedTicks); - - if (decompressed.Length != header.OriginalLength) - { - throw new InvalidDataException( - $"Corrupted compressed payload: decompressed length {decompressed.Length} does not match expected {header.OriginalLength} at {location.PageId}:{location.SlotIndex}."); - } - - telemetry.RecordDecompressionSuccess(decompressed.Length); - return decompressed; - } - catch (InvalidDataException) - { - telemetry.RecordDecompressionFailure(); - throw; - } - catch (Exception ex) - { - telemetry.RecordDecompressionFailure(); - throw new InvalidDataException( - $"Failed to decompress payload at {location.PageId}:{location.SlotIndex}.", ex); - } - } - - /// - /// Serializes an entity with adaptive buffer sizing (Stepped Retry). - /// Strategies: - /// 1. 64KB (Covers 99% of docs, small overhead) - /// 2. 2MB (Covers large docs) - /// 3. 16MB (Max limit) - /// - private int SerializeWithRetry(T entity, out byte[] rentedBuffer) - { - // 64KB, 2MB, 16MB - int[] steps = { 65536, 2097152, 16777216 }; - - for (int i = 0; i < steps.Length; i++) - { - int size = steps[i]; - - // Ensure we at least cover PageSize (unlikely to be > 64KB but safe) - if (size < _storage.PageSize) size = _storage.PageSize; - - var buffer = ArrayPool.Shared.Rent(size); - try - { - int bytesWritten = _mapper.Serialize(entity, new BsonSpanWriter(buffer, _storage.GetKeyMap())); - - // Inject schema version if available - if (CurrentSchemaVersion != null) - { - if (bytesWritten + 8 > buffer.Length) - { - throw new IndexOutOfRangeException("Not enough space for version field"); - } - AppendVersionField(buffer, ref bytesWritten); - } - - rentedBuffer = buffer; - return bytesWritten; - } - catch (Exception ex) when (ex is ArgumentException || ex is IndexOutOfRangeException || ex is ArgumentOutOfRangeException) - { - ArrayPool.Shared.Return(buffer); - // Continue to next step - } - catch - { - ArrayPool.Shared.Return(buffer); - throw; - } - } - - rentedBuffer = null!; // specific compiler satisfaction, though we throw - throw new InvalidOperationException($"Document too large. Maximum size allowed is 16MB."); } /// - /// Appends a version field to the specified BSON buffer if a current schema version is set. + /// Find entities matching predicate (alias for FindAll with predicate) /// - /// The version field is only appended if a current schema version is available. The method - /// updates the BSON document's size and ensures the buffer remains in a valid BSON format. - /// The byte array buffer to which the version field is appended. Must be large enough to accommodate the additional - /// bytes. - /// A reference to the number of bytes written to the buffer. Updated to reflect the new total after the version - /// field is appended. - private void AppendVersionField(byte[] buffer, ref int bytesWritten) + /// Predicate used to filter documents. + /// Documents that match the predicate. + public IEnumerable Find(Func predicate) { - if (CurrentSchemaVersion == null) return; - - int version = CurrentSchemaVersion.Value.Version; - - // BSON element for _v (Int32) with Compressed Key: - // Type (1 byte: 0x10) - // Key ID (2 bytes, little-endian) - // Value (4 bytes: int32) - // Total = 7 bytes - - int pos = bytesWritten - 1; // Position of old 0x00 terminator - buffer[pos++] = 0x10; // Int32 - - ushort versionKeyId = _storage.GetOrAddDictionaryEntry("_v"); - BinaryPrimitives.WriteUInt16LittleEndian(buffer.AsSpan(pos, 2), versionKeyId); - pos += 2; - - BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(pos, 4), version); - pos += 4; - - buffer[pos++] = 0x00; // new document terminator - - bytesWritten = pos; - - // Update total size (first 4 bytes) - BinaryPrimitives.WriteInt32LittleEndian(buffer.AsSpan(0, 4), bytesWritten); + return FindAll(predicate); } - /// - /// Performs a vector similarity search on the specified index and returns up to the top-k matching documents of - /// type T. - /// - /// The search uses approximate nearest neighbor algorithms for efficient retrieval. The efSearch - /// parameter can be tuned to balance search speed and accuracy. Results are filtered to include only documents that - /// can be successfully retrieved from storage. - /// The name of the index to search. Cannot be null or empty. - /// The query vector used to find similar documents. The array length must match the dimensionality of the index. - /// The maximum number of nearest neighbors to return. Must be greater than zero. - /// The size of the dynamic candidate list during search. Higher values may improve recall at the cost of - /// performance. Must be greater than zero. The default is 100. - /// An optional transaction context to use for the search. If null, the operation is performed without a - /// transaction. - /// An enumerable collection of up to k documents of type T that are most similar to the query vector. The - /// collection may be empty if no matches are found. - /// Thrown if indexName is null, empty, or does not correspond to an existing index. - public IEnumerable VectorSearch(string indexName, float[] query, int k, int efSearch = 100) - { - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var index = _indexManager.GetIndex(indexName); - if (index == null) - throw new ArgumentException($"Index '{indexName}' not found.", nameof(indexName)); - - foreach (var result in index.VectorSearch(query, k, efSearch, transaction)) - { - var doc = FindByLocation(result.Location); - if (doc != null) yield return doc; - } - } - - /// - /// Finds all documents located within a specified radius of a geographic center point using a spatial index. - /// - /// The name of the spatial index to use for the search. Cannot be null or empty. - /// A tuple representing the latitude and longitude of the center point, in decimal degrees. - /// The search radius, in kilometers. Must be greater than zero. - /// An optional transaction context to use for the operation. If null, the default transaction is used. - /// An enumerable collection of documents of type T that are located within the specified radius of the center - /// point. The collection is empty if no documents are found. - /// Thrown if indexName is null, empty, or does not correspond to an existing index. - public IEnumerable Near(string indexName, (double Latitude, double Longitude) center, double radiusKm) - { - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var index = _indexManager.GetIndex(indexName); - if (index == null) - throw new ArgumentException($"Index '{indexName}' not found.", nameof(indexName)); - - foreach (var loc in index.Near(center, radiusKm, transaction)) - { - var doc = FindByLocation(loc); - if (doc != null) yield return doc; - } - } - - /// - /// Returns all documents within the specified rectangular geographic area from the given spatial index. - /// - /// The name of the spatial index to search within. Cannot be null or empty. - /// The minimum latitude and longitude coordinates defining one corner of the search rectangle. - /// The maximum latitude and longitude coordinates defining the opposite corner of the search rectangle. - /// An enumerable collection of documents of type T that are located within the specified geographic bounds. The - /// collection is empty if no documents are found. - /// - /// Transactions are managed implicitly through the collection's ; callers do not supply a transaction parameter. - /// - /// Thrown if indexName is null, empty, or does not correspond to an existing index. - public IEnumerable Within(string indexName, (double Latitude, double Longitude) min, (double Latitude, double Longitude) max) - { - var transaction = _transactionHolder.GetCurrentTransactionOrStart(); - var index = _indexManager.GetIndex(indexName); - if (index == null) - throw new ArgumentException($"Index '{indexName}' not found.", nameof(indexName)); - - foreach (var loc in index.Within(min, max, transaction)) - { - var doc = FindByLocation(loc); - if (doc != null) yield return doc; - } - } - - /// - /// Subscribes to a change stream that notifies observers of changes to the collection. - /// - /// The returned observable emits events as changes are detected in the collection. Observers can - /// subscribe to receive real-time updates. The behavior of the event payload depends on the value of the - /// capturePayload parameter. - /// true to include the full payload of changed documents in each event; otherwise, false to include only metadata - /// about the change. The default is false. - /// An observable sequence of change stream events for the collection. Subscribers receive notifications as changes - /// occur. - /// Thrown if change data capture (CDC) is not initialized for the storage. - public IObservable> Watch(bool capturePayload = false) - { - return _cdcPublisher.Watch(capturePayload); - } - - private void NotifyCdc(OperationType type, TId id, ReadOnlySpan docData = default) - { - _cdcPublisher.Notify(type, id, docData); - } - - /// - /// Releases all resources used by the current instance of the class. - /// - /// Call this method when you are finished using the object to free unmanaged resources - /// immediately. After calling Dispose, the object should not be used. - public void Dispose() - { - _indexManager.Dispose(); - GC.SuppressFinalize(this); - } -} + #endregion +} \ No newline at end of file diff --git a/src/CBDD.Core/Collections/IDocumentMapper.cs b/src/CBDD.Core/Collections/IDocumentMapper.cs index e7b0303..9802a7a 100755 --- a/src/CBDD.Core/Collections/IDocumentMapper.cs +++ b/src/CBDD.Core/Collections/IDocumentMapper.cs @@ -1,42 +1,39 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Indexing; -using System; -using System.Buffers; -using System.Collections.Generic; -using ZB.MOM.WW.CBDD.Bson.Schema; - -namespace ZB.MOM.WW.CBDD.Core.Collections; - -/// -/// Non-generic interface for common mapper operations. -/// +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Bson.Schema; +using ZB.MOM.WW.CBDD.Core.Indexing; + +namespace ZB.MOM.WW.CBDD.Core.Collections; + +/// +/// Non-generic interface for common mapper operations. +/// public interface IDocumentMapper { /// - /// Gets the collection name handled by this mapper. + /// Gets the collection name handled by this mapper. /// string CollectionName { get; } /// - /// Gets the set of document keys used during mapping. + /// Gets the set of document keys used during mapping. /// IEnumerable UsedKeys { get; } /// - /// Gets the BSON schema for the mapped document. + /// Gets the BSON schema for the mapped document. /// /// The BSON schema. BsonSchema GetSchema(); } - -/// -/// Interface for mapping between entities and BSON using zero-allocation serialization. -/// Handles bidirectional mapping between TId and IndexKey. -/// + +/// +/// Interface for mapping between entities and BSON using zero-allocation serialization. +/// Handles bidirectional mapping between TId and IndexKey. +/// public interface IDocumentMapper : IDocumentMapper where T : class { /// - /// Serializes an entity to BSON. + /// Serializes an entity to BSON. /// /// The entity to serialize. /// The BSON writer. @@ -44,44 +41,44 @@ public interface IDocumentMapper : IDocumentMapper where T : class int Serialize(T entity, BsonSpanWriter writer); /// - /// Deserializes an entity from BSON. + /// Deserializes an entity from BSON. /// /// The BSON reader. /// The deserialized entity. T Deserialize(BsonSpanReader reader); /// - /// Gets the identifier value from an entity. + /// Gets the identifier value from an entity. /// /// The entity. /// The identifier value. TId GetId(T entity); /// - /// Sets the identifier value on an entity. + /// Sets the identifier value on an entity. /// /// The entity. /// The identifier value. void SetId(T entity, TId id); /// - /// Converts an identifier to an index key. + /// Converts an identifier to an index key. /// /// The identifier value. /// The index key representation. IndexKey ToIndexKey(TId id); /// - /// Converts an index key back to an identifier. + /// Converts an index key back to an identifier. /// /// The index key. /// The identifier value. TId FromIndexKey(IndexKey key); } - -/// -/// Legacy interface for compatibility with existing ObjectId-based collections. -/// -public interface IDocumentMapper : IDocumentMapper where T : class -{ -} + +/// +/// Legacy interface for compatibility with existing ObjectId-based collections. +/// +public interface IDocumentMapper : IDocumentMapper where T : class +{ +} \ No newline at end of file diff --git a/src/CBDD.Core/Collections/SchemaVersion.cs b/src/CBDD.Core/Collections/SchemaVersion.cs index 6891d0d..fb54961 100755 --- a/src/CBDD.Core/Collections/SchemaVersion.cs +++ b/src/CBDD.Core/Collections/SchemaVersion.cs @@ -1,21 +1,19 @@ -using System; - -namespace ZB.MOM.WW.CBDD.Core.Collections; - +namespace ZB.MOM.WW.CBDD.Core.Collections; + public readonly struct SchemaVersion { /// - /// Gets the schema version number. + /// Gets the schema version number. /// public int Version { get; } /// - /// Gets the schema hash. + /// Gets the schema hash. /// public long Hash { get; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The schema version number. /// The schema hash. @@ -26,5 +24,8 @@ public readonly struct SchemaVersion } /// - public override string ToString() => $"v{Version} (0x{Hash:X16})"; -} + public override string ToString() + { + return $"v{Version} (0x{Hash:X16})"; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/CompressedPayloadHeader.cs b/src/CBDD.Core/Compression/CompressedPayloadHeader.cs index 217e86a..0d4f411 100644 --- a/src/CBDD.Core/Compression/CompressedPayloadHeader.cs +++ b/src/CBDD.Core/Compression/CompressedPayloadHeader.cs @@ -3,34 +3,34 @@ using System.Buffers.Binary; namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Fixed header prefix for compressed payload blobs. +/// Fixed header prefix for compressed payload blobs. /// public readonly struct CompressedPayloadHeader { public const int Size = 16; /// - /// Compression codec used for payload bytes. + /// Compression codec used for payload bytes. /// public CompressionCodec Codec { get; } /// - /// Original uncompressed payload length. + /// Original uncompressed payload length. /// public int OriginalLength { get; } /// - /// Compressed payload length. + /// Compressed payload length. /// public int CompressedLength { get; } /// - /// CRC32 checksum of compressed payload bytes. + /// CRC32 checksum of compressed payload bytes. /// public uint Checksum { get; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Compression codec used for payload bytes. /// Original uncompressed payload length. @@ -50,19 +50,20 @@ public readonly struct CompressedPayloadHeader } /// - /// Create. + /// Create. /// /// Compression codec used for payload bytes. /// Original uncompressed payload length. /// Compressed payload bytes. - public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength, ReadOnlySpan compressedPayload) + public static CompressedPayloadHeader Create(CompressionCodec codec, int originalLength, + ReadOnlySpan compressedPayload) { - var checksum = ComputeChecksum(compressedPayload); + uint checksum = ComputeChecksum(compressedPayload); return new CompressedPayloadHeader(codec, originalLength, compressedPayload.Length, checksum); } /// - /// Write To. + /// Write To. /// /// Destination span that receives the serialized header. public void WriteTo(Span destination) @@ -80,7 +81,7 @@ public readonly struct CompressedPayloadHeader } /// - /// Read From. + /// Read From. /// /// Source span containing a serialized header. public static CompressedPayloadHeader ReadFrom(ReadOnlySpan source) @@ -89,14 +90,14 @@ public readonly struct CompressedPayloadHeader throw new ArgumentException($"Source must be at least {Size} bytes.", nameof(source)); var codec = (CompressionCodec)source[0]; - var originalLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(4, 4)); - var compressedLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(8, 4)); - var checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(12, 4)); + int originalLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(4, 4)); + int compressedLength = BinaryPrimitives.ReadInt32LittleEndian(source.Slice(8, 4)); + uint checksum = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(12, 4)); return new CompressedPayloadHeader(codec, originalLength, compressedLength, checksum); } /// - /// Validate Checksum. + /// Validate Checksum. /// /// Compressed payload bytes to validate. public bool ValidateChecksum(ReadOnlySpan compressedPayload) @@ -105,10 +106,13 @@ public readonly struct CompressedPayloadHeader } /// - /// Compute Checksum. + /// Compute Checksum. /// /// Payload bytes. - public static uint ComputeChecksum(ReadOnlySpan payload) => Crc32Calculator.Compute(payload); + public static uint ComputeChecksum(ReadOnlySpan payload) + { + return Crc32Calculator.Compute(payload); + } private static class Crc32Calculator { @@ -116,15 +120,15 @@ public readonly struct CompressedPayloadHeader private static readonly uint[] Table = CreateTable(); /// - /// Compute. + /// Compute. /// /// Payload bytes. public static uint Compute(ReadOnlySpan payload) { - uint crc = 0xFFFFFFFFu; - for (int i = 0; i < payload.Length; i++) + var crc = 0xFFFFFFFFu; + for (var i = 0; i < payload.Length; i++) { - var index = (crc ^ payload[i]) & 0xFF; + uint index = (crc ^ payload[i]) & 0xFF; crc = (crc >> 8) ^ Table[index]; } @@ -137,10 +141,7 @@ public readonly struct CompressedPayloadHeader for (uint i = 0; i < table.Length; i++) { uint value = i; - for (int bit = 0; bit < 8; bit++) - { - value = (value & 1) != 0 ? (value >> 1) ^ Polynomial : value >> 1; - } + for (var bit = 0; bit < 8; bit++) value = (value & 1) != 0 ? (value >> 1) ^ Polynomial : value >> 1; table[i] = value; } @@ -148,4 +149,4 @@ public readonly struct CompressedPayloadHeader return table; } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/CompressionCodec.cs b/src/CBDD.Core/Compression/CompressionCodec.cs index 7797a03..ba80eea 100644 --- a/src/CBDD.Core/Compression/CompressionCodec.cs +++ b/src/CBDD.Core/Compression/CompressionCodec.cs @@ -1,11 +1,11 @@ namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Supported payload compression codecs. +/// Supported payload compression codecs. /// public enum CompressionCodec : byte { None = 0, Brotli = 1, Deflate = 2 -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/CompressionOptions.cs b/src/CBDD.Core/Compression/CompressionOptions.cs index 2294322..0a2bd6b 100644 --- a/src/CBDD.Core/Compression/CompressionOptions.cs +++ b/src/CBDD.Core/Compression/CompressionOptions.cs @@ -3,52 +3,52 @@ using System.IO.Compression; namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Compression configuration for document payload processing. +/// Compression configuration for document payload processing. /// public sealed class CompressionOptions { /// - /// Default compression options (compression disabled). + /// Default compression options (compression disabled). /// public static CompressionOptions Default { get; } = new(); /// - /// Enables payload compression for new writes. + /// Enables payload compression for new writes. /// public bool EnableCompression { get; init; } = false; /// - /// Minimum payload size (bytes) required before compression is attempted. + /// Minimum payload size (bytes) required before compression is attempted. /// public int MinSizeBytes { get; init; } = 1024; /// - /// Minimum percentage of size reduction required to keep compressed output. + /// Minimum percentage of size reduction required to keep compressed output. /// public int MinSavingsPercent { get; init; } = 10; /// - /// Preferred default codec for new writes. + /// Preferred default codec for new writes. /// public CompressionCodec Codec { get; init; } = CompressionCodec.Brotli; /// - /// Compression level passed to codec implementations. + /// Compression level passed to codec implementations. /// public CompressionLevel Level { get; init; } = CompressionLevel.Fastest; /// - /// Maximum allowed decompressed payload size. + /// Maximum allowed decompressed payload size. /// public int MaxDecompressedSizeBytes { get; init; } = 16 * 1024 * 1024; /// - /// Optional maximum input size allowed for compression attempts. + /// Optional maximum input size allowed for compression attempts. /// public int? MaxCompressionInputBytes { get; init; } /// - /// Normalizes and validates compression options. + /// Normalizes and validates compression options. /// /// Optional user-provided options. internal static CompressionOptions Normalize(CompressionOptions? options) @@ -59,17 +59,20 @@ public sealed class CompressionOptions throw new ArgumentOutOfRangeException(nameof(MinSizeBytes), "MinSizeBytes must be non-negative."); if (candidate.MinSavingsPercent is < 0 or > 100) - throw new ArgumentOutOfRangeException(nameof(MinSavingsPercent), "MinSavingsPercent must be between 0 and 100."); + throw new ArgumentOutOfRangeException(nameof(MinSavingsPercent), + "MinSavingsPercent must be between 0 and 100."); if (!Enum.IsDefined(candidate.Codec)) throw new ArgumentOutOfRangeException(nameof(Codec), $"Unsupported codec: {candidate.Codec}."); if (candidate.MaxDecompressedSizeBytes <= 0) - throw new ArgumentOutOfRangeException(nameof(MaxDecompressedSizeBytes), "MaxDecompressedSizeBytes must be greater than 0."); + throw new ArgumentOutOfRangeException(nameof(MaxDecompressedSizeBytes), + "MaxDecompressedSizeBytes must be greater than 0."); if (candidate.MaxCompressionInputBytes is <= 0) - throw new ArgumentOutOfRangeException(nameof(MaxCompressionInputBytes), "MaxCompressionInputBytes must be greater than 0 when provided."); + throw new ArgumentOutOfRangeException(nameof(MaxCompressionInputBytes), + "MaxCompressionInputBytes must be greater than 0 when provided."); return candidate; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/CompressionService.cs b/src/CBDD.Core/Compression/CompressionService.cs index 152463d..f6126e3 100644 --- a/src/CBDD.Core/Compression/CompressionService.cs +++ b/src/CBDD.Core/Compression/CompressionService.cs @@ -5,14 +5,14 @@ using System.IO.Compression; namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Compression codec registry and utility service. +/// Compression codec registry and utility service. /// public sealed class CompressionService { private readonly ConcurrentDictionary _codecs = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Optional additional codecs to register. public CompressionService(IEnumerable? additionalCodecs = null) @@ -24,14 +24,11 @@ public sealed class CompressionService if (additionalCodecs == null) return; - foreach (var codec in additionalCodecs) - { - RegisterCodec(codec); - } + foreach (var codec in additionalCodecs) RegisterCodec(codec); } /// - /// Registers or replaces a compression codec implementation. + /// Registers or replaces a compression codec implementation. /// /// The codec implementation to register. public void RegisterCodec(ICompressionCodec codec) @@ -41,18 +38,21 @@ public sealed class CompressionService } /// - /// Attempts to resolve a registered codec implementation. + /// Attempts to resolve a registered codec implementation. /// /// The codec identifier to resolve. /// When this method returns, contains the resolved codec when found. - /// when a codec is registered for ; otherwise, . + /// + /// when a codec is registered for ; otherwise, + /// . + /// public bool TryGetCodec(CompressionCodec codec, out ICompressionCodec compressionCodec) { return _codecs.TryGetValue(codec, out compressionCodec!); } /// - /// Gets a registered codec implementation. + /// Gets a registered codec implementation. /// /// The codec identifier to resolve. /// The registered codec implementation. @@ -65,7 +65,7 @@ public sealed class CompressionService } /// - /// Compresses payload bytes using the selected codec and level. + /// Compresses payload bytes using the selected codec and level. /// /// The payload bytes to compress. /// The codec to use. @@ -77,131 +77,40 @@ public sealed class CompressionService } /// - /// Decompresses payload bytes using the selected codec. + /// Decompresses payload bytes using the selected codec. /// /// The compressed payload bytes. /// The codec to use. - /// The expected decompressed byte length, or a negative value to skip exact-length validation. + /// + /// The expected decompressed byte length, or a negative value to skip exact-length + /// validation. + /// /// The maximum allowed decompressed byte length. /// The decompressed payload bytes. - public byte[] Decompress(ReadOnlySpan input, CompressionCodec codec, int expectedLength, int maxDecompressedSizeBytes) + public byte[] Decompress(ReadOnlySpan input, CompressionCodec codec, int expectedLength, + int maxDecompressedSizeBytes) { return GetCodec(codec).Decompress(input, expectedLength, maxDecompressedSizeBytes); } /// - /// Compresses and then decompresses payload bytes using the selected codec. + /// Compresses and then decompresses payload bytes using the selected codec. /// /// The payload bytes to roundtrip. /// The codec to use. /// The compression level. /// The maximum allowed decompressed byte length. /// The decompressed payload bytes after roundtrip. - public byte[] Roundtrip(ReadOnlySpan input, CompressionCodec codec, CompressionLevel level, int maxDecompressedSizeBytes) + public byte[] Roundtrip(ReadOnlySpan input, CompressionCodec codec, CompressionLevel level, + int maxDecompressedSizeBytes) { - var compressed = Compress(input, codec, level); + byte[] compressed = Compress(input, codec, level); return Decompress(compressed, codec, input.Length, maxDecompressedSizeBytes); } - private sealed class NoneCompressionCodec : ICompressionCodec - { - /// - /// Gets the codec identifier. - /// - public CompressionCodec Codec => CompressionCodec.None; - - /// - /// Returns a copy of the input payload without compression. - /// - /// The payload bytes to copy. - /// The requested compression level. - /// The copied payload bytes. - public byte[] Compress(ReadOnlySpan input, CompressionLevel level) => input.ToArray(); - - /// - /// Validates and returns an uncompressed payload copy. - /// - /// The payload bytes to validate and copy. - /// The expected payload length, or a negative value to skip exact-length validation. - /// The maximum allowed payload size in bytes. - /// The copied payload bytes. - public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) - { - if (input.Length > maxDecompressedSizeBytes) - throw new InvalidDataException($"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes)."); - - if (expectedLength >= 0 && expectedLength != input.Length) - throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {input.Length}."); - - return input.ToArray(); - } - } - - private sealed class BrotliCompressionCodec : ICompressionCodec - { - /// - /// Gets the codec identifier. - /// - public CompressionCodec Codec => CompressionCodec.Brotli; - - /// - /// Compresses payload bytes using Brotli. - /// - /// The payload bytes to compress. - /// The compression level. - /// The compressed payload bytes. - public byte[] Compress(ReadOnlySpan input, CompressionLevel level) - { - return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, leaveOpen: true)); - } - - /// - /// Decompresses Brotli-compressed payload bytes. - /// - /// The compressed payload bytes. - /// The expected decompressed byte length, or a negative value to skip exact-length validation. - /// The maximum allowed decompressed byte length. - /// The decompressed payload bytes. - public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) - { - return DecompressWithCodecStream(input, stream => new BrotliStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes); - } - } - - private sealed class DeflateCompressionCodec : ICompressionCodec - { - /// - /// Gets the codec identifier. - /// - public CompressionCodec Codec => CompressionCodec.Deflate; - - /// - /// Compresses payload bytes using Deflate. - /// - /// The payload bytes to compress. - /// The compression level. - /// The compressed payload bytes. - public byte[] Compress(ReadOnlySpan input, CompressionLevel level) - { - return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, leaveOpen: true)); - } - - /// - /// Decompresses Deflate-compressed payload bytes. - /// - /// The compressed payload bytes. - /// The expected decompressed byte length, or a negative value to skip exact-length validation. - /// The maximum allowed decompressed byte length. - /// The decompressed payload bytes. - public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) - { - return DecompressWithCodecStream(input, stream => new DeflateStream(stream, CompressionMode.Decompress, leaveOpen: true), expectedLength, maxDecompressedSizeBytes); - } - } - private static byte[] CompressWithCodecStream(ReadOnlySpan input, Func streamFactory) { - using var output = new MemoryStream(capacity: input.Length); + using var output = new MemoryStream(input.Length); using (var codecStream = streamFactory(output)) { codecStream.Write(input); @@ -220,31 +129,33 @@ public sealed class CompressionService if (maxDecompressedSizeBytes <= 0) throw new ArgumentOutOfRangeException(nameof(maxDecompressedSizeBytes)); - using var compressed = new MemoryStream(input.ToArray(), writable: false); + using var compressed = new MemoryStream(input.ToArray(), false); using var codecStream = streamFactory(compressed); using var output = expectedLength > 0 - ? new MemoryStream(capacity: expectedLength) + ? new MemoryStream(expectedLength) : new MemoryStream(); - var buffer = ArrayPool.Shared.Rent(8192); + byte[] buffer = ArrayPool.Shared.Rent(8192); try { - int totalWritten = 0; + var totalWritten = 0; while (true) { - var bytesRead = codecStream.Read(buffer, 0, buffer.Length); + int bytesRead = codecStream.Read(buffer, 0, buffer.Length); if (bytesRead <= 0) break; totalWritten += bytesRead; if (totalWritten > maxDecompressedSizeBytes) - throw new InvalidDataException($"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes)."); + throw new InvalidDataException( + $"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes)."); output.Write(buffer, 0, bytesRead); } if (expectedLength >= 0 && totalWritten != expectedLength) - throw new InvalidDataException($"Expected decompressed length {expectedLength}, actual {totalWritten}."); + throw new InvalidDataException( + $"Expected decompressed length {expectedLength}, actual {totalWritten}."); return output.ToArray(); } @@ -253,4 +164,115 @@ public sealed class CompressionService ArrayPool.Shared.Return(buffer); } } -} + + private sealed class NoneCompressionCodec : ICompressionCodec + { + /// + /// Gets the codec identifier. + /// + public CompressionCodec Codec => CompressionCodec.None; + + /// + /// Returns a copy of the input payload without compression. + /// + /// The payload bytes to copy. + /// The requested compression level. + /// The copied payload bytes. + public byte[] Compress(ReadOnlySpan input, CompressionLevel level) + { + return input.ToArray(); + } + + /// + /// Validates and returns an uncompressed payload copy. + /// + /// The payload bytes to validate and copy. + /// The expected payload length, or a negative value to skip exact-length validation. + /// The maximum allowed payload size in bytes. + /// The copied payload bytes. + public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) + { + if (input.Length > maxDecompressedSizeBytes) + throw new InvalidDataException( + $"Decompressed payload exceeds max allowed size ({maxDecompressedSizeBytes} bytes)."); + + if (expectedLength >= 0 && expectedLength != input.Length) + throw new InvalidDataException( + $"Expected decompressed length {expectedLength}, actual {input.Length}."); + + return input.ToArray(); + } + } + + private sealed class BrotliCompressionCodec : ICompressionCodec + { + /// + /// Gets the codec identifier. + /// + public CompressionCodec Codec => CompressionCodec.Brotli; + + /// + /// Compresses payload bytes using Brotli. + /// + /// The payload bytes to compress. + /// The compression level. + /// The compressed payload bytes. + public byte[] Compress(ReadOnlySpan input, CompressionLevel level) + { + return CompressWithCodecStream(input, stream => new BrotliStream(stream, level, true)); + } + + /// + /// Decompresses Brotli-compressed payload bytes. + /// + /// The compressed payload bytes. + /// + /// The expected decompressed byte length, or a negative value to skip exact-length + /// validation. + /// + /// The maximum allowed decompressed byte length. + /// The decompressed payload bytes. + public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) + { + return DecompressWithCodecStream(input, + stream => new BrotliStream(stream, CompressionMode.Decompress, true), expectedLength, + maxDecompressedSizeBytes); + } + } + + private sealed class DeflateCompressionCodec : ICompressionCodec + { + /// + /// Gets the codec identifier. + /// + public CompressionCodec Codec => CompressionCodec.Deflate; + + /// + /// Compresses payload bytes using Deflate. + /// + /// The payload bytes to compress. + /// The compression level. + /// The compressed payload bytes. + public byte[] Compress(ReadOnlySpan input, CompressionLevel level) + { + return CompressWithCodecStream(input, stream => new DeflateStream(stream, level, true)); + } + + /// + /// Decompresses Deflate-compressed payload bytes. + /// + /// The compressed payload bytes. + /// + /// The expected decompressed byte length, or a negative value to skip exact-length + /// validation. + /// + /// The maximum allowed decompressed byte length. + /// The decompressed payload bytes. + public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) + { + return DecompressWithCodecStream(input, + stream => new DeflateStream(stream, CompressionMode.Decompress, true), expectedLength, + maxDecompressedSizeBytes); + } + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/CompressionStats.cs b/src/CBDD.Core/Compression/CompressionStats.cs index 9f5c76e..42ed837 100644 --- a/src/CBDD.Core/Compression/CompressionStats.cs +++ b/src/CBDD.Core/Compression/CompressionStats.cs @@ -1,40 +1,47 @@ namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Snapshot of aggregated compression and decompression telemetry. +/// Snapshot of aggregated compression and decompression telemetry. /// public readonly struct CompressionStats { /// - /// Gets or sets the CompressedDocumentCount. + /// Gets or sets the CompressedDocumentCount. /// public long CompressedDocumentCount { get; init; } + /// - /// Gets or sets the BytesBeforeCompression. + /// Gets or sets the BytesBeforeCompression. /// public long BytesBeforeCompression { get; init; } + /// - /// Gets or sets the BytesAfterCompression. + /// Gets or sets the BytesAfterCompression. /// public long BytesAfterCompression { get; init; } + /// - /// Gets or sets the CompressionCpuTicks. + /// Gets or sets the CompressionCpuTicks. /// public long CompressionCpuTicks { get; init; } + /// - /// Gets or sets the DecompressionCpuTicks. + /// Gets or sets the DecompressionCpuTicks. /// public long DecompressionCpuTicks { get; init; } + /// - /// Gets or sets the CompressionFailureCount. + /// Gets or sets the CompressionFailureCount. /// public long CompressionFailureCount { get; init; } + /// - /// Gets or sets the ChecksumFailureCount. + /// Gets or sets the ChecksumFailureCount. /// public long ChecksumFailureCount { get; init; } + /// - /// Gets or sets the SafetyLimitRejectionCount. + /// Gets or sets the SafetyLimitRejectionCount. /// public long SafetyLimitRejectionCount { get; init; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/CompressionTelemetry.cs b/src/CBDD.Core/Compression/CompressionTelemetry.cs index e146a6b..4ab304e 100644 --- a/src/CBDD.Core/Compression/CompressionTelemetry.cs +++ b/src/CBDD.Core/Compression/CompressionTelemetry.cs @@ -1,111 +1,109 @@ -using System.Threading; - namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Thread-safe counters for compression/decompression lifecycle events. +/// Thread-safe counters for compression/decompression lifecycle events. /// public sealed class CompressionTelemetry { + private long _checksumFailureCount; + private long _compressedDocumentCount; private long _compressionAttempts; - private long _compressionSuccesses; + private long _compressionCpuTicks; private long _compressionFailures; - private long _compressionSkippedTooSmall; - private long _compressionSkippedInsufficientSavings; - private long _decompressionAttempts; - private long _decompressionSuccesses; - private long _decompressionFailures; private long _compressionInputBytes; private long _compressionOutputBytes; - private long _decompressionOutputBytes; - private long _compressedDocumentCount; - private long _compressionCpuTicks; + private long _compressionSkippedInsufficientSavings; + private long _compressionSkippedTooSmall; + private long _compressionSuccesses; + private long _decompressionAttempts; private long _decompressionCpuTicks; - private long _checksumFailureCount; + private long _decompressionFailures; + private long _decompressionOutputBytes; + private long _decompressionSuccesses; private long _safetyLimitRejectionCount; /// - /// Gets the number of attempted compression operations. + /// Gets the number of attempted compression operations. /// public long CompressionAttempts => Interlocked.Read(ref _compressionAttempts); /// - /// Gets the number of successful compression operations. + /// Gets the number of successful compression operations. /// public long CompressionSuccesses => Interlocked.Read(ref _compressionSuccesses); /// - /// Gets the number of failed compression operations. + /// Gets the number of failed compression operations. /// public long CompressionFailures => Interlocked.Read(ref _compressionFailures); /// - /// Gets the number of compression attempts skipped because payloads were too small. + /// Gets the number of compression attempts skipped because payloads were too small. /// public long CompressionSkippedTooSmall => Interlocked.Read(ref _compressionSkippedTooSmall); /// - /// Gets the number of compression attempts skipped due to insufficient savings. + /// Gets the number of compression attempts skipped due to insufficient savings. /// public long CompressionSkippedInsufficientSavings => Interlocked.Read(ref _compressionSkippedInsufficientSavings); /// - /// Gets the number of attempted decompression operations. + /// Gets the number of attempted decompression operations. /// public long DecompressionAttempts => Interlocked.Read(ref _decompressionAttempts); /// - /// Gets the number of successful decompression operations. + /// Gets the number of successful decompression operations. /// public long DecompressionSuccesses => Interlocked.Read(ref _decompressionSuccesses); /// - /// Gets the number of failed decompression operations. + /// Gets the number of failed decompression operations. /// public long DecompressionFailures => Interlocked.Read(ref _decompressionFailures); /// - /// Gets the total input bytes observed by compression attempts. + /// Gets the total input bytes observed by compression attempts. /// public long CompressionInputBytes => Interlocked.Read(ref _compressionInputBytes); /// - /// Gets the total output bytes produced by successful compression attempts. + /// Gets the total output bytes produced by successful compression attempts. /// public long CompressionOutputBytes => Interlocked.Read(ref _compressionOutputBytes); /// - /// Gets the total output bytes produced by successful decompression attempts. + /// Gets the total output bytes produced by successful decompression attempts. /// public long DecompressionOutputBytes => Interlocked.Read(ref _decompressionOutputBytes); /// - /// Gets the number of documents stored in compressed form. + /// Gets the number of documents stored in compressed form. /// public long CompressedDocumentCount => Interlocked.Read(ref _compressedDocumentCount); /// - /// Gets the total CPU ticks spent on compression. + /// Gets the total CPU ticks spent on compression. /// public long CompressionCpuTicks => Interlocked.Read(ref _compressionCpuTicks); /// - /// Gets the total CPU ticks spent on decompression. + /// Gets the total CPU ticks spent on decompression. /// public long DecompressionCpuTicks => Interlocked.Read(ref _decompressionCpuTicks); /// - /// Gets the number of checksum validation failures. + /// Gets the number of checksum validation failures. /// public long ChecksumFailureCount => Interlocked.Read(ref _checksumFailureCount); /// - /// Gets the number of decompression safety-limit rejections. + /// Gets the number of decompression safety-limit rejections. /// public long SafetyLimitRejectionCount => Interlocked.Read(ref _safetyLimitRejectionCount); /// - /// Records a compression attempt and its input byte size. + /// Records a compression attempt and its input byte size. /// /// The number of input bytes provided to compression. public void RecordCompressionAttempt(int inputBytes) @@ -115,7 +113,7 @@ public sealed class CompressionTelemetry } /// - /// Records a successful compression operation. + /// Records a successful compression operation. /// /// The number of compressed bytes produced. public void RecordCompressionSuccess(int outputBytes) @@ -126,49 +124,73 @@ public sealed class CompressionTelemetry } /// - /// Records a failed compression operation. + /// Records a failed compression operation. /// - public void RecordCompressionFailure() => Interlocked.Increment(ref _compressionFailures); + public void RecordCompressionFailure() + { + Interlocked.Increment(ref _compressionFailures); + } /// - /// Records that compression was skipped because the payload was too small. + /// Records that compression was skipped because the payload was too small. /// - public void RecordCompressionSkippedTooSmall() => Interlocked.Increment(ref _compressionSkippedTooSmall); + public void RecordCompressionSkippedTooSmall() + { + Interlocked.Increment(ref _compressionSkippedTooSmall); + } /// - /// Records that compression was skipped due to insufficient expected savings. + /// Records that compression was skipped due to insufficient expected savings. /// - public void RecordCompressionSkippedInsufficientSavings() => Interlocked.Increment(ref _compressionSkippedInsufficientSavings); + public void RecordCompressionSkippedInsufficientSavings() + { + Interlocked.Increment(ref _compressionSkippedInsufficientSavings); + } /// - /// Records a decompression attempt. + /// Records a decompression attempt. /// - public void RecordDecompressionAttempt() => Interlocked.Increment(ref _decompressionAttempts); + public void RecordDecompressionAttempt() + { + Interlocked.Increment(ref _decompressionAttempts); + } /// - /// Adds CPU ticks spent performing compression. + /// Adds CPU ticks spent performing compression. /// /// The CPU ticks to add. - public void RecordCompressionCpuTicks(long ticks) => Interlocked.Add(ref _compressionCpuTicks, ticks); + public void RecordCompressionCpuTicks(long ticks) + { + Interlocked.Add(ref _compressionCpuTicks, ticks); + } /// - /// Adds CPU ticks spent performing decompression. + /// Adds CPU ticks spent performing decompression. /// /// The CPU ticks to add. - public void RecordDecompressionCpuTicks(long ticks) => Interlocked.Add(ref _decompressionCpuTicks, ticks); + public void RecordDecompressionCpuTicks(long ticks) + { + Interlocked.Add(ref _decompressionCpuTicks, ticks); + } /// - /// Records a checksum validation failure. + /// Records a checksum validation failure. /// - public void RecordChecksumFailure() => Interlocked.Increment(ref _checksumFailureCount); + public void RecordChecksumFailure() + { + Interlocked.Increment(ref _checksumFailureCount); + } /// - /// Records a decompression rejection due to safety limits. + /// Records a decompression rejection due to safety limits. /// - public void RecordSafetyLimitRejection() => Interlocked.Increment(ref _safetyLimitRejectionCount); + public void RecordSafetyLimitRejection() + { + Interlocked.Increment(ref _safetyLimitRejectionCount); + } /// - /// Records a successful decompression operation. + /// Records a successful decompression operation. /// /// The number of decompressed bytes produced. public void RecordDecompressionSuccess(int outputBytes) @@ -178,12 +200,15 @@ public sealed class CompressionTelemetry } /// - /// Records a failed decompression operation. + /// Records a failed decompression operation. /// - public void RecordDecompressionFailure() => Interlocked.Increment(ref _decompressionFailures); + public void RecordDecompressionFailure() + { + Interlocked.Increment(ref _decompressionFailures); + } /// - /// Returns a point-in-time snapshot of compression telemetry. + /// Returns a point-in-time snapshot of compression telemetry. /// /// The aggregated compression statistics. public CompressionStats GetSnapshot() @@ -200,4 +225,4 @@ public sealed class CompressionTelemetry SafetyLimitRejectionCount = SafetyLimitRejectionCount }; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Compression/ICompressionCodec.cs b/src/CBDD.Core/Compression/ICompressionCodec.cs index 70f0479..3ef8b5d 100644 --- a/src/CBDD.Core/Compression/ICompressionCodec.cs +++ b/src/CBDD.Core/Compression/ICompressionCodec.cs @@ -3,27 +3,27 @@ using System.IO.Compression; namespace ZB.MOM.WW.CBDD.Core.Compression; /// -/// Codec abstraction for payload compression and decompression. +/// Codec abstraction for payload compression and decompression. /// public interface ICompressionCodec { /// - /// Codec identifier. + /// Codec identifier. /// CompressionCodec Codec { get; } /// - /// Compresses input bytes. + /// Compresses input bytes. /// /// Input payload bytes to compress. /// Compression level to apply. byte[] Compress(ReadOnlySpan input, CompressionLevel level); /// - /// Decompresses payload bytes with output bounds validation. + /// Decompresses payload bytes with output bounds validation. /// /// Input payload bytes to decompress. /// Expected decompressed length. /// Maximum allowed decompressed payload size in bytes. byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes); -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Context/DocumentDbContext.cs b/src/CBDD.Core/Context/DocumentDbContext.cs index 4cbbefa..7d88cb8 100755 --- a/src/CBDD.Core/Context/DocumentDbContext.cs +++ b/src/CBDD.Core/Context/DocumentDbContext.cs @@ -1,52 +1,39 @@ +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.CDC; using ZB.MOM.WW.CBDD.Core.Collections; +using ZB.MOM.WW.CBDD.Core.Compression; +using ZB.MOM.WW.CBDD.Core.Metadata; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; -using ZB.MOM.WW.CBDD.Core.Metadata; -using ZB.MOM.WW.CBDD.Core.Compression; -using System.Threading; -using System; -using System.Collections.Generic; -using System.Threading.Tasks; -using ZB.MOM.WW.CBDD.Bson; namespace ZB.MOM.WW.CBDD.Core; internal interface ICompactionAwareCollection { /// - /// Refreshes index bindings after compaction. + /// Refreshes index bindings after compaction. /// void RefreshIndexBindingsAfterCompaction(); } /// -/// Base class for database contexts. -/// Inherit and add DocumentCollection{T} properties for your entities. -/// Use partial class for Source Generator integration. +/// Base class for database contexts. +/// Inherit and add DocumentCollection{T} properties for your entities. +/// Use partial class for Source Generator integration. /// -public abstract partial class DocumentDbContext : IDisposable, ITransactionHolder +public abstract class DocumentDbContext : IDisposable, ITransactionHolder { + internal readonly ChangeStreamDispatcher _cdc; + private readonly List _compactionAwareCollections = new(); + + private readonly IReadOnlyDictionary _model; + private readonly List _registeredMappers = new(); private readonly IStorageEngine _storage; - internal readonly CDC.ChangeStreamDispatcher _cdc; + private readonly SemaphoreSlim _transactionLock = new(1, 1); protected bool _disposed; - private readonly SemaphoreSlim _transactionLock = new SemaphoreSlim(1, 1); /// - /// Gets the current active transaction, if any. - /// - public ITransaction? CurrentTransaction - { - get - { - if (_disposed) - throw new ObjectDisposedException(nameof(DocumentDbContext)); - return field != null && (field.State == TransactionState.Active) ? field : null; - } - private set; - } - - /// - /// Creates a new database context with default configuration + /// Creates a new database context with default configuration /// /// The database file path. protected DocumentDbContext(string databasePath) @@ -55,7 +42,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Creates a new database context with default storage configuration and custom compression settings. + /// Creates a new database context with default storage configuration and custom compression settings. /// /// The database file path. /// Compression behavior options. @@ -65,7 +52,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Creates a new database context with custom configuration + /// Creates a new database context with custom configuration /// /// The database file path. /// The page file configuration. @@ -75,7 +62,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Creates a new database context with custom storage and compression configuration. + /// Creates a new database context with custom storage and compression configuration. /// /// The database file path. /// The page file configuration. @@ -91,7 +78,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde throw new ArgumentNullException(nameof(databasePath)); _storage = new StorageEngine(databasePath, config, compressionOptions, maintenanceOptions); - _cdc = new CDC.ChangeStreamDispatcher(); + _cdc = new ChangeStreamDispatcher(); _storage.RegisterCdc(_cdc); // Initialize model before collections @@ -102,108 +89,41 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Initializes document collections for the context. + /// Gets the current active transaction, if any. /// - protected virtual void InitializeCollections() + public ITransaction? CurrentTransaction { - // Derived classes can override to initialize collections + get + { + if (_disposed) + throw new ObjectDisposedException(nameof(DocumentDbContext)); + return field != null && field.State == TransactionState.Active ? field : null; + } + private set; } - private readonly IReadOnlyDictionary _model; - private readonly List _registeredMappers = new(); - private readonly List _compactionAwareCollections = new(); - /// - /// Gets the concrete storage engine for advanced scenarios in derived contexts. + /// Gets the concrete storage engine for advanced scenarios in derived contexts. /// protected StorageEngine Engine => (StorageEngine)_storage; /// - /// Gets compression options bound to this context's storage engine. + /// Gets compression options bound to this context's storage engine. /// protected CompressionOptions CompressionOptions => _storage.CompressionOptions; /// - /// Gets the compression service for codec operations. + /// Gets the compression service for codec operations. /// protected CompressionService CompressionService => _storage.CompressionService; /// - /// Gets compression telemetry counters. + /// Gets compression telemetry counters. /// protected CompressionTelemetry CompressionTelemetry => _storage.CompressionTelemetry; /// - /// Override to configure the model using Fluent API. - /// - /// The model builder instance. - protected virtual void OnModelCreating(ModelBuilder modelBuilder) - { - } - - /// - /// Helper to create a DocumentCollection instance with custom TId. - /// Used by derived classes in InitializeCollections for typed primary keys. - /// - /// The document identifier type. - /// The document type. - /// The mapper used for document serialization and key access. - /// The created document collection. - protected DocumentCollection CreateCollection(IDocumentMapper mapper) - where T : class - { - if (_disposed) - throw new ObjectDisposedException(nameof(DocumentDbContext)); - - string? customName = null; - EntityTypeBuilder? builder = null; - - if (_model.TryGetValue(typeof(T), out var builderObj)) - { - builder = builderObj as EntityTypeBuilder; - customName = builder?.CollectionName; - } - - _registeredMappers.Add(mapper); - var collection = new DocumentCollection(_storage, this, mapper, customName); - if (collection is ICompactionAwareCollection compactionAwareCollection) - { - _compactionAwareCollections.Add(compactionAwareCollection); - } - - // Apply configurations from ModelBuilder - if (builder != null) - { - foreach (var indexBuilder in builder.Indexes) - { - collection.ApplyIndexBuilder(indexBuilder); - } - } - - _storage.RegisterMappers(_registeredMappers); - - return collection; - } - - /// - /// Gets the document collection for the specified entity type using an ObjectId as the key. - /// - /// The type of entity to retrieve the document collection for. Must be a reference type. - /// A DocumentCollection<ObjectId, T> instance for the specified entity type. - public DocumentCollection Set() where T : class => Set(); - - /// - /// Gets a collection for managing documents of type T, identified by keys of type TId. - /// Override is generated automatically by the Source Generator for partial DbContext classes. - /// - /// The type of the unique identifier for documents in the collection. - /// The type of the document to be managed. Must be a reference type. - /// A DocumentCollection<TId, T> instance for performing operations on documents of type T. - public virtual DocumentCollection Set() where T : class - => throw new InvalidOperationException($"No collection registered for entity type '{typeof(T).Name}' with key type '{typeof(TId).Name}'."); - - /// - /// Releases resources used by the context. + /// Releases resources used by the context. /// public void Dispose() { @@ -220,7 +140,102 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Begins a transaction or returns the current active transaction. + /// Gets the current active transaction or starts a new one. + /// + /// The active transaction. + public ITransaction GetCurrentTransactionOrStart() + { + return BeginTransaction(); + } + + /// + /// Gets the current active transaction or starts a new one asynchronously. + /// + /// The active transaction. + public async Task GetCurrentTransactionOrStartAsync() + { + return await BeginTransactionAsync(); + } + + /// + /// Initializes document collections for the context. + /// + protected virtual void InitializeCollections() + { + // Derived classes can override to initialize collections + } + + /// + /// Override to configure the model using Fluent API. + /// + /// The model builder instance. + protected virtual void OnModelCreating(ModelBuilder modelBuilder) + { + } + + /// + /// Helper to create a DocumentCollection instance with custom TId. + /// Used by derived classes in InitializeCollections for typed primary keys. + /// + /// The document identifier type. + /// The document type. + /// The mapper used for document serialization and key access. + /// The created document collection. + protected DocumentCollection CreateCollection(IDocumentMapper mapper) + where T : class + { + if (_disposed) + throw new ObjectDisposedException(nameof(DocumentDbContext)); + + string? customName = null; + EntityTypeBuilder? builder = null; + + if (_model.TryGetValue(typeof(T), out object? builderObj)) + { + builder = builderObj as EntityTypeBuilder; + customName = builder?.CollectionName; + } + + _registeredMappers.Add(mapper); + var collection = new DocumentCollection(_storage, this, mapper, customName); + if (collection is ICompactionAwareCollection compactionAwareCollection) + _compactionAwareCollections.Add(compactionAwareCollection); + + // Apply configurations from ModelBuilder + if (builder != null) + foreach (var indexBuilder in builder.Indexes) + collection.ApplyIndexBuilder(indexBuilder); + + _storage.RegisterMappers(_registeredMappers); + + return collection; + } + + /// + /// Gets the document collection for the specified entity type using an ObjectId as the key. + /// + /// The type of entity to retrieve the document collection for. Must be a reference type. + /// A DocumentCollection<ObjectId, T> instance for the specified entity type. + public DocumentCollection Set() where T : class + { + return Set(); + } + + /// + /// Gets a collection for managing documents of type T, identified by keys of type TId. + /// Override is generated automatically by the Source Generator for partial DbContext classes. + /// + /// The type of the unique identifier for documents in the collection. + /// The type of the document to be managed. Must be a reference type. + /// A DocumentCollection<TId, T> instance for performing operations on documents of type T. + public virtual DocumentCollection Set() where T : class + { + throw new InvalidOperationException( + $"No collection registered for entity type '{typeof(T).Name}' with key type '{typeof(TId).Name}'."); + } + + /// + /// Begins a transaction or returns the current active transaction. /// /// The active transaction. public ITransaction BeginTransaction() @@ -243,7 +258,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Begins a transaction asynchronously or returns the current active transaction. + /// Begins a transaction asynchronously or returns the current active transaction. /// /// The cancellation token. /// The active transaction. @@ -252,7 +267,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde if (_disposed) throw new ObjectDisposedException(nameof(DocumentDbContext)); - bool lockAcquired = false; + var lockAcquired = false; try { await _transactionLock.WaitAsync(ct); @@ -271,32 +286,13 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Gets the current active transaction or starts a new one. - /// - /// The active transaction. - public ITransaction GetCurrentTransactionOrStart() - { - return BeginTransaction(); - } - - /// - /// Gets the current active transaction or starts a new one asynchronously. - /// - /// The active transaction. - public async Task GetCurrentTransactionOrStartAsync() - { - return await BeginTransactionAsync(); - } - - /// - /// Commits the current transaction if one is active. + /// Commits the current transaction if one is active. /// public void SaveChanges() { if (_disposed) throw new ObjectDisposedException(nameof(DocumentDbContext)); if (CurrentTransaction != null) - { try { CurrentTransaction.Commit(); @@ -305,19 +301,17 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde { CurrentTransaction = null; } - } } /// - /// Commits the current transaction asynchronously if one is active. + /// Commits the current transaction asynchronously if one is active. /// /// The cancellation token. - public async Task SaveChangesAsync(CancellationToken ct = default) - { - if (_disposed) - throw new ObjectDisposedException(nameof(DocumentDbContext)); + public async Task SaveChangesAsync(CancellationToken ct = default) + { + if (_disposed) + throw new ObjectDisposedException(nameof(DocumentDbContext)); if (CurrentTransaction != null) - { try { await CurrentTransaction.CommitAsync(ct); @@ -325,40 +319,40 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde finally { CurrentTransaction = null; - } - } - } - - /// - /// Executes a checkpoint using the requested mode. - /// - /// Checkpoint mode to execute. - /// The checkpoint execution result. - public CheckpointResult Checkpoint(CheckpointMode mode = CheckpointMode.Truncate) - { - if (_disposed) - throw new ObjectDisposedException(nameof(DocumentDbContext)); - - return Engine.Checkpoint(mode); - } - - /// - /// Executes a checkpoint asynchronously using the requested mode. - /// - /// Checkpoint mode to execute. - /// The cancellation token. - /// The checkpoint execution result. - public Task CheckpointAsync(CheckpointMode mode = CheckpointMode.Truncate, CancellationToken ct = default) - { - if (_disposed) - throw new ObjectDisposedException(nameof(DocumentDbContext)); - - return Engine.CheckpointAsync(mode, ct); - } - - /// - /// Returns a point-in-time snapshot of compression telemetry counters. - /// + } + } + + /// + /// Executes a checkpoint using the requested mode. + /// + /// Checkpoint mode to execute. + /// The checkpoint execution result. + public CheckpointResult Checkpoint(CheckpointMode mode = CheckpointMode.Truncate) + { + if (_disposed) + throw new ObjectDisposedException(nameof(DocumentDbContext)); + + return Engine.Checkpoint(mode); + } + + /// + /// Executes a checkpoint asynchronously using the requested mode. + /// + /// Checkpoint mode to execute. + /// The cancellation token. + /// The checkpoint execution result. + public Task CheckpointAsync(CheckpointMode mode = CheckpointMode.Truncate, + CancellationToken ct = default) + { + if (_disposed) + throw new ObjectDisposedException(nameof(DocumentDbContext)); + + return Engine.CheckpointAsync(mode, ct); + } + + /// + /// Returns a point-in-time snapshot of compression telemetry counters. + /// public CompressionStats GetCompressionStats() { if (_disposed) @@ -368,7 +362,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Runs offline compaction by default. Set options to online mode for a bounded online pass. + /// Runs offline compaction by default. Set options to online mode for a bounded online pass. /// /// Compaction execution options. public CompactionStats Compact(CompactionOptions? options = null) @@ -382,7 +376,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Runs offline compaction by default. Set options to online mode for a bounded online pass. + /// Runs offline compaction by default. Set options to online mode for a bounded online pass. /// /// Compaction execution options. /// Cancellation token for the asynchronous operation. @@ -395,7 +389,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Alias for . + /// Alias for . /// /// Compaction execution options. public CompactionStats Vacuum(CompactionOptions? options = null) @@ -409,7 +403,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Async alias for . + /// Async alias for . /// /// Compaction execution options. /// Cancellation token for the asynchronous operation. @@ -437,14 +431,11 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde private void RefreshCollectionBindingsAfterCompaction() { - foreach (var collection in _compactionAwareCollections) - { - collection.RefreshIndexBindingsAfterCompaction(); - } + foreach (var collection in _compactionAwareCollections) collection.RefreshIndexBindingsAfterCompaction(); } /// - /// Gets page usage grouped by page type. + /// Gets page usage grouped by page type. /// public IReadOnlyList GetPageUsageByPageType() { @@ -455,7 +446,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Gets per-collection page usage diagnostics. + /// Gets per-collection page usage diagnostics. /// public IReadOnlyList GetPageUsageByCollection() { @@ -466,7 +457,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Gets per-collection compression ratio diagnostics. + /// Gets per-collection compression ratio diagnostics. /// public IReadOnlyList GetCompressionRatioByCollection() { @@ -477,7 +468,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Gets free-list summary diagnostics. + /// Gets free-list summary diagnostics. /// public FreeListSummary GetFreeListSummary() { @@ -488,7 +479,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Gets page-level fragmentation diagnostics. + /// Gets page-level fragmentation diagnostics. /// public FragmentationMapReport GetFragmentationMap() { @@ -499,7 +490,7 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Runs compression migration as dry-run estimation by default. + /// Runs compression migration as dry-run estimation by default. /// /// Compression migration options. public CompressionMigrationResult MigrateCompression(CompressionMigrationOptions? options = null) @@ -511,15 +502,16 @@ public abstract partial class DocumentDbContext : IDisposable, ITransactionHolde } /// - /// Runs compression migration asynchronously as dry-run estimation by default. + /// Runs compression migration asynchronously as dry-run estimation by default. /// /// Compression migration options. /// Cancellation token for the asynchronous operation. - public Task MigrateCompressionAsync(CompressionMigrationOptions? options = null, CancellationToken ct = default) + public Task MigrateCompressionAsync(CompressionMigrationOptions? options = null, + CancellationToken ct = default) { if (_disposed) throw new ObjectDisposedException(nameof(DocumentDbContext)); return Engine.MigrateCompressionAsync(options, ct); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/BTreeCursor.cs b/src/CBDD.Core/Indexing/BTreeCursor.cs index bb5fab3..cbae47e 100755 --- a/src/CBDD.Core/Indexing/BTreeCursor.cs +++ b/src/CBDD.Core/Indexing/BTreeCursor.cs @@ -1,131 +1,129 @@ using System.Buffers; using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Bson; -using System.Collections.Generic; -using System; namespace ZB.MOM.WW.CBDD.Core.Indexing; -internal sealed class BTreeCursor : IBTreeCursor -{ - private readonly BTreeIndex _index; - private readonly ulong _transactionId; - private readonly IIndexStorage _storage; - - // State - private byte[] _pageBuffer; - private uint _currentPageId; +internal sealed class BTreeCursor : IBTreeCursor +{ + private readonly List _currentEntries; + private readonly BTreeIndex _index; + private readonly IIndexStorage _storage; + private readonly ulong _transactionId; private int _currentEntryIndex; private BTreeNodeHeader _currentHeader; - private List _currentEntries; + private uint _currentPageId; private bool _isValid; - /// - /// Initializes a new instance of the class. - /// - /// The index to traverse. - /// The storage engine for page access. - /// The transaction identifier used for reads. - public BTreeCursor(BTreeIndex index, IIndexStorage storage, ulong transactionId) - { - _index = index; - _storage = storage; + // State + private byte[] _pageBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The index to traverse. + /// The storage engine for page access. + /// The transaction identifier used for reads. + public BTreeCursor(BTreeIndex index, IIndexStorage storage, ulong transactionId) + { + _index = index; + _storage = storage; _transactionId = transactionId; _pageBuffer = ArrayPool.Shared.Rent(storage.PageSize); _currentEntries = new List(); - _isValid = false; - } - - /// - /// Gets the current index entry at the cursor position. - /// - public IndexEntry Current - { - get + _isValid = false; + } + + /// + /// Gets the current index entry at the cursor position. + /// + public IndexEntry Current + { + get { if (!_isValid) throw new InvalidOperationException("Cursor is not valid."); - return _currentEntries[_currentEntryIndex]; - } - } - - /// - /// Moves the cursor to the first entry in the index. - /// - /// if an entry is available; otherwise, . - public bool MoveToFirst() - { + return _currentEntries[_currentEntryIndex]; + } + } + + /// + /// Moves the cursor to the first entry in the index. + /// + /// if an entry is available; otherwise, . + public bool MoveToFirst() + { // Find left-most leaf - var pageId = _index.RootPageId; + uint pageId = _index.RootPageId; while (true) { LoadPage(pageId); - if (_currentHeader.IsLeaf) break; - - // Go to first child (P0) - // Internal node format: [Header] [P0] [Entry1] ... - var dataOffset = 32 + 20; + if (_currentHeader.IsLeaf) break; + + // Go to first child (P0) + // Internal node format: [Header] [P0] [Entry1] ... + int dataOffset = 32 + 20; pageId = BitConverter.ToUInt32(_pageBuffer.AsSpan(dataOffset, 4)); } - - return PositionAtStart(); - } - - /// - /// Moves the cursor to the last entry in the index. - /// - /// if an entry is available; otherwise, . - public bool MoveToLast() - { + + return PositionAtStart(); + } + + /// + /// Moves the cursor to the last entry in the index. + /// + /// if an entry is available; otherwise, . + public bool MoveToLast() + { // Find right-most leaf - var pageId = _index.RootPageId; + uint pageId = _index.RootPageId; while (true) { LoadPage(pageId); - if (_currentHeader.IsLeaf) break; - - // Go to last child (last pointer) - // Iterate all entries to find last pointer - // P0 is at 32+20 (4 bytes). Entry 0 starts at 32+20+4. - - // Wait, we need the last pointer. - // P0 is at offset. - // Then EncryCount entries: Key + Pointer. - // We want the last pointer. - - // Re-read P0 just in case - uint lastPointer = BitConverter.ToUInt32(_pageBuffer.AsSpan(32 + 20, 4)); + if (_currentHeader.IsLeaf) break; - var offset = 32 + 20 + 4; - for (int i = 0; i < _currentHeader.EntryCount; i++) + // Go to last child (last pointer) + // Iterate all entries to find last pointer + // P0 is at 32+20 (4 bytes). Entry 0 starts at 32+20+4. + + // Wait, we need the last pointer. + // P0 is at offset. + // Then EncryCount entries: Key + Pointer. + // We want the last pointer. + + // Re-read P0 just in case + var lastPointer = BitConverter.ToUInt32(_pageBuffer.AsSpan(32 + 20, 4)); + + int offset = 32 + 20 + 4; + for (var i = 0; i < _currentHeader.EntryCount; i++) { var keyLen = BitConverter.ToInt32(_pageBuffer.AsSpan(offset, 4)); offset += 4 + keyLen; lastPointer = BitConverter.ToUInt32(_pageBuffer.AsSpan(offset, 4)); offset += 4; } + pageId = lastPointer; } - - return PositionAtEnd(); - } - - /// - /// Seeks to the specified key or the next greater key. - /// - /// The key to seek. - /// - /// if an exact key match is found; otherwise, . - /// - public bool Seek(IndexKey key) - { + + return PositionAtEnd(); + } + + /// + /// Seeks to the specified key or the next greater key. + /// + /// The key to seek. + /// + /// if an exact key match is found; otherwise, . + /// + public bool Seek(IndexKey key) + { // Use Index to find leaf - var leafPageId = _index.FindLeafNode(key, _transactionId); + uint leafPageId = _index.FindLeafNode(key, _transactionId); LoadPage(leafPageId); ParseEntries(); // Binary search in entries - var idx = _currentEntries.BinarySearch(new IndexEntry(key, default(DocumentLocation))); - + int idx = _currentEntries.BinarySearch(new IndexEntry(key, default(DocumentLocation))); + if (idx >= 0) { // Found exact match @@ -133,51 +131,44 @@ internal sealed class BTreeCursor : IBTreeCursor _isValid = true; return true; } - else + + // Not found, ~idx is the next larger value + _currentEntryIndex = ~idx; + + if (_currentEntryIndex < _currentEntries.Count) { - // Not found, ~idx is the next larger value - _currentEntryIndex = ~idx; - - if (_currentEntryIndex < _currentEntries.Count) + _isValid = true; + return false; // Positioned at next greater + } + + // Key is larger than max in this page, move to next page + if (_currentHeader.NextLeafPageId != 0) + { + LoadPage(_currentHeader.NextLeafPageId); + ParseEntries(); + _currentEntryIndex = 0; + if (_currentEntries.Count > 0) { _isValid = true; - return false; // Positioned at next greater - } - else - { - // Key is larger than max in this page, move to next page - if (_currentHeader.NextLeafPageId != 0) - { - LoadPage(_currentHeader.NextLeafPageId); - ParseEntries(); - _currentEntryIndex = 0; - if (_currentEntries.Count > 0) - { - _isValid = true; - return false; - } - } - - // End of index - _isValid = false; return false; - } - } - } - - /// - /// Moves the cursor to the next entry. - /// - /// if the cursor moved to a valid entry; otherwise, . - public bool MoveNext() - { + } + } + + // End of index + _isValid = false; + return false; + } + + /// + /// Moves the cursor to the next entry. + /// + /// if the cursor moved to a valid entry; otherwise, . + public bool MoveNext() + { if (!_isValid) return false; _currentEntryIndex++; - if (_currentEntryIndex < _currentEntries.Count) - { - return true; - } + if (_currentEntryIndex < _currentEntries.Count) return true; // Move to next page if (_currentHeader.NextLeafPageId != 0) @@ -186,23 +177,20 @@ internal sealed class BTreeCursor : IBTreeCursor return PositionAtStart(); } - _isValid = false; - return false; - } - - /// - /// Moves the cursor to the previous entry. - /// - /// if the cursor moved to a valid entry; otherwise, . - public bool MovePrev() - { + _isValid = false; + return false; + } + + /// + /// Moves the cursor to the previous entry. + /// + /// if the cursor moved to a valid entry; otherwise, . + public bool MovePrev() + { if (!_isValid) return false; _currentEntryIndex--; - if (_currentEntryIndex >= 0) - { - return true; - } + if (_currentEntryIndex >= 0) return true; // Move to prev page if (_currentHeader.PrevLeafPageId != 0) @@ -211,9 +199,21 @@ internal sealed class BTreeCursor : IBTreeCursor return PositionAtEnd(); } - _isValid = false; - return false; - } + _isValid = false; + return false; + } + + /// + /// Releases cursor resources. + /// + public void Dispose() + { + if (_pageBuffer != null) + { + ArrayPool.Shared.Return(_pageBuffer); + _pageBuffer = null!; + } + } private void LoadPage(uint pageId) { @@ -229,9 +229,9 @@ internal sealed class BTreeCursor : IBTreeCursor // Helper to parse entries from current page buffer // (Similar to BTreeIndex.ReadLeafEntries) _currentEntries.Clear(); - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; - for (int i = 0; i < _currentHeader.EntryCount; i++) + for (var i = 0; i < _currentHeader.EntryCount; i++) { // Read Key var keyLen = BitConverter.ToInt32(_pageBuffer.AsSpan(dataOffset, 4)); @@ -257,12 +257,10 @@ internal sealed class BTreeCursor : IBTreeCursor _isValid = true; return true; } - else - { - // Empty page? Should not happen in helper logic unless root leaf is empty - _isValid = false; - return false; - } + + // Empty page? Should not happen in helper logic unless root leaf is empty + _isValid = false; + return false; } private bool PositionAtEnd() @@ -274,22 +272,8 @@ internal sealed class BTreeCursor : IBTreeCursor _isValid = true; return true; } - else - { - _isValid = false; - return false; - } - } - /// - /// Releases cursor resources. - /// - public void Dispose() - { - if (_pageBuffer != null) - { - ArrayPool.Shared.Return(_pageBuffer); - _pageBuffer = null!; - } + _isValid = false; + return false; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/BTreeIndex.cs b/src/CBDD.Core/Indexing/BTreeIndex.cs index 00db189..6413353 100755 --- a/src/CBDD.Core/Indexing/BTreeIndex.cs +++ b/src/CBDD.Core/Indexing/BTreeIndex.cs @@ -1,55 +1,52 @@ -using ZB.MOM.WW.CBDD.Bson; +using System.Buffers; +using System.Text.RegularExpressions; using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; -using System; -using System.Collections.Generic; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// B+Tree index implementation for ordered index operations. +/// B+Tree index implementation for ordered index operations. /// -public sealed class BTreeIndex -{ - private readonly IIndexStorage _storage; - private readonly IndexOptions _options; - private uint _rootPageId; +public sealed class BTreeIndex +{ internal const int MaxEntriesPerNode = 100; // Low value to test splitting + private readonly IndexOptions _options; + private readonly IIndexStorage _storage; - /// - /// Initializes a new instance of the class. - /// - /// The storage engine used to read and write index pages. - /// The index options. - /// The existing root page identifier, or 0 to create a new root. - public BTreeIndex(StorageEngine storage, - IndexOptions options, - uint rootPageId = 0) - : this((IStorageEngine)storage, options, rootPageId) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The index storage used to read and write index pages. - /// The index options. - /// The existing root page identifier, or 0 to create a new root. - internal BTreeIndex(IIndexStorage storage, - IndexOptions options, - uint rootPageId = 0) - { + /// + /// Initializes a new instance of the class. + /// + /// The storage engine used to read and write index pages. + /// The index options. + /// The existing root page identifier, or 0 to create a new root. + public BTreeIndex(StorageEngine storage, + IndexOptions options, + uint rootPageId = 0) + : this((IStorageEngine)storage, options, rootPageId) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The index storage used to read and write index pages. + /// The index options. + /// The existing root page identifier, or 0 to create a new root. + internal BTreeIndex(IIndexStorage storage, + IndexOptions options, + uint rootPageId = 0) + { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); _options = options; - _rootPageId = rootPageId; + RootPageId = rootPageId; - if (_rootPageId == 0) + if (RootPageId == 0) { // Allocate new root page (cannot use page 0 which is file header) - _rootPageId = _storage.AllocatePage(); + RootPageId = _storage.AllocatePage(); // Initialize as empty leaf - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { // Clear buffer @@ -58,7 +55,7 @@ public sealed class BTreeIndex // Write headers var pageHeader = new PageHeader { - PageId = _rootPageId, + PageId = RootPageId, PageType = PageType.Index, FreeBytes = (ushort)(_storage.PageSize - 32), NextPageId = 0, @@ -75,68 +72,68 @@ public sealed class BTreeIndex }; nodeHeader.WriteTo(pageBuffer.AsSpan(32)); - _storage.WritePageImmediate(_rootPageId, pageBuffer); + _storage.WritePageImmediate(RootPageId, pageBuffer); } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } } - /// - /// Gets the current root page identifier for the B+Tree. - /// - public uint RootPageId => _rootPageId; - - /// - /// Updates the in-memory root page identifier. - /// - /// The root page identifier to use for subsequent operations. - internal void SetRootPageId(uint rootPageId) - { - if (rootPageId == 0) - throw new ArgumentOutOfRangeException(nameof(rootPageId)); - - _rootPageId = rootPageId; - } + /// + /// Gets the current root page identifier for the B+Tree. + /// + public uint RootPageId { get; private set; } - /// - /// Reads a page using StorageEngine for transaction isolation. - /// Implements "Read Your Own Writes" isolation. - /// - /// The page identifier to read. - /// The transaction identifier used for isolation. - /// The destination span that receives page bytes. - internal void ReadPage(uint pageId, ulong transactionId, Span destination) + /// + /// Updates the in-memory root page identifier. + /// + /// The root page identifier to use for subsequent operations. + internal void SetRootPageId(uint rootPageId) + { + if (rootPageId == 0) + throw new ArgumentOutOfRangeException(nameof(rootPageId)); + + RootPageId = rootPageId; + } + + /// + /// Reads a page using StorageEngine for transaction isolation. + /// Implements "Read Your Own Writes" isolation. + /// + /// The page identifier to read. + /// The transaction identifier used for isolation. + /// The destination span that receives page bytes. + internal void ReadPage(uint pageId, ulong transactionId, Span destination) { _storage.ReadPage(pageId, transactionId, destination); } /// - /// Writes a page using StorageEngine for transaction isolation. + /// Writes a page using StorageEngine for transaction isolation. /// private void WritePage(uint pageId, ulong transactionId, ReadOnlySpan data) { _storage.WritePage(pageId, transactionId, data); } - /// - /// Inserts a key-location pair into the index - /// - /// The index key to insert. - /// The document location associated with the key. - /// The optional transaction identifier. - public void Insert(IndexKey key, DocumentLocation location, ulong? transactionId = null) + /// + /// Inserts a key-location pair into the index + /// + /// The index key to insert. + /// The document location associated with the key. + /// The optional transaction identifier. + public void Insert(IndexKey key, DocumentLocation location, ulong? transactionId = null) { - var txnId = transactionId ?? 0; + ulong txnId = transactionId ?? 0; var entry = new IndexEntry(key, location); var path = new List(); // Find the leaf node for insertion - var leafPageId = FindLeafNodeWithPath(key, path, txnId); + uint leafPageId = FindLeafNodeWithPath(key, path, txnId); - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { ReadPage(leafPageId, txnId, pageBuffer); @@ -158,38 +155,38 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } - /// - /// Finds a document location by exact key match - /// - /// The key to search for. - /// When this method returns, contains the matching document location if found. - /// The optional transaction identifier. - public bool TryFind(IndexKey key, out DocumentLocation location, ulong? transactionId = null) + /// + /// Finds a document location by exact key match + /// + /// The key to search for. + /// When this method returns, contains the matching document location if found. + /// The optional transaction identifier. + public bool TryFind(IndexKey key, out DocumentLocation location, ulong? transactionId = null) { location = default; - var txnId = transactionId ?? 0; + ulong txnId = transactionId ?? 0; - var leafPageId = FindLeafNode(key, txnId); + uint leafPageId = FindLeafNode(key, txnId); Span pageBuffer = stackalloc byte[_storage.PageSize]; ReadPage(leafPageId, txnId, pageBuffer); var header = BTreeNodeHeader.ReadFrom(pageBuffer[32..]); - var dataOffset = 32 + 20; // Page header + BTree node header + int dataOffset = 32 + 20; // Page header + BTree node header // Linear search in leaf (could be optimized with binary search) - for (int i = 0; i < header.EntryCount; i++) + for (var i = 0; i < header.EntryCount; i++) { var entryKey = ReadIndexKey(pageBuffer, dataOffset); if (entryKey.Equals(key)) { // Found - read DocumentLocation (6 bytes: 4 for PageId + 2 for SlotIndex) - var locationOffset = dataOffset + entryKey.Data.Length + 4; // +4 for key length prefix + int locationOffset = dataOffset + entryKey.Data.Length + 4; // +4 for key length prefix location = DocumentLocation.ReadFrom(pageBuffer.Slice(locationOffset, DocumentLocation.SerializedSize)); return true; } @@ -201,39 +198,42 @@ public sealed class BTreeIndex return false; } - /// - /// Range scan: finds all entries between minKey and maxKey (inclusive) - /// - /// The lower bound key. - /// The upper bound key. - /// The scan direction. - /// The optional transaction identifier. - public IEnumerable Range(IndexKey minKey, IndexKey maxKey, IndexDirection direction = IndexDirection.Forward, ulong? transactionId = null) + /// + /// Range scan: finds all entries between minKey and maxKey (inclusive) + /// + /// The lower bound key. + /// The upper bound key. + /// The scan direction. + /// The optional transaction identifier. + public IEnumerable Range(IndexKey minKey, IndexKey maxKey, + IndexDirection direction = IndexDirection.Forward, ulong? transactionId = null) { - var txnId = transactionId ?? 0; - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); - + ulong txnId = transactionId ?? 0; + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); + try { if (direction == IndexDirection.Forward) { - var leafPageId = FindLeafNode(minKey, txnId); + uint leafPageId = FindLeafNode(minKey, txnId); while (leafPageId != 0) { ReadPage(leafPageId, txnId, pageBuffer); var header = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); - var dataOffset = 32 + 20; // Adjusted for 20-byte header + int dataOffset = 32 + 20; // Adjusted for 20-byte header - for (int i = 0; i < header.EntryCount; i++) + for (var i = 0; i < header.EntryCount; i++) { var entryKey = ReadIndexKey(pageBuffer, dataOffset); if (entryKey >= minKey && entryKey <= maxKey) { - var locationOffset = dataOffset + 4 + entryKey.Data.Length; - var location = DocumentLocation.ReadFrom(pageBuffer.AsSpan(locationOffset, DocumentLocation.SerializedSize)); + int locationOffset = dataOffset + 4 + entryKey.Data.Length; + var location = + DocumentLocation.ReadFrom(pageBuffer.AsSpan(locationOffset, + DocumentLocation.SerializedSize)); yield return new IndexEntry(entryKey, location); } else if (entryKey > maxKey) @@ -250,45 +250,40 @@ public sealed class BTreeIndex else // Backward { // Start from the end of the range (maxKey) - var leafPageId = FindLeafNode(maxKey, txnId); + uint leafPageId = FindLeafNode(maxKey, txnId); while (leafPageId != 0) { ReadPage(leafPageId, txnId, pageBuffer); - var header = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); - - // Parse all entries in leaf first (since variable length, we have to scan forward to find offsets) - // Optimization: Could cache offsets or scan once. For now, read all entries then iterate in reverse. - var entries = ReadLeafEntries(pageBuffer, header.EntryCount); - - // Iterate valid entries in reverse order + var header = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); + + // Parse all entries in leaf first (since variable length, we have to scan forward to find offsets) + // Optimization: Could cache offsets or scan once. For now, read all entries then iterate in reverse. + var entries = ReadLeafEntries(pageBuffer, header.EntryCount); + + // Iterate valid entries in reverse order for (int i = entries.Count - 1; i >= 0; i--) { var entry = entries[i]; if (entry.Key <= maxKey && entry.Key >= minKey) - { yield return entry; - } - else if (entry.Key < minKey) - { - yield break; // Exceeded range (below min) - } - } - - // Check if we need to continue to previous leaf - // If the first entry in this page is still >= minKey, we might have more matches in PrevLeaf - // "Check previous page" logic... + else if (entry.Key < minKey) yield break; // Exceeded range (below min) + } + + // Check if we need to continue to previous leaf + // If the first entry in this page is still >= minKey, we might have more matches in PrevLeaf + // "Check previous page" logic... if (entries.Count > 0 && entries[0].Key >= minKey) { leafPageId = header.PrevLeafPageId; } else - { - // We found an entry < minKey (handled in loop break) OR page was empty (unlikely) + { + // We found an entry < minKey (handled in loop break) OR page was empty (unlikely) if (entries.Count > 0 && entries[0].Key < minKey) - yield break; - + yield break; + leafPageId = header.PrevLeafPageId; } } @@ -296,17 +291,17 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } - /// - /// Finds the leaf page that should contain the specified key. - /// - /// The key to locate. - /// The transaction identifier used for isolation. - /// The leaf page identifier. - internal uint FindLeafNode(IndexKey key, ulong transactionId) + /// + /// Finds the leaf page that should contain the specified key. + /// + /// The key to locate. + /// The transaction identifier used for isolation. + /// The leaf page identifier. + internal uint FindLeafNode(IndexKey key, ulong transactionId) { var path = new List(); return FindLeafNodeWithPath(key, path, transactionId); @@ -314,8 +309,8 @@ public sealed class BTreeIndex private uint FindLeafNodeWithPath(IndexKey key, List path, ulong transactionId) { - var currentPageId = _rootPageId; - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + uint currentPageId = RootPageId; + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { @@ -324,10 +319,7 @@ public sealed class BTreeIndex ReadPage(currentPageId, transactionId, pageBuffer); var header = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); - if (header.IsLeaf) - { - return currentPageId; - } + if (header.IsLeaf) return currentPageId; path.Add(currentPageId); currentPageId = FindChildNode(pageBuffer, header, key); @@ -335,7 +327,7 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } @@ -348,24 +340,21 @@ public sealed class BTreeIndex // [Entry 2: Key2, P2] // ... - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; var p0 = BitConverter.ToUInt32(nodeBuffer.Slice(dataOffset, 4)); dataOffset += 4; uint childPageId = p0; // Linear search for now (optimize to binary search later) - for (int i = 0; i < header.EntryCount; i++) + for (var i = 0; i < header.EntryCount; i++) { var entryKey = ReadIndexKey(nodeBuffer, dataOffset); - var keyLen = 4 + entryKey.Data.Length; - var pointerOffset = dataOffset + keyLen; + int keyLen = 4 + entryKey.Data.Length; + int pointerOffset = dataOffset + keyLen; var nextPointer = BitConverter.ToUInt32(nodeBuffer.Slice(pointerOffset, 4)); - if (key < entryKey) - { - return childPageId; - } + if (key < entryKey) return childPageId; childPageId = nextPointer; dataOffset += keyLen + 4; // Key + Pointer @@ -374,69 +363,65 @@ public sealed class BTreeIndex return childPageId; // Return last pointer (>= last key) } - /// - /// Creates a cursor over this index. - /// - /// The transaction identifier used for isolation. - /// A cursor positioned on this index. - public IBTreeCursor CreateCursor(ulong transactionId) + /// + /// Creates a cursor over this index. + /// + /// The transaction identifier used for isolation. + /// A cursor positioned on this index. + public IBTreeCursor CreateCursor(ulong transactionId) { return new BTreeCursor(this, _storage, transactionId); } // --- Query Primitives --- - /// - /// Returns entries that exactly match the specified key. - /// - /// The key to match. - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable Equal(IndexKey key, ulong transactionId) + /// + /// Returns entries that exactly match the specified key. + /// + /// The key to match. + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable Equal(IndexKey key, ulong transactionId) { using var cursor = CreateCursor(transactionId); - if (cursor.Seek(key)) - { - yield return cursor.Current; - // Handle duplicates if we support them? Current impl looks unique-ish per key unless multi-value index. - // BTreeIndex doesn't strictly prevent duplicates in structure, but usually unique keys. - // If unique, yield one. If not, loop. - // Assuming unique for now based on TryFind. - } + if (cursor.Seek(key)) yield return cursor.Current; + // Handle duplicates if we support them? Current impl looks unique-ish per key unless multi-value index. + // BTreeIndex doesn't strictly prevent duplicates in structure, but usually unique keys. + // If unique, yield one. If not, loop. + // Assuming unique for now based on TryFind. } - /// - /// Returns entries greater than the specified key. - /// - /// The comparison key. - /// If true, includes entries equal to . - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable GreaterThan(IndexKey key, bool orEqual, ulong transactionId) + /// + /// Returns entries greater than the specified key. + /// + /// The comparison key. + /// If true, includes entries equal to . + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable GreaterThan(IndexKey key, bool orEqual, ulong transactionId) { using var cursor = CreateCursor(transactionId); - bool found = cursor.Seek(key); - + bool found = cursor.Seek(key); + if (found && !orEqual) - { - if (!cursor.MoveNext()) yield break; - } - - // Loop forward + if (!cursor.MoveNext()) + yield break; + + // Loop forward do - { + { yield return cursor.Current; } while (cursor.MoveNext()); } - /// - /// Returns entries less than the specified key. - /// - /// The comparison key. - /// If true, includes entries equal to . - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable LessThan(IndexKey key, bool orEqual, ulong transactionId) + /// + /// Returns entries less than the specified key. + /// + /// The comparison key. + /// If true, includes entries equal to . + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable LessThan(IndexKey key, bool orEqual, ulong transactionId) { using var cursor = CreateCursor(transactionId); bool found = cursor.Seek(key); @@ -446,40 +431,40 @@ public sealed class BTreeIndex if (!cursor.MovePrev()) yield break; } else if (!found) - { - // Seek landed on next greater (or invalid if end) - // We want < key. - // If Seek returns false, it is at Next Greater. - // So Current > Key. - // MovePrev to get < Key. - if (!cursor.MovePrev()) yield break; + { + // Seek landed on next greater (or invalid if end) + // We want < key. + // If Seek returns false, it is at Next Greater. + // So Current > Key. + // MovePrev to get < Key. + if (!cursor.MovePrev()) yield break; } // Loop backward do - { + { yield return cursor.Current; } while (cursor.MovePrev()); } - /// - /// Returns entries between the specified start and end keys. - /// - /// The start key. - /// The end key. - /// If true, includes entries equal to . - /// If true, includes entries equal to . - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable Between(IndexKey start, IndexKey end, bool startInclusive, bool endInclusive, ulong transactionId) + /// + /// Returns entries between the specified start and end keys. + /// + /// The start key. + /// The end key. + /// If true, includes entries equal to . + /// If true, includes entries equal to . + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable Between(IndexKey start, IndexKey end, bool startInclusive, bool endInclusive, + ulong transactionId) { using var cursor = CreateCursor(transactionId); bool found = cursor.Seek(start); if (found && !startInclusive) - { - if (!cursor.MoveNext()) yield break; - } + if (!cursor.MoveNext()) + yield break; // Iterate while <= end do @@ -489,72 +474,72 @@ public sealed class BTreeIndex if (current.Key == end && !endInclusive) yield break; yield return current; - } while (cursor.MoveNext()); } - /// - /// Returns string-key entries that start with the specified prefix. - /// - /// The prefix to match. - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable StartsWith(string prefix, ulong transactionId) + /// + /// Returns string-key entries that start with the specified prefix. + /// + /// The prefix to match. + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable StartsWith(string prefix, ulong transactionId) { var startKey = IndexKey.Create(prefix); using var cursor = CreateCursor(transactionId); - cursor.Seek(startKey); - + cursor.Seek(startKey); + do { var current = cursor.Current; string val; - try { val = current.Key.As(); } - catch { break; } - - if (!val.StartsWith(prefix)) break; - - yield return current; + try + { + val = current.Key.As(); + } + catch + { + break; + } + if (!val.StartsWith(prefix)) break; + + yield return current; } while (cursor.MoveNext()); } - /// - /// Returns entries for keys that exist in the provided key set. - /// - /// The keys to match. - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable In(IEnumerable keys, ulong transactionId) + /// + /// Returns entries for keys that exist in the provided key set. + /// + /// The keys to match. + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable In(IEnumerable keys, ulong transactionId) { var sortedKeys = keys.OrderBy(k => k); using var cursor = CreateCursor(transactionId); foreach (var key in sortedKeys) - { if (cursor.Seek(key)) - { yield return cursor.Current; - } - } - } - - /// - /// Returns string-key entries that match a SQL-like pattern. - /// - /// The pattern to evaluate where '%' matches many characters and '_' matches one character. - /// The transaction identifier used for isolation. - /// An enumerable sequence of matching entries. - public IEnumerable Like(string pattern, ulong transactionId) + } + + /// + /// Returns string-key entries that match a SQL-like pattern. + /// + /// The pattern to evaluate where '%' matches many characters and '_' matches one character. + /// The transaction identifier used for isolation. + /// An enumerable sequence of matching entries. + public IEnumerable Like(string pattern, ulong transactionId) { - string regexPattern = "^" + System.Text.RegularExpressions.Regex.Escape(pattern) + string regexPattern = "^" + Regex.Escape(pattern) .Replace("%", ".*") - .Replace("_", ".") + "$"; - - var regex = new System.Text.RegularExpressions.Regex(regexPattern, System.Text.RegularExpressions.RegexOptions.Compiled); - - string prefix = ""; - for (int i = 0; i < pattern.Length; i++) + .Replace("_", ".") + "$"; + + var regex = new Regex(regexPattern, RegexOptions.Compiled); + + var prefix = ""; + for (var i = 0; i < pattern.Length; i++) { if (pattern[i] == '%' || pattern[i] == '_') break; prefix += pattern[i]; @@ -563,41 +548,44 @@ public sealed class BTreeIndex using var cursor = CreateCursor(transactionId); if (!string.IsNullOrEmpty(prefix)) - { cursor.Seek(IndexKey.Create(prefix)); - } else - { cursor.MoveToFirst(); - } - do + do { IndexEntry current; - try { current = cursor.Current; } catch { break; } // Safe break if cursor invalid - - if (!string.IsNullOrEmpty(prefix)) + try { - try - { - string val = current.Key.As(); + current = cursor.Current; + } + catch + { + break; + } // Safe break if cursor invalid + + if (!string.IsNullOrEmpty(prefix)) + try + { + var val = current.Key.As(); if (!val.StartsWith(prefix)) break; } - catch { break; } - } - - bool match = false; + catch + { + break; + } + + var match = false; try { match = regex.IsMatch(current.Key.As()); } - catch - { - // Ignore mismatch types - } - - if (match) yield return current; + catch + { + // Ignore mismatch types + } + if (match) yield return current; } while (cursor.MoveNext()); } @@ -605,10 +593,10 @@ public sealed class BTreeIndex { // Read current entries to determine offset var header = BTreeNodeHeader.ReadFrom(pageBuffer[32..]); - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; // Skip existing entries to find free space - for (int i = 0; i < header.EntryCount; i++) + for (var i = 0; i < header.EntryCount; i++) { var keyLen = BitConverter.ToInt32(pageBuffer.Slice(dataOffset, 4)); dataOffset += 4 + keyLen + DocumentLocation.SerializedSize; // Length + Key + DocumentLocation @@ -635,37 +623,34 @@ public sealed class BTreeIndex private void SplitNode(uint nodePageId, List path, ulong transactionId) { - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { ReadPage(nodePageId, transactionId, pageBuffer); var header = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); if (header.IsLeaf) - { SplitLeafNode(nodePageId, header, pageBuffer, path, transactionId); - } else - { SplitInternalNode(nodePageId, header, pageBuffer, path, transactionId); - } } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } - private void SplitLeafNode(uint nodePageId, BTreeNodeHeader header, Span pageBuffer, List path, ulong transactionId) + private void SplitLeafNode(uint nodePageId, BTreeNodeHeader header, Span pageBuffer, List path, + ulong transactionId) { var entries = ReadLeafEntries(pageBuffer, header.EntryCount); - var splitPoint = entries.Count / 2; + int splitPoint = entries.Count / 2; var leftEntries = entries.Take(splitPoint).ToList(); var rightEntries = entries.Skip(splitPoint).ToList(); // Create new node for right half - var newNodeId = CreateNode(isLeaf: true, transactionId); + uint newNodeId = CreateNode(true, transactionId); // Update original node (left) // Next -> RightNode @@ -678,10 +663,7 @@ public sealed class BTreeIndex WriteLeafNode(newNodeId, rightEntries, header.NextLeafPageId, nodePageId, transactionId); // Update Original Next Node's Prev pointer to point to New Node - if (header.NextLeafPageId != 0) - { - UpdatePrevPointer(header.NextLeafPageId, newNodeId, transactionId); - } + if (header.NextLeafPageId != 0) UpdatePrevPointer(header.NextLeafPageId, newNodeId, transactionId); // Promote key to parent (first key of right node) var promoteKey = rightEntries[0].Key; @@ -690,7 +672,7 @@ public sealed class BTreeIndex private void UpdatePrevPointer(uint pageId, uint newPrevId, ulong transactionId) { - var buffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { ReadPage(pageId, transactionId, buffer); @@ -701,24 +683,27 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer); } } - private void SplitInternalNode(uint nodePageId, BTreeNodeHeader header, Span pageBuffer, List path, ulong transactionId) + private void SplitInternalNode(uint nodePageId, BTreeNodeHeader header, Span pageBuffer, List path, + ulong transactionId) { - var (p0, entries) = ReadInternalEntries(pageBuffer, header.EntryCount); - var splitPoint = entries.Count / 2; + (uint p0, var entries) = ReadInternalEntries(pageBuffer, header.EntryCount); + int splitPoint = entries.Count / 2; // For internal nodes, the median key moves UP to parent and is excluded from children var promoteKey = entries[splitPoint].Key; var leftEntries = entries.Take(splitPoint).ToList(); var rightEntries = entries.Skip(splitPoint + 1).ToList(); - var rightP0 = entries[splitPoint].PageId; // Attempting to use the pointer associated with promoted key as P0 for right node + uint rightP0 = + entries[splitPoint] + .PageId; // Attempting to use the pointer associated with promoted key as P0 for right node // Create new internal node - var newNodeId = CreateNode(isLeaf: false, transactionId); + uint newNodeId = CreateNode(false, transactionId); // Update left node WriteInternalNode(nodePageId, p0, leftEntries, transactionId); @@ -730,7 +715,8 @@ public sealed class BTreeIndex InsertIntoParent(nodePageId, promoteKey, newNodeId, path, transactionId); } - private void InsertIntoParent(uint leftChildPageId, IndexKey key, uint rightChildPageId, List path, ulong transactionId) + private void InsertIntoParent(uint leftChildPageId, IndexKey key, uint rightChildPageId, List path, + ulong transactionId) { if (path.Count == 0 || path.Last() == leftChildPageId) { @@ -747,10 +733,10 @@ public sealed class BTreeIndex } } - var parentPageId = path.Last(); + uint parentPageId = path.Last(); path.RemoveAt(path.Count - 1); // Pop parent for recursive calls - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { ReadPage(parentPageId, transactionId, pageBuffer); @@ -764,7 +750,8 @@ public sealed class BTreeIndex // But wait, to Split we need the median. // Better approach: Read all, add new entry, then split the collection and write back. - var (p0, entries) = ReadInternalEntries(pageBuffer.AsSpan(0, _storage.PageSize), header.EntryCount); + (uint p0, var entries) = + ReadInternalEntries(pageBuffer.AsSpan(0, _storage.PageSize), header.EntryCount); // Insert new key/pointer in sorted order var newEntry = new InternalEntry(key, rightChildPageId); @@ -773,14 +760,14 @@ public sealed class BTreeIndex else entries.Insert(insertIndex, newEntry); // Now split these extended entries - var splitPoint = entries.Count / 2; + int splitPoint = entries.Count / 2; var promoteKey = entries[splitPoint].Key; - var rightP0 = entries[splitPoint].PageId; + uint rightP0 = entries[splitPoint].PageId; var leftEntries = entries.Take(splitPoint).ToList(); var rightEntries = entries.Skip(splitPoint + 1).ToList(); - var newParentId = CreateNode(isLeaf: false, transactionId); + uint newParentId = CreateNode(false, transactionId); WriteInternalNode(parentPageId, p0, leftEntries, transactionId); WriteInternalNode(newParentId, rightP0, rightEntries, transactionId); @@ -790,21 +777,22 @@ public sealed class BTreeIndex else { // Insert directly - InsertIntoInternal(parentPageId, header, pageBuffer.AsSpan(0, _storage.PageSize), key, rightChildPageId, transactionId); + InsertIntoInternal(parentPageId, header, pageBuffer.AsSpan(0, _storage.PageSize), key, rightChildPageId, + transactionId); } } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } private void CreateNewRoot(uint leftChildId, IndexKey key, uint rightChildId, ulong transactionId) { - var newRootId = CreateNode(isLeaf: false, transactionId); - var entries = new List { new InternalEntry(key, rightChildId) }; + uint newRootId = CreateNode(false, transactionId); + var entries = new List { new(key, rightChildId) }; WriteInternalNode(newRootId, leftChildId, entries, transactionId); - _rootPageId = newRootId; // Update in-memory root + RootPageId = newRootId; // Update in-memory root // TODO: Update root in file header/metadata block so it persists? // For now user passes rootPageId to ctor. BTreeIndex doesn't manage master root pointer persistence yet. @@ -812,8 +800,8 @@ public sealed class BTreeIndex private uint CreateNode(bool isLeaf, ulong transactionId) { - var pageId = _storage.AllocatePage(); - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + uint pageId = _storage.AllocatePage(); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { Array.Clear(pageBuffer, 0, _storage.PageSize); @@ -846,7 +834,7 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } return pageId; @@ -855,42 +843,45 @@ public sealed class BTreeIndex private List ReadLeafEntries(Span pageBuffer, int count) { var entries = new List(count); - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { var key = ReadIndexKey(pageBuffer, dataOffset); - var locationOffset = dataOffset + 4 + key.Data.Length; + int locationOffset = dataOffset + 4 + key.Data.Length; var location = DocumentLocation.ReadFrom(pageBuffer.Slice(locationOffset, DocumentLocation.SerializedSize)); entries.Add(new IndexEntry(key, location)); dataOffset = locationOffset + DocumentLocation.SerializedSize; } + return entries; } private (uint P0, List Entries) ReadInternalEntries(Span pageBuffer, int count) { var entries = new List(count); - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; var p0 = BitConverter.ToUInt32(pageBuffer.Slice(dataOffset, 4)); dataOffset += 4; - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { var key = ReadIndexKey(pageBuffer, dataOffset); - var ptrOffset = dataOffset + 4 + key.Data.Length; + int ptrOffset = dataOffset + 4 + key.Data.Length; var pageId = BitConverter.ToUInt32(pageBuffer.Slice(ptrOffset, 4)); entries.Add(new InternalEntry(key, pageId)); dataOffset = ptrOffset + 4; } + return (p0, entries); } - private void WriteLeafNode(uint pageId, List entries, uint nextLeafId, uint prevLeafId, ulong? transactionId = null) + private void WriteLeafNode(uint pageId, List entries, uint nextLeafId, uint prevLeafId, + ulong? transactionId = null) { - var txnId = transactionId ?? 0; - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + ulong txnId = transactionId ?? 0; + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { Array.Clear(pageBuffer, 0, _storage.PageSize); @@ -919,7 +910,7 @@ public sealed class BTreeIndex nodeHeader.WriteTo(pageBuffer.AsSpan(32, 20)); // Write entries with DocumentLocation (6 bytes instead of ObjectId 12 bytes) - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; foreach (var entry in entries) { BitConverter.TryWriteBytes(pageBuffer.AsSpan(dataOffset, 4), entry.Key.Data.Length); @@ -933,13 +924,13 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } private void WriteInternalNode(uint pageId, uint p0, List entries, ulong transactionId) { - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { Array.Clear(pageBuffer, 0, _storage.PageSize); @@ -967,7 +958,7 @@ public sealed class BTreeIndex nodeHeader.WriteTo(pageBuffer.AsSpan(32, 20)); // Write P0 - var dataOffset = 32 + 20; + int dataOffset = 32 + 20; BitConverter.TryWriteBytes(pageBuffer.AsSpan(dataOffset, 4), p0); dataOffset += 4; @@ -985,14 +976,15 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } - private void InsertIntoInternal(uint pageId, BTreeNodeHeader header, Span pageBuffer, IndexKey key, uint rightChildId, ulong transactionId) + private void InsertIntoInternal(uint pageId, BTreeNodeHeader header, Span pageBuffer, IndexKey key, + uint rightChildId, ulong transactionId) { // Read, insert, write back. In production do in-place shift. - var (p0, entries) = ReadInternalEntries(pageBuffer, header.EntryCount); + (uint p0, var entries) = ReadInternalEntries(pageBuffer, header.EntryCount); var newEntry = new InternalEntry(key, rightChildId); int insertIndex = entries.FindIndex(e => e.Key > key); @@ -1017,19 +1009,19 @@ public sealed class BTreeIndex return new IndexKey(keyData); } - /// - /// Deletes a key-location pair from the index - /// - /// The key to delete. - /// The document location associated with the key. - /// The optional transaction identifier. - public bool Delete(IndexKey key, DocumentLocation location, ulong? transactionId = null) + /// + /// Deletes a key-location pair from the index + /// + /// The key to delete. + /// The document location associated with the key. + /// The optional transaction identifier. + public bool Delete(IndexKey key, DocumentLocation location, ulong? transactionId = null) { - var txnId = transactionId ?? 0; + ulong txnId = transactionId ?? 0; var path = new List(); - var leafPageId = FindLeafNodeWithPath(key, path, txnId); + uint leafPageId = FindLeafNodeWithPath(key, path, txnId); - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { ReadPage(leafPageId, txnId, pageBuffer); @@ -1037,14 +1029,11 @@ public sealed class BTreeIndex // Check if key exists in leaf var entries = ReadLeafEntries(pageBuffer, header.EntryCount); - var entryIndex = entries.FindIndex(e => e.Key.Equals(key) && - e.Location.PageId == location.PageId && - e.Location.SlotIndex == location.SlotIndex); + int entryIndex = entries.FindIndex(e => e.Key.Equals(key) && + e.Location.PageId == location.PageId && + e.Location.SlotIndex == location.SlotIndex); - if (entryIndex == -1) - { - return false; // Not found - } + if (entryIndex == -1) return false; // Not found // Remove entry entries.RemoveAt(entryIndex); @@ -1055,73 +1044,65 @@ public sealed class BTreeIndex // Check for underflow (min 50% fill) // Simplified: min 1 entry for now, or MaxEntries/2 int minEntries = MaxEntriesPerNode / 2; - if (entries.Count < minEntries && _rootPageId != leafPageId) - { - HandleUnderflow(leafPageId, path, txnId); - } + if (entries.Count < minEntries && RootPageId != leafPageId) HandleUnderflow(leafPageId, path, txnId); return true; } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } private void HandleUnderflow(uint nodeId, List path, ulong transactionId) { if (path.Count == 0) - { // Node is root - if (nodeId == _rootPageId) - { + if (nodeId == RootPageId) // Special case: Collapse root if it has only 1 child (and is not a leaf) // For now, simpliest implementation: do nothing for root underflow unless it's empty // If it's a leaf root, it can be empty. return; - } - } - var parentPageId = path[^1]; // Parent is last in path (before current node removed? No, path contains ancestors) - // Wait, FindLeafNodeWithPath adds ancestors. So path.Last() is not current node, it's parent. - // Let's verify FindLeafNodeWithPath: - // path.Add(currentPageId); currentPageId = FindChildNode(...); - // It adds PARENTS. It does NOT add the leaf itself. - - // Correct. - // So path.Last() is the parent. - - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + uint + parentPageId = + path[^1]; // Parent is last in path (before current node removed? No, path contains ancestors) + // Wait, FindLeafNodeWithPath adds ancestors. So path.Last() is not current node, it's parent. + // Let's verify FindLeafNodeWithPath: + // path.Add(currentPageId); currentPageId = FindChildNode(...); + // It adds PARENTS. It does NOT add the leaf itself. + + // Correct. + // So path.Last() is the parent. + + byte[] pageBuffer = ArrayPool.Shared.Rent(_storage.PageSize); try { ReadPage(parentPageId, transactionId, pageBuffer); var parentHeader = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); - var (p0, parentEntries) = ReadInternalEntries(pageBuffer, parentHeader.EntryCount); + (uint p0, var parentEntries) = ReadInternalEntries(pageBuffer, parentHeader.EntryCount); // Find index of current node in parent int childIndex = -1; if (p0 == nodeId) childIndex = -1; // -1 indicates P0 else - { childIndex = parentEntries.FindIndex(e => e.PageId == nodeId); - } // Try to borrow from siblings - if (BorrowFromSibling(nodeId, parentPageId, childIndex, parentEntries, p0, transactionId)) - { - return; // Rebalanced - } + if (BorrowFromSibling(nodeId, parentPageId, childIndex, parentEntries, p0, + transactionId)) return; // Rebalanced // Borrow failed, valid siblings are too small -> MERGE MergeWithSibling(nodeId, parentPageId, childIndex, parentEntries, p0, path, transactionId); } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } - private bool BorrowFromSibling(uint nodeId, uint parentId, int childIndex, List parentEntries, uint p0, ulong transactionId) + private bool BorrowFromSibling(uint nodeId, uint parentId, int childIndex, List parentEntries, + uint p0, ulong transactionId) { // TODO: Implement rotation (borrow from left or right sibling) // Complexity: High. Need to update Parent, Sibling, and Node. @@ -1130,7 +1111,8 @@ public sealed class BTreeIndex return false; } - private void MergeWithSibling(uint nodeId, uint parentId, int childIndex, List parentEntries, uint p0, List path, ulong transactionId) + private void MergeWithSibling(uint nodeId, uint parentId, int childIndex, List parentEntries, + uint p0, List path, ulong transactionId) { // Identify sibling to merge with. // If P0 (childIndex -1), merge with right sibling (Entry 0). @@ -1167,42 +1149,38 @@ public sealed class BTreeIndex // Remove separator key and right pointer from Parent if (childIndex == -1) - { parentEntries.RemoveAt(0); // Removing Entry 0 (Key 0, P1) - P1 was Right Node - // P0 remains P0 (which was Left Node) - } + // P0 remains P0 (which was Left Node) else - { parentEntries.RemoveAt(childIndex); // Remove entry pointing to Right Node - } // Write updated Parent WriteInternalNode(parentId, p0, parentEntries, transactionId); // Free the empty Right Node - _storage.FreePage(rightNodeId); // Need to verify this works safely with Txn logic? - // Actually, FreePage is immediate in current impl. Might need TransactionalFreePage. - // Or just leave it allocated but unused for now. - - // Recursive Underflow Check on Parent + _storage.FreePage(rightNodeId); // Need to verify this works safely with Txn logic? + // Actually, FreePage is immediate in current impl. Might need TransactionalFreePage. + // Or just leave it allocated but unused for now. + + // Recursive Underflow Check on Parent int minInternal = MaxEntriesPerNode / 2; - if (parentEntries.Count < minInternal && parentId != _rootPageId) + if (parentEntries.Count < minInternal && parentId != RootPageId) { var parentPath = new List(path.Take(path.Count - 1)); // Path to grandparent HandleUnderflow(parentId, parentPath, transactionId); } - else if (parentId == _rootPageId && parentEntries.Count == 0) + else if (parentId == RootPageId && parentEntries.Count == 0) { // Root collapse: Root has 0 entries (only P0). // P0 becomes new root. - _rootPageId = p0; // P0 is the merged node (LeftNode) + RootPageId = p0; // P0 is the merged node (LeftNode) // TODO: Update persistent root pointer if stored } } private void MergeNodes(uint leftNodeId, uint rightNodeId, IndexKey separatorKey, ulong transactionId) { - var buffer = System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); + byte[] buffer = ArrayPool.Shared.Rent(_storage.PageSize); try { // Read both nodes @@ -1218,7 +1196,9 @@ public sealed class BTreeIndex var leftEntries = ReadLeafEntries(buffer, leftHeader.EntryCount); ReadPage(rightNodeId, transactionId, buffer); - var rightEntries = ReadLeafEntries(buffer.AsSpan(0, _storage.PageSize), ((BTreeNodeHeader.ReadFrom(buffer.AsSpan(32))).EntryCount)); // Dirty read reuse buffer? No, bad hygiene. + var rightEntries = ReadLeafEntries(buffer.AsSpan(0, _storage.PageSize), + BTreeNodeHeader.ReadFrom(buffer.AsSpan(32)) + .EntryCount); // Dirty read reuse buffer? No, bad hygiene. // Re-read right clean var rightHeader = BTreeNodeHeader.ReadFrom(buffer.AsSpan(32)); rightEntries = ReadLeafEntries(buffer, rightHeader.EntryCount); @@ -1229,24 +1209,23 @@ public sealed class BTreeIndex // Update Left // Next -> Right.Next // Prev -> Left.Prev (unchanged) - WriteLeafNode(leftNodeId, leftEntries, rightHeader.NextLeafPageId, leftHeader.PrevLeafPageId, transactionId); + WriteLeafNode(leftNodeId, leftEntries, rightHeader.NextLeafPageId, leftHeader.PrevLeafPageId, + transactionId); // Update Right.Next's Prev pointer to point to Left (since Right is gone) if (rightHeader.NextLeafPageId != 0) - { UpdatePrevPointer(rightHeader.NextLeafPageId, leftNodeId, transactionId); - } } else { // Internal Node Merge ReadPage(leftNodeId, transactionId, buffer); // leftHeader is already read and valid - var (leftP0, leftEntries) = ReadInternalEntries(buffer, leftHeader.EntryCount); + (uint leftP0, var leftEntries) = ReadInternalEntries(buffer, leftHeader.EntryCount); ReadPage(rightNodeId, transactionId, buffer); var rightHeader = BTreeNodeHeader.ReadFrom(buffer.AsSpan(32)); - var (rightP0, rightEntries) = ReadInternalEntries(buffer, rightHeader.EntryCount); + (uint rightP0, var rightEntries) = ReadInternalEntries(buffer, rightHeader.EntryCount); // Add Separator Key (from parent) pointing to Right's P0 leftEntries.Add(new InternalEntry(separatorKey, rightP0)); @@ -1260,7 +1239,7 @@ public sealed class BTreeIndex } finally { - System.Buffers.ArrayPool.Shared.Return(buffer); + ArrayPool.Shared.Return(buffer); } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/BTreeStructures.cs b/src/CBDD.Core/Indexing/BTreeStructures.cs index fe1bb50..575861e 100755 --- a/src/CBDD.Core/Indexing/BTreeStructures.cs +++ b/src/CBDD.Core/Indexing/BTreeStructures.cs @@ -1,27 +1,26 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Storage; -using System; - -namespace ZB.MOM.WW.CBDD.Core.Indexing; - -/// -/// Represents an entry in an index mapping a key to a document location. -/// Implemented as struct for memory efficiency. -/// +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.Storage; + +namespace ZB.MOM.WW.CBDD.Core.Indexing; + +/// +/// Represents an entry in an index mapping a key to a document location. +/// Implemented as struct for memory efficiency. +/// public struct IndexEntry : IComparable, IComparable { /// - /// Gets or sets the index key. + /// Gets or sets the index key. /// public IndexKey Key { get; set; } /// - /// Gets or sets the document location for the key. + /// Gets or sets the document location for the key. /// public DocumentLocation Location { get; set; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The index key. /// The document location. @@ -34,7 +33,7 @@ public struct IndexEntry : IComparable, IComparable // Backward compatibility: constructor that takes ObjectId (for migration) // Will be removed once all code is migrated /// - /// Initializes a legacy instance of the struct for migration scenarios. + /// Initializes a legacy instance of the struct for migration scenarios. /// /// The index key. /// The legacy document identifier. @@ -47,12 +46,12 @@ public struct IndexEntry : IComparable, IComparable } /// - /// Compares this entry to another entry by key. + /// Compares this entry to another entry by key. /// /// The other index entry to compare. /// - /// A value less than zero if this instance is less than , - /// zero if they are equal, or greater than zero if this instance is greater. + /// A value less than zero if this instance is less than , + /// zero if they are equal, or greater than zero if this instance is greater. /// public int CompareTo(IndexEntry other) { @@ -60,76 +59,76 @@ public struct IndexEntry : IComparable, IComparable } /// - /// Compares this entry to another object. + /// Compares this entry to another object. /// /// The object to compare. /// - /// A value less than zero if this instance is less than , - /// zero if they are equal, or greater than zero if this instance is greater. + /// A value less than zero if this instance is less than , + /// zero if they are equal, or greater than zero if this instance is greater. /// - /// Thrown when is not an . + /// Thrown when is not an . public int CompareTo(object? obj) { if (obj is IndexEntry other) return CompareTo(other); throw new ArgumentException("Object is not an IndexEntry"); - } -} - -/// -/// B+Tree node for index storage. -/// Uses struct for node metadata to minimize allocations. -/// + } +} + +/// +/// B+Tree node for index storage. +/// Uses struct for node metadata to minimize allocations. +/// public struct BTreeNodeHeader { /// - /// Gets or sets the page identifier. + /// Gets or sets the page identifier. /// public uint PageId { get; set; } /// - /// Gets or sets a value indicating whether this node is a leaf node. + /// Gets or sets a value indicating whether this node is a leaf node. /// public bool IsLeaf { get; set; } /// - /// Gets or sets the number of entries in the node. + /// Gets or sets the number of entries in the node. /// public ushort EntryCount { get; set; } /// - /// Gets or sets the parent page identifier. + /// Gets or sets the parent page identifier. /// public uint ParentPageId { get; set; } /// - /// Gets or sets the next leaf page identifier. + /// Gets or sets the next leaf page identifier. /// - public uint NextLeafPageId { get; set; } // For leaf nodes only + public uint NextLeafPageId { get; set; } // For leaf nodes only /// - /// Gets or sets the previous leaf page identifier. + /// Gets or sets the previous leaf page identifier. /// - public uint PrevLeafPageId { get; set; } // For leaf nodes only (added for reverse scan) + public uint PrevLeafPageId { get; set; } // For leaf nodes only (added for reverse scan) /// - /// Writes the header to a byte span. + /// Writes the header to a byte span. /// /// The destination span. public void WriteTo(Span destination) { if (destination.Length < 20) throw new ArgumentException("Destination must be at least 20 bytes"); - - BitConverter.TryWriteBytes(destination[0..4], PageId); - destination[4] = (byte)(IsLeaf ? 1 : 0); - BitConverter.TryWriteBytes(destination[5..7], EntryCount); - BitConverter.TryWriteBytes(destination[7..11], ParentPageId); + + BitConverter.TryWriteBytes(destination[..4], PageId); + destination[4] = (byte)(IsLeaf ? 1 : 0); + BitConverter.TryWriteBytes(destination[5..7], EntryCount); + BitConverter.TryWriteBytes(destination[7..11], ParentPageId); BitConverter.TryWriteBytes(destination[11..15], NextLeafPageId); BitConverter.TryWriteBytes(destination[15..19], PrevLeafPageId); } /// - /// Reads a node header from a byte span. + /// Reads a node header from a byte span. /// /// The source span. /// The parsed node header. @@ -137,21 +136,18 @@ public struct BTreeNodeHeader { if (source.Length < 20) throw new ArgumentException("Source must be at least 16 bytes"); - - var header = new BTreeNodeHeader - { - PageId = BitConverter.ToUInt32(source[0..4]), - IsLeaf = source[4] != 0, - EntryCount = BitConverter.ToUInt16(source[5..7]), - ParentPageId = BitConverter.ToUInt32(source[7..11]), - NextLeafPageId = BitConverter.ToUInt32(source[11..15]) - }; - - if (source.Length >= 20) - { - header.PrevLeafPageId = BitConverter.ToUInt32(source[15..19]); - } - - return header; - } -} + + var header = new BTreeNodeHeader + { + PageId = BitConverter.ToUInt32(source[..4]), + IsLeaf = source[4] != 0, + EntryCount = BitConverter.ToUInt16(source[5..7]), + ParentPageId = BitConverter.ToUInt32(source[7..11]), + NextLeafPageId = BitConverter.ToUInt32(source[11..15]) + }; + + if (source.Length >= 20) header.PrevLeafPageId = BitConverter.ToUInt32(source[15..19]); + + return header; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/CollectionIndexDefinition.cs b/src/CBDD.Core/Indexing/CollectionIndexDefinition.cs index b971601..0dcc22d 100755 --- a/src/CBDD.Core/Indexing/CollectionIndexDefinition.cs +++ b/src/CBDD.Core/Indexing/CollectionIndexDefinition.cs @@ -1,71 +1,26 @@ -using System; using System.Linq.Expressions; -using ZB.MOM.WW.CBDD.Bson; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// High-level metadata and configuration for a custom index on a document collection. -/// Wraps low-level IndexOptions and provides strongly-typed expression-based key extraction. +/// High-level metadata and configuration for a custom index on a document collection. +/// Wraps low-level IndexOptions and provides strongly-typed expression-based key extraction. /// /// Document type public sealed class CollectionIndexDefinition where T : class { /// - /// Unique name for this index (auto-generated or user-specified) - /// - public string Name { get; } - - /// - /// Property paths that make up this index key. - /// Examples: ["Age"] for simple index, ["City", "Age"] for compound index - /// - public string[] PropertyPaths { get; } - - /// - /// If true, enforces uniqueness constraint on the indexed values - /// - public bool IsUnique { get; } - - /// - /// Type of index structure (from existing IndexType enum) - /// - public IndexType Type { get; } - - /// Vector dimensions (only for Vector index) - public int Dimensions { get; } - - /// Distance metric (only for Vector index) - public VectorMetric Metric { get; } - - /// - /// Compiled function to extract the index key from a document. - /// Compiled for maximum performance (10-100x faster than interpreting Expression). - /// - public Func KeySelector { get; } - - /// - /// Original expression for the key selector (for analysis and serialization) - /// - public Expression> KeySelectorExpression { get; } - - /// - /// If true, this is the primary key index (_id) - /// - public bool IsPrimary { get; } - - /// - /// Creates a new index definition + /// Creates a new index definition /// /// Index name /// Property paths for the index /// Expression to extract key from document - /// Enforce uniqueness - /// Index structure type (BTree or Hash) - /// Is this the primary key index - /// The vector dimensions for vector indexes. - /// The distance metric for vector indexes. - public CollectionIndexDefinition( + /// Enforce uniqueness + /// Index structure type (BTree or Hash) + /// Is this the primary key index + /// The vector dimensions for vector indexes. + /// The distance metric for vector indexes. + public CollectionIndexDefinition( string name, string[] propertyPaths, Expression> keySelectorExpression, @@ -76,11 +31,11 @@ public sealed class CollectionIndexDefinition where T : class VectorMetric metric = VectorMetric.Cosine) { if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentException("Index name cannot be empty", nameof(name)); - + throw new ArgumentException("Index name cannot be empty", nameof(name)); + if (propertyPaths == null || propertyPaths.Length == 0) - throw new ArgumentException("Property paths cannot be empty", nameof(propertyPaths)); - + throw new ArgumentException("Property paths cannot be empty", nameof(propertyPaths)); + Name = name; PropertyPaths = propertyPaths; KeySelectorExpression = keySelectorExpression ?? throw new ArgumentNullException(nameof(keySelectorExpression)); @@ -90,10 +45,53 @@ public sealed class CollectionIndexDefinition where T : class IsPrimary = isPrimary; Dimensions = dimensions; Metric = metric; - } - + } + /// - /// Converts this high-level definition to low-level IndexOptions for BTreeIndex + /// Unique name for this index (auto-generated or user-specified) + /// + public string Name { get; } + + /// + /// Property paths that make up this index key. + /// Examples: ["Age"] for simple index, ["City", "Age"] for compound index + /// + public string[] PropertyPaths { get; } + + /// + /// If true, enforces uniqueness constraint on the indexed values + /// + public bool IsUnique { get; } + + /// + /// Type of index structure (from existing IndexType enum) + /// + public IndexType Type { get; } + + /// Vector dimensions (only for Vector index) + public int Dimensions { get; } + + /// Distance metric (only for Vector index) + public VectorMetric Metric { get; } + + /// + /// Compiled function to extract the index key from a document. + /// Compiled for maximum performance (10-100x faster than interpreting Expression). + /// + public Func KeySelector { get; } + + /// + /// Original expression for the key selector (for analysis and serialization) + /// + public Expression> KeySelectorExpression { get; } + + /// + /// If true, this is the primary key index (_id) + /// + public bool IsPrimary { get; } + + /// + /// Converts this high-level definition to low-level IndexOptions for BTreeIndex /// public IndexOptions ToIndexOptions() { @@ -105,98 +103,97 @@ public sealed class CollectionIndexDefinition where T : class Dimensions = Dimensions, Metric = Metric }; - } - - /// - /// Checks if this index can be used for a query on the specified property path - /// - /// The property path to validate. - public bool CanSupportQuery(string propertyPath) + } + + /// + /// Checks if this index can be used for a query on the specified property path + /// + /// The property path to validate. + public bool CanSupportQuery(string propertyPath) { // Simple index: exact match required if (PropertyPaths.Length == 1) - return PropertyPaths[0].Equals(propertyPath, StringComparison.OrdinalIgnoreCase); - - // Compound index: can support if queried property is the first component - // e.g., index on ["City", "Age"] can support query on "City" but not just "Age" + return PropertyPaths[0].Equals(propertyPath, StringComparison.OrdinalIgnoreCase); + + // Compound index: can support if queried property is the first component + // e.g., index on ["City", "Age"] can support query on "City" but not just "Age" return PropertyPaths[0].Equals(propertyPath, StringComparison.OrdinalIgnoreCase); - } - - /// - /// Checks if this index can support queries on multiple properties (compound queries) - /// - /// The ordered property paths to validate. - public bool CanSupportCompoundQuery(string[] propertyPaths) + } + + /// + /// Checks if this index can support queries on multiple properties (compound queries) + /// + /// The ordered property paths to validate. + public bool CanSupportCompoundQuery(string[] propertyPaths) { if (propertyPaths == null || propertyPaths.Length == 0) - return false; - - // Check if queried paths are a prefix of this index - // e.g., index on ["City", "Age", "Name"] can support ["City"] or ["City", "Age"] + return false; + + // Check if queried paths are a prefix of this index + // e.g., index on ["City", "Age", "Name"] can support ["City"] or ["City", "Age"] if (propertyPaths.Length > PropertyPaths.Length) - return false; - - for (int i = 0; i < propertyPaths.Length; i++) - { + return false; + + for (var i = 0; i < propertyPaths.Length; i++) if (!PropertyPaths[i].Equals(propertyPaths[i], StringComparison.OrdinalIgnoreCase)) return false; - } - + return true; } - /// - public override string ToString() + /// + public override string ToString() { - var uniqueStr = IsUnique ? "Unique" : "Non-Unique"; - var paths = string.Join(", ", PropertyPaths); + string uniqueStr = IsUnique ? "Unique" : "Non-Unique"; + string paths = string.Join(", ", PropertyPaths); return $"{Name} ({uniqueStr} {Type} on [{paths}])"; } } /// -/// Information about an existing index (for querying index metadata) +/// Information about an existing index (for querying index metadata) /// -public sealed class CollectionIndexInfo -{ - /// - /// Gets the index name. - /// - public string Name { get; init; } = string.Empty; - - /// - /// Gets the indexed property paths. - /// - public string[] PropertyPaths { get; init; } = Array.Empty(); - - /// - /// Gets a value indicating whether the index is unique. - /// - public bool IsUnique { get; init; } - - /// - /// Gets the index type. - /// - public IndexType Type { get; init; } - - /// - /// Gets a value indicating whether this index is the primary index. - /// - public bool IsPrimary { get; init; } - - /// - /// Gets the estimated number of indexed documents. - /// - public long EstimatedDocumentCount { get; init; } - - /// - /// Gets the estimated storage size, in bytes. - /// - public long EstimatedSizeBytes { get; init; } - - /// - public override string ToString() +public sealed class CollectionIndexInfo +{ + /// + /// Gets the index name. + /// + public string Name { get; init; } = string.Empty; + + /// + /// Gets the indexed property paths. + /// + public string[] PropertyPaths { get; init; } = Array.Empty(); + + /// + /// Gets a value indicating whether the index is unique. + /// + public bool IsUnique { get; init; } + + /// + /// Gets the index type. + /// + public IndexType Type { get; init; } + + /// + /// Gets a value indicating whether this index is the primary index. + /// + public bool IsPrimary { get; init; } + + /// + /// Gets the estimated number of indexed documents. + /// + public long EstimatedDocumentCount { get; init; } + + /// + /// Gets the estimated storage size, in bytes. + /// + public long EstimatedSizeBytes { get; init; } + + /// + public override string ToString() { - return $"{Name}: {string.Join(", ", PropertyPaths)} ({EstimatedDocumentCount} docs, {EstimatedSizeBytes:N0} bytes)"; + return + $"{Name}: {string.Join(", ", PropertyPaths)} ({EstimatedDocumentCount} docs, {EstimatedSizeBytes:N0} bytes)"; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/CollectionIndexManager.cs b/src/CBDD.Core/Indexing/CollectionIndexManager.cs index cf3d1bf..de43705 100755 --- a/src/CBDD.Core/Indexing/CollectionIndexManager.cs +++ b/src/CBDD.Core/Indexing/CollectionIndexManager.cs @@ -1,7 +1,4 @@ -using System; -using System.Collections.Generic; using System.Linq.Expressions; -using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; @@ -9,57 +6,94 @@ using ZB.MOM.WW.CBDD.Core.Transactions; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// Manages a collection of secondary indexes on a document collection. -/// Handles index creation, deletion, automatic selection, and maintenance. +/// Manages a collection of secondary indexes on a document collection. +/// Handles index creation, deletion, automatic selection, and maintenance. /// /// Primary key type /// Document type -public sealed class CollectionIndexManager : IDisposable where T : class -{ - private readonly Dictionary> _indexes; - private readonly IStorageEngine _storage; - private readonly IDocumentMapper _mapper; +public sealed class CollectionIndexManager : IDisposable where T : class +{ + private readonly string _collectionName; + private readonly Dictionary> _indexes; private readonly object _lock = new(); + private readonly IDocumentMapper _mapper; + private readonly IStorageEngine _storage; private bool _disposed; - private readonly string _collectionName; - private CollectionMetadata _metadata; - - /// - /// Initializes a new instance of the class. - /// - /// The storage engine used to persist index data and metadata. - /// The document mapper for the collection type. - /// The optional collection name override. - public CollectionIndexManager(StorageEngine storage, IDocumentMapper mapper, string? collectionName = null) - : this((IStorageEngine)storage, mapper, collectionName) - { - } - - /// - /// Initializes a new instance of the class from the storage abstraction. - /// - /// The storage abstraction used to persist index state. - /// The document mapper for the collection. - /// An optional collection name override. - internal CollectionIndexManager(IStorageEngine storage, IDocumentMapper mapper, string? collectionName = null) - { + private CollectionMetadata _metadata; + + /// + /// Initializes a new instance of the class. + /// + /// The storage engine used to persist index data and metadata. + /// The document mapper for the collection type. + /// The optional collection name override. + public CollectionIndexManager(StorageEngine storage, IDocumentMapper mapper, string? collectionName = null) + : this((IStorageEngine)storage, mapper, collectionName) + { + } + + /// + /// Initializes a new instance of the class from the storage abstraction. + /// + /// The storage abstraction used to persist index state. + /// The document mapper for the collection. + /// An optional collection name override. + internal CollectionIndexManager(IStorageEngine storage, IDocumentMapper mapper, + string? collectionName = null) + { _storage = storage ?? throw new ArgumentNullException(nameof(storage)); _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); _collectionName = collectionName ?? _mapper.CollectionName; - _indexes = new Dictionary>(StringComparer.OrdinalIgnoreCase); - - // Load existing metadata via storage - _metadata = _storage.GetCollectionMetadata(_collectionName) ?? new CollectionMetadata { Name = _collectionName }; - - // Initialize indexes from metadata + _indexes = new Dictionary>(StringComparer.OrdinalIgnoreCase); + + // Load existing metadata via storage + _metadata = _storage.GetCollectionMetadata(_collectionName) ?? + new CollectionMetadata { Name = _collectionName }; + + // Initialize indexes from metadata foreach (var idxMeta in _metadata.Indexes) { - var definition = RebuildDefinition(idxMeta.Name, idxMeta.PropertyPaths, idxMeta.IsUnique, idxMeta.Type, idxMeta.Dimensions, idxMeta.Metric); + var definition = RebuildDefinition(idxMeta.Name, idxMeta.PropertyPaths, idxMeta.IsUnique, idxMeta.Type, + idxMeta.Dimensions, idxMeta.Metric); var index = new CollectionSecondaryIndex(definition, _storage, _mapper, idxMeta.RootPageId); _indexes[idxMeta.Name] = index; } } + /// + /// Gets the root page identifier for the primary index. + /// + public uint PrimaryRootPageId => _metadata.PrimaryRootPageId; + + /// + /// Releases resources used by the index manager. + /// + public void Dispose() + { + if (_disposed) + return; + + // No auto-save on dispose to avoid unnecessary I/O if no changes + + lock (_lock) + { + foreach (var index in _indexes.Values) + try + { + index.Dispose(); + } + catch + { + /* Best effort */ + } + + _indexes.Clear(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } + private void UpdateMetadata() { _metadata.Indexes.Clear(); @@ -80,7 +114,7 @@ public sealed class CollectionIndexManager : IDisposable where T : class } /// - /// Creates a new secondary index + /// Creates a new secondary index /// /// Index definition /// The created secondary index @@ -100,9 +134,9 @@ public sealed class CollectionIndexManager : IDisposable where T : class // Create secondary index var secondaryIndex = new CollectionSecondaryIndex(definition, _storage, _mapper); - _indexes[definition.Name] = secondaryIndex; - - // Persist metadata + _indexes[definition.Name] = secondaryIndex; + + // Persist metadata UpdateMetadata(); _storage.SaveCollectionMetadata(_metadata); @@ -113,7 +147,7 @@ public sealed class CollectionIndexManager : IDisposable where T : class // ... methods ... /// - /// Creates a simple index on a single property + /// Creates a simple index on a single property /// /// Key type /// Expression to extract key from document @@ -129,9 +163,9 @@ public sealed class CollectionIndexManager : IDisposable where T : class throw new ArgumentNullException(nameof(keySelector)); // Extract property paths from expression - var propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); - - // Generate name if not provided + string[] propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); + + // Generate name if not provided name ??= GenerateIndexName(propertyPaths); // Convert expression to object-returning expression (required for definition) @@ -149,52 +183,51 @@ public sealed class CollectionIndexManager : IDisposable where T : class return CreateIndex(definition); } - /// - /// Creates a vector index for a collection property. - /// - /// The selected key type. - /// Expression to extract the indexed field. - /// Vector dimensionality. - /// Distance metric used by the vector index. - /// Optional index name. - /// The created or existing index. - public CollectionSecondaryIndex CreateVectorIndex(Expression> keySelector, int dimensions, VectorMetric metric = VectorMetric.Cosine, string? name = null) - { - var propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); - var indexName = name ?? GenerateIndexName(propertyPaths); + /// + /// Creates a vector index for a collection property. + /// + /// The selected key type. + /// Expression to extract the indexed field. + /// Vector dimensionality. + /// Distance metric used by the vector index. + /// Optional index name. + /// The created or existing index. + public CollectionSecondaryIndex CreateVectorIndex(Expression> keySelector, + int dimensions, VectorMetric metric = VectorMetric.Cosine, string? name = null) + { + string[] propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); + string indexName = name ?? GenerateIndexName(propertyPaths); lock (_lock) { if (_indexes.TryGetValue(indexName, out var existing)) return existing; - var body = keySelector.Body; - if (body.Type != typeof(object)) - { - body = Expression.Convert(body, typeof(object)); - } - - // Reuse the original parameter from keySelector to avoid invalid expression trees. - var lambda = Expression.Lambda>(body, keySelector.Parameters); - - var definition = new CollectionIndexDefinition(indexName, propertyPaths, lambda, false, IndexType.Vector, false, dimensions, metric); - return CreateIndex(definition); - } - } + var body = keySelector.Body; + if (body.Type != typeof(object)) body = Expression.Convert(body, typeof(object)); - /// - /// Ensures that an index exists for the specified key selector. - /// - /// Expression to extract the indexed field. - /// Optional index name. - /// Whether the index enforces uniqueness. - /// The existing or newly created index. - public CollectionSecondaryIndex EnsureIndex( - Expression> keySelector, - string? name = null, - bool unique = false) + // Reuse the original parameter from keySelector to avoid invalid expression trees. + var lambda = Expression.Lambda>(body, keySelector.Parameters); + + var definition = new CollectionIndexDefinition(indexName, propertyPaths, lambda, false, IndexType.Vector, + false, dimensions, metric); + return CreateIndex(definition); + } + } + + /// + /// Ensures that an index exists for the specified key selector. + /// + /// Expression to extract the indexed field. + /// Optional index name. + /// Whether the index enforces uniqueness. + /// The existing or newly created index. + public CollectionSecondaryIndex EnsureIndex( + Expression> keySelector, + string? name = null, + bool unique = false) { - var propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); + string[] propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); name ??= GenerateIndexName(propertyPaths); lock (_lock) @@ -206,46 +239,43 @@ public sealed class CollectionIndexManager : IDisposable where T : class } } - /// - /// Ensures that an index exists for the specified untyped key selector. - /// - /// Untyped expression that selects indexed fields. - /// Optional index name. - /// Whether the index enforces uniqueness. - /// The existing or newly created index. - internal CollectionSecondaryIndex EnsureIndexUntyped( - LambdaExpression keySelector, - string? name = null, - bool unique = false) + /// + /// Ensures that an index exists for the specified untyped key selector. + /// + /// Untyped expression that selects indexed fields. + /// Optional index name. + /// Whether the index enforces uniqueness. + /// The existing or newly created index. + internal CollectionSecondaryIndex EnsureIndexUntyped( + LambdaExpression keySelector, + string? name = null, + bool unique = false) { // Convert LambdaExpression to Expression> properly by sharing parameters var body = keySelector.Body; - if (body.Type != typeof(object)) - { - body = Expression.Convert(body, typeof(object)); - } - + if (body.Type != typeof(object)) body = Expression.Convert(body, typeof(object)); + var lambda = Expression.Lambda>(body, keySelector.Parameters); return EnsureIndex(lambda, name, unique); } - /// - /// Creates a vector index from an untyped key selector. - /// - /// Untyped expression that selects indexed fields. - /// Vector dimensionality. - /// Distance metric used by the vector index. - /// Optional index name. - /// The created or existing index. - public CollectionSecondaryIndex CreateVectorIndexUntyped( - LambdaExpression keySelector, - int dimensions, - VectorMetric metric = VectorMetric.Cosine, - string? name = null) + /// + /// Creates a vector index from an untyped key selector. + /// + /// Untyped expression that selects indexed fields. + /// Vector dimensionality. + /// Distance metric used by the vector index. + /// Optional index name. + /// The created or existing index. + public CollectionSecondaryIndex CreateVectorIndexUntyped( + LambdaExpression keySelector, + int dimensions, + VectorMetric metric = VectorMetric.Cosine, + string? name = null) { - var propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); - var indexName = name ?? GenerateIndexName(propertyPaths); + string[] propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); + string indexName = name ?? GenerateIndexName(propertyPaths); lock (_lock) { @@ -253,51 +283,47 @@ public sealed class CollectionIndexManager : IDisposable where T : class return existing; var body = keySelector.Body; - if (body.Type != typeof(object)) - { - body = Expression.Convert(body, typeof(object)); - } + if (body.Type != typeof(object)) body = Expression.Convert(body, typeof(object)); var lambda = Expression.Lambda>(body, keySelector.Parameters); - var definition = new CollectionIndexDefinition(indexName, propertyPaths, lambda, false, IndexType.Vector, false, dimensions, metric); - return CreateIndex(definition); - } - } - - /// - /// Creates a spatial index from an untyped key selector. - /// - /// Untyped expression that selects indexed fields. - /// Optional index name. - /// The created or existing index. - public CollectionSecondaryIndex CreateSpatialIndexUntyped( - LambdaExpression keySelector, - string? name = null) - { - var propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); - var indexName = name ?? GenerateIndexName(propertyPaths); - - lock (_lock) - { - if (_indexes.TryGetValue(indexName, out var existing)) - return existing; - - var body = keySelector.Body; - if (body.Type != typeof(object)) - { - body = Expression.Convert(body, typeof(object)); - } - - var lambda = Expression.Lambda>(body, keySelector.Parameters); - - var definition = new CollectionIndexDefinition(indexName, propertyPaths, lambda, false, IndexType.Spatial); + var definition = new CollectionIndexDefinition(indexName, propertyPaths, lambda, false, IndexType.Vector, + false, dimensions, metric); return CreateIndex(definition); } } /// - /// Drops an existing index by name + /// Creates a spatial index from an untyped key selector. + /// + /// Untyped expression that selects indexed fields. + /// Optional index name. + /// The created or existing index. + public CollectionSecondaryIndex CreateSpatialIndexUntyped( + LambdaExpression keySelector, + string? name = null) + { + string[] propertyPaths = ExpressionAnalyzer.ExtractPropertyPaths(keySelector); + string indexName = name ?? GenerateIndexName(propertyPaths); + + lock (_lock) + { + if (_indexes.TryGetValue(indexName, out var existing)) + return existing; + + var body = keySelector.Body; + if (body.Type != typeof(object)) body = Expression.Convert(body, typeof(object)); + + var lambda = Expression.Lambda>(body, keySelector.Parameters); + + var definition = + new CollectionIndexDefinition(indexName, propertyPaths, lambda, false, IndexType.Spatial); + return CreateIndex(definition); + } + } + + /// + /// Drops an existing index by name /// /// Index name /// True if index was found and dropped, false otherwise @@ -311,10 +337,10 @@ public sealed class CollectionIndexManager : IDisposable where T : class if (_indexes.TryGetValue(name, out var index)) { index.Dispose(); - _indexes.Remove(name); - - // TODO: Free pages used by index in PageFile - + _indexes.Remove(name); + + // TODO: Free pages used by index in PageFile + SaveMetadata(); // Save metadata after dropping index return true; } @@ -323,11 +349,11 @@ public sealed class CollectionIndexManager : IDisposable where T : class } } - /// - /// Gets an index by name - /// - /// The index name. - public CollectionSecondaryIndex? GetIndex(string name) + /// + /// Gets an index by name + /// + /// The index name. + public CollectionSecondaryIndex? GetIndex(string name) { lock (_lock) { @@ -336,7 +362,7 @@ public sealed class CollectionIndexManager : IDisposable where T : class } /// - /// Gets all indexes + /// Gets all indexes /// public IEnumerable> GetAllIndexes() { @@ -347,7 +373,7 @@ public sealed class CollectionIndexManager : IDisposable where T : class } /// - /// Gets information about all indexes + /// Gets information about all indexes /// public IEnumerable GetIndexInfo() { @@ -358,8 +384,8 @@ public sealed class CollectionIndexManager : IDisposable where T : class } /// - /// Finds the best index to use for a query on the specified property. - /// Returns null if no suitable index found (requires full scan). + /// Finds the best index to use for a query on the specified property. + /// Returns null if no suitable index found (requires full scan). /// /// Property path being queried /// Best index for the query, or null if none suitable @@ -386,11 +412,11 @@ public sealed class CollectionIndexManager : IDisposable where T : class } } - /// - /// Finds the best index for a compound query on multiple properties - /// - /// The ordered list of queried property paths. - public CollectionSecondaryIndex? FindBestCompoundIndex(string[] propertyPaths) + /// + /// Finds the best index for a compound query on multiple properties + /// + /// The ordered list of queried property paths. + public CollectionSecondaryIndex? FindBestCompoundIndex(string[] propertyPaths) { if (propertyPaths == null || propertyPaths.Length == 0) return null; @@ -413,7 +439,7 @@ public sealed class CollectionIndexManager : IDisposable where T : class } /// - /// Inserts a document into all indexes + /// Inserts a document into all indexes /// /// Document to insert /// Physical location of the document @@ -425,22 +451,20 @@ public sealed class CollectionIndexManager : IDisposable where T : class lock (_lock) { - foreach (var index in _indexes.Values) - { - index.Insert(document, location, transaction); - } + foreach (var index in _indexes.Values) index.Insert(document, location, transaction); } } /// - /// Updates a document in all indexes + /// Updates a document in all indexes /// /// Old version of document /// New version of document /// Physical location of old document /// Physical location of new document /// Transaction context - public void UpdateInAll(T oldDocument, T newDocument, DocumentLocation oldLocation, DocumentLocation newLocation, ITransaction transaction) + public void UpdateInAll(T oldDocument, T newDocument, DocumentLocation oldLocation, DocumentLocation newLocation, + ITransaction transaction) { if (oldDocument == null) throw new ArgumentNullException(nameof(oldDocument)); @@ -450,14 +474,12 @@ public sealed class CollectionIndexManager : IDisposable where T : class lock (_lock) { foreach (var index in _indexes.Values) - { index.Update(oldDocument, newDocument, oldLocation, newLocation, transaction); - } } } /// - /// Deletes a document from all indexes + /// Deletes a document from all indexes /// /// Document to delete /// Physical location of the document @@ -469,83 +491,78 @@ public sealed class CollectionIndexManager : IDisposable where T : class lock (_lock) { - foreach (var index in _indexes.Values) - { - index.Delete(document, location, transaction); - } + foreach (var index in _indexes.Values) index.Delete(document, location, transaction); } } /// - /// Generates an index name from property paths + /// Generates an index name from property paths /// private static string GenerateIndexName(string[] propertyPaths) { return $"idx_{string.Join("_", propertyPaths)}"; } - private CollectionIndexDefinition RebuildDefinition(string name, string[] paths, bool isUnique, IndexType type, int dimensions = 0, VectorMetric metric = VectorMetric.Cosine) + private CollectionIndexDefinition RebuildDefinition(string name, string[] paths, bool isUnique, IndexType type, + int dimensions = 0, VectorMetric metric = VectorMetric.Cosine) { var param = Expression.Parameter(typeof(T), "u"); - Expression body; - + Expression body; + if (paths.Length == 1) - { body = Expression.PropertyOrField(param, paths[0]); - } else - { - body = Expression.NewArrayInit(typeof(object), + body = Expression.NewArrayInit(typeof(object), paths.Select(p => Expression.Convert(Expression.PropertyOrField(param, p), typeof(object)))); - } - + var objectBody = Expression.Convert(body, typeof(object)); - var lambda = Expression.Lambda>(objectBody, param); - + var lambda = Expression.Lambda>(objectBody, param); + return new CollectionIndexDefinition(name, paths, lambda, isUnique, type, false, dimensions, metric); } - /// - /// Gets the root page identifier for the primary index. - /// - public uint PrimaryRootPageId => _metadata.PrimaryRootPageId; - - /// - /// Rebinds cached metadata and index instances from persisted metadata. - /// - /// The collection metadata used to rebuild index state. - internal void RebindFromMetadata(CollectionMetadata metadata) - { - if (metadata == null) - throw new ArgumentNullException(nameof(metadata)); - - lock (_lock) - { - if (_disposed) - throw new ObjectDisposedException(nameof(CollectionIndexManager)); - - foreach (var index in _indexes.Values) - { - try { index.Dispose(); } catch { /* Best effort */ } - } - - _indexes.Clear(); - _metadata = metadata; - - foreach (var idxMeta in _metadata.Indexes) - { - var definition = RebuildDefinition(idxMeta.Name, idxMeta.PropertyPaths, idxMeta.IsUnique, idxMeta.Type, idxMeta.Dimensions, idxMeta.Metric); - var index = new CollectionSecondaryIndex(definition, _storage, _mapper, idxMeta.RootPageId); - _indexes[idxMeta.Name] = index; - } - } - } - - /// - /// Sets the root page identifier for the primary index. - /// - /// The root page identifier. - public void SetPrimaryRootPageId(uint pageId) + /// + /// Rebinds cached metadata and index instances from persisted metadata. + /// + /// The collection metadata used to rebuild index state. + internal void RebindFromMetadata(CollectionMetadata metadata) + { + if (metadata == null) + throw new ArgumentNullException(nameof(metadata)); + + lock (_lock) + { + if (_disposed) + throw new ObjectDisposedException(nameof(CollectionIndexManager)); + + foreach (var index in _indexes.Values) + try + { + index.Dispose(); + } + catch + { + /* Best effort */ + } + + _indexes.Clear(); + _metadata = metadata; + + foreach (var idxMeta in _metadata.Indexes) + { + var definition = RebuildDefinition(idxMeta.Name, idxMeta.PropertyPaths, idxMeta.IsUnique, idxMeta.Type, + idxMeta.Dimensions, idxMeta.Metric); + var index = new CollectionSecondaryIndex(definition, _storage, _mapper, idxMeta.RootPageId); + _indexes[idxMeta.Name] = index; + } + } + } + + /// + /// Sets the root page identifier for the primary index. + /// + /// The root page identifier. + public void SetPrimaryRootPageId(uint pageId) { lock (_lock) { @@ -557,88 +574,62 @@ public sealed class CollectionIndexManager : IDisposable where T : class } } - /// - /// Gets the current collection metadata. - /// - /// The collection metadata. - public CollectionMetadata GetMetadata() => _metadata; - - private void SaveMetadata() - { - UpdateMetadata(); - _storage.SaveCollectionMetadata(_metadata); - } - - /// - /// Releases resources used by the index manager. - /// - public void Dispose() + /// + /// Gets the current collection metadata. + /// + /// The collection metadata. + public CollectionMetadata GetMetadata() { - if (_disposed) - return; - - // No auto-save on dispose to avoid unnecessary I/O if no changes - - lock (_lock) - { - foreach (var index in _indexes.Values) - { - try { index.Dispose(); } catch { /* Best effort */ } - } - - _indexes.Clear(); - _disposed = true; - } + return _metadata; + } - GC.SuppressFinalize(this); + private void SaveMetadata() + { + UpdateMetadata(); + _storage.SaveCollectionMetadata(_metadata); } } /// -/// Helper class to analyze LINQ expressions and extract property paths +/// Helper class to analyze LINQ expressions and extract property paths /// public static class ExpressionAnalyzer { - /// - /// Extracts property paths from a lambda expression. - /// Supports simple property access (p => p.Age) and anonymous types (p => new { p.City, p.Age }). - /// - /// The lambda expression to analyze. - public static string[] ExtractPropertyPaths(LambdaExpression expression) + /// + /// Extracts property paths from a lambda expression. + /// Supports simple property access (p => p.Age) and anonymous types (p => new { p.City, p.Age }). + /// + /// The lambda expression to analyze. + public static string[] ExtractPropertyPaths(LambdaExpression expression) { if (expression.Body is MemberExpression memberExpr) - { // Simple property: p => p.Age return new[] { memberExpr.Member.Name }; - } - else if (expression.Body is NewExpression newExpr) - { + + if (expression.Body is NewExpression newExpr) // Compound key via anonymous type: p => new { p.City, p.Age } return newExpr.Arguments .OfType() .Select(m => m.Member.Name) .ToArray(); - } - else if (expression.Body is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpr) + + if (expression.Body is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpr) { // Handle Convert(Member) or Convert(New) if (unaryExpr.Operand is MemberExpression innerMember) - { // Wrapped property: p => (object)p.Age return new[] { innerMember.Member.Name }; - } - else if (unaryExpr.Operand is NewExpression innerNew) - { - // Wrapped anonymous type: p => (object)new { p.City, p.Age } - return innerNew.Arguments - .OfType() - .Select(m => m.Member.Name) - .ToArray(); - } + + if (unaryExpr.Operand is NewExpression innerNew) + // Wrapped anonymous type: p => (object)new { p.City, p.Age } + return innerNew.Arguments + .OfType() + .Select(m => m.Member.Name) + .ToArray(); } throw new ArgumentException( "Expression must be a property accessor (p => p.Property) or anonymous type (p => new { p.Prop1, p.Prop2 })", nameof(expression)); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/CollectionSecondaryIndex.cs b/src/CBDD.Core/Indexing/CollectionSecondaryIndex.cs index 1685163..35fa48c 100755 --- a/src/CBDD.Core/Indexing/CollectionSecondaryIndex.cs +++ b/src/CBDD.Core/Indexing/CollectionSecondaryIndex.cs @@ -1,101 +1,112 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; +using ZB.MOM.WW.CBDD.Core.Indexing.Internal; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; -using ZB.MOM.WW.CBDD.Core.Indexing.Internal; -using System; -using System.Linq; -using System.Collections.Generic; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// Represents a secondary (non-primary) index on a document collection. -/// Provides a high-level, strongly-typed wrapper around the low-level BTreeIndex. -/// Handles automatic key extraction from documents using compiled expressions. +/// Represents a secondary (non-primary) index on a document collection. +/// Provides a high-level, strongly-typed wrapper around the low-level BTreeIndex. +/// Handles automatic key extraction from documents using compiled expressions. /// /// Primary key type /// Document type public sealed class CollectionSecondaryIndex : IDisposable where T : class { - private readonly CollectionIndexDefinition _definition; - private readonly BTreeIndex? _btreeIndex; - private readonly VectorSearchIndex? _vectorIndex; - private readonly RTreeIndex? _spatialIndex; private readonly IDocumentMapper _mapper; + private readonly RTreeIndex? _spatialIndex; + private readonly VectorSearchIndex? _vectorIndex; private bool _disposed; /// - /// Gets the index definition + /// Initializes a new instance of the class. /// - public CollectionIndexDefinition Definition => _definition; - - /// - /// Gets the underlying BTree index (for advanced scenarios) - /// - public BTreeIndex? BTreeIndex => _btreeIndex; - - /// - /// Gets the root page identifier for the underlying index structure. - /// - public uint RootPageId => _btreeIndex?.RootPageId ?? _vectorIndex?.RootPageId ?? _spatialIndex?.RootPageId ?? 0; - - /// - /// Initializes a new instance of the class. - /// - /// The index definition. - /// The storage engine. - /// The document mapper. - /// The existing root page ID, or 0 to create a new one. - public CollectionSecondaryIndex( - CollectionIndexDefinition definition, - StorageEngine storage, - IDocumentMapper mapper, - uint rootPageId = 0) - : this(definition, (IStorageEngine)storage, mapper, rootPageId) - { - } - - /// - /// Initializes a new instance of the class from index storage abstractions. - /// - /// The index definition. - /// The index storage abstraction. - /// The document mapper. - /// The existing root page identifier, if any. - internal CollectionSecondaryIndex( - CollectionIndexDefinition definition, - IIndexStorage storage, - IDocumentMapper mapper, - uint rootPageId = 0) + /// The index definition. + /// The storage engine. + /// The document mapper. + /// The existing root page ID, or 0 to create a new one. + public CollectionSecondaryIndex( + CollectionIndexDefinition definition, + StorageEngine storage, + IDocumentMapper mapper, + uint rootPageId = 0) + : this(definition, (IStorageEngine)storage, mapper, rootPageId) { - _definition = definition ?? throw new ArgumentNullException(nameof(definition)); - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); - - var indexOptions = definition.ToIndexOptions(); - + } + + /// + /// Initializes a new instance of the class from index storage + /// abstractions. + /// + /// The index definition. + /// The index storage abstraction. + /// The document mapper. + /// The existing root page identifier, if any. + internal CollectionSecondaryIndex( + CollectionIndexDefinition definition, + IIndexStorage storage, + IDocumentMapper mapper, + uint rootPageId = 0) + { + Definition = definition ?? throw new ArgumentNullException(nameof(definition)); + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + + var indexOptions = definition.ToIndexOptions(); + if (indexOptions.Type == IndexType.Vector) { _vectorIndex = new VectorSearchIndex(storage, indexOptions, rootPageId); - _btreeIndex = null; + BTreeIndex = null; _spatialIndex = null; } else if (indexOptions.Type == IndexType.Spatial) { _spatialIndex = new RTreeIndex(storage, indexOptions, rootPageId); - _btreeIndex = null; + BTreeIndex = null; _vectorIndex = null; } else { - _btreeIndex = new BTreeIndex(storage, indexOptions, rootPageId); + BTreeIndex = new BTreeIndex(storage, indexOptions, rootPageId); _vectorIndex = null; _spatialIndex = null; } } /// - /// Inserts a document into this index + /// Gets the index definition + /// + public CollectionIndexDefinition Definition { get; } + + /// + /// Gets the underlying BTree index (for advanced scenarios) + /// + public BTreeIndex? BTreeIndex { get; } + + /// + /// Gets the root page identifier for the underlying index structure. + /// + public uint RootPageId => BTreeIndex?.RootPageId ?? _vectorIndex?.RootPageId ?? _spatialIndex?.RootPageId ?? 0; + + /// + /// Releases resources used by this index wrapper. + /// + public void Dispose() + { + if (_disposed) + return; + + // BTreeIndex doesn't currently implement IDisposable + // Future: may need to flush buffers, close resources + + _disposed = true; + GC.SuppressFinalize(this); + } + + /// + /// Inserts a document into this index /// /// Document to index /// Physical location of the document @@ -103,91 +114,84 @@ public sealed class CollectionSecondaryIndex : IDisposable where T : cla public void Insert(T document, DocumentLocation location, ITransaction transaction) { if (document == null) - throw new ArgumentNullException(nameof(document)); - - // Extract key using compiled selector (fast!) - var keyValue = _definition.KeySelector(document); + throw new ArgumentNullException(nameof(document)); + + // Extract key using compiled selector (fast!) + object? keyValue = Definition.KeySelector(document); if (keyValue == null) - return; // Skip null keys - + return; // Skip null keys + if (_vectorIndex != null) { // Vector Index Support if (keyValue is float[] singleVector) - { _vectorIndex.Insert(singleVector, location, transaction); - } else if (keyValue is IEnumerable vectors) - { - foreach (var v in vectors) - { + foreach (float[] v in vectors) _vectorIndex.Insert(v, location, transaction); - } - } } else if (_spatialIndex != null) { // Geospatial Index Support if (keyValue is ValueTuple t) - { _spatialIndex.Insert(GeoBox.FromPoint(new GeoPoint(t.Item1, t.Item2)), location, transaction); - } } - else if (_btreeIndex != null) + else if (BTreeIndex != null) { // BTree Index logic var userKey = ConvertToIndexKey(keyValue); var documentId = _mapper.GetId(document); var compositeKey = CreateCompositeKey(userKey, _mapper.ToIndexKey(documentId)); - _btreeIndex.Insert(compositeKey, location, transaction?.TransactionId); + BTreeIndex.Insert(compositeKey, location, transaction?.TransactionId); } } /// - /// Updates a document in this index (delete old, insert new). - /// Only updates if the indexed key has changed. + /// Updates a document in this index (delete old, insert new). + /// Only updates if the indexed key has changed. /// /// Old version of document /// New version of document /// Physical location of old document /// Physical location of new document /// Optional transaction - public void Update(T oldDocument, T newDocument, DocumentLocation oldLocation, DocumentLocation newLocation, ITransaction transaction) + public void Update(T oldDocument, T newDocument, DocumentLocation oldLocation, DocumentLocation newLocation, + ITransaction transaction) { if (oldDocument == null) throw new ArgumentNullException(nameof(oldDocument)); if (newDocument == null) - throw new ArgumentNullException(nameof(newDocument)); - - // Extract keys from both versions - var oldKey = _definition.KeySelector(oldDocument); - var newKey = _definition.KeySelector(newDocument); - - // If keys are the same, no index update needed (optimization) + throw new ArgumentNullException(nameof(newDocument)); + + // Extract keys from both versions + object? oldKey = Definition.KeySelector(oldDocument); + object? newKey = Definition.KeySelector(newDocument); + + // If keys are the same, no index update needed (optimization) if (Equals(oldKey, newKey)) - return; - - var documentId = _mapper.GetId(oldDocument); - - // Delete old entry if it had a key + return; + + var documentId = _mapper.GetId(oldDocument); + + // Delete old entry if it had a key if (oldKey != null) { var oldUserKey = ConvertToIndexKey(oldKey); var oldCompositeKey = CreateCompositeKey(oldUserKey, _mapper.ToIndexKey(documentId)); - _btreeIndex?.Delete(oldCompositeKey, oldLocation, transaction?.TransactionId); - } - - // Insert new entry if it has a key + BTreeIndex?.Delete(oldCompositeKey, oldLocation, transaction?.TransactionId); + } + + // Insert new entry if it has a key if (newKey != null) { var newUserKey = ConvertToIndexKey(newKey); var newCompositeKey = CreateCompositeKey(newUserKey, _mapper.ToIndexKey(documentId)); - _btreeIndex?.Insert(newCompositeKey, newLocation, transaction?.TransactionId); + BTreeIndex?.Insert(newCompositeKey, newLocation, transaction?.TransactionId); } } /// - /// Deletes a document from this index + /// Deletes a document from this index /// /// Document to remove from index /// Physical location of the document @@ -195,23 +199,23 @@ public sealed class CollectionSecondaryIndex : IDisposable where T : cla public void Delete(T document, DocumentLocation location, ITransaction transaction) { if (document == null) - throw new ArgumentNullException(nameof(document)); - - // Extract key - var keyValue = _definition.KeySelector(document); + throw new ArgumentNullException(nameof(document)); + + // Extract key + object? keyValue = Definition.KeySelector(document); if (keyValue == null) - return; // Nothing to delete - + return; // Nothing to delete + var userKey = ConvertToIndexKey(keyValue); - var documentId = _mapper.GetId(document); - - // Create composite key and delete + var documentId = _mapper.GetId(document); + + // Create composite key and delete var compositeKey = CreateCompositeKey(userKey, _mapper.ToIndexKey(documentId)); - _btreeIndex?.Delete(compositeKey, location, transaction?.TransactionId); + BTreeIndex?.Delete(compositeKey, location, transaction?.TransactionId); } /// - /// Seeks a single document by exact key match (O(log n)) + /// Seeks a single document by exact key match (O(log n)) /// /// Key value to seek /// Optional transaction to read uncommitted changes @@ -219,68 +223,67 @@ public sealed class CollectionSecondaryIndex : IDisposable where T : cla public DocumentLocation? Seek(object key, ITransaction? transaction = null) { if (key == null) - return null; - - if (_vectorIndex != null && key is float[] query) - { - return _vectorIndex.Search(query, 1, transaction: transaction).FirstOrDefault().Location; - } + return null; - if (_btreeIndex != null) + if (_vectorIndex != null && key is float[] query) + return _vectorIndex.Search(query, 1, transaction: transaction).FirstOrDefault().Location; + + if (BTreeIndex != null) { var userKey = ConvertToIndexKey(key); - var minComposite = CreateCompositeKeyBoundary(userKey, useMinObjectId: true); - var maxComposite = CreateCompositeKeyBoundary(userKey, useMinObjectId: false); - var firstEntry = _btreeIndex.Range(minComposite, maxComposite, IndexDirection.Forward, transaction?.TransactionId).FirstOrDefault(); - return firstEntry.Location.PageId == 0 ? null : (DocumentLocation?)firstEntry.Location; - } - - return null; - } - - /// - /// Performs a vector nearest-neighbor search. - /// - /// The query vector. - /// The number of results to return. - /// The search breadth parameter. - /// Optional transaction. - /// The matching vector search results. - public IEnumerable VectorSearch(float[] query, int k, int efSearch = 100, ITransaction? transaction = null) - { - if (_vectorIndex == null) - throw new InvalidOperationException("This index is not a vector index."); - + var minComposite = CreateCompositeKeyBoundary(userKey, true); + var maxComposite = CreateCompositeKeyBoundary(userKey, false); + var firstEntry = BTreeIndex + .Range(minComposite, maxComposite, IndexDirection.Forward, transaction?.TransactionId).FirstOrDefault(); + return firstEntry.Location.PageId == 0 ? null : firstEntry.Location; + } + + return null; + } + + /// + /// Performs a vector nearest-neighbor search. + /// + /// The query vector. + /// The number of results to return. + /// The search breadth parameter. + /// Optional transaction. + /// The matching vector search results. + public IEnumerable VectorSearch(float[] query, int k, int efSearch = 100, + ITransaction? transaction = null) + { + if (_vectorIndex == null) + throw new InvalidOperationException("This index is not a vector index."); + return _vectorIndex.Search(query, k, efSearch, transaction); } - /// - /// Performs geospatial distance search - /// - /// The center point. - /// The search radius in kilometers. - /// Optional transaction. - public IEnumerable Near((double Latitude, double Longitude) center, double radiusKm, ITransaction? transaction = null) - { - if (_spatialIndex == null) + /// + /// Performs geospatial distance search + /// + /// The center point. + /// The search radius in kilometers. + /// Optional transaction. + public IEnumerable Near((double Latitude, double Longitude) center, double radiusKm, + ITransaction? transaction = null) + { + if (_spatialIndex == null) throw new InvalidOperationException("This index is not a spatial index."); var queryBox = SpatialMath.BoundingBox(center.Latitude, center.Longitude, radiusKm); - foreach (var loc in _spatialIndex.Search(queryBox, transaction)) - { - yield return loc; - } + foreach (var loc in _spatialIndex.Search(queryBox, transaction)) yield return loc; } - /// - /// Performs geospatial bounding box search - /// - /// The minimum latitude/longitude corner. - /// The maximum latitude/longitude corner. - /// Optional transaction. - public IEnumerable Within((double Latitude, double Longitude) min, (double Latitude, double Longitude) max, ITransaction? transaction = null) - { - if (_spatialIndex == null) + /// + /// Performs geospatial bounding box search + /// + /// The minimum latitude/longitude corner. + /// The maximum latitude/longitude corner. + /// Optional transaction. + public IEnumerable Within((double Latitude, double Longitude) min, + (double Latitude, double Longitude) max, ITransaction? transaction = null) + { + if (_spatialIndex == null) throw new InvalidOperationException("This index is not a spatial index."); var area = new GeoBox(min.Latitude, min.Longitude, max.Latitude, max.Longitude); @@ -288,21 +291,22 @@ public sealed class CollectionSecondaryIndex : IDisposable where T : cla } /// - /// Scans a range of keys (O(log n + k) where k is result count) + /// Scans a range of keys (O(log n + k) where k is result count) /// - /// Minimum key (inclusive), null for unbounded - /// Maximum key (inclusive), null for unbounded - /// Scan direction. - /// Optional transaction to read uncommitted changes - /// Enumerable of document locations in key order - public IEnumerable Range(object? minKey, object? maxKey, IndexDirection direction = IndexDirection.Forward, ITransaction? transaction = null) + /// Minimum key (inclusive), null for unbounded + /// Maximum key (inclusive), null for unbounded + /// Scan direction. + /// Optional transaction to read uncommitted changes + /// Enumerable of document locations in key order + public IEnumerable Range(object? minKey, object? maxKey, + IndexDirection direction = IndexDirection.Forward, ITransaction? transaction = null) { - if (_btreeIndex == null) yield break; - - // Handle unbounded ranges + if (BTreeIndex == null) yield break; + + // Handle unbounded ranges IndexKey actualMinKey; - IndexKey actualMaxKey; - + IndexKey actualMaxKey; + if (minKey == null && maxKey == null) { // Full scan - use extreme values @@ -313,108 +317,53 @@ public sealed class CollectionSecondaryIndex : IDisposable where T : cla { actualMinKey = new IndexKey(new byte[0]); var userMaxKey = ConvertToIndexKey(maxKey!); - actualMaxKey = CreateCompositeKeyBoundary(userMaxKey, useMinObjectId: false); // Max boundary + actualMaxKey = CreateCompositeKeyBoundary(userMaxKey, false); // Max boundary } else if (maxKey == null) { var userMinKey = ConvertToIndexKey(minKey); - actualMinKey = CreateCompositeKeyBoundary(userMinKey, useMinObjectId: true); // Min boundary + actualMinKey = CreateCompositeKeyBoundary(userMinKey, true); // Min boundary actualMaxKey = new IndexKey(Enumerable.Repeat((byte)0xFF, 255).ToArray()); } else { // Both bounds specified var userMinKey = ConvertToIndexKey(minKey); - var userMaxKey = ConvertToIndexKey(maxKey); - - // Create composite boundaries: - // Min: (userMinKey, ObjectId.Empty) - captures all docs with key >= userMinKey - // Max: (userMaxKey, ObjectId.MaxValue) - captures all docs with key <= userMaxKey - actualMinKey = CreateCompositeKeyBoundary(userMinKey, useMinObjectId: true); - actualMaxKey = CreateCompositeKeyBoundary(userMaxKey, useMinObjectId: false); - } - - // Use BTreeIndex.Range with WAL-aware reads and direction - // Extract DocumentLocation from each entry - foreach (var entry in _btreeIndex.Range(actualMinKey, actualMaxKey, direction, transaction?.TransactionId)) - { - yield return entry.Location; + var userMaxKey = ConvertToIndexKey(maxKey); + + // Create composite boundaries: + // Min: (userMinKey, ObjectId.Empty) - captures all docs with key >= userMinKey + // Max: (userMaxKey, ObjectId.MaxValue) - captures all docs with key <= userMaxKey + actualMinKey = CreateCompositeKeyBoundary(userMinKey, true); + actualMaxKey = CreateCompositeKeyBoundary(userMaxKey, false); } + + // Use BTreeIndex.Range with WAL-aware reads and direction + // Extract DocumentLocation from each entry + foreach (var entry in BTreeIndex.Range(actualMinKey, actualMaxKey, direction, transaction?.TransactionId)) + yield return entry.Location; } /// - /// Gets statistics about this index + /// Gets statistics about this index /// public CollectionIndexInfo GetInfo() { return new CollectionIndexInfo { - Name = _definition.Name, - PropertyPaths = _definition.PropertyPaths, - IsUnique = _definition.IsUnique, - Type = _definition.Type, - IsPrimary = _definition.IsPrimary, + Name = Definition.Name, + PropertyPaths = Definition.PropertyPaths, + IsUnique = Definition.IsUnique, + Type = Definition.Type, + IsPrimary = Definition.IsPrimary, EstimatedDocumentCount = 0, // TODO: Track or calculate document count - EstimatedSizeBytes = 0 // TODO: Calculate index size + EstimatedSizeBytes = 0 // TODO: Calculate index size }; } - #region Composite Key Support (SQLite-style for Duplicate Keys) - /// - /// Creates a composite key by concatenating user key with document ID. - /// This allows duplicate user keys while maintaining BTree uniqueness. - /// Format: [UserKeyBytes] + [DocumentIdKey] - /// - private IndexKey CreateCompositeKey(IndexKey userKey, IndexKey documentIdKey) - { - // Allocate buffer: user key + document ID key length - var compositeBytes = new byte[userKey.Data.Length + documentIdKey.Data.Length]; - - // Copy user key - userKey.Data.CopyTo(compositeBytes.AsSpan(0, userKey.Data.Length)); - - // Append document ID key - documentIdKey.Data.CopyTo(compositeBytes.AsSpan(userKey.Data.Length)); - - return new IndexKey(compositeBytes); - } - - /// - /// Creates a composite key for range query boundary. - /// Uses MIN or MAX ID representation to capture all documents with the user key. - /// - private IndexKey CreateCompositeKeyBoundary(IndexKey userKey, bool useMinObjectId) - { - // For range boundaries, we use an empty key for Min and a very large key for Max - // to wrap around all possible IDs for this user key. - IndexKey idBoundary = useMinObjectId - ? new IndexKey(Array.Empty()) - : new IndexKey(Enumerable.Repeat((byte)0xFF, 16).ToArray()); // Using 16 as a safe max for GUID/ObjectId - - return CreateCompositeKey(userKey, idBoundary); - } - - /// - /// Extracts the original user key from a composite key by removing the ObjectId suffix. - /// Used when we need to return the original indexed value. - /// - private IndexKey ExtractUserKey(IndexKey compositeKey) - { - // Composite key = UserKey + ObjectId(12 bytes) - var userKeyLength = compositeKey.Data.Length - 12; - if (userKeyLength <= 0) - return compositeKey; // Fallback for malformed keys - - var userKeyBytes = compositeKey.Data.Slice(0, userKeyLength); - return new IndexKey(userKeyBytes); - } - - #endregion - - /// - /// Converts a CLR value to an IndexKey for BTree storage. - /// Supports all common .NET types. + /// Converts a CLR value to an IndexKey for BTree storage. + /// Supports all common .NET types. /// private IndexKey ConvertToIndexKey(object value) { @@ -426,26 +375,64 @@ public sealed class CollectionSecondaryIndex : IDisposable where T : cla long longVal => new IndexKey(longVal), DateTime dateTime => new IndexKey(dateTime.Ticks), bool boolVal => new IndexKey(boolVal ? 1 : 0), - byte[] byteArray => new IndexKey(byteArray), - - // For compound keys or complex types, use ToString and serialize - // TODO: Better compound key serialization + byte[] byteArray => new IndexKey(byteArray), + + // For compound keys or complex types, use ToString and serialize + // TODO: Better compound key serialization _ => new IndexKey(value.ToString() ?? string.Empty) - }; - } - - /// - /// Releases resources used by this index wrapper. - /// - public void Dispose() - { - if (_disposed) - return; - - // BTreeIndex doesn't currently implement IDisposable - // Future: may need to flush buffers, close resources - - _disposed = true; - GC.SuppressFinalize(this); + }; } -} + + #region Composite Key Support (SQLite-style for Duplicate Keys) + + /// + /// Creates a composite key by concatenating user key with document ID. + /// This allows duplicate user keys while maintaining BTree uniqueness. + /// Format: [UserKeyBytes] + [DocumentIdKey] + /// + private IndexKey CreateCompositeKey(IndexKey userKey, IndexKey documentIdKey) + { + // Allocate buffer: user key + document ID key length + var compositeBytes = new byte[userKey.Data.Length + documentIdKey.Data.Length]; + + // Copy user key + userKey.Data.CopyTo(compositeBytes.AsSpan(0, userKey.Data.Length)); + + // Append document ID key + documentIdKey.Data.CopyTo(compositeBytes.AsSpan(userKey.Data.Length)); + + return new IndexKey(compositeBytes); + } + + /// + /// Creates a composite key for range query boundary. + /// Uses MIN or MAX ID representation to capture all documents with the user key. + /// + private IndexKey CreateCompositeKeyBoundary(IndexKey userKey, bool useMinObjectId) + { + // For range boundaries, we use an empty key for Min and a very large key for Max + // to wrap around all possible IDs for this user key. + var idBoundary = useMinObjectId + ? new IndexKey(Array.Empty()) + : new IndexKey(Enumerable.Repeat((byte)0xFF, 16).ToArray()); // Using 16 as a safe max for GUID/ObjectId + + return CreateCompositeKey(userKey, idBoundary); + } + + /// + /// Extracts the original user key from a composite key by removing the ObjectId suffix. + /// Used when we need to return the original indexed value. + /// + private IndexKey ExtractUserKey(IndexKey compositeKey) + { + // Composite key = UserKey + ObjectId(12 bytes) + int userKeyLength = compositeKey.Data.Length - 12; + if (userKeyLength <= 0) + return compositeKey; // Fallback for malformed keys + + var userKeyBytes = compositeKey.Data.Slice(0, userKeyLength); + return new IndexKey(userKeyBytes); + } + + #endregion +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/GeoSpatialExtensions.cs b/src/CBDD.Core/Indexing/GeoSpatialExtensions.cs index e99ada8..c6a73a2 100755 --- a/src/CBDD.Core/Indexing/GeoSpatialExtensions.cs +++ b/src/CBDD.Core/Indexing/GeoSpatialExtensions.cs @@ -3,28 +3,30 @@ namespace ZB.MOM.WW.CBDD.Core.Indexing; public static class GeoSpatialExtensions { /// - /// Performs a geospatial proximity search (Near) on a coordinate tuple property. - /// This method is a marker for the LINQ query provider and is optimized using R-Tree indexes if available. + /// Performs a geospatial proximity search (Near) on a coordinate tuple property. + /// This method is a marker for the LINQ query provider and is optimized using R-Tree indexes if available. /// /// The coordinate tuple (Latitude, Longitude) property of the entity. /// The center point (Latitude, Longitude) for the proximity search. /// The radius in kilometers. /// True if the point is within the specified radius. - public static bool Near(this (double Latitude, double Longitude) point, (double Latitude, double Longitude) center, double radiusKm) + public static bool Near(this (double Latitude, double Longitude) point, (double Latitude, double Longitude) center, + double radiusKm) { - return true; + return true; } /// - /// Performs a geospatial bounding box search (Within) on a coordinate tuple property. - /// This method is a marker for the LINQ query provider and is optimized using R-Tree indexes if available. + /// Performs a geospatial bounding box search (Within) on a coordinate tuple property. + /// This method is a marker for the LINQ query provider and is optimized using R-Tree indexes if available. /// /// The coordinate tuple (Latitude, Longitude) property of the entity. /// The minimum (Latitude, Longitude) of the bounding box. /// The maximum (Latitude, Longitude) of the bounding box. /// True if the point is within the specified bounding box. - public static bool Within(this (double Latitude, double Longitude) point, (double Latitude, double Longitude) min, (double Latitude, double Longitude) max) + public static bool Within(this (double Latitude, double Longitude) point, (double Latitude, double Longitude) min, + (double Latitude, double Longitude) max) { return true; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/HashIndex.cs b/src/CBDD.Core/Indexing/HashIndex.cs index 8597183..8c767ac 100755 --- a/src/CBDD.Core/Indexing/HashIndex.cs +++ b/src/CBDD.Core/Indexing/HashIndex.cs @@ -1,13 +1,10 @@ -using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Storage; -using System; -using System.Collections.Generic; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// Hash-based index for exact-match lookups. -/// Uses simple bucket-based hashing with collision handling. +/// Hash-based index for exact-match lookups. +/// Uses simple bucket-based hashing with collision handling. /// public sealed class HashIndex { @@ -15,7 +12,7 @@ public sealed class HashIndex private readonly IndexOptions _options; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The index options. public HashIndex(IndexOptions options) @@ -25,16 +22,16 @@ public sealed class HashIndex } /// - /// Inserts a key-location pair into the hash index + /// Inserts a key-location pair into the hash index /// /// The index key. /// The document location. public void Insert(IndexKey key, DocumentLocation location) { if (_options.Unique && TryFind(key, out _)) - throw new InvalidOperationException($"Duplicate key violation for unique index"); + throw new InvalidOperationException("Duplicate key violation for unique index"); - var hashCode = key.GetHashCode(); + int hashCode = key.GetHashCode(); if (!_buckets.TryGetValue(hashCode, out var bucket)) { @@ -46,46 +43,43 @@ public sealed class HashIndex } /// - /// Finds a document location by exact key match + /// Finds a document location by exact key match /// /// The index key. /// When this method returns, contains the matched document location if found. - /// if a matching entry is found; otherwise, . + /// if a matching entry is found; otherwise, . public bool TryFind(IndexKey key, out DocumentLocation location) { location = default; - var hashCode = key.GetHashCode(); + int hashCode = key.GetHashCode(); if (!_buckets.TryGetValue(hashCode, out var bucket)) return false; foreach (var entry in bucket) - { if (entry.Key == key) { location = entry.Location; return true; } - } return false; } /// - /// Removes an entry from the index + /// Removes an entry from the index /// /// The index key. /// The document location. - /// if an entry is removed; otherwise, . + /// if an entry is removed; otherwise, . public bool Remove(IndexKey key, DocumentLocation location) { - var hashCode = key.GetHashCode(); + int hashCode = key.GetHashCode(); if (!_buckets.TryGetValue(hashCode, out var bucket)) return false; - for (int i = 0; i < bucket.Count; i++) - { + for (var i = 0; i < bucket.Count; i++) if (bucket[i].Key == key && bucket[i].Location.PageId == location.PageId && bucket[i].Location.SlotIndex == location.SlotIndex) @@ -97,27 +91,24 @@ public sealed class HashIndex return true; } - } return false; } /// - /// Gets all entries matching the key + /// Gets all entries matching the key /// /// The index key. /// All matching index entries. public IEnumerable FindAll(IndexKey key) { - var hashCode = key.GetHashCode(); + int hashCode = key.GetHashCode(); if (!_buckets.TryGetValue(hashCode, out var bucket)) yield break; foreach (var entry in bucket) - { if (entry.Key == key) yield return entry; - } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/IBTreeCursor.cs b/src/CBDD.Core/Indexing/IBTreeCursor.cs index 3f6a177..1029408 100755 --- a/src/CBDD.Core/Indexing/IBTreeCursor.cs +++ b/src/CBDD.Core/Indexing/IBTreeCursor.cs @@ -1,50 +1,47 @@ -using ZB.MOM.WW.CBDD.Core.Indexing; -using System; - namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// Represents a cursor for traversing a B+Tree index. -/// Provides low-level primitives for building complex queries. +/// Represents a cursor for traversing a B+Tree index. +/// Provides low-level primitives for building complex queries. /// public interface IBTreeCursor : IDisposable { /// - /// Gets the current entry at the cursor position. - /// Throws InvalidOperationException if cursor is invalid or uninitialized. + /// Gets the current entry at the cursor position. + /// Throws InvalidOperationException if cursor is invalid or uninitialized. /// IndexEntry Current { get; } /// - /// Moves the cursor to the first entry in the index. + /// Moves the cursor to the first entry in the index. /// /// True if the index is not empty; otherwise, false. bool MoveToFirst(); /// - /// Moves the cursor to the last entry in the index. + /// Moves the cursor to the last entry in the index. /// /// True if the index is not empty; otherwise, false. bool MoveToLast(); /// - /// Seeks to the specified key. - /// If exact match found, positions there and returns true. - /// If not found, positions at the next greater key and returns false. + /// Seeks to the specified key. + /// If exact match found, positions there and returns true. + /// If not found, positions at the next greater key and returns false. /// /// Key to seek /// True if exact match found; false if positioned at next greater key. bool Seek(IndexKey key); /// - /// Advances the cursor to the next entry. + /// Advances the cursor to the next entry. /// /// True if successfully moved; false if end of index reached. bool MoveNext(); /// - /// Moves the cursor to the previous entry. + /// Moves the cursor to the previous entry. /// /// True if successfully moved; false if start of index reached. bool MovePrev(); -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/IndexDirection.cs b/src/CBDD.Core/Indexing/IndexDirection.cs index 164ad5f..921c195 100755 --- a/src/CBDD.Core/Indexing/IndexDirection.cs +++ b/src/CBDD.Core/Indexing/IndexDirection.cs @@ -4,4 +4,4 @@ public enum IndexDirection { Forward, Backward -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/IndexKey.cs b/src/CBDD.Core/Indexing/IndexKey.cs index af847f9..97fb724 100755 --- a/src/CBDD.Core/Indexing/IndexKey.cs +++ b/src/CBDD.Core/Indexing/IndexKey.cs @@ -1,31 +1,30 @@ -using ZB.MOM.WW.CBDD.Bson; -using System; -using System.Linq; - -namespace ZB.MOM.WW.CBDD.Core.Indexing; - -/// -/// Represents a key in an index. -/// Implemented as struct for efficient index operations. -/// Note: Contains byte array so cannot be readonly struct. -/// -public struct IndexKey : IEquatable, IComparable -{ - private readonly byte[] _data; - private readonly int _hashCode; - - /// - /// Gets the minimum possible index key. - /// - public static IndexKey MinKey => new IndexKey(Array.Empty()); +using System.Text; +using ZB.MOM.WW.CBDD.Bson; + +namespace ZB.MOM.WW.CBDD.Core.Indexing; + +/// +/// Represents a key in an index. +/// Implemented as struct for efficient index operations. +/// Note: Contains byte array so cannot be readonly struct. +/// +public struct IndexKey : IEquatable, IComparable +{ + private readonly byte[] _data; + private readonly int _hashCode; /// - /// Gets the maximum possible index key. + /// Gets the minimum possible index key. /// - public static IndexKey MaxKey => new IndexKey(Enumerable.Repeat((byte)0xFF, 32).ToArray()); + public static IndexKey MinKey => new(Array.Empty()); /// - /// Initializes a new instance of the struct from raw key bytes. + /// Gets the maximum possible index key. + /// + public static IndexKey MaxKey => new(Enumerable.Repeat((byte)0xFF, 32).ToArray()); + + /// + /// Initializes a new instance of the struct from raw key bytes. /// /// The key bytes. public IndexKey(ReadOnlySpan data) @@ -35,7 +34,7 @@ public struct IndexKey : IEquatable, IComparable } /// - /// Initializes a new instance of the struct from an object identifier. + /// Initializes a new instance of the struct from an object identifier. /// /// The object identifier value. public IndexKey(ObjectId objectId) @@ -46,7 +45,7 @@ public struct IndexKey : IEquatable, IComparable } /// - /// Initializes a new instance of the struct from a 32-bit integer. + /// Initializes a new instance of the struct from a 32-bit integer. /// /// The integer value. public IndexKey(int value) @@ -56,7 +55,7 @@ public struct IndexKey : IEquatable, IComparable } /// - /// Initializes a new instance of the struct from a 64-bit integer. + /// Initializes a new instance of the struct from a 64-bit integer. /// /// The integer value. public IndexKey(long value) @@ -66,17 +65,17 @@ public struct IndexKey : IEquatable, IComparable } /// - /// Initializes a new instance of the struct from a string. + /// Initializes a new instance of the struct from a string. /// /// The string value. public IndexKey(string value) { - _data = System.Text.Encoding.UTF8.GetBytes(value); + _data = Encoding.UTF8.GetBytes(value); _hashCode = ComputeHashCode(_data); } /// - /// Initializes a new instance of the struct from a GUID. + /// Initializes a new instance of the struct from a GUID. /// /// The GUID value. public IndexKey(Guid value) @@ -86,72 +85,102 @@ public struct IndexKey : IEquatable, IComparable } /// - /// Gets the raw byte data for this key. + /// Gets the raw byte data for this key. /// public readonly ReadOnlySpan Data => _data; /// - /// Compares this key to another key. + /// Compares this key to another key. /// /// The key to compare with. /// - /// A value less than zero if this key is less than , zero if equal, or greater than zero if greater. + /// A value less than zero if this key is less than , zero if equal, or greater than zero if + /// greater. /// public readonly int CompareTo(IndexKey other) { if (_data == null) return other._data == null ? 0 : -1; if (other._data == null) return 1; - - var minLength = Math.Min(_data.Length, other._data.Length); - for (int i = 0; i < minLength; i++) - { - var cmp = _data[i].CompareTo(other._data[i]); - if (cmp != 0) - return cmp; - } - + int minLength = Math.Min(_data.Length, other._data.Length); + + for (var i = 0; i < minLength; i++) + { + int cmp = _data[i].CompareTo(other._data[i]); + if (cmp != 0) + return cmp; + } + return _data.Length.CompareTo(other._data.Length); } /// - /// Determines whether this key equals another key. + /// Determines whether this key equals another key. /// /// The key to compare with. - /// if the keys are equal; otherwise, . + /// if the keys are equal; otherwise, . public readonly bool Equals(IndexKey other) { if (_hashCode != other._hashCode) return false; - - if (_data == null) return other._data == null; - if (other._data == null) return false; - + + if (_data == null) return other._data == null; + if (other._data == null) return false; + return _data.AsSpan().SequenceEqual(other._data); } /// - public override readonly bool Equals(object? obj) => obj is IndexKey other && Equals(other); + public readonly override bool Equals(object? obj) + { + return obj is IndexKey other && Equals(other); + } /// - public override readonly int GetHashCode() => _hashCode; - - public static bool operator ==(IndexKey left, IndexKey right) => left.Equals(right); - public static bool operator !=(IndexKey left, IndexKey right) => !left.Equals(right); - public static bool operator <(IndexKey left, IndexKey right) => left.CompareTo(right) < 0; - public static bool operator >(IndexKey left, IndexKey right) => left.CompareTo(right) > 0; - public static bool operator <=(IndexKey left, IndexKey right) => left.CompareTo(right) <= 0; - public static bool operator >=(IndexKey left, IndexKey right) => left.CompareTo(right) >= 0; - - private static int ComputeHashCode(ReadOnlySpan data) - { - var hash = new HashCode(); - hash.AddBytes(data); - return hash.ToHashCode(); - } - + public readonly override int GetHashCode() + { + return _hashCode; + } + + public static bool operator ==(IndexKey left, IndexKey right) + { + return left.Equals(right); + } + + public static bool operator !=(IndexKey left, IndexKey right) + { + return !left.Equals(right); + } + + public static bool operator <(IndexKey left, IndexKey right) + { + return left.CompareTo(right) < 0; + } + + public static bool operator >(IndexKey left, IndexKey right) + { + return left.CompareTo(right) > 0; + } + + public static bool operator <=(IndexKey left, IndexKey right) + { + return left.CompareTo(right) <= 0; + } + + public static bool operator >=(IndexKey left, IndexKey right) + { + return left.CompareTo(right) >= 0; + } + + private static int ComputeHashCode(ReadOnlySpan data) + { + var hash = new HashCode(); + hash.AddBytes(data); + return hash.ToHashCode(); + } + /// - /// Creates an from a supported CLR value. + /// Creates an from a supported CLR value. /// /// The CLR type of the value. /// The value to convert. @@ -159,33 +188,35 @@ public struct IndexKey : IEquatable, IComparable public static IndexKey Create(T value) { if (value == null) return default; - - if (typeof(T) == typeof(ObjectId)) return new IndexKey((ObjectId)(object)value); - if (typeof(T) == typeof(int)) return new IndexKey((int)(object)value); - if (typeof(T) == typeof(long)) return new IndexKey((long)(object)value); - if (typeof(T) == typeof(string)) return new IndexKey((string)(object)value); - if (typeof(T) == typeof(Guid)) return new IndexKey((Guid)(object)value); - if (typeof(T) == typeof(byte[])) return new IndexKey((byte[])(object)value); - - throw new NotSupportedException($"Type {typeof(T).Name} is not supported as an IndexKey. Provide a custom mapping."); + + if (typeof(T) == typeof(ObjectId)) return new IndexKey((ObjectId)(object)value); + if (typeof(T) == typeof(int)) return new IndexKey((int)(object)value); + if (typeof(T) == typeof(long)) return new IndexKey((long)(object)value); + if (typeof(T) == typeof(string)) return new IndexKey((string)(object)value); + if (typeof(T) == typeof(Guid)) return new IndexKey((Guid)(object)value); + if (typeof(T) == typeof(byte[])) return new IndexKey((byte[])(object)value); + + throw new NotSupportedException( + $"Type {typeof(T).Name} is not supported as an IndexKey. Provide a custom mapping."); } /// - /// Converts this key to a CLR value of type . + /// Converts this key to a CLR value of type . /// /// The CLR type to read from this key. /// The converted value. public readonly T As() { if (_data == null) return default!; - - if (typeof(T) == typeof(ObjectId)) return (T)(object)new ObjectId(_data); - if (typeof(T) == typeof(int)) return (T)(object)BitConverter.ToInt32(_data); - if (typeof(T) == typeof(long)) return (T)(object)BitConverter.ToInt64(_data); - if (typeof(T) == typeof(string)) return (T)(object)System.Text.Encoding.UTF8.GetString(_data); - if (typeof(T) == typeof(Guid)) return (T)(object)new Guid(_data); - if (typeof(T) == typeof(byte[])) return (T)(object)_data; - - throw new NotSupportedException($"Type {typeof(T).Name} cannot be extracted from IndexKey. Provide a custom mapping."); - } -} + + if (typeof(T) == typeof(ObjectId)) return (T)(object)new ObjectId(_data); + if (typeof(T) == typeof(int)) return (T)(object)BitConverter.ToInt32(_data); + if (typeof(T) == typeof(long)) return (T)(object)BitConverter.ToInt64(_data); + if (typeof(T) == typeof(string)) return (T)(object)Encoding.UTF8.GetString(_data); + if (typeof(T) == typeof(Guid)) return (T)(object)new Guid(_data); + if (typeof(T) == typeof(byte[])) return (T)(object)_data; + + throw new NotSupportedException( + $"Type {typeof(T).Name} cannot be extracted from IndexKey. Provide a custom mapping."); + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/IndexOptions.cs b/src/CBDD.Core/Indexing/IndexOptions.cs index 1d6c010..f70b82c 100755 --- a/src/CBDD.Core/Indexing/IndexOptions.cs +++ b/src/CBDD.Core/Indexing/IndexOptions.cs @@ -1,121 +1,130 @@ -namespace ZB.MOM.WW.CBDD.Core.Indexing; - -/// -/// Types of indices supported -/// -public enum IndexType : byte -{ - /// B+Tree index for range queries and ordering +namespace ZB.MOM.WW.CBDD.Core.Indexing; + +/// +/// Types of indices supported +/// +public enum IndexType : byte +{ + /// B+Tree index for range queries and ordering BTree = 1, - /// Hash index for exact match lookups + /// Hash index for exact match lookups Hash = 2, - /// Unique index constraint - Unique = 3, - - /// Vector index (HNSW) for similarity search - Vector = 4, - - /// Geospatial index (R-Tree) for spatial queries - Spatial = 5 -} - -/// -/// Distance metrics for vector search -/// -public enum VectorMetric : byte -{ - /// Cosine Similarity (Standard for embeddings) - Cosine = 1, - - /// Euclidean Distance (L2) - L2 = 2, - - /// Dot Product - DotProduct = 3 -} - -/// -/// Index options and configuration. -/// Implemented as readonly struct for efficiency. -/// + /// Unique index constraint + Unique = 3, + + /// Vector index (HNSW) for similarity search + Vector = 4, + + /// Geospatial index (R-Tree) for spatial queries + Spatial = 5 +} + +/// +/// Distance metrics for vector search +/// +public enum VectorMetric : byte +{ + /// Cosine Similarity (Standard for embeddings) + Cosine = 1, + + /// Euclidean Distance (L2) + L2 = 2, + + /// Dot Product + DotProduct = 3 +} + +/// +/// Index options and configuration. +/// Implemented as readonly struct for efficiency. +/// public readonly struct IndexOptions { /// - /// Gets the configured index type. + /// Gets the configured index type. /// public IndexType Type { get; init; } /// - /// Gets a value indicating whether the index enforces uniqueness. + /// Gets a value indicating whether the index enforces uniqueness. /// public bool Unique { get; init; } /// - /// Gets the indexed field names. + /// Gets the indexed field names. /// public string[] Fields { get; init; } // Vector search options /// - /// Gets the vector dimensionality for vector indexes. + /// Gets the vector dimensionality for vector indexes. /// public int Dimensions { get; init; } /// - /// Gets the distance metric used for vector similarity. + /// Gets the distance metric used for vector similarity. /// public VectorMetric Metric { get; init; } /// - /// Gets the minimum number of graph connections per node. + /// Gets the minimum number of graph connections per node. /// public int M { get; init; } // Min number of connections per node /// - /// Gets the size of the dynamic candidate list during index construction. + /// Gets the size of the dynamic candidate list during index construction. /// public int EfConstruction { get; init; } // Size of dynamic candidate list for construction /// - /// Creates non-unique B+Tree index options. + /// Creates non-unique B+Tree index options. /// /// The indexed field names. /// The configured index options. - public static IndexOptions CreateBTree(params string[] fields) => new() + public static IndexOptions CreateBTree(params string[] fields) { - Type = IndexType.BTree, - Unique = false, - Fields = fields - }; + return new IndexOptions + { + Type = IndexType.BTree, + Unique = false, + Fields = fields + }; + } /// - /// Creates unique B+Tree index options. + /// Creates unique B+Tree index options. /// /// The indexed field names. /// The configured index options. - public static IndexOptions CreateUnique(params string[] fields) => new() + public static IndexOptions CreateUnique(params string[] fields) { - Type = IndexType.BTree, - Unique = true, - Fields = fields - }; + return new IndexOptions + { + Type = IndexType.BTree, + Unique = true, + Fields = fields + }; + } /// - /// Creates hash index options. + /// Creates hash index options. /// /// The indexed field names. /// The configured index options. - public static IndexOptions CreateHash(params string[] fields) => new() + public static IndexOptions CreateHash(params string[] fields) { - Type = IndexType.Hash, - Unique = false, - Fields = fields - }; + return new IndexOptions + { + Type = IndexType.Hash, + Unique = false, + Fields = fields + }; + } /// - /// Creates vector index options. + /// Creates vector index options. /// /// The vector dimensionality. /// The similarity metric. @@ -123,26 +132,33 @@ public readonly struct IndexOptions /// The candidate list size used during index construction. /// The indexed field names. /// The configured index options. - public static IndexOptions CreateVector(int dimensions, VectorMetric metric = VectorMetric.Cosine, int m = 16, int ef = 200, params string[] fields) => new() + public static IndexOptions CreateVector(int dimensions, VectorMetric metric = VectorMetric.Cosine, int m = 16, + int ef = 200, params string[] fields) { - Type = IndexType.Vector, - Unique = false, - Fields = fields, - Dimensions = dimensions, - Metric = metric, - M = m, - EfConstruction = ef - }; + return new IndexOptions + { + Type = IndexType.Vector, + Unique = false, + Fields = fields, + Dimensions = dimensions, + Metric = metric, + M = m, + EfConstruction = ef + }; + } /// - /// Creates spatial index options. + /// Creates spatial index options. /// /// The indexed field names. /// The configured index options. - public static IndexOptions CreateSpatial(params string[] fields) => new() + public static IndexOptions CreateSpatial(params string[] fields) { - Type = IndexType.Spatial, - Unique = false, - Fields = fields - }; -} + return new IndexOptions + { + Type = IndexType.Spatial, + Unique = false, + Fields = fields + }; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/Internal/GeoTypes.cs b/src/CBDD.Core/Indexing/Internal/GeoTypes.cs index 7336b9c..f085cec 100755 --- a/src/CBDD.Core/Indexing/Internal/GeoTypes.cs +++ b/src/CBDD.Core/Indexing/Internal/GeoTypes.cs @@ -1,90 +1,90 @@ -namespace ZB.MOM.WW.CBDD.Core.Indexing.Internal; - -/// -/// Basic spatial point (Latitude/Longitude) -/// Internal primitive for R-Tree logic. -/// +namespace ZB.MOM.WW.CBDD.Core.Indexing.Internal; + +/// +/// Basic spatial point (Latitude/Longitude) +/// Internal primitive for R-Tree logic. +/// internal record struct GeoPoint(double Latitude, double Longitude) { /// - /// Gets an empty point at coordinate origin. + /// Gets an empty point at coordinate origin. /// public static GeoPoint Empty => new(0, 0); } - -/// -/// Minimum Bounding Box (MBR) for spatial indexing -/// Internal primitive for R-Tree logic. -/// + +/// +/// Minimum Bounding Box (MBR) for spatial indexing +/// Internal primitive for R-Tree logic. +/// internal record struct GeoBox(double MinLat, double MinLon, double MaxLat, double MaxLon) { /// - /// Gets an empty bounding box sentinel value. + /// Gets an empty bounding box sentinel value. /// public static GeoBox Empty => new(double.MaxValue, double.MaxValue, double.MinValue, double.MinValue); /// - /// Determines whether this box contains the specified point. + /// Gets the area of this bounding box. + /// + public double Area => Math.Max(0, MaxLat - MinLat) * Math.Max(0, MaxLon - MinLon); + + /// + /// Determines whether this box contains the specified point. /// /// The point to test. - /// if the point is inside this box; otherwise, . + /// if the point is inside this box; otherwise, . public bool Contains(GeoPoint point) - { - return point.Latitude >= MinLat && point.Latitude <= MaxLat && - point.Longitude >= MinLon && point.Longitude <= MaxLon; - } - + { + return point.Latitude >= MinLat && point.Latitude <= MaxLat && + point.Longitude >= MinLon && point.Longitude <= MaxLon; + } + /// - /// Determines whether this box intersects another box. + /// Determines whether this box intersects another box. /// /// The other box to test. - /// if the boxes intersect; otherwise, . + /// if the boxes intersect; otherwise, . public bool Intersects(GeoBox other) - { - return !(other.MinLat > MaxLat || other.MaxLat < MinLat || - other.MinLon > MaxLon || other.MaxLon < MinLon); - } - + { + return !(other.MinLat > MaxLat || other.MaxLat < MinLat || + other.MinLon > MaxLon || other.MaxLon < MinLon); + } + /// - /// Creates a box that contains a single point. + /// Creates a box that contains a single point. /// /// The point to convert. /// A bounding box containing the specified point. public static GeoBox FromPoint(GeoPoint point) - { - return new GeoBox(point.Latitude, point.Longitude, point.Latitude, point.Longitude); - } - + { + return new GeoBox(point.Latitude, point.Longitude, point.Latitude, point.Longitude); + } + /// - /// Expands this box to include the specified point. + /// Expands this box to include the specified point. /// /// The point to include. /// A new expanded bounding box. public GeoBox ExpandTo(GeoPoint point) - { - return new GeoBox( - Math.Min(MinLat, point.Latitude), - Math.Min(MinLon, point.Longitude), - Math.Max(MaxLat, point.Latitude), - Math.Max(MaxLon, point.Longitude)); - } - + { + return new GeoBox( + Math.Min(MinLat, point.Latitude), + Math.Min(MinLon, point.Longitude), + Math.Max(MaxLat, point.Latitude), + Math.Max(MaxLon, point.Longitude)); + } + /// - /// Expands this box to include the specified box. + /// Expands this box to include the specified box. /// /// The box to include. /// A new expanded bounding box. public GeoBox ExpandTo(GeoBox other) - { - return new GeoBox( - Math.Min(MinLat, other.MinLat), - Math.Min(MinLon, other.MinLon), - Math.Max(MaxLat, other.MaxLat), - Math.Max(MaxLon, other.MaxLon)); - } - - /// - /// Gets the area of this bounding box. - /// - public double Area => Math.Max(0, MaxLat - MinLat) * Math.Max(0, MaxLon - MinLon); -} + { + return new GeoBox( + Math.Min(MinLat, other.MinLat), + Math.Min(MinLon, other.MinLon), + Math.Max(MaxLat, other.MaxLat), + Math.Max(MaxLon, other.MaxLon)); + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/InternalEntry.cs b/src/CBDD.Core/Indexing/InternalEntry.cs index b45f880..96fc979 100755 --- a/src/CBDD.Core/Indexing/InternalEntry.cs +++ b/src/CBDD.Core/Indexing/InternalEntry.cs @@ -1,27 +1,25 @@ -using ZB.MOM.WW.CBDD.Core.Indexing; - -namespace ZB.MOM.WW.CBDD.Core.Indexing; - +namespace ZB.MOM.WW.CBDD.Core.Indexing; + public struct InternalEntry { /// - /// Gets or sets the separator key. + /// Gets or sets the separator key. /// public IndexKey Key { get; set; } /// - /// Gets or sets the child page identifier. + /// Gets or sets the child page identifier. /// public uint PageId { get; set; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The separator key. /// The child page identifier. public InternalEntry(IndexKey key, uint pageId) - { - Key = key; - PageId = pageId; - } -} + { + Key = key; + PageId = pageId; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/RTreeIndex.cs b/src/CBDD.Core/Indexing/RTreeIndex.cs index f82a3a5..4a16c12 100755 --- a/src/CBDD.Core/Indexing/RTreeIndex.cs +++ b/src/CBDD.Core/Indexing/RTreeIndex.cs @@ -1,73 +1,78 @@ +using System.Buffers; +using ZB.MOM.WW.CBDD.Core.Indexing.Internal; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Indexing.Internal; -using System.Buffers; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// R-Tree Index implementation for Geospatial Indexing. -/// Uses Quadratic Split algorithm for simplicity and efficiency in paged storage. +/// R-Tree Index implementation for Geospatial Indexing. +/// Uses Quadratic Split algorithm for simplicity and efficiency in paged storage. /// -internal class RTreeIndex : IDisposable -{ - private readonly IIndexStorage _storage; - private readonly IndexOptions _options; - private uint _rootPageId; +internal class RTreeIndex : IDisposable +{ private readonly object _lock = new(); + private readonly IndexOptions _options; private readonly int _pageSize; + private readonly IIndexStorage _storage; - /// - /// Initializes a new instance of the class. - /// - /// The storage engine used for page operations. - /// The index options. - /// The root page identifier, or 0 to create a new root. - public RTreeIndex(IIndexStorage storage, IndexOptions options, uint rootPageId) - { - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - _options = options; - _rootPageId = rootPageId; + /// + /// Initializes a new instance of the class. + /// + /// The storage engine used for page operations. + /// The index options. + /// The root page identifier, or 0 to create a new root. + public RTreeIndex(IIndexStorage storage, IndexOptions options, uint rootPageId) + { + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _options = options; + RootPageId = rootPageId; _pageSize = _storage.PageSize; - if (_rootPageId == 0) - { - InitializeNewIndex(); - } - } - - /// - /// Gets the current root page identifier. - /// - public uint RootPageId => _rootPageId; + if (RootPageId == 0) InitializeNewIndex(); + } + + /// + /// Gets the current root page identifier. + /// + public uint RootPageId { get; private set; } + + /// + /// Releases resources used by the index. + /// + public void Dispose() + { + } private void InitializeNewIndex() { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { - _rootPageId = _storage.AllocatePage(); - SpatialPage.Initialize(buffer, _rootPageId, true, 0); - _storage.WritePageImmediate(_rootPageId, buffer); + RootPageId = _storage.AllocatePage(); + SpatialPage.Initialize(buffer, RootPageId, true, 0); + _storage.WritePageImmediate(RootPageId, buffer); + } + finally + { + ReturnPageBuffer(buffer); } - finally { ReturnPageBuffer(buffer); } } - /// - /// Searches for document locations whose minimum bounding rectangles intersect the specified area. - /// - /// The area to search. - /// The optional transaction context. - /// A sequence of matching document locations. - public IEnumerable Search(GeoBox area, ITransaction? transaction = null) - { - if (_rootPageId == 0) yield break; + /// + /// Searches for document locations whose minimum bounding rectangles intersect the specified area. + /// + /// The area to search. + /// The optional transaction context. + /// A sequence of matching document locations. + public IEnumerable Search(GeoBox area, ITransaction? transaction = null) + { + if (RootPageId == 0) yield break; var stack = new Stack(); - stack.Push(_rootPageId); + stack.Push(RootPageId); - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { while (stack.Count > 0) @@ -78,38 +83,37 @@ internal class RTreeIndex : IDisposable bool isLeaf = SpatialPage.GetIsLeaf(buffer); ushort count = SpatialPage.GetEntryCount(buffer); - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { SpatialPage.ReadEntry(buffer, i, out var mbr, out var pointer); if (area.Intersects(mbr)) { if (isLeaf) - { yield return pointer; - } else - { stack.Push(pointer.PageId); - } } } } } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } - /// - /// Inserts a bounding rectangle and document location into the index. - /// - /// The minimum bounding rectangle to index. - /// The document location associated with the rectangle. - /// The optional transaction context. - public void Insert(GeoBox mbr, DocumentLocation loc, ITransaction? transaction = null) - { - lock (_lock) - { - var leafPageId = ChooseLeaf(_rootPageId, mbr, transaction); + /// + /// Inserts a bounding rectangle and document location into the index. + /// + /// The minimum bounding rectangle to index. + /// The document location associated with the rectangle. + /// The optional transaction context. + public void Insert(GeoBox mbr, DocumentLocation loc, ITransaction? transaction = null) + { + lock (_lock) + { + uint leafPageId = ChooseLeaf(RootPageId, mbr, transaction); InsertIntoNode(leafPageId, mbr, loc, transaction); } } @@ -117,7 +121,7 @@ internal class RTreeIndex : IDisposable private uint ChooseLeaf(uint rootId, GeoBox mbr, ITransaction? transaction) { uint currentId = rootId; - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { while (true) @@ -127,13 +131,13 @@ internal class RTreeIndex : IDisposable ushort count = SpatialPage.GetEntryCount(buffer); uint bestChild = 0; - double minEnlargement = double.MaxValue; - double minArea = double.MaxValue; + var minEnlargement = double.MaxValue; + var minArea = double.MaxValue; - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { - SpatialPage.ReadEntry(buffer, i, out var childMbr, out var pointer); - + SpatialPage.ReadEntry(buffer, i, out var childMbr, out var pointer); + var expanded = childMbr.ExpandTo(mbr); double enlargement = expanded.Area - childMbr.Area; @@ -156,12 +160,15 @@ internal class RTreeIndex : IDisposable currentId = bestChild; } } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } private void InsertIntoNode(uint pageId, GeoBox mbr, DocumentLocation pointer, ITransaction? transaction) { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { _storage.ReadPage(pageId, transaction?.TransactionId, buffer); @@ -171,8 +178,8 @@ internal class RTreeIndex : IDisposable if (count < maxEntries) { SpatialPage.WriteEntry(buffer, count, mbr, pointer); - SpatialPage.SetEntryCount(buffer, (ushort)(count + 1)); - + SpatialPage.SetEntryCount(buffer, (ushort)(count + 1)); + if (transaction != null) _storage.WritePage(pageId, transaction.TransactionId, buffer); else @@ -186,17 +193,20 @@ internal class RTreeIndex : IDisposable SplitNode(pageId, mbr, pointer, transaction); } } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } private void UpdateMBRUpwards(uint pageId, ITransaction? transaction) { - var buffer = RentPageBuffer(); - var parentBuffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); + byte[] parentBuffer = RentPageBuffer(); try { uint currentId = pageId; - while (currentId != _rootPageId) + while (currentId != RootPageId) { _storage.ReadPage(currentId, transaction?.TransactionId, buffer); var currentMbr = SpatialPage.CalculateMBR(buffer); @@ -206,9 +216,9 @@ internal class RTreeIndex : IDisposable _storage.ReadPage(parentId, transaction?.TransactionId, parentBuffer); ushort count = SpatialPage.GetEntryCount(parentBuffer); - bool changed = false; + var changed = false; - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { SpatialPage.ReadEntry(parentBuffer, i, out var mbr, out var pointer); if (pointer.PageId == currentId) @@ -218,6 +228,7 @@ internal class RTreeIndex : IDisposable SpatialPage.WriteEntry(parentBuffer, i, currentMbr, pointer); changed = true; } + break; } } @@ -232,17 +243,17 @@ internal class RTreeIndex : IDisposable currentId = parentId; } } - finally - { - ReturnPageBuffer(buffer); + finally + { + ReturnPageBuffer(buffer); ReturnPageBuffer(parentBuffer); } } private void SplitNode(uint pageId, GeoBox newMbr, DocumentLocation newPointer, ITransaction? transaction) { - var buffer = RentPageBuffer(); - var newBuffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); + byte[] newBuffer = RentPageBuffer(); try { _storage.ReadPage(pageId, transaction?.TransactionId, buffer); @@ -253,11 +264,12 @@ internal class RTreeIndex : IDisposable // Collect all entries including the new one var entries = new List<(GeoBox Mbr, DocumentLocation Pointer)>(); - for (int i = 0; i < count; i++) + for (var i = 0; i < count; i++) { SpatialPage.ReadEntry(buffer, i, out var m, out var p); entries.Add((m, p)); } + entries.Add((newMbr, newPointer)); // Pick Seeds @@ -277,8 +289,8 @@ internal class RTreeIndex : IDisposable SpatialPage.WriteEntry(newBuffer, 0, seed2.Mbr, seed2.Pointer); SpatialPage.SetEntryCount(newBuffer, 1); - GeoBox mbr1 = seed1.Mbr; - GeoBox mbr2 = seed2.Mbr; + var mbr1 = seed1.Mbr; + var mbr2 = seed2.Mbr; // Distribute remaining entries while (entries.Count > 0) @@ -320,23 +332,23 @@ internal class RTreeIndex : IDisposable } // Propagate split upwards - if (pageId == _rootPageId) + if (pageId == RootPageId) { // New Root uint newRootId = _storage.AllocatePage(); SpatialPage.Initialize(buffer, newRootId, false, (byte)(level + 1)); SpatialPage.WriteEntry(buffer, 0, mbr1, new DocumentLocation(pageId, 0)); SpatialPage.WriteEntry(buffer, 1, mbr2, new DocumentLocation(newPageId, 0)); - SpatialPage.SetEntryCount(buffer, 2); - + SpatialPage.SetEntryCount(buffer, 2); + if (transaction != null) _storage.WritePage(newRootId, transaction.TransactionId, buffer); else _storage.WritePageImmediate(newRootId, buffer); - _rootPageId = newRootId; - - // Update parent pointers + RootPageId = newRootId; + + // Update parent pointers UpdateParentPointer(pageId, newRootId, transaction); UpdateParentPointer(newPageId, newRootId, transaction); } @@ -347,16 +359,16 @@ internal class RTreeIndex : IDisposable UpdateMBRUpwards(pageId, transaction); } } - finally - { - ReturnPageBuffer(buffer); + finally + { + ReturnPageBuffer(buffer); ReturnPageBuffer(newBuffer); } } private void UpdateParentPointer(uint pageId, uint parentId, ITransaction? transaction) { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { _storage.ReadPage(pageId, transaction?.TransactionId, buffer); @@ -366,27 +378,29 @@ internal class RTreeIndex : IDisposable else _storage.WritePageImmediate(pageId, buffer); } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } - private void PickSeeds(List<(GeoBox Mbr, DocumentLocation Pointer)> entries, out (GeoBox Mbr, DocumentLocation Pointer) s1, out (GeoBox Mbr, DocumentLocation Pointer) s2) + private void PickSeeds(List<(GeoBox Mbr, DocumentLocation Pointer)> entries, + out (GeoBox Mbr, DocumentLocation Pointer) s1, out (GeoBox Mbr, DocumentLocation Pointer) s2) { - double maxWaste = double.MinValue; + var maxWaste = double.MinValue; s1 = entries[0]; s2 = entries[1]; - for (int i = 0; i < entries.Count; i++) + for (var i = 0; i < entries.Count; i++) + for (int j = i + 1; j < entries.Count; j++) { - for (int j = i + 1; j < entries.Count; j++) + var combined = entries[i].Mbr.ExpandTo(entries[j].Mbr); + double waste = combined.Area - entries[i].Mbr.Area - entries[j].Mbr.Area; + if (waste > maxWaste) { - var combined = entries[i].Mbr.ExpandTo(entries[j].Mbr); - double waste = combined.Area - entries[i].Mbr.Area - entries[j].Mbr.Area; - if (waste > maxWaste) - { - maxWaste = waste; - s1 = entries[i]; - s2 = entries[j]; - } + maxWaste = waste; + s1 = entries[i]; + s2 = entries[j]; } } } @@ -400,11 +414,4 @@ internal class RTreeIndex : IDisposable { ArrayPool.Shared.Return(buffer); } - - /// - /// Releases resources used by the index. - /// - public void Dispose() - { - } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/SpatialMath.cs b/src/CBDD.Core/Indexing/SpatialMath.cs index 97056f0..d0f3a11 100755 --- a/src/CBDD.Core/Indexing/SpatialMath.cs +++ b/src/CBDD.Core/Indexing/SpatialMath.cs @@ -1,22 +1,25 @@ -using ZB.MOM.WW.CBDD.Core.Indexing.Internal; - -namespace ZB.MOM.WW.CBDD.Core.Indexing; - -public static class SpatialMath -{ - private const double EarthRadiusKm = 6371.0; - +using ZB.MOM.WW.CBDD.Core.Indexing.Internal; + +namespace ZB.MOM.WW.CBDD.Core.Indexing; + +public static class SpatialMath +{ + private const double EarthRadiusKm = 6371.0; + /// - /// Calculates distance between two points on Earth using Haversine formula. - /// Result in kilometers. + /// Calculates distance between two points on Earth using Haversine formula. + /// Result in kilometers. /// /// The first point. /// The second point. /// The distance in kilometers. - internal static double DistanceKm(GeoPoint p1, GeoPoint p2) => DistanceKm(p1.Latitude, p1.Longitude, p2.Latitude, p2.Longitude); + internal static double DistanceKm(GeoPoint p1, GeoPoint p2) + { + return DistanceKm(p1.Latitude, p1.Longitude, p2.Latitude, p2.Longitude); + } /// - /// Calculates distance between two coordinates on Earth using Haversine formula. + /// Calculates distance between two coordinates on Earth using Haversine formula. /// /// The latitude of the first point. /// The longitude of the first point. @@ -27,34 +30,40 @@ public static class SpatialMath { double dLat = ToRadians(lat2 - lat1); double dLon = ToRadians(lon2 - lon1); - - double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + - Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) * - Math.Sin(dLon / 2) * Math.Sin(dLon / 2); - - double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); - return EarthRadiusKm * c; - } - + + double a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2) + + Math.Cos(ToRadians(lat1)) * Math.Cos(ToRadians(lat2)) * + Math.Sin(dLon / 2) * Math.Sin(dLon / 2); + + double c = 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a)); + return EarthRadiusKm * c; + } + /// - /// Creates a Bounding Box (MBR) centered at a point with a given radius in km. + /// Creates a Bounding Box (MBR) centered at a point with a given radius in km. /// /// The center point. /// The radius in kilometers. /// The bounding box. - internal static GeoBox BoundingBox(GeoPoint center, double radiusKm) => BoundingBox(center.Latitude, center.Longitude, radiusKm); + internal static GeoBox BoundingBox(GeoPoint center, double radiusKm) + { + return BoundingBox(center.Latitude, center.Longitude, radiusKm); + } /// - /// Creates a bounding box from a coordinate and radius. + /// Creates a bounding box from a coordinate and radius. /// /// The center latitude. /// The center longitude. /// The radius in kilometers. /// The bounding box. - internal static GeoBox InternalBoundingBox(double lat, double lon, double radiusKm) => BoundingBox(lat, lon, radiusKm); + internal static GeoBox InternalBoundingBox(double lat, double lon, double radiusKm) + { + return BoundingBox(lat, lon, radiusKm); + } /// - /// Creates a bounding box centered at a coordinate with a given radius in kilometers. + /// Creates a bounding box centered at a coordinate with a given radius in kilometers. /// /// The center latitude. /// The center longitude. @@ -64,14 +73,21 @@ public static class SpatialMath { double dLat = ToDegrees(radiusKm / EarthRadiusKm); double dLon = ToDegrees(radiusKm / (EarthRadiusKm * Math.Cos(ToRadians(lat)))); - - return new GeoBox( - lat - dLat, - lon - dLon, - lat + dLat, - lon + dLon); - } - - private static double ToRadians(double degrees) => degrees * Math.PI / 180.0; - private static double ToDegrees(double radians) => radians * 180.0 / Math.PI; -} + + return new GeoBox( + lat - dLat, + lon - dLon, + lat + dLat, + lon + dLon); + } + + private static double ToRadians(double degrees) + { + return degrees * Math.PI / 180.0; + } + + private static double ToDegrees(double radians) + { + return radians * 180.0 / Math.PI; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/VectorMath.cs b/src/CBDD.Core/Indexing/VectorMath.cs index 054306d..7447d75 100755 --- a/src/CBDD.Core/Indexing/VectorMath.cs +++ b/src/CBDD.Core/Indexing/VectorMath.cs @@ -1,23 +1,21 @@ -using System.Runtime.Intrinsics; -using System.Runtime.Intrinsics.X86; -using System.Runtime.InteropServices; using System.Numerics; +using System.Runtime.InteropServices; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// Optimized vector math utilities using SIMD if available. +/// Optimized vector math utilities using SIMD if available. /// -public static class VectorMath -{ - /// - /// Computes vector distance according to the selected metric. - /// - /// The first vector. - /// The second vector. - /// The metric used to compute distance. - /// The distance value for the selected metric. - public static float Distance(ReadOnlySpan v1, ReadOnlySpan v2, VectorMetric metric) +public static class VectorMath +{ + /// + /// Computes vector distance according to the selected metric. + /// + /// The first vector. + /// The second vector. + /// The metric used to compute distance. + /// The distance value for the selected metric. + public static float Distance(ReadOnlySpan v1, ReadOnlySpan v2, VectorMetric metric) { return metric switch { @@ -28,13 +26,13 @@ public static class VectorMath }; } - /// - /// Computes cosine similarity between two vectors. - /// - /// The first vector. - /// The second vector. - /// The cosine similarity in the range [-1, 1]. - public static float CosineSimilarity(ReadOnlySpan v1, ReadOnlySpan v2) + /// + /// Computes cosine similarity between two vectors. + /// + /// The first vector. + /// The second vector. + /// The cosine similarity in the range [-1, 1]. + public static float CosineSimilarity(ReadOnlySpan v1, ReadOnlySpan v2) { float dot = DotProduct(v1, v2); float mag1 = DotProduct(v1, v1); @@ -44,19 +42,19 @@ public static class VectorMath return dot / (MathF.Sqrt(mag1) * MathF.Sqrt(mag2)); } - /// - /// Computes the dot product of two vectors. - /// - /// The first vector. - /// The second vector. - /// The dot product value. - public static float DotProduct(ReadOnlySpan v1, ReadOnlySpan v2) + /// + /// Computes the dot product of two vectors. + /// + /// The first vector. + /// The second vector. + /// The dot product value. + public static float DotProduct(ReadOnlySpan v1, ReadOnlySpan v2) { if (v1.Length != v2.Length) throw new ArgumentException("Vectors must have same length"); float dot = 0; - int i = 0; + var i = 0; // SIMD Optimization for .NET if (Vector.IsHardwareAccelerated && v1.Length >= Vector.Count) @@ -65,37 +63,31 @@ public static class VectorMath var v1Span = MemoryMarshal.Cast>(v1); var v2Span = MemoryMarshal.Cast>(v2); - foreach (var chunk in Enumerable.Range(0, v1Span.Length)) - { - vDot += v1Span[chunk] * v2Span[chunk]; - } - + foreach (int chunk in Enumerable.Range(0, v1Span.Length)) vDot += v1Span[chunk] * v2Span[chunk]; + dot = Vector.Dot(vDot, Vector.One); i = v1Span.Length * Vector.Count; } // Remaining elements - for (; i < v1.Length; i++) - { - dot += v1[i] * v2[i]; - } + for (; i < v1.Length; i++) dot += v1[i] * v2[i]; return dot; } - /// - /// Computes squared Euclidean distance between two vectors. - /// - /// The first vector. - /// The second vector. - /// The squared Euclidean distance. - public static float EuclideanDistanceSquared(ReadOnlySpan v1, ReadOnlySpan v2) + /// + /// Computes squared Euclidean distance between two vectors. + /// + /// The first vector. + /// The second vector. + /// The squared Euclidean distance. + public static float EuclideanDistanceSquared(ReadOnlySpan v1, ReadOnlySpan v2) { if (v1.Length != v2.Length) throw new ArgumentException("Vectors must have same length"); float dist = 0; - int i = 0; + var i = 0; if (Vector.IsHardwareAccelerated && v1.Length >= Vector.Count) { @@ -103,7 +95,7 @@ public static class VectorMath var v1Span = MemoryMarshal.Cast>(v1); var v2Span = MemoryMarshal.Cast>(v2); - foreach (var chunk in Enumerable.Range(0, v1Span.Length)) + foreach (int chunk in Enumerable.Range(0, v1Span.Length)) { var diff = v1Span[chunk] - v2Span[chunk]; vDist += diff * diff; @@ -121,4 +113,4 @@ public static class VectorMath return dist; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/VectorSearchExtensions.cs b/src/CBDD.Core/Indexing/VectorSearchExtensions.cs index 8b18960..e16bd4f 100755 --- a/src/CBDD.Core/Indexing/VectorSearchExtensions.cs +++ b/src/CBDD.Core/Indexing/VectorSearchExtensions.cs @@ -3,28 +3,34 @@ namespace ZB.MOM.WW.CBDD.Core.Indexing; public static class VectorSearchExtensions { /// - /// Performs a similarity search on a vector property. - /// This method is a marker for the LINQ query provider and is optimized using HNSW indexes if available. + /// Performs a similarity search on a vector property. + /// This method is a marker for the LINQ query provider and is optimized using HNSW indexes if available. /// /// The vector property of the entity. /// The query vector to compare against. /// Number of nearest neighbors to return. - /// True if the document is part of the top-k results (always returns true when evaluated in memory for compilation purposes). + /// + /// True if the document is part of the top-k results (always returns true when evaluated in memory for + /// compilation purposes). + /// public static bool VectorSearch(this float[] vector, float[] query, int k) { return true; } - /// - /// Performs a similarity search on a collection of vector properties. - /// Used for entities with multiple vectors per document. - /// - /// The vector collection of the entity. - /// The query vector to compare against. - /// Number of nearest neighbors to return. - /// True if the document is part of the top-k results (always returns true when evaluated in memory for compilation purposes). - public static bool VectorSearch(this IEnumerable vectors, float[] query, int k) - { - return true; - } -} + /// + /// Performs a similarity search on a collection of vector properties. + /// Used for entities with multiple vectors per document. + /// + /// The vector collection of the entity. + /// The query vector to compare against. + /// Number of nearest neighbors to return. + /// + /// True if the document is part of the top-k results (always returns true when evaluated in memory for + /// compilation purposes). + /// + public static bool VectorSearch(this IEnumerable vectors, float[] query, int k) + { + return true; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Indexing/VectorSearchIndex.cs b/src/CBDD.Core/Indexing/VectorSearchIndex.cs index 53f609a..bc91bc0 100755 --- a/src/CBDD.Core/Indexing/VectorSearchIndex.cs +++ b/src/CBDD.Core/Indexing/VectorSearchIndex.cs @@ -1,87 +1,85 @@ +using System.Buffers; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; -using System.Collections.Generic; namespace ZB.MOM.WW.CBDD.Core.Indexing; /// -/// HNSW (Hierarchical Navigable Small World) index implementation. -/// Handles multi-vector indexing and similarity searches. +/// HNSW (Hierarchical Navigable Small World) index implementation. +/// Handles multi-vector indexing and similarity searches. /// -public sealed class VectorSearchIndex -{ - private struct NodeReference - { - public uint PageId; - public int NodeIndex; - public int MaxLevel; - } - - private readonly IIndexStorage _storage; +public sealed class VectorSearchIndex +{ private readonly IndexOptions _options; - private uint _rootPageId; private readonly Random _random = new(42); - /// - /// Initializes a new vector search index. - /// - /// The storage engine used by the index. - /// Index configuration options. - /// Optional existing root page identifier. - public VectorSearchIndex(StorageEngine storage, IndexOptions options, uint rootPageId = 0) - : this((IStorageEngine)storage, options, rootPageId) - { - } - - /// - /// Initializes a new vector search index. - /// - /// The index storage abstraction used by the index. - /// Index configuration options. - /// Optional existing root page identifier. - internal VectorSearchIndex(IIndexStorage storage, IndexOptions options, uint rootPageId = 0) - { - _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - _options = options; - _rootPageId = rootPageId; - } + private readonly IIndexStorage _storage; - /// - /// Gets the root page identifier of the index. - /// - public uint RootPageId => _rootPageId; + /// + /// Initializes a new vector search index. + /// + /// The storage engine used by the index. + /// Index configuration options. + /// Optional existing root page identifier. + public VectorSearchIndex(StorageEngine storage, IndexOptions options, uint rootPageId = 0) + : this((IStorageEngine)storage, options, rootPageId) + { + } - /// - /// Inserts a vector and its document location into the index. - /// - /// The vector values to index. - /// The document location associated with the vector. - /// Optional transaction context. - public void Insert(float[] vector, DocumentLocation docLocation, ITransaction? transaction = null) + /// + /// Initializes a new vector search index. + /// + /// The index storage abstraction used by the index. + /// Index configuration options. + /// Optional existing root page identifier. + internal VectorSearchIndex(IIndexStorage storage, IndexOptions options, uint rootPageId = 0) + { + _storage = storage ?? throw new ArgumentNullException(nameof(storage)); + _options = options; + RootPageId = rootPageId; + } + + /// + /// Gets the root page identifier of the index. + /// + public uint RootPageId { get; private set; } + + /// + /// Inserts a vector and its document location into the index. + /// + /// The vector values to index. + /// The document location associated with the vector. + /// Optional transaction context. + public void Insert(float[] vector, DocumentLocation docLocation, ITransaction? transaction = null) { if (vector.Length != _options.Dimensions) - throw new ArgumentException($"Vector dimension mismatch. Expected {_options.Dimensions}, got {vector.Length}"); + throw new ArgumentException( + $"Vector dimension mismatch. Expected {_options.Dimensions}, got {vector.Length}"); // 1. Determine level for new node int targetLevel = GetRandomLevel(); // 2. If index is empty, create first page and first node - if (_rootPageId == 0) + if (RootPageId == 0) { - _rootPageId = CreateNewPage(transaction); - var pageBuffer = RentPageBuffer(); + RootPageId = CreateNewPage(transaction); + byte[] pageBuffer = RentPageBuffer(); try { - _storage.ReadPage(_rootPageId, transaction?.TransactionId, pageBuffer); + _storage.ReadPage(RootPageId, transaction?.TransactionId, pageBuffer); VectorPage.WriteNode(pageBuffer, 0, docLocation, targetLevel, vector, _options.Dimensions); - VectorPage.IncrementNodeCount(pageBuffer); // Helper needs to be added or handled - + VectorPage.IncrementNodeCount(pageBuffer); // Helper needs to be added or handled + if (transaction != null) - _storage.WritePage(_rootPageId, transaction.TransactionId, pageBuffer); + _storage.WritePage(RootPageId, transaction.TransactionId, pageBuffer); else - _storage.WritePageImmediate(_rootPageId, pageBuffer); + _storage.WritePageImmediate(RootPageId, pageBuffer); } - finally { ReturnPageBuffer(pageBuffer); } + finally + { + ReturnPageBuffer(pageBuffer); + } + return; } @@ -92,9 +90,7 @@ public sealed class VectorSearchIndex // 4. Greedy search down to targetLevel+1 for (int l = entryPoint.MaxLevel; l > targetLevel; l--) - { currentPoint = GreedySearch(currentPoint, vector, l, transaction); - } // 5. Create the new node var newNode = AllocateNode(vector, docLocation, targetLevel, transaction); @@ -103,25 +99,20 @@ public sealed class VectorSearchIndex for (int l = Math.Min(targetLevel, entryPoint.MaxLevel); l >= 0; l--) { var neighbors = SearchLayer(currentPoint, vector, _options.EfConstruction, l, transaction); - var selectedNeighbors = SelectNeighbors(neighbors, vector, _options.M, l, transaction); - - foreach (var neighbor in selectedNeighbors) - { - AddBidirectionalLink(newNode, neighbor, l, transaction); - } - - // Move currentPoint down for next level if available + var selectedNeighbors = SelectNeighbors(neighbors, vector, _options.M, l, transaction); + + foreach (var neighbor in selectedNeighbors) AddBidirectionalLink(newNode, neighbor, l, transaction); + + // Move currentPoint down for next level if available currentPoint = GreedySearch(currentPoint, vector, l, transaction); } // 7. Update entry point if new node is higher - if (targetLevel > entryPoint.MaxLevel) - { - UpdateEntryPoint(newNode, transaction); - } + if (targetLevel > entryPoint.MaxLevel) UpdateEntryPoint(newNode, transaction); } - private IEnumerable SelectNeighbors(IEnumerable candidates, float[] query, int m, int level, ITransaction? transaction) + private IEnumerable SelectNeighbors(IEnumerable candidates, float[] query, int m, + int level, ITransaction? transaction) { // Simple heuristic: just take top M nearest. // HNSW Paper suggests more complex heuristic to maintain connectivity diversity. @@ -136,20 +127,20 @@ public sealed class VectorSearchIndex private void Link(NodeReference from, NodeReference to, int level, ITransaction? transaction) { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { _storage.ReadPage(from.PageId, transaction?.TransactionId, buffer); - var links = VectorPage.GetLinksSpan(buffer, from.NodeIndex, level, _options.Dimensions, _options.M); - - // Find first empty slot (PageId == 0) - for (int i = 0; i < links.Length; i += 6) + var links = VectorPage.GetLinksSpan(buffer, from.NodeIndex, level, _options.Dimensions, _options.M); + + // Find first empty slot (PageId == 0) + for (var i = 0; i < links.Length; i += 6) { var existing = DocumentLocation.ReadFrom(links.Slice(i, 6)); if (existing.PageId == 0) { - new DocumentLocation(to.PageId, (ushort)to.NodeIndex).WriteTo(links.Slice(i, 6)); - + new DocumentLocation(to.PageId, (ushort)to.NodeIndex).WriteTo(links.Slice(i, 6)); + if (transaction != null) _storage.WritePage(from.PageId, transaction.TransactionId, buffer); else @@ -160,7 +151,10 @@ public sealed class VectorSearchIndex // If full, we should technically prune or redistribute links as per HNSW paper. // For now, we assume M is large enough or we skip (limited connectivity). } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } private NodeReference AllocateNode(float[] vector, DocumentLocation docLoc, int level, ITransaction? transaction) @@ -168,24 +162,27 @@ public sealed class VectorSearchIndex // Find a page with space or create new // For simplicity, we search for a page with available slots or append to a new one. // Implementation omitted for brevity but required for full persistence. - uint pageId = _rootPageId; // Placeholder: need allocation strategy - int index = 0; - - var buffer = RentPageBuffer(); + uint pageId = RootPageId; // Placeholder: need allocation strategy + var index = 0; + + byte[] buffer = RentPageBuffer(); try - { - _storage.ReadPage(pageId, transaction?.TransactionId, buffer); - index = VectorPage.GetNodeCount(buffer); - VectorPage.WriteNode(buffer, index, docLoc, level, vector, _options.Dimensions); - VectorPage.IncrementNodeCount(buffer); - + { + _storage.ReadPage(pageId, transaction?.TransactionId, buffer); + index = VectorPage.GetNodeCount(buffer); + VectorPage.WriteNode(buffer, index, docLoc, level, vector, _options.Dimensions); + VectorPage.IncrementNodeCount(buffer); + if (transaction != null) - _storage.WritePage(pageId, transaction.TransactionId, buffer); + _storage.WritePage(pageId, transaction.TransactionId, buffer); else _storage.WritePageImmediate(pageId, buffer); } - finally { ReturnPageBuffer(buffer); } - + finally + { + ReturnPageBuffer(buffer); + } + return new NodeReference { PageId = pageId, NodeIndex = index, MaxLevel = level }; } @@ -197,7 +194,7 @@ public sealed class VectorSearchIndex private NodeReference GreedySearch(NodeReference entryPoint, float[] query, int level, ITransaction? transaction) { - bool changed = true; + var changed = true; var current = entryPoint; float currentDist = VectorMath.Distance(query, LoadVector(current, transaction), _options.Metric); @@ -215,10 +212,12 @@ public sealed class VectorSearchIndex } } } + return current; } - private IEnumerable SearchLayer(NodeReference entryPoint, float[] query, int ef, int level, ITransaction? transaction) + private IEnumerable SearchLayer(NodeReference entryPoint, float[] query, int ef, int level, + ITransaction? transaction) { var visited = new HashSet(); var candidates = new PriorityQueue(); @@ -233,14 +232,13 @@ public sealed class VectorSearchIndex { float d_c = 0; candidates.TryPeek(out var c, out d_c); - result.TryPeek(out var f, out var d_f); - + result.TryPeek(out var f, out float d_f); + if (d_c > -d_f) break; candidates.Dequeue(); foreach (var e in GetNeighbors(c, level, transaction)) - { if (!visited.Contains(e)) { visited.Add(e); @@ -254,7 +252,6 @@ public sealed class VectorSearchIndex if (result.Count > ef) result.Dequeue(); } } - } } // Convert result to list (ordered by distance) @@ -268,52 +265,53 @@ public sealed class VectorSearchIndex { // For now, assume a fixed location or track it in page 0 of index // TODO: Real implementation - return new NodeReference { PageId = _rootPageId, NodeIndex = 0, MaxLevel = 0 }; + return new NodeReference { PageId = RootPageId, NodeIndex = 0, MaxLevel = 0 }; } private float[] LoadVector(NodeReference node, ITransaction? transaction) { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { _storage.ReadPage(node.PageId, transaction?.TransactionId, buffer); - float[] vector = new float[_options.Dimensions]; + var vector = new float[_options.Dimensions]; VectorPage.ReadNodeData(buffer, node.NodeIndex, out _, out _, vector); return vector; } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } - /// - /// Searches the index for the nearest vectors to the query. - /// - /// The query vector. - /// The number of nearest results to return. - /// The search breadth parameter. - /// Optional transaction context. - /// The nearest vector search results. - public IEnumerable Search(float[] query, int k, int efSearch = 100, ITransaction? transaction = null) + /// + /// Searches the index for the nearest vectors to the query. + /// + /// The query vector. + /// The number of nearest results to return. + /// The search breadth parameter. + /// Optional transaction context. + /// The nearest vector search results. + public IEnumerable Search(float[] query, int k, int efSearch = 100, + ITransaction? transaction = null) { - if (_rootPageId == 0) yield break; + if (RootPageId == 0) yield break; var entryPoint = GetEntryPoint(); var currentPoint = entryPoint; // 1. Greedy search through higher layers to find entry point for level 0 - for (int l = entryPoint.MaxLevel; l > 0; l--) - { - currentPoint = GreedySearch(currentPoint, query, l, transaction); - } + for (int l = entryPoint.MaxLevel; l > 0; l--) currentPoint = GreedySearch(currentPoint, query, l, transaction); // 2. Comprehensive search on level 0 - var nearest = SearchLayer(currentPoint, query, Math.Max(efSearch, k), 0, transaction); - - // 3. Return top-k results - int count = 0; + var nearest = SearchLayer(currentPoint, query, Math.Max(efSearch, k), 0, transaction); + + // 3. Return top-k results + var count = 0; foreach (var node in nearest) { - if (count++ >= k) break; - + if (count++ >= k) break; + float dist = VectorMath.Distance(query, LoadVector(node, transaction), _options.Metric); var loc = LoadDocumentLocation(node, transaction); yield return new VectorSearchResult(loc, dist); @@ -322,34 +320,41 @@ public sealed class VectorSearchIndex private DocumentLocation LoadDocumentLocation(NodeReference node, ITransaction? transaction) { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { _storage.ReadPage(node.PageId, transaction?.TransactionId, buffer); VectorPage.ReadNodeData(buffer, node.NodeIndex, out var loc, out _, new float[0]); // Vector not needed here return loc; } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } private IEnumerable GetNeighbors(NodeReference node, int level, ITransaction? transaction) { - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); var results = new List(); try { _storage.ReadPage(node.PageId, transaction?.TransactionId, buffer); - var links = VectorPage.GetLinksSpan(buffer, node.NodeIndex, level, _options.Dimensions, _options.M); - - for (int i = 0; i < links.Length; i += 6) + var links = VectorPage.GetLinksSpan(buffer, node.NodeIndex, level, _options.Dimensions, _options.M); + + for (var i = 0; i < links.Length; i += 6) { var loc = DocumentLocation.ReadFrom(links.Slice(i, 6)); - if (loc.PageId == 0) break; // End of links - + if (loc.PageId == 0) break; // End of links + results.Add(new NodeReference { PageId = loc.PageId, NodeIndex = loc.SlotIndex }); } } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } + return results; } @@ -357,29 +362,43 @@ public sealed class VectorSearchIndex { // Probability p = 1/M for each level double p = 1.0 / _options.M; - int level = 0; - while (_random.NextDouble() < p && level < 15) - { - level++; - } + var level = 0; + while (_random.NextDouble() < p && level < 15) level++; return level; } private uint CreateNewPage(ITransaction? transaction) { uint pageId = _storage.AllocatePage(); - var buffer = RentPageBuffer(); + byte[] buffer = RentPageBuffer(); try { VectorPage.Initialize(buffer, pageId, _options.Dimensions, _options.M); _storage.WritePageImmediate(pageId, buffer); return pageId; } - finally { ReturnPageBuffer(buffer); } + finally + { + ReturnPageBuffer(buffer); + } } - private byte[] RentPageBuffer() => System.Buffers.ArrayPool.Shared.Rent(_storage.PageSize); - private void ReturnPageBuffer(byte[] buffer) => System.Buffers.ArrayPool.Shared.Return(buffer); + private byte[] RentPageBuffer() + { + return ArrayPool.Shared.Rent(_storage.PageSize); + } + + private void ReturnPageBuffer(byte[] buffer) + { + ArrayPool.Shared.Return(buffer); + } + + private struct NodeReference + { + public uint PageId; + public int NodeIndex; + public int MaxLevel; + } } -public record struct VectorSearchResult(DocumentLocation Location, float Distance); +public record struct VectorSearchResult(DocumentLocation Location, float Distance); \ No newline at end of file diff --git a/src/CBDD.Core/Metadata/EntityTypeBuilder.cs b/src/CBDD.Core/Metadata/EntityTypeBuilder.cs index f4ff0df..13c6d52 100755 --- a/src/CBDD.Core/Metadata/EntityTypeBuilder.cs +++ b/src/CBDD.Core/Metadata/EntityTypeBuilder.cs @@ -1,42 +1,42 @@ -using ZB.MOM.WW.CBDD.Core.Indexing; -using System.Linq.Expressions; - -namespace ZB.MOM.WW.CBDD.Core.Metadata; - +using System.Linq.Expressions; +using ZB.MOM.WW.CBDD.Core.Indexing; + +namespace ZB.MOM.WW.CBDD.Core.Metadata; + public class EntityTypeBuilder where T : class { /// - /// Gets the configured collection name for the entity type. + /// Gets the configured collection name for the entity type. /// public string? CollectionName { get; private set; } /// - /// Gets the configured indexes for the entity type. + /// Gets the configured indexes for the entity type. /// public List> Indexes { get; } = new(); /// - /// Gets the primary key selector expression. + /// Gets the primary key selector expression. /// public LambdaExpression? PrimaryKeySelector { get; private set; } /// - /// Gets a value indicating whether the primary key value is generated on add. + /// Gets a value indicating whether the primary key value is generated on add. /// public bool ValueGeneratedOnAdd { get; private set; } /// - /// Gets the configured primary key property name. + /// Gets the configured primary key property name. /// public string? PrimaryKeyName { get; private set; } /// - /// Gets the configured property converter types keyed by property name. + /// Gets the configured property converter types keyed by property name. /// public Dictionary PropertyConverters { get; } = new(); /// - /// Sets the collection name for the entity type. + /// Sets the collection name for the entity type. /// /// The collection name. /// The current entity type builder. @@ -47,21 +47,22 @@ public class EntityTypeBuilder where T : class } /// - /// Adds an index for the specified key selector. + /// Adds an index for the specified key selector. /// /// The key type. /// The key selector expression. /// The optional index name. /// A value indicating whether the index is unique. /// The current entity type builder. - public EntityTypeBuilder HasIndex(Expression> keySelector, string? name = null, bool unique = false) + public EntityTypeBuilder HasIndex(Expression> keySelector, string? name = null, + bool unique = false) { Indexes.Add(new IndexBuilder(keySelector, name, unique)); return this; } /// - /// Adds a vector index for the specified key selector. + /// Adds a vector index for the specified key selector. /// /// The key type. /// The key selector expression. @@ -69,14 +70,15 @@ public class EntityTypeBuilder where T : class /// The vector similarity metric. /// The optional index name. /// The current entity type builder. - public EntityTypeBuilder HasVectorIndex(Expression> keySelector, int dimensions, VectorMetric metric = VectorMetric.Cosine, string? name = null) + public EntityTypeBuilder HasVectorIndex(Expression> keySelector, int dimensions, + VectorMetric metric = VectorMetric.Cosine, string? name = null) { Indexes.Add(new IndexBuilder(keySelector, name, false, IndexType.Vector, dimensions, metric)); return this; } /// - /// Adds a spatial index for the specified key selector. + /// Adds a spatial index for the specified key selector. /// /// The key type. /// The key selector expression. @@ -89,7 +91,7 @@ public class EntityTypeBuilder where T : class } /// - /// Sets the primary key selector for the entity type. + /// Sets the primary key selector for the entity type. /// /// The key type. /// The primary key selector expression. @@ -102,38 +104,35 @@ public class EntityTypeBuilder where T : class } /// - /// Configures a converter for the primary key property. + /// Configures a converter for the primary key property. /// /// The converter type. /// The current entity type builder. public EntityTypeBuilder HasConversion() { - if (!string.IsNullOrEmpty(PrimaryKeyName)) - { - PropertyConverters[PrimaryKeyName] = typeof(TConverter); - } + if (!string.IsNullOrEmpty(PrimaryKeyName)) PropertyConverters[PrimaryKeyName] = typeof(TConverter); return this; } /// - /// Configures a specific property on the entity type. + /// Configures a specific property on the entity type. /// /// The property type. /// The property expression. /// A builder for the selected property. public PropertyBuilder Property(Expression> propertyExpression) { - var propertyName = ExpressionAnalyzer.ExtractPropertyPaths(propertyExpression).FirstOrDefault(); + string? propertyName = ExpressionAnalyzer.ExtractPropertyPaths(propertyExpression).FirstOrDefault(); return new PropertyBuilder(this, propertyName); } - - public class PropertyBuilder - { + + public class PropertyBuilder + { private readonly EntityTypeBuilder _parent; private readonly string? _propertyName; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The parent entity type builder. /// The property name. @@ -144,68 +143,32 @@ public class EntityTypeBuilder where T : class } /// - /// Marks the configured property as value generated on add. + /// Marks the configured property as value generated on add. /// /// The current property builder. public PropertyBuilder ValueGeneratedOnAdd() { - if (_propertyName == _parent.PrimaryKeyName) - { - _parent.ValueGeneratedOnAdd = true; - } + if (_propertyName == _parent.PrimaryKeyName) _parent.ValueGeneratedOnAdd = true; return this; } /// - /// Configures a converter for the configured property. + /// Configures a converter for the configured property. /// /// The converter type. /// The current property builder. public PropertyBuilder HasConversion() { - if (!string.IsNullOrEmpty(_propertyName)) - { - _parent.PropertyConverters[_propertyName] = typeof(TConverter); - } - return this; - } - } -} - + if (!string.IsNullOrEmpty(_propertyName)) _parent.PropertyConverters[_propertyName] = typeof(TConverter); + return this; + } + } +} + public class IndexBuilder { /// - /// Gets the index key selector expression. - /// - public LambdaExpression KeySelector { get; } - - /// - /// Gets the configured index name. - /// - public string? Name { get; } - - /// - /// Gets a value indicating whether the index is unique. - /// - public bool IsUnique { get; } - - /// - /// Gets the index type. - /// - public IndexType Type { get; } - - /// - /// Gets the vector dimensions. - /// - public int Dimensions { get; } - - /// - /// Gets the vector metric. - /// - public VectorMetric Metric { get; } - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The index key selector expression. /// The optional index name. @@ -213,13 +176,44 @@ public class IndexBuilder /// The index type. /// The vector dimensions. /// The vector metric. - public IndexBuilder(LambdaExpression keySelector, string? name, bool unique, IndexType type = IndexType.BTree, int dimensions = 0, VectorMetric metric = VectorMetric.Cosine) + public IndexBuilder(LambdaExpression keySelector, string? name, bool unique, IndexType type = IndexType.BTree, + int dimensions = 0, VectorMetric metric = VectorMetric.Cosine) { KeySelector = keySelector; Name = name; - IsUnique = unique; - Type = type; - Dimensions = dimensions; - Metric = metric; - } -} + IsUnique = unique; + Type = type; + Dimensions = dimensions; + Metric = metric; + } + + /// + /// Gets the index key selector expression. + /// + public LambdaExpression KeySelector { get; } + + /// + /// Gets the configured index name. + /// + public string? Name { get; } + + /// + /// Gets a value indicating whether the index is unique. + /// + public bool IsUnique { get; } + + /// + /// Gets the index type. + /// + public IndexType Type { get; } + + /// + /// Gets the vector dimensions. + /// + public int Dimensions { get; } + + /// + /// Gets the vector metric. + /// + public VectorMetric Metric { get; } +} \ No newline at end of file diff --git a/src/CBDD.Core/Metadata/ModelBuilder.cs b/src/CBDD.Core/Metadata/ModelBuilder.cs index f93104a..146f01b 100755 --- a/src/CBDD.Core/Metadata/ModelBuilder.cs +++ b/src/CBDD.Core/Metadata/ModelBuilder.cs @@ -1,30 +1,31 @@ -using System.Linq.Expressions; -using ZB.MOM.WW.CBDD.Core.Indexing; - namespace ZB.MOM.WW.CBDD.Core.Metadata; public class ModelBuilder { private readonly Dictionary _entityBuilders = new(); - /// - /// Gets or creates the entity builder for the specified entity type. - /// - /// The entity type. - /// The entity builder for . - public EntityTypeBuilder Entity() where T : class + /// + /// Gets or creates the entity builder for the specified entity type. + /// + /// The entity type. + /// The entity builder for . + public EntityTypeBuilder Entity() where T : class { - if (!_entityBuilders.TryGetValue(typeof(T), out var builder)) + if (!_entityBuilders.TryGetValue(typeof(T), out object? builder)) { builder = new EntityTypeBuilder(); _entityBuilders[typeof(T)] = builder; } + return (EntityTypeBuilder)builder; } - /// - /// Gets all registered entity builders. - /// - /// A read-only dictionary of entity builders keyed by entity type. - public IReadOnlyDictionary GetEntityBuilders() => _entityBuilders; -} + /// + /// Gets all registered entity builders. + /// + /// A read-only dictionary of entity builders keyed by entity type. + public IReadOnlyDictionary GetEntityBuilders() + { + return _entityBuilders; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Metadata/ValueConverter.cs b/src/CBDD.Core/Metadata/ValueConverter.cs index dd991bf..1d4d7c9 100755 --- a/src/CBDD.Core/Metadata/ValueConverter.cs +++ b/src/CBDD.Core/Metadata/ValueConverter.cs @@ -1,20 +1,20 @@ -namespace ZB.MOM.WW.CBDD.Core.Metadata; - -/// -/// Defines a bidirectional conversion between a model type (e.g. ValueObject) -/// and a provider type supported by the storage engine (e.g. string, int, Guid, ObjectId). -/// -public abstract class ValueConverter -{ +namespace ZB.MOM.WW.CBDD.Core.Metadata; + +/// +/// Defines a bidirectional conversion between a model type (e.g. ValueObject) +/// and a provider type supported by the storage engine (e.g. string, int, Guid, ObjectId). +/// +public abstract class ValueConverter +{ /// - /// Converts the model value to the provider value. + /// Converts the model value to the provider value. /// /// The model value to convert. public abstract TProvider ConvertToProvider(TModel model); /// - /// Converts the provider value back to the model value. + /// Converts the provider value back to the model value. /// /// The provider value to convert. public abstract TModel ConvertFromProvider(TProvider provider); -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/BTreeExpressionVisitor.cs b/src/CBDD.Core/Query/BTreeExpressionVisitor.cs index 9970b3d..62b4ea7 100755 --- a/src/CBDD.Core/Query/BTreeExpressionVisitor.cs +++ b/src/CBDD.Core/Query/BTreeExpressionVisitor.cs @@ -4,18 +4,20 @@ namespace ZB.MOM.WW.CBDD.Core.Query; internal class BTreeExpressionVisitor : ExpressionVisitor { - private readonly QueryModel _model = new(); - - /// - /// Gets the query model built while visiting an expression tree. - /// - public QueryModel GetModel() => _model; - - /// - protected override Expression VisitMethodCall(MethodCallExpression node) - { - if (node.Method.DeclaringType == typeof(Queryable)) - { + private readonly QueryModel _model = new(); + + /// + /// Gets the query model built while visiting an expression tree. + /// + public QueryModel GetModel() + { + return _model; + } + + /// + protected override Expression VisitMethodCall(MethodCallExpression node) + { + if (node.Method.DeclaringType == typeof(Queryable)) switch (node.Method.Name) { case "Where": @@ -35,8 +37,7 @@ internal class BTreeExpressionVisitor : ExpressionVisitor VisitSkip(node); break; } - } - + return base.VisitMethodCall(node); } @@ -94,4 +95,4 @@ internal class BTreeExpressionVisitor : ExpressionVisitor if (countExpression.Value != null) _model.Skip = (int)countExpression.Value; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/BTreeQueryProvider.cs b/src/CBDD.Core/Query/BTreeQueryProvider.cs index e6dda19..331f826 100755 --- a/src/CBDD.Core/Query/BTreeQueryProvider.cs +++ b/src/CBDD.Core/Query/BTreeQueryProvider.cs @@ -1,17 +1,17 @@ -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using ZB.MOM.WW.CBDD.Core.Collections; -using static ZB.MOM.WW.CBDD.Core.Query.IndexOptimizer; - -namespace ZB.MOM.WW.CBDD.Core.Query; - +using System.Linq.Expressions; +using System.Reflection; +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.Collections; +using static ZB.MOM.WW.CBDD.Core.Query.IndexOptimizer; + +namespace ZB.MOM.WW.CBDD.Core.Query; + public class BTreeQueryProvider : IQueryProvider where T : class { private readonly DocumentCollection _collection; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The backing document collection. public BTreeQueryProvider(DocumentCollection collection) @@ -20,38 +20,37 @@ public class BTreeQueryProvider : IQueryProvider where T : class } /// - /// Creates a query from the specified expression. + /// Creates a query from the specified expression. /// /// The query expression. - /// An representing the query. + /// An representing the query. public IQueryable CreateQuery(Expression expression) { var elementType = expression.Type.GetGenericArguments()[0]; try - { - return (IQueryable)Activator.CreateInstance( - typeof(BTreeQueryable<>).MakeGenericType(elementType), - new object[] { this, expression })!; - } - catch (TargetInvocationException ex) - { + { + return (IQueryable)Activator.CreateInstance( + typeof(BTreeQueryable<>).MakeGenericType(elementType), this, expression)!; + } + catch (TargetInvocationException ex) + { throw ex.InnerException ?? ex; } } /// - /// Creates a strongly typed query from the specified expression. + /// Creates a strongly typed query from the specified expression. /// /// The element type of the query. /// The query expression. - /// An representing the query. + /// An representing the query. public IQueryable CreateQuery(Expression expression) { return new BTreeQueryable(this, expression); } /// - /// Executes a query expression. + /// Executes a query expression. /// /// The query expression. /// The query result. @@ -61,7 +60,7 @@ public class BTreeQueryProvider : IQueryProvider where T : class } /// - /// Executes a query expression and returns a strongly typed result. + /// Executes a query expression and returns a strongly typed result. /// /// The result type. /// The query expression. @@ -72,88 +71,71 @@ public class BTreeQueryProvider : IQueryProvider where T : class // We only care about WHERE clause for optimization. // GroupBy, Select, OrderBy, etc. are handled by EnumerableRewriter. - var visitor = new BTreeExpressionVisitor(); - visitor.Visit(expression); + var visitor = new BTreeExpressionVisitor(); + visitor.Visit(expression); var model = visitor.GetModel(); // 2. Data Fetching Strategy (Optimized or Full Scan) IEnumerable sourceData = null!; // A. Try Index Optimization (Only if Where clause exists) - var indexOpt = IndexOptimizer.TryOptimize(model, _collection.GetIndexes()); - if (indexOpt != null) + var indexOpt = TryOptimize(model, _collection.GetIndexes()); + if (indexOpt != null) { if (indexOpt.IsVectorSearch) - { sourceData = _collection.VectorSearch(indexOpt.IndexName, indexOpt.VectorQuery!, indexOpt.K); - } else if (indexOpt.IsSpatialSearch) - { sourceData = indexOpt.SpatialType == SpatialQueryType.Near ? _collection.Near(indexOpt.IndexName, indexOpt.SpatialPoint, indexOpt.RadiusKm) : _collection.Within(indexOpt.IndexName, indexOpt.SpatialMin, indexOpt.SpatialMax); - } else - { sourceData = _collection.QueryIndex(indexOpt.IndexName, indexOpt.MinValue, indexOpt.MaxValue); - } } // B. Try Scan Optimization (if no index used) - if (sourceData == null) - { - Func? bsonPredicate = null; - if (model.WhereClause != null) - { - bsonPredicate = BsonExpressionEvaluator.TryCompile(model.WhereClause); - } - - if (bsonPredicate != null) - { - sourceData = _collection.Scan(bsonPredicate); - } - } - - // C. Fallback to Full Scan - if (sourceData == null) - { - sourceData = _collection.FindAll(); + if (sourceData == null) + { + Func? bsonPredicate = null; + if (model.WhereClause != null) bsonPredicate = BsonExpressionEvaluator.TryCompile(model.WhereClause); + + if (bsonPredicate != null) sourceData = _collection.Scan(bsonPredicate); } + // C. Fallback to Full Scan + if (sourceData == null) sourceData = _collection.FindAll(); + // 3. Rewrite Expression Tree to use Enumerable // Replace the "Root" IQueryable with our sourceData IEnumerable // We need to find the root IQueryable in the expression to replace it. // It's likely the first argument of the first method call, or a constant. - var rootFinder = new RootFinder(); - rootFinder.Visit(expression); + var rootFinder = new RootFinder(); + rootFinder.Visit(expression); var root = rootFinder.Root; - if (root == null) throw new InvalidOperationException("Could not find root Queryable in expression"); - - var rewriter = new EnumerableRewriter(root, sourceData); + if (root == null) throw new InvalidOperationException("Could not find root Queryable in expression"); + + var rewriter = new EnumerableRewriter(root, sourceData); var rewrittenExpression = rewriter.Visit(expression); // 4. Compile and Execute // The rewritten expression is now a tree of IEnumerable calls returning TResult. // We need to turn it into a Func and invoke it. - if (rewrittenExpression.Type != typeof(TResult)) - { + if (rewrittenExpression.Type != typeof(TResult)) // If TResult is object (non-generic Execute), we need to cast - rewrittenExpression = Expression.Convert(rewrittenExpression, typeof(TResult)); - } - - var lambda = Expression.Lambda>(rewrittenExpression); - var compiled = lambda.Compile(); - return compiled(); + rewrittenExpression = Expression.Convert(rewrittenExpression, typeof(TResult)); + + var lambda = Expression.Lambda>(rewrittenExpression); + var compiled = lambda.Compile(); + return compiled(); } private class RootFinder : ExpressionVisitor { /// - /// Gets the root queryable found in the expression tree. + /// Gets the root queryable found in the expression tree. /// public IQueryable? Root { get; private set; } @@ -161,13 +143,11 @@ public class BTreeQueryProvider : IQueryProvider where T : class protected override Expression VisitConstant(ConstantExpression node) { // If we found a Queryable, that's our root source - if (Root == null && node.Value is IQueryable q) - { - // We typically want the "base" queryable (the BTreeQueryable instance) - // In a chain like Coll.Where.Select, the root is Coll. - Root = q; - } - return base.VisitConstant(node); - } - } -} + if (Root == null && node.Value is IQueryable q) + // We typically want the "base" queryable (the BTreeQueryable instance) + // In a chain like Coll.Where.Select, the root is Coll. + Root = q; + return base.VisitConstant(node); + } + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/BTreeQueryable.cs b/src/CBDD.Core/Query/BTreeQueryable.cs index c70e96d..ad8a5ba 100755 --- a/src/CBDD.Core/Query/BTreeQueryable.cs +++ b/src/CBDD.Core/Query/BTreeQueryable.cs @@ -1,13 +1,12 @@ -using System.Collections; -using System.Linq; -using System.Linq.Expressions; - -namespace ZB.MOM.WW.CBDD.Core.Query; - +using System.Collections; +using System.Linq.Expressions; + +namespace ZB.MOM.WW.CBDD.Core.Query; + internal class BTreeQueryable : IOrderedQueryable { /// - /// Initializes a new queryable wrapper for the specified provider and expression. + /// Initializes a new queryable wrapper for the specified provider and expression. /// /// The query provider. /// The expression tree. @@ -18,7 +17,7 @@ internal class BTreeQueryable : IOrderedQueryable } /// - /// Initializes a new queryable wrapper for the specified provider. + /// Initializes a new queryable wrapper for the specified provider. /// /// The query provider. public BTreeQueryable(IQueryProvider provider) @@ -28,17 +27,17 @@ internal class BTreeQueryable : IOrderedQueryable } /// - /// Gets the element type returned by this query. + /// Gets the element type returned by this query. /// public Type ElementType => typeof(T); /// - /// Gets the expression tree associated with this query. + /// Gets the expression tree associated with this query. /// public Expression Expression { get; } /// - /// Gets the query provider for this query. + /// Gets the query provider for this query. /// public IQueryProvider Provider { get; } @@ -53,4 +52,4 @@ internal class BTreeQueryable : IOrderedQueryable { return GetEnumerator(); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/BsonExpressionEvaluator.cs b/src/CBDD.Core/Query/BsonExpressionEvaluator.cs index 73abac2..763d6f6 100755 --- a/src/CBDD.Core/Query/BsonExpressionEvaluator.cs +++ b/src/CBDD.Core/Query/BsonExpressionEvaluator.cs @@ -6,11 +6,11 @@ namespace ZB.MOM.WW.CBDD.Core.Query; internal static class BsonExpressionEvaluator { /// - /// Attempts to compile a LINQ predicate expression into a BSON reader predicate. + /// Attempts to compile a LINQ predicate expression into a BSON reader predicate. /// /// The entity type of the original expression. /// The lambda expression to compile. - /// A compiled predicate when supported; otherwise, . + /// A compiled predicate when supported; otherwise, . public static Func? TryCompile(LambdaExpression expression) { // Simple optimization for: x => x.Prop op Constant @@ -29,12 +29,11 @@ internal static class BsonExpressionEvaluator } if (left is MemberExpression member && right is ConstantExpression constant) - { // Check if member is property of parameter if (member.Expression == expression.Parameters[0]) { - var propertyName = member.Member.Name.ToLowerInvariant(); - var value = constant.Value; + string propertyName = member.Member.Name.ToLowerInvariant(); + object? value = constant.Value; // Handle Id mapping? // If property is "id", Bson field is "_id" @@ -42,22 +41,25 @@ internal static class BsonExpressionEvaluator return CreatePredicate(propertyName, value, nodeType); } - } } return null; } - private static ExpressionType Flip(ExpressionType type) => type switch + private static ExpressionType Flip(ExpressionType type) { - ExpressionType.GreaterThan => ExpressionType.LessThan, - ExpressionType.LessThan => ExpressionType.GreaterThan, - ExpressionType.GreaterThanOrEqual => ExpressionType.LessThanOrEqual, - ExpressionType.LessThanOrEqual => ExpressionType.GreaterThanOrEqual, - _ => type - }; + return type switch + { + ExpressionType.GreaterThan => ExpressionType.LessThan, + ExpressionType.LessThan => ExpressionType.GreaterThan, + ExpressionType.GreaterThanOrEqual => ExpressionType.LessThanOrEqual, + ExpressionType.LessThanOrEqual => ExpressionType.GreaterThanOrEqual, + _ => type + }; + } - private static Func? CreatePredicate(string propertyName, object? targetValue, ExpressionType op) + private static Func? CreatePredicate(string propertyName, object? targetValue, + ExpressionType op) { // We need to return a delegate that searches for propertyName in BsonSpanReader and compares @@ -71,13 +73,11 @@ internal static class BsonExpressionEvaluator var type = reader.ReadBsonType(); if (type == 0) break; - var name = reader.ReadElementHeader(); + string name = reader.ReadElementHeader(); if (name == propertyName) - { // Found -> read value and compare return Compare(ref reader, type, targetValue, op); - } reader.SkipValue(type); } @@ -86,6 +86,7 @@ internal static class BsonExpressionEvaluator { return false; } + return false; // Not found }; } @@ -97,9 +98,8 @@ internal static class BsonExpressionEvaluator if (type == BsonType.Int32) { - var val = reader.ReadInt32(); + int val = reader.ReadInt32(); if (target is int targetInt) - { return op switch { ExpressionType.Equal => val == targetInt, @@ -110,14 +110,13 @@ internal static class BsonExpressionEvaluator ExpressionType.LessThanOrEqual => val <= targetInt, _ => false }; - } } else if (type == BsonType.String) { - var val = reader.ReadString(); + string val = reader.ReadString(); if (target is string targetStr) { - var cmp = string.Compare(val, targetStr, StringComparison.Ordinal); + int cmp = string.Compare(val, targetStr, StringComparison.Ordinal); return op switch { ExpressionType.Equal => cmp == 0, @@ -140,4 +139,4 @@ internal static class BsonExpressionEvaluator return false; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/EnumerableRewriter.cs b/src/CBDD.Core/Query/EnumerableRewriter.cs index 4de5ba2..abc5637 100755 --- a/src/CBDD.Core/Query/EnumerableRewriter.cs +++ b/src/CBDD.Core/Query/EnumerableRewriter.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -8,50 +5,48 @@ namespace ZB.MOM.WW.CBDD.Core.Query; internal class EnumerableRewriter : ExpressionVisitor { - private readonly IQueryable _source; - private readonly object _target; - - /// - /// Initializes a new instance of the class. - /// - /// The original queryable source to replace. - /// The target enumerable-backed object. - public EnumerableRewriter(IQueryable source, object target) - { - _source = source; - _target = target; - } - - /// - protected override Expression VisitConstant(ConstantExpression node) + private readonly IQueryable _source; + private readonly object _target; + + /// + /// Initializes a new instance of the class. + /// + /// The original queryable source to replace. + /// The target enumerable-backed object. + public EnumerableRewriter(IQueryable source, object target) + { + _source = source; + _target = target; + } + + /// + protected override Expression VisitConstant(ConstantExpression node) { // Replace the IQueryable source with the materialized IEnumerable - if (node.Value == _source) - { - return Expression.Constant(_target); - } + if (node.Value == _source) return Expression.Constant(_target); return base.VisitConstant(node); } - /// - protected override Expression VisitMethodCall(MethodCallExpression node) + /// + protected override Expression VisitMethodCall(MethodCallExpression node) { if (node.Method.DeclaringType == typeof(Queryable)) { - var methodName = node.Method.Name; + string methodName = node.Method.Name; var typeArgs = node.Method.GetGenericArguments(); var args = new Expression[node.Arguments.Count]; - for (int i = 0; i < node.Arguments.Count; i++) + for (var i = 0; i < node.Arguments.Count; i++) { - var arg = Visit(node.Arguments[i]); - - // Strip Quote from lambda arguments + var arg = Visit(node.Arguments[i]); + + // Strip Quote from lambda arguments if (arg is UnaryExpression quote && quote.NodeType == ExpressionType.Quote) { var lambda = (LambdaExpression)quote.Operand; arg = Expression.Constant(lambda.Compile()); } + args[i] = arg; } @@ -83,4 +78,4 @@ internal class EnumerableRewriter : ExpressionVisitor return base.VisitMethodCall(node); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/IndexOptimizer.cs b/src/CBDD.Core/Query/IndexOptimizer.cs index b056b61..f4e38f2 100755 --- a/src/CBDD.Core/Query/IndexOptimizer.cs +++ b/src/CBDD.Core/Query/IndexOptimizer.cs @@ -3,96 +3,30 @@ using ZB.MOM.WW.CBDD.Core.Indexing; namespace ZB.MOM.WW.CBDD.Core.Query; -internal static class IndexOptimizer -{ - /// - /// Represents the selected index and bounds for an optimized query. - /// - public class OptimizationResult - { - /// - /// Gets or sets the selected index name. - /// - public string IndexName { get; set; } = ""; - - /// - /// Gets or sets the minimum bound value. - /// - public object? MinValue { get; set; } - - /// - /// Gets or sets the maximum bound value. - /// - public object? MaxValue { get; set; } - - /// - /// Gets or sets a value indicating whether the query uses a range. - /// - public bool IsRange { get; set; } - - /// - /// Gets or sets a value indicating whether the query uses vector search. - /// - public bool IsVectorSearch { get; set; } - - /// - /// Gets or sets the vector query values. - /// - public float[]? VectorQuery { get; set; } - - /// - /// Gets or sets the number of nearest neighbors for vector search. - /// - public int K { get; set; } - - /// - /// Gets or sets a value indicating whether the query uses spatial search. - /// - public bool IsSpatialSearch { get; set; } - - /// - /// Gets or sets the center point for near queries. - /// - public (double Latitude, double Longitude) SpatialPoint { get; set; } - - /// - /// Gets or sets the search radius in kilometers. - /// - public double RadiusKm { get; set; } - - /// - /// Gets or sets the minimum point for within queries. - /// - public (double Latitude, double Longitude) SpatialMin { get; set; } - - /// - /// Gets or sets the maximum point for within queries. - /// - public (double Latitude, double Longitude) SpatialMax { get; set; } - - /// - /// Gets or sets the spatial query type. - /// - public SpatialQueryType SpatialType { get; set; } - } +internal static class IndexOptimizer +{ + public enum SpatialQueryType + { + Near, + Within + } - public enum SpatialQueryType { Near, Within } - - /// - /// Attempts to optimize a query model using available indexes. - /// - /// The document type. - /// The query model. - /// The available collection indexes. - /// An optimization result when optimization is possible; otherwise, . - public static OptimizationResult? TryOptimize(QueryModel model, IEnumerable indexes) - { - if (model.WhereClause == null) return null; + /// + /// Attempts to optimize a query model using available indexes. + /// + /// The document type. + /// The query model. + /// The available collection indexes. + /// An optimization result when optimization is possible; otherwise, . + public static OptimizationResult? TryOptimize(QueryModel model, IEnumerable indexes) + { + if (model.WhereClause == null) return null; return OptimizeExpression(model.WhereClause.Body, model.WhereClause.Parameters[0], indexes); } - private static OptimizationResult? OptimizeExpression(Expression expression, ParameterExpression parameter, IEnumerable indexes) + private static OptimizationResult? OptimizeExpression(Expression expression, ParameterExpression parameter, + IEnumerable indexes) { // ... (Existing AndAlso logic remains the same) ... if (expression is BinaryExpression binary && binary.NodeType == ExpressionType.AndAlso) @@ -101,7 +35,6 @@ internal static class IndexOptimizer var right = OptimizeExpression(binary.Right, parameter, indexes); if (left != null && right != null && left.IndexName == right.IndexName) - { return new OptimizationResult { IndexName = left.IndexName, @@ -109,12 +42,11 @@ internal static class IndexOptimizer MaxValue = left.MaxValue ?? right.MaxValue, IsRange = true }; - } return left ?? right; } // Handle Simple Binary Predicates - var (propertyName, value, op) = ParseSimplePredicate(expression, parameter); + (string? propertyName, object? value, var op) = ParseSimplePredicate(expression, parameter); if (propertyName != null) { var index = indexes.FirstOrDefault(i => Matches(i, propertyName)); @@ -128,55 +60,56 @@ internal static class IndexOptimizer result.MaxValue = value; result.IsRange = false; break; - case ExpressionType.GreaterThan: + case ExpressionType.GreaterThan: case ExpressionType.GreaterThanOrEqual: result.MinValue = value; result.MaxValue = null; result.IsRange = true; - break; - case ExpressionType.LessThan: + break; + case ExpressionType.LessThan: case ExpressionType.LessThanOrEqual: result.MinValue = null; result.MaxValue = value; result.IsRange = true; break; } + return result; } - } - - // Handle StartsWith - if (expression is MethodCallExpression call && call.Method.Name == "StartsWith" && call.Object is MemberExpression member) - { - if (member.Expression == parameter && call.Arguments[0] is ConstantExpression constant && constant.Value is string prefix) - { - var index = indexes.FirstOrDefault(i => Matches(i, member.Member.Name)); - if (index != null && index.Type == IndexType.BTree) - { - var nextPrefix = IncrementPrefix(prefix); - return new OptimizationResult - { - IndexName = index.Name, - MinValue = prefix, - MaxValue = nextPrefix, - IsRange = true - }; - } - } } + // Handle StartsWith + if (expression is MethodCallExpression call && call.Method.Name == "StartsWith" && + call.Object is MemberExpression member) + if (member.Expression == parameter && call.Arguments[0] is ConstantExpression constant && + constant.Value is string prefix) + { + var index = indexes.FirstOrDefault(i => Matches(i, member.Member.Name)); + if (index != null && index.Type == IndexType.BTree) + { + string nextPrefix = IncrementPrefix(prefix); + return new OptimizationResult + { + IndexName = index.Name, + MinValue = prefix, + MaxValue = nextPrefix, + IsRange = true + }; + } + } + // Handle Method Calls (VectorSearch, Near, Within) if (expression is MethodCallExpression mcall) { // VectorSearch(this float[] vector, float[] query, int k) - if (mcall.Method.Name == "VectorSearch" && mcall.Arguments[0] is MemberExpression vMember && vMember.Expression == parameter) + if (mcall.Method.Name == "VectorSearch" && mcall.Arguments[0] is MemberExpression vMember && + vMember.Expression == parameter) { - var query = EvaluateExpression(mcall.Arguments[1]); - var k = EvaluateExpression(mcall.Arguments[2]); - + float[] query = EvaluateExpression(mcall.Arguments[1]); + var k = EvaluateExpression(mcall.Arguments[2]); + var index = indexes.FirstOrDefault(i => i.Type == IndexType.Vector && Matches(i, vMember.Member.Name)); if (index != null) - { return new OptimizationResult { IndexName = index.Name, @@ -184,18 +117,17 @@ internal static class IndexOptimizer VectorQuery = query, K = k }; - } - } - - // Near(this (double, double) point, (double, double) center, double radiusKm) - if (mcall.Method.Name == "Near" && mcall.Arguments[0] is MemberExpression nMember && nMember.Expression == parameter) + } + + // Near(this (double, double) point, (double, double) center, double radiusKm) + if (mcall.Method.Name == "Near" && mcall.Arguments[0] is MemberExpression nMember && + nMember.Expression == parameter) { var center = EvaluateExpression<(double, double)>(mcall.Arguments[1]); var radius = EvaluateExpression(mcall.Arguments[2]); var index = indexes.FirstOrDefault(i => i.Type == IndexType.Spatial && Matches(i, nMember.Member.Name)); if (index != null) - { return new OptimizationResult { IndexName = index.Name, @@ -204,18 +136,17 @@ internal static class IndexOptimizer SpatialPoint = center, RadiusKm = radius }; - } } // Within(this (double, double) point, (double, double) min, (double, double) max) - if (mcall.Method.Name == "Within" && mcall.Arguments[0] is MemberExpression wMember && wMember.Expression == parameter) + if (mcall.Method.Name == "Within" && mcall.Arguments[0] is MemberExpression wMember && + wMember.Expression == parameter) { var min = EvaluateExpression<(double, double)>(mcall.Arguments[1]); var max = EvaluateExpression<(double, double)>(mcall.Arguments[2]); var index = indexes.FirstOrDefault(i => i.Type == IndexType.Spatial && Matches(i, wMember.Member.Name)); if (index != null) - { return new OptimizationResult { IndexName = index.Name, @@ -224,7 +155,6 @@ internal static class IndexOptimizer SpatialMin = min, SpatialMax = max }; - } } } @@ -241,10 +171,7 @@ internal static class IndexOptimizer private static T EvaluateExpression(Expression expression) { - if (expression is ConstantExpression constant) - { - return (T)constant.Value!; - } + if (expression is ConstantExpression constant) return (T)constant.Value!; // Evaluate more complex expressions (closures, properties, etc.) var lambda = Expression.Lambda(expression); @@ -258,7 +185,8 @@ internal static class IndexOptimizer return index.PropertyPaths[0].Equals(propertyName, StringComparison.OrdinalIgnoreCase); } - private static (string? propertyName, object? value, ExpressionType op) ParseSimplePredicate(Expression expression, ParameterExpression parameter) + private static (string? propertyName, object? value, ExpressionType op) ParseSimplePredicate(Expression expression, + ParameterExpression parameter) { if (expression is BinaryExpression binary) { @@ -273,27 +201,99 @@ internal static class IndexOptimizer } if (left is MemberExpression member && right is ConstantExpression constant) - { if (member.Expression == parameter) return (member.Member.Name, constant.Value, nodeType); - } - - // Handle Convert - if (left is UnaryExpression unary && unary.Operand is MemberExpression member2 && right is ConstantExpression constant2) - { + + // Handle Convert + if (left is UnaryExpression unary && unary.Operand is MemberExpression member2 && + right is ConstantExpression constant2) if (member2.Expression == parameter) return (member2.Member.Name, constant2.Value, nodeType); - } } + return (null, null, ExpressionType.Default); } - private static ExpressionType Flip(ExpressionType type) => type switch + private static ExpressionType Flip(ExpressionType type) { - ExpressionType.GreaterThan => ExpressionType.LessThan, - ExpressionType.LessThan => ExpressionType.GreaterThan, - ExpressionType.GreaterThanOrEqual => ExpressionType.LessThanOrEqual, - ExpressionType.LessThanOrEqual => ExpressionType.GreaterThanOrEqual, - _ => type - }; -} + return type switch + { + ExpressionType.GreaterThan => ExpressionType.LessThan, + ExpressionType.LessThan => ExpressionType.GreaterThan, + ExpressionType.GreaterThanOrEqual => ExpressionType.LessThanOrEqual, + ExpressionType.LessThanOrEqual => ExpressionType.GreaterThanOrEqual, + _ => type + }; + } + + /// + /// Represents the selected index and bounds for an optimized query. + /// + public class OptimizationResult + { + /// + /// Gets or sets the selected index name. + /// + public string IndexName { get; set; } = ""; + + /// + /// Gets or sets the minimum bound value. + /// + public object? MinValue { get; set; } + + /// + /// Gets or sets the maximum bound value. + /// + public object? MaxValue { get; set; } + + /// + /// Gets or sets a value indicating whether the query uses a range. + /// + public bool IsRange { get; set; } + + /// + /// Gets or sets a value indicating whether the query uses vector search. + /// + public bool IsVectorSearch { get; set; } + + /// + /// Gets or sets the vector query values. + /// + public float[]? VectorQuery { get; set; } + + /// + /// Gets or sets the number of nearest neighbors for vector search. + /// + public int K { get; set; } + + /// + /// Gets or sets a value indicating whether the query uses spatial search. + /// + public bool IsSpatialSearch { get; set; } + + /// + /// Gets or sets the center point for near queries. + /// + public (double Latitude, double Longitude) SpatialPoint { get; set; } + + /// + /// Gets or sets the search radius in kilometers. + /// + public double RadiusKm { get; set; } + + /// + /// Gets or sets the minimum point for within queries. + /// + public (double Latitude, double Longitude) SpatialMin { get; set; } + + /// + /// Gets or sets the maximum point for within queries. + /// + public (double Latitude, double Longitude) SpatialMax { get; set; } + + /// + /// Gets or sets the spatial query type. + /// + public SpatialQueryType SpatialType { get; set; } + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Query/QueryModel.cs b/src/CBDD.Core/Query/QueryModel.cs index 28c03bb..7c1921a 100755 --- a/src/CBDD.Core/Query/QueryModel.cs +++ b/src/CBDD.Core/Query/QueryModel.cs @@ -1,36 +1,36 @@ -using System.Linq.Expressions; - -namespace ZB.MOM.WW.CBDD.Core.Query; - +using System.Linq.Expressions; + +namespace ZB.MOM.WW.CBDD.Core.Query; + internal class QueryModel { /// - /// Gets or sets the filter expression. + /// Gets or sets the filter expression. /// public LambdaExpression? WhereClause { get; set; } /// - /// Gets or sets the projection expression. + /// Gets or sets the projection expression. /// public LambdaExpression? SelectClause { get; set; } /// - /// Gets or sets the ordering expression. + /// Gets or sets the ordering expression. /// public LambdaExpression? OrderByClause { get; set; } /// - /// Gets or sets the maximum number of results to return. + /// Gets or sets the maximum number of results to return. /// public int? Take { get; set; } /// - /// Gets or sets the number of results to skip. + /// Gets or sets the number of results to skip. /// public int? Skip { get; set; } /// - /// Gets or sets a value indicating whether ordering is descending. + /// Gets or sets a value indicating whether ordering is descending. /// public bool OrderDescending { get; set; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/DictionaryPage.cs b/src/CBDD.Core/Storage/DictionaryPage.cs index 2c91103..d1a5276 100755 --- a/src/CBDD.Core/Storage/DictionaryPage.cs +++ b/src/CBDD.Core/Storage/DictionaryPage.cs @@ -1,13 +1,13 @@ -using System.Runtime.InteropServices; +using System.Buffers; +using System.Buffers.Binary; using System.Text; -using ZB.MOM.WW.CBDD.Core; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Page for storing dictionary entries (Key -> Value map). -/// Uses a sorted list of keys for binary search within the page. -/// Supports chaining via PageHeader.NextPageId for dictionaries larger than one page. +/// Page for storing dictionary entries (Key -> Value map). +/// Uses a sorted list of keys for binary search within the page. +/// Supports chaining via PageHeader.NextPageId for dictionaries larger than one page. /// public struct DictionaryPage { @@ -25,16 +25,16 @@ public struct DictionaryPage private const int OffsetsStart = 36; /// - /// Values 0-100 are reserved for internal system keys (e.g. _id, _v). + /// Values 0-100 are reserved for internal system keys (e.g. _id, _v). /// public const ushort ReservedValuesEnd = 100; - /// - /// Initialize a new dictionary page - /// - /// The page buffer to initialize. - /// The page identifier. - public static void Initialize(Span page, uint pageId) + /// + /// Initialize a new dictionary page + /// + /// The page buffer to initialize. + /// The page identifier. + public static void Initialize(Span page, uint pageId) { // 1. Write Page Header var header = new PageHeader @@ -49,43 +49,40 @@ public struct DictionaryPage header.WriteTo(page); // 2. Initialize Counts - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(CountOffset), 0); - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(FreeSpaceEndOffset), (ushort)page.Length); + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(CountOffset), 0); + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(FreeSpaceEndOffset), (ushort)page.Length); } - /// - /// Inserts a key-value pair into the page. - /// Returns false if there is not enough space. - /// - /// The page buffer. - /// The dictionary key. - /// The value mapped to the key. - /// if the entry was inserted; otherwise, . - public static bool Insert(Span page, string key, ushort value) + /// + /// Inserts a key-value pair into the page. + /// Returns false if there is not enough space. + /// + /// The page buffer. + /// The dictionary key. + /// The value mapped to the key. + /// if the entry was inserted; otherwise, . + public static bool Insert(Span page, string key, ushort value) { - var keyByteCount = Encoding.UTF8.GetByteCount(key); + int keyByteCount = Encoding.UTF8.GetByteCount(key); if (keyByteCount > 255) throw new ArgumentException("Key length must be <= 255 bytes"); // Entry Size: KeyLen(1) + Key(N) + Value(2) - var entrySize = 1 + keyByteCount + 2; - var requiredSpace = entrySize + 2; // +2 for Offset entry + int entrySize = 1 + keyByteCount + 2; + int requiredSpace = entrySize + 2; // +2 for Offset entry - var count = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(CountOffset)); - var freeSpaceEnd = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(FreeSpaceEndOffset)); - - var offsetsEnd = OffsetsStart + (count * 2); - var freeSpace = freeSpaceEnd - offsetsEnd; + ushort count = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(CountOffset)); + ushort freeSpaceEnd = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(FreeSpaceEndOffset)); - if (freeSpace < requiredSpace) - { - return false; // Page Full - } + int offsetsEnd = OffsetsStart + count * 2; + int freeSpace = freeSpaceEnd - offsetsEnd; + + if (freeSpace < requiredSpace) return false; // Page Full // 1. Prepare Data var insertionOffset = (ushort)(freeSpaceEnd - entrySize); page[insertionOffset] = (byte)keyByteCount; // Write Key Length Encoding.UTF8.GetBytes(key, page.Slice(insertionOffset + 1, keyByteCount)); // Write Key - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(insertionOffset + 1 + keyByteCount), value); // Write Value + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(insertionOffset + 1 + keyByteCount), value); // Write Value // 2. Insert Offset into Sorted List // Find insert Index using spans @@ -95,57 +92,57 @@ public struct DictionaryPage // Shift offsets if needed if (insertIndex < count) { - var src = page.Slice(OffsetsStart + (insertIndex * 2), (count - insertIndex) * 2); - var dest = page.Slice(OffsetsStart + ((insertIndex + 1) * 2)); + var src = page.Slice(OffsetsStart + insertIndex * 2, (count - insertIndex) * 2); + var dest = page.Slice(OffsetsStart + (insertIndex + 1) * 2); src.CopyTo(dest); } // Write new offset - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(OffsetsStart + (insertIndex * 2)), insertionOffset); + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(OffsetsStart + insertIndex * 2), insertionOffset); // 3. Update Metadata - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(CountOffset), (ushort)(count + 1)); - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(FreeSpaceEndOffset), insertionOffset); - - // Update FreeBytes in header (approximate) + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(CountOffset), (ushort)(count + 1)); + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(FreeSpaceEndOffset), insertionOffset); + + // Update FreeBytes in header (approximate) var pageHeader = PageHeader.ReadFrom(page); - pageHeader.FreeBytes = (ushort)(insertionOffset - (OffsetsStart + ((count + 1) * 2))); + pageHeader.FreeBytes = (ushort)(insertionOffset - (OffsetsStart + (count + 1) * 2)); pageHeader.WriteTo(page); return true; } - /// - /// Tries to find a value for the given key in THIS page. - /// - /// The page buffer. - /// The UTF-8 encoded key bytes. - /// When this method returns, contains the found value. - /// if the key was found; otherwise, . - public static bool TryFind(ReadOnlySpan page, ReadOnlySpan keyBytes, out ushort value) + /// + /// Tries to find a value for the given key in THIS page. + /// + /// The page buffer. + /// The UTF-8 encoded key bytes. + /// When this method returns, contains the found value. + /// if the key was found; otherwise, . + public static bool TryFind(ReadOnlySpan page, ReadOnlySpan keyBytes, out ushort value) { value = 0; - var count = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(CountOffset)); + ushort count = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(CountOffset)); if (count == 0) return false; // Binary Search - int low = 0; + var low = 0; int high = count - 1; while (low <= high) { int mid = low + (high - low) / 2; - var offset = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(OffsetsStart + (mid * 2))); - - // Read Key at Offset - var keyLen = page[offset]; - var entryKeySpan = page.Slice(offset + 1, keyLen); - + ushort offset = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(OffsetsStart + mid * 2)); + + // Read Key at Offset + byte keyLen = page[offset]; + var entryKeySpan = page.Slice(offset + 1, keyLen); + int comparison = entryKeySpan.SequenceCompareTo(keyBytes); if (comparison == 0) { - value = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(offset + 1 + keyLen)); + value = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(offset + 1 + keyLen)); return true; } @@ -158,126 +155,125 @@ public struct DictionaryPage return false; } - /// - /// Tries to find a value for the given key across a chain of DictionaryPages. - /// - /// The storage engine used to read pages. - /// The first page in the dictionary chain. - /// The key to search for. - /// When this method returns, contains the found value. - /// Optional transaction identifier for isolated reads. - /// if the key was found; otherwise, . - public static bool TryFindGlobal(StorageEngine storage, uint startPageId, string key, out ushort value, ulong? transactionId = null) + /// + /// Tries to find a value for the given key across a chain of DictionaryPages. + /// + /// The storage engine used to read pages. + /// The first page in the dictionary chain. + /// The key to search for. + /// When this method returns, contains the found value. + /// Optional transaction identifier for isolated reads. + /// if the key was found; otherwise, . + public static bool TryFindGlobal(StorageEngine storage, uint startPageId, string key, out ushort value, + ulong? transactionId = null) { - var keyByteCount = Encoding.UTF8.GetByteCount(key); - Span keyBytes = keyByteCount <= 256 ? stackalloc byte[keyByteCount] : new byte[keyByteCount]; + int keyByteCount = Encoding.UTF8.GetByteCount(key); + var keyBytes = keyByteCount <= 256 ? stackalloc byte[keyByteCount] : new byte[keyByteCount]; Encoding.UTF8.GetBytes(key, keyBytes); - var pageId = startPageId; - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(storage.PageSize); + uint pageId = startPageId; + byte[] pageBuffer = ArrayPool.Shared.Rent(storage.PageSize); try { while (pageId != 0) - { - // Read page - storage.ReadPage(pageId, transactionId, pageBuffer); - - // TryFind in this page - if (TryFind(pageBuffer, keyBytes, out value)) - { - return true; - } - - // Move to next page - var header = PageHeader.ReadFrom(pageBuffer); + { + // Read page + storage.ReadPage(pageId, transactionId, pageBuffer); + + // TryFind in this page + if (TryFind(pageBuffer, keyBytes, out value)) return true; + + // Move to next page + var header = PageHeader.ReadFrom(pageBuffer); pageId = header.NextPageId; } } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); - } - + ArrayPool.Shared.Return(pageBuffer); + } + value = 0; return false; } private static int FindInsertIndex(ReadOnlySpan page, int count, ReadOnlySpan keyBytes) { - int low = 0; + var low = 0; int high = count - 1; while (low <= high) { int mid = low + (high - low) / 2; - var offset = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(OffsetsStart + (mid * 2))); - - var keyLen = page[offset]; - var entryKeySpan = page.Slice(offset + 1, keyLen); - + ushort offset = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(OffsetsStart + mid * 2)); + + byte keyLen = page[offset]; + var entryKeySpan = page.Slice(offset + 1, keyLen); + int comparison = entryKeySpan.SequenceCompareTo(keyBytes); - if (comparison == 0) return mid; + if (comparison == 0) return mid; if (comparison < 0) low = mid + 1; else high = mid - 1; } + return low; } - /// - /// Gets all entries in the page (for debugging/dumping) - /// - /// The page buffer. - /// All key-value pairs in the page. - public static IEnumerable<(string Key, ushort Value)> GetAll(ReadOnlySpan page) - { - var count = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(CountOffset)); - var list = new List<(string Key, ushort Value)>(); - for (int i = 0; i < count; i++) + /// + /// Gets all entries in the page (for debugging/dumping) + /// + /// The page buffer. + /// All key-value pairs in the page. + public static IEnumerable<(string Key, ushort Value)> GetAll(ReadOnlySpan page) + { + ushort count = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(CountOffset)); + var list = new List<(string Key, ushort Value)>(); + for (var i = 0; i < count; i++) { - var offset = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(OffsetsStart + (i * 2))); - var keyLen = page[offset]; - var keyStr = Encoding.UTF8.GetString(page.Slice(offset + 1, keyLen)); - var val = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(offset + 1 + keyLen)); - list.Add((keyStr, val)); - } + ushort offset = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(OffsetsStart + i * 2)); + byte keyLen = page[offset]; + string keyStr = Encoding.UTF8.GetString(page.Slice(offset + 1, keyLen)); + ushort val = BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(offset + 1 + keyLen)); + list.Add((keyStr, val)); + } + return list; } - /// - /// Retrieves all key-value pairs across a chain of DictionaryPages. - /// Used for rebuilding the in-memory cache. - /// - /// The storage engine used to read pages. - /// The first page in the dictionary chain. - /// Optional transaction identifier for isolated reads. - /// All key-value pairs across the page chain. - public static IEnumerable<(string Key, ushort Value)> FindAllGlobal(StorageEngine storage, uint startPageId, ulong? transactionId = null) + + /// + /// Retrieves all key-value pairs across a chain of DictionaryPages. + /// Used for rebuilding the in-memory cache. + /// + /// The storage engine used to read pages. + /// The first page in the dictionary chain. + /// Optional transaction identifier for isolated reads. + /// All key-value pairs across the page chain. + public static IEnumerable<(string Key, ushort Value)> FindAllGlobal(StorageEngine storage, uint startPageId, + ulong? transactionId = null) { - var pageId = startPageId; - var pageBuffer = System.Buffers.ArrayPool.Shared.Rent(storage.PageSize); + uint pageId = startPageId; + byte[] pageBuffer = ArrayPool.Shared.Rent(storage.PageSize); try { while (pageId != 0) - { - // Read page - storage.ReadPage(pageId, transactionId, pageBuffer); - - // Get all entries in this page - foreach (var entry in GetAll(pageBuffer)) - { - yield return entry; - } - - // Move to next page - var header = PageHeader.ReadFrom(pageBuffer); + { + // Read page + storage.ReadPage(pageId, transactionId, pageBuffer); + + // Get all entries in this page + foreach (var entry in GetAll(pageBuffer)) yield return entry; + + // Move to next page + var header = PageHeader.ReadFrom(pageBuffer); pageId = header.NextPageId; } } finally { - System.Buffers.ArrayPool.Shared.Return(pageBuffer); + ArrayPool.Shared.Return(pageBuffer); } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/IIndexStorage.cs b/src/CBDD.Core/Storage/IIndexStorage.cs index d805c90..55e0882 100644 --- a/src/CBDD.Core/Storage/IIndexStorage.cs +++ b/src/CBDD.Core/Storage/IIndexStorage.cs @@ -1,41 +1,46 @@ namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Narrow storage port for index structures (page operations + allocation only). +/// Narrow storage port for index structures (page operations + allocation only). /// internal interface IIndexStorage { /// - /// Gets or sets the PageSize. + /// Gets or sets the PageSize. /// int PageSize { get; } + /// - /// Executes AllocatePage. + /// Executes AllocatePage. /// uint AllocatePage(); + /// - /// Executes FreePage. + /// Executes FreePage. /// /// The page identifier. void FreePage(uint pageId); + /// - /// Executes ReadPage. + /// Executes ReadPage. /// /// The page identifier. /// The optional transaction identifier. /// The destination buffer. void ReadPage(uint pageId, ulong? transactionId, Span destination); + /// - /// Executes WritePage. + /// Executes WritePage. /// /// The page identifier. /// The transaction identifier. /// The source page data. void WritePage(uint pageId, ulong transactionId, ReadOnlySpan data); + /// - /// Executes WritePageImmediate. + /// Executes WritePageImmediate. /// /// The page identifier. /// The source page data. void WritePageImmediate(uint pageId, ReadOnlySpan data); -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/IStorageEngine.cs b/src/CBDD.Core/Storage/IStorageEngine.cs index d859fd3..077770b 100644 --- a/src/CBDD.Core/Storage/IStorageEngine.cs +++ b/src/CBDD.Core/Storage/IStorageEngine.cs @@ -8,111 +8,112 @@ using ZB.MOM.WW.CBDD.Core.Transactions; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Storage port used by collection/index orchestration to avoid concrete engine coupling. +/// Storage port used by collection/index orchestration to avoid concrete engine coupling. /// internal interface IStorageEngine : IIndexStorage, IDisposable { /// - /// Gets the current page count. + /// Gets the current page count. /// uint PageCount { get; } /// - /// Gets the active change stream dispatcher. + /// Gets the active change stream dispatcher. /// ChangeStreamDispatcher? Cdc { get; } /// - /// Gets compression options used by the storage engine. + /// Gets compression options used by the storage engine. /// CompressionOptions CompressionOptions { get; } /// - /// Gets the compression service. + /// Gets the compression service. /// CompressionService CompressionService { get; } /// - /// Gets compression telemetry for the storage engine. + /// Gets compression telemetry for the storage engine. /// CompressionTelemetry CompressionTelemetry { get; } /// - /// Determines whether a page is locked. + /// Determines whether a page is locked. /// /// The page identifier to inspect. /// A transaction identifier to exclude from lock checks. bool IsPageLocked(uint pageId, ulong excludingTxId); /// - /// Registers the change stream dispatcher. + /// Registers the change stream dispatcher. /// /// The change stream dispatcher instance. void RegisterCdc(ChangeStreamDispatcher cdc); /// - /// Begins a transaction. + /// Begins a transaction. /// /// The transaction isolation level. Transaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted); /// - /// Begins a transaction asynchronously. + /// Begins a transaction asynchronously. /// /// The transaction isolation level. /// A cancellation token. - Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, CancellationToken ct = default); + Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, + CancellationToken ct = default); /// - /// Gets collection metadata by name. + /// Gets collection metadata by name. /// /// The collection name. CollectionMetadata? GetCollectionMetadata(string name); /// - /// Saves collection metadata. + /// Saves collection metadata. /// /// The metadata to persist. void SaveCollectionMetadata(CollectionMetadata metadata); /// - /// Registers document mappers. + /// Registers document mappers. /// /// The mapper instances to register. void RegisterMappers(IEnumerable mappers); /// - /// Gets schema chain entries for the specified root page. + /// Gets schema chain entries for the specified root page. /// /// The schema root page identifier. List GetSchemas(uint rootPageId); /// - /// Appends a schema to the specified schema chain. + /// Appends a schema to the specified schema chain. /// /// The schema root page identifier. /// The schema to append. uint AppendSchema(uint rootPageId, BsonSchema schema); /// - /// Gets the key-to-token mapping. + /// Gets the key-to-token mapping. /// ConcurrentDictionary GetKeyMap(); /// - /// Gets the token-to-key mapping. + /// Gets the token-to-key mapping. /// ConcurrentDictionary GetKeyReverseMap(); /// - /// Gets or creates a dictionary token for the specified key. + /// Gets or creates a dictionary token for the specified key. /// /// The key value. ushort GetOrAddDictionaryEntry(string key); /// - /// Registers key values in the dictionary mapping. + /// Registers key values in the dictionary mapping. /// /// The keys to register. void RegisterKeys(IEnumerable keys); -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/PageFile.cs b/src/CBDD.Core/Storage/PageFile.cs index 262349b..c0e093c 100755 --- a/src/CBDD.Core/Storage/PageFile.cs +++ b/src/CBDD.Core/Storage/PageFile.cs @@ -1,107 +1,104 @@ using System.IO.MemoryMappedFiles; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - -/// -/// Configuration for page-based storage -/// + +namespace ZB.MOM.WW.CBDD.Core.Storage; + +/// +/// Configuration for page-based storage +/// public readonly struct PageFileConfig { /// - /// Gets the size of each page in bytes. + /// Gets the size of each page in bytes. /// public int PageSize { get; init; } /// - /// Gets the initial file size in bytes. + /// Gets the initial file size in bytes. /// public long InitialFileSize { get; init; } /// - /// Gets the memory-mapped file access mode. + /// Gets the memory-mapped file access mode. /// public MemoryMappedFileAccess Access { get; init; } - - /// - /// Small pages for embedded scenarios with many tiny documents - /// - public static PageFileConfig Small => new() - { - PageSize = 8192, // 8KB pages - InitialFileSize = 1024 * 1024, // 1MB initial - Access = MemoryMappedFileAccess.ReadWrite - }; - - /// - /// Default balanced configuration for document databases (16KB like MySQL InnoDB) - /// - public static PageFileConfig Default => new() - { - PageSize = 16384, // 16KB pages - InitialFileSize = 2 * 1024 * 1024, // 2MB initial - Access = MemoryMappedFileAccess.ReadWrite - }; - - /// - /// Large pages for databases with big documents (32KB like MongoDB WiredTiger) - /// - public static PageFileConfig Large => new() - { - PageSize = 32768, // 32KB pages - InitialFileSize = 4 * 1024 * 1024, // 4MB initial - Access = MemoryMappedFileAccess.ReadWrite - }; -} - -/// -/// Page-based file storage with memory-mapped I/O. -/// Manages fixed-size pages for efficient storage and retrieval. -/// + + /// + /// Small pages for embedded scenarios with many tiny documents + /// + public static PageFileConfig Small => new() + { + PageSize = 8192, // 8KB pages + InitialFileSize = 1024 * 1024, // 1MB initial + Access = MemoryMappedFileAccess.ReadWrite + }; + + /// + /// Default balanced configuration for document databases (16KB like MySQL InnoDB) + /// + public static PageFileConfig Default => new() + { + PageSize = 16384, // 16KB pages + InitialFileSize = 2 * 1024 * 1024, // 2MB initial + Access = MemoryMappedFileAccess.ReadWrite + }; + + /// + /// Large pages for databases with big documents (32KB like MongoDB WiredTiger) + /// + public static PageFileConfig Large => new() + { + PageSize = 32768, // 32KB pages + InitialFileSize = 4 * 1024 * 1024, // 4MB initial + Access = MemoryMappedFileAccess.ReadWrite + }; +} + +/// +/// Page-based file storage with memory-mapped I/O. +/// Manages fixed-size pages for efficient storage and retrieval. +/// public sealed class PageFile : IDisposable { - private readonly string _filePath; - private readonly PageFileConfig _config; - private FileStream? _fileStream; - private MemoryMappedFile? _mappedFile; + private readonly PageFileConfig _config; private readonly object _lock = new(); private bool _disposed; - private bool _wasCreated; - private uint _nextPageId; + private FileStream? _fileStream; private uint _firstFreePageId; - - /// - /// Gets the next page identifier that will be allocated. - /// - public uint NextPageId => _nextPageId; + private MemoryMappedFile? _mappedFile; /// - /// Indicates whether this file was newly created on the current open call. - /// - public bool WasCreated => _wasCreated; - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The file path for the page file. /// The page file configuration. public PageFile(string filePath, PageFileConfig config) - { - _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); - _config = config; - } - + { + FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); + _config = config; + } + /// - /// Gets the configured page size in bytes. + /// Gets the next page identifier that will be allocated. + /// + public uint NextPageId { get; private set; } + + /// + /// Indicates whether this file was newly created on the current open call. + /// + public bool WasCreated { get; private set; } + + /// + /// Gets the configured page size in bytes. /// public int PageSize => _config.PageSize; /// - /// Gets the underlying file path. + /// Gets the underlying file path. /// - public string FilePath => _filePath; + public string FilePath { get; } /// - /// Gets the current physical file length in bytes. + /// Gets the current physical file length in bytes. /// public long FileLengthBytes { @@ -116,189 +113,214 @@ public sealed class PageFile : IDisposable } /// - /// Gets the effective page-file configuration. + /// Gets the effective page-file configuration. /// public PageFileConfig Config => _config; - - /// - /// Opens the page file, creating it if it doesn't exist - /// - public void Open() - { - lock (_lock) - { + + /// + /// Releases resources used by the page file. + /// + public void Dispose() + { + if (_disposed) + return; + + lock (_lock) + { + // 1. Flush any pending writes from memory-mapped file + if (_fileStream != null) _fileStream.Flush(true); + + // 2. Close memory-mapped file first + _mappedFile?.Dispose(); + + // 3. Then close file stream + _fileStream?.Dispose(); + + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + /// + /// Opens the page file, creating it if it doesn't exist + /// + public void Open() + { + lock (_lock) + { if (_fileStream != null) return; // Already open - var fileExists = File.Exists(_filePath); + bool fileExists = File.Exists(FilePath); - _fileStream = new FileStream( - _filePath, + _fileStream = new FileStream( + FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, - FileShare.None, - bufferSize: 4096, - FileOptions.RandomAccess); - - _wasCreated = !fileExists || _fileStream.Length == 0; - if (_wasCreated) + FileShare.None, + 4096, + FileOptions.RandomAccess); + + WasCreated = !fileExists || _fileStream.Length == 0; + if (WasCreated) { // Initialize new file with 2 pages (Header + Collection Metadata) - _fileStream.SetLength(_config.InitialFileSize < _config.PageSize * 2 ? _config.PageSize * 2 : _config.InitialFileSize); - InitializeHeader(); - } - - // Initialize next page ID based on file length - _nextPageId = (uint)(_fileStream.Length / _config.PageSize); - - _mappedFile = MemoryMappedFile.CreateFromFile( - _fileStream, - null, - _fileStream.Length, - _config.Access, - HandleInheritability.None, - leaveOpen: true); + _fileStream.SetLength(_config.InitialFileSize < _config.PageSize * 2 + ? _config.PageSize * 2 + : _config.InitialFileSize); + InitializeHeader(); + } + + // Initialize next page ID based on file length + NextPageId = (uint)(_fileStream.Length / _config.PageSize); + + _mappedFile = MemoryMappedFile.CreateFromFile( + _fileStream, + null, + _fileStream.Length, + _config.Access, + HandleInheritability.None, + true); // Read free list head from Page 0 - if (_fileStream.Length >= _config.PageSize) - { - var headerSpan = new byte[32]; // PageHeader.Size - using var accessor = _mappedFile.CreateViewAccessor(0, 32, MemoryMappedFileAccess.Read); - accessor.ReadArray(0, headerSpan, 0, 32); - var header = PageHeader.ReadFrom(headerSpan); - _firstFreePageId = header.NextPageId; - } - } - } - - /// - /// Initializes the file header (page 0) and collection metadata (page 1) - /// - private void InitializeHeader() - { - // 1. Initialize Header (Page 0) - var header = new PageHeader - { - PageId = 0, - PageType = PageType.Header, - FreeBytes = (ushort)(_config.PageSize - 32), - NextPageId = 0, // No free pages initially - TransactionId = 0, - Checksum = 0 - }; - - Span buffer = stackalloc byte[_config.PageSize]; - header.WriteTo(buffer); - - _fileStream!.Position = 0; - _fileStream.Write(buffer); - - // 2. Initialize Collection Metadata (Page 1) - // This page is reserved for storing index definitions - buffer.Clear(); - var metaHeader = new SlottedPageHeader - { - PageId = 1, - PageType = PageType.Collection, - SlotCount = 0, - FreeSpaceStart = SlottedPageHeader.Size, - FreeSpaceEnd = (ushort)_config.PageSize, - NextOverflowPage = 0, - TransactionId = 0 - }; - metaHeader.WriteTo(buffer); - - _fileStream.Position = _config.PageSize; + if (_fileStream.Length >= _config.PageSize) + { + var headerSpan = new byte[32]; // PageHeader.Size + using var accessor = _mappedFile.CreateViewAccessor(0, 32, MemoryMappedFileAccess.Read); + accessor.ReadArray(0, headerSpan, 0, 32); + var header = PageHeader.ReadFrom(headerSpan); + _firstFreePageId = header.NextPageId; + } + } + } + + /// + /// Initializes the file header (page 0) and collection metadata (page 1) + /// + private void InitializeHeader() + { + // 1. Initialize Header (Page 0) + var header = new PageHeader + { + PageId = 0, + PageType = PageType.Header, + FreeBytes = (ushort)(_config.PageSize - 32), + NextPageId = 0, // No free pages initially + TransactionId = 0, + Checksum = 0 + }; + + Span buffer = stackalloc byte[_config.PageSize]; + header.WriteTo(buffer); + + _fileStream!.Position = 0; _fileStream.Write(buffer); - _fileStream.Flush(); - } - - // ... (ReadPage / WritePage unchanged) ... + // 2. Initialize Collection Metadata (Page 1) + // This page is reserved for storing index definitions + buffer.Clear(); + var metaHeader = new SlottedPageHeader + { + PageId = 1, + PageType = PageType.Collection, + SlotCount = 0, + FreeSpaceStart = SlottedPageHeader.Size, + FreeSpaceEnd = (ushort)_config.PageSize, + NextOverflowPage = 0, + TransactionId = 0 + }; + metaHeader.WriteTo(buffer); + + _fileStream.Position = _config.PageSize; + _fileStream.Write(buffer); + + _fileStream.Flush(); + } + + // ... (ReadPage / WritePage unchanged) ... /// - /// Reads a page by ID into the provided span + /// Reads a page by ID into the provided span /// /// The page identifier to read. /// The destination span that receives page bytes. public void ReadPage(uint pageId, Span destination) - { - if (destination.Length < _config.PageSize) - throw new ArgumentException($"Destination must be at least {_config.PageSize} bytes"); - - if (_mappedFile == null) - throw new InvalidOperationException("File not open"); - - var offset = (long)pageId * _config.PageSize; + { + if (destination.Length < _config.PageSize) + throw new ArgumentException($"Destination must be at least {_config.PageSize} bytes"); + + if (_mappedFile == null) + throw new InvalidOperationException("File not open"); + + long offset = pageId * _config.PageSize; + + using var accessor = _mappedFile.CreateViewAccessor(offset, _config.PageSize, MemoryMappedFileAccess.Read); + var temp = new byte[_config.PageSize]; + accessor.ReadArray(0, temp, 0, _config.PageSize); + temp.CopyTo(destination); + } - using var accessor = _mappedFile.CreateViewAccessor(offset, _config.PageSize, MemoryMappedFileAccess.Read); - var temp = new byte[_config.PageSize]; - accessor.ReadArray(0, temp, 0, _config.PageSize); - temp.CopyTo(destination); - } - /// - /// Writes a page at the specified ID from the provided span + /// Writes a page at the specified ID from the provided span /// /// The page identifier to write. /// The source span that contains page bytes. public void WritePage(uint pageId, ReadOnlySpan source) - { - if (source.Length < _config.PageSize) - throw new ArgumentException($"Source must be at least {_config.PageSize} bytes"); - - if (_mappedFile == null) - throw new InvalidOperationException("File not open"); - - var offset = (long)pageId * _config.PageSize; + { + if (source.Length < _config.PageSize) + throw new ArgumentException($"Source must be at least {_config.PageSize} bytes"); + + if (_mappedFile == null) + throw new InvalidOperationException("File not open"); + + long offset = pageId * _config.PageSize; // Ensure file is large enough - if (offset + _config.PageSize > _fileStream!.Length) - { - lock (_lock) - { - if (offset + _config.PageSize > _fileStream.Length) - { - var newSize = Math.Max(offset + _config.PageSize, _fileStream.Length * 2); + if (offset + _config.PageSize > _fileStream!.Length) + lock (_lock) + { + if (offset + _config.PageSize > _fileStream.Length) + { + long newSize = Math.Max(offset + _config.PageSize, _fileStream.Length * 2); _fileStream.SetLength(newSize); // Recreate memory-mapped file with new size - _mappedFile.Dispose(); - _mappedFile = MemoryMappedFile.CreateFromFile( - _fileStream, - null, - _fileStream.Length, - _config.Access, - HandleInheritability.None, - leaveOpen: true); - } - } - } - - // Write to memory-mapped file - using (var accessor = _mappedFile.CreateViewAccessor(offset, _config.PageSize, MemoryMappedFileAccess.Write)) - { - accessor.WriteArray(0, source.ToArray(), 0, _config.PageSize); - } - } - - /// - /// Allocates a new page (reuses free page if available) and returns its ID - /// - public uint AllocatePage() - { - lock (_lock) - { - if (_fileStream == null) - throw new InvalidOperationException("File not open"); - - // 1. Try to reuse a free page - if (_firstFreePageId != 0) - { - var recycledPageId = _firstFreePageId; + _mappedFile.Dispose(); + _mappedFile = MemoryMappedFile.CreateFromFile( + _fileStream, + null, + _fileStream.Length, + _config.Access, + HandleInheritability.None, + true); + } + } + + // Write to memory-mapped file + using (var accessor = _mappedFile.CreateViewAccessor(offset, _config.PageSize, MemoryMappedFileAccess.Write)) + { + accessor.WriteArray(0, source.ToArray(), 0, _config.PageSize); + } + } + + /// + /// Allocates a new page (reuses free page if available) and returns its ID + /// + public uint AllocatePage() + { + lock (_lock) + { + if (_fileStream == null) + throw new InvalidOperationException("File not open"); + + // 1. Try to reuse a free page + if (_firstFreePageId != 0) + { + uint recycledPageId = _firstFreePageId; // Read the recycled page to update the free list head - var buffer = new byte[_config.PageSize]; - ReadPage(recycledPageId, buffer); + var buffer = new byte[_config.PageSize]; + ReadPage(recycledPageId, buffer); var header = PageHeader.ReadFrom(buffer); // The new head is what the recycled page pointed to @@ -307,56 +329,56 @@ public sealed class PageFile : IDisposable // Update file header (Page 0) to point to new head UpdateFileHeaderFreePtr(_firstFreePageId); - return recycledPageId; - } - - // 2. No free pages, append new one - var pageId = _nextPageId++; + return recycledPageId; + } + + // 2. No free pages, append new one + uint pageId = NextPageId++; // Extend file if necessary - var requiredLength = (long)(pageId + 1) * _config.PageSize; - if (requiredLength > _fileStream.Length) - { - var newSize = Math.Max(requiredLength, _fileStream.Length * 2); + long requiredLength = (pageId + 1) * _config.PageSize; + if (requiredLength > _fileStream.Length) + { + long newSize = Math.Max(requiredLength, _fileStream.Length * 2); _fileStream.SetLength(newSize); // Recreate memory-mapped file with new size - _mappedFile?.Dispose(); - _mappedFile = MemoryMappedFile.CreateFromFile( - _fileStream, - null, - _fileStream.Length, - _config.Access, - HandleInheritability.None, - leaveOpen: true); + _mappedFile?.Dispose(); + _mappedFile = MemoryMappedFile.CreateFromFile( + _fileStream, + null, + _fileStream.Length, + _config.Access, + HandleInheritability.None, + true); } - return pageId; - } + return pageId; + } } /// - /// Marks a page as free and adds it to the free list + /// Marks a page as free and adds it to the free list /// /// The page identifier to mark as free. public void FreePage(uint pageId) - { - lock (_lock) - { - if (_fileStream == null) throw new InvalidOperationException("File not open"); + { + lock (_lock) + { + if (_fileStream == null) throw new InvalidOperationException("File not open"); if (pageId == 0) throw new InvalidOperationException("Cannot free header page 0"); // 1. Create a free page header pointing to current head - var header = new PageHeader - { - PageId = pageId, - PageType = PageType.Free, - NextPageId = _firstFreePageId, // Point to previous head - TransactionId = 0, - Checksum = 0 + var header = new PageHeader + { + PageId = pageId, + PageType = PageType.Free, + NextPageId = _firstFreePageId, // Point to previous head + TransactionId = 0, + Checksum = 0 }; - var buffer = new byte[_config.PageSize]; + var buffer = new byte[_config.PageSize]; header.WriteTo(buffer); // 2. Write the freed page @@ -366,8 +388,8 @@ public sealed class PageFile : IDisposable _firstFreePageId = pageId; // 4. Update file header (Page 0) - UpdateFileHeaderFreePtr(_firstFreePageId); - } + UpdateFileHeaderFreePtr(_firstFreePageId); + } } private void UpdateFileHeaderFreePtr(uint newHead) @@ -386,7 +408,7 @@ public sealed class PageFile : IDisposable } /// - /// Reads bytes from the page 0 extension region (immediately after the 32-byte file header). + /// Reads bytes from the page 0 extension region (immediately after the 32-byte file header). /// /// Offset into the extension region, relative to byte 32. /// Destination span receiving bytes. @@ -401,18 +423,20 @@ public sealed class PageFile : IDisposable if (_mappedFile == null) throw new InvalidOperationException("File not open"); - var absoluteOffset = 32 + extensionOffset; + int absoluteOffset = 32 + extensionOffset; if (absoluteOffset + destination.Length > _config.PageSize) - throw new ArgumentOutOfRangeException(nameof(destination), "Requested range exceeds page 0 extension region."); + throw new ArgumentOutOfRangeException(nameof(destination), + "Requested range exceeds page 0 extension region."); - using var accessor = _mappedFile.CreateViewAccessor(absoluteOffset, destination.Length, MemoryMappedFileAccess.Read); + using var accessor = + _mappedFile.CreateViewAccessor(absoluteOffset, destination.Length, MemoryMappedFileAccess.Read); var temp = new byte[destination.Length]; accessor.ReadArray(0, temp, 0, temp.Length); temp.CopyTo(destination); } /// - /// Writes bytes to the page 0 extension region (immediately after the 32-byte file header). + /// Writes bytes to the page 0 extension region (immediately after the 32-byte file header). /// /// Offset into the extension region, relative to byte 32. /// Source bytes to write. @@ -427,28 +451,29 @@ public sealed class PageFile : IDisposable if (_mappedFile == null) throw new InvalidOperationException("File not open"); - var absoluteOffset = 32 + extensionOffset; + int absoluteOffset = 32 + extensionOffset; if (absoluteOffset + source.Length > _config.PageSize) throw new ArgumentOutOfRangeException(nameof(source), "Requested range exceeds page 0 extension region."); - using var accessor = _mappedFile.CreateViewAccessor(absoluteOffset, source.Length, MemoryMappedFileAccess.Write); + using var accessor = + _mappedFile.CreateViewAccessor(absoluteOffset, source.Length, MemoryMappedFileAccess.Write); accessor.WriteArray(0, source.ToArray(), 0, source.Length); } - - /// - /// Flushes all pending writes to disk. - /// Called by CheckpointManager after applying WAL changes. - /// + + /// + /// Flushes all pending writes to disk. + /// Called by CheckpointManager after applying WAL changes. + /// public void Flush() { lock (_lock) { - _fileStream?.Flush(flushToDisk: true); + _fileStream?.Flush(true); } } /// - /// Writes a durable snapshot of the currently opened page file to a separate path. + /// Writes a durable snapshot of the currently opened page file to a separate path. /// /// Destination file path for the snapshot. public void SnapshotToFile(string destinationPath) @@ -460,14 +485,11 @@ public sealed class PageFile : IDisposable { EnsureFileOpen(); - var directory = Path.GetDirectoryName(destinationPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } + string? directory = Path.GetDirectoryName(destinationPath); + if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); - _fileStream!.Flush(flushToDisk: true); - var originalPosition = _fileStream.Position; + _fileStream!.Flush(true); + long originalPosition = _fileStream.Position; try { _fileStream.Position = 0; @@ -476,10 +498,10 @@ public sealed class PageFile : IDisposable FileMode.Create, FileAccess.Write, FileShare.None, - bufferSize: 128 * 1024, + 128 * 1024, FileOptions.SequentialScan | FileOptions.WriteThrough); _fileStream.CopyTo(destination); - destination.Flush(flushToDisk: true); + destination.Flush(true); } finally { @@ -489,7 +511,7 @@ public sealed class PageFile : IDisposable } /// - /// Replaces the current file bytes with bytes from a source file and remaps the memory-mapped view. + /// Replaces the current file bytes with bytes from a source file and remaps the memory-mapped view. /// /// The source file path used as replacement content. public void ReplaceFromFile(string sourcePath) @@ -508,11 +530,12 @@ public sealed class PageFile : IDisposable FileMode.Open, FileAccess.Read, FileShare.Read, - bufferSize: 128 * 1024, + 128 * 1024, FileOptions.SequentialScan); if (source.Length <= 0 || source.Length % _config.PageSize != 0) - throw new InvalidDataException($"Replacement file length must be a positive multiple of page size ({_config.PageSize})."); + throw new InvalidDataException( + $"Replacement file length must be a positive multiple of page size ({_config.PageSize})."); _mappedFile?.Dispose(); _mappedFile = null; @@ -520,7 +543,7 @@ public sealed class PageFile : IDisposable _fileStream!.SetLength(source.Length); _fileStream.Position = 0; source.CopyTo(_fileStream); - _fileStream.Flush(flushToDisk: true); + _fileStream.Flush(true); _mappedFile = MemoryMappedFile.CreateFromFile( _fileStream, @@ -528,16 +551,17 @@ public sealed class PageFile : IDisposable _fileStream.Length, _config.Access, HandleInheritability.None, - leaveOpen: true); + true); - _nextPageId = (uint)(_fileStream.Length / _config.PageSize); + NextPageId = (uint)(_fileStream.Length / _config.PageSize); _firstFreePageId = 0; if (_fileStream.Length >= _config.PageSize) { const int pageHeaderSizeBytes = 32; var headerSpan = new byte[pageHeaderSizeBytes]; - using var accessor = _mappedFile.CreateViewAccessor(0, pageHeaderSizeBytes, MemoryMappedFileAccess.Read); + using var accessor = + _mappedFile.CreateViewAccessor(0, pageHeaderSizeBytes, MemoryMappedFileAccess.Read); accessor.ReadArray(0, headerSpan, 0, pageHeaderSizeBytes); var header = PageHeader.ReadFrom(headerSpan); _firstFreePageId = header.NextPageId; @@ -546,9 +570,9 @@ public sealed class PageFile : IDisposable } /// - /// Enumerates all free pages, combining explicit free-list entries and reclaimable empty pages. + /// Enumerates all free pages, combining explicit free-list entries and reclaimable empty pages. /// - /// If set to , includes all-zero pages as reclaimable. + /// If set to , includes all-zero pages as reclaimable. /// A sorted list of free page identifiers. public IReadOnlyList EnumerateFreePages(bool includeEmptyPages = true) { @@ -561,9 +585,12 @@ public sealed class PageFile : IDisposable } /// - /// Normalizes the free-list by rebuilding it from a deterministic sorted free page set. + /// Normalizes the free-list by rebuilding it from a deterministic sorted free page set. /// - /// If set to , all-zero pages are converted into explicit free-list pages. + /// + /// If set to , all-zero pages are converted into explicit free-list + /// pages. + /// /// The number of pages in the normalized free-list. public int NormalizeFreeList(bool includeEmptyPages = true) { @@ -577,10 +604,13 @@ public sealed class PageFile : IDisposable } /// - /// Truncates contiguous reclaimable pages at the end of the file. - /// Reclaimable tail pages include explicit free pages and truly empty pages. + /// Truncates contiguous reclaimable pages at the end of the file. + /// Reclaimable tail pages include explicit free pages and truly empty pages. /// - /// Minimum number of pages that must remain after truncation (defaults to header + metadata pages). + /// + /// Minimum number of pages that must remain after truncation (defaults to header + metadata + /// pages). + /// /// Details about the truncation operation. public TailTruncationResult TruncateReclaimableTailPages(uint minimumPageCount = 2) { @@ -588,31 +618,22 @@ public sealed class PageFile : IDisposable { EnsureFileOpen(); - if (_nextPageId <= minimumPageCount) - { - return TailTruncationResult.None(_nextPageId); - } + if (NextPageId <= minimumPageCount) return TailTruncationResult.None(NextPageId); - var freePages = new HashSet(CollectFreePageIds(includeEmptyPages: true)); - var originalPageCount = _nextPageId; - var newPageCount = _nextPageId; + var freePages = new HashSet(CollectFreePageIds(true)); + uint originalPageCount = NextPageId; + uint newPageCount = NextPageId; var pageBuffer = new byte[_config.PageSize]; while (newPageCount > minimumPageCount) { - var candidatePageId = newPageCount - 1; - if (!IsReclaimableTailPage(candidatePageId, freePages, pageBuffer)) - { - break; - } + uint candidatePageId = newPageCount - 1; + if (!IsReclaimableTailPage(candidatePageId, freePages, pageBuffer)) break; newPageCount--; } - if (newPageCount == originalPageCount) - { - return TailTruncationResult.None(originalPageCount); - } + if (newPageCount == originalPageCount) return TailTruncationResult.None(originalPageCount); freePages.RemoveWhere(pageId => pageId >= newPageCount); var remainingFreePages = freePages.ToList(); @@ -622,10 +643,10 @@ public sealed class PageFile : IDisposable _mappedFile?.Dispose(); _mappedFile = null; - var previousLengthBytes = _fileStream!.Length; - var newLengthBytes = (long)newPageCount * _config.PageSize; + long previousLengthBytes = _fileStream!.Length; + long newLengthBytes = newPageCount * _config.PageSize; _fileStream.SetLength(newLengthBytes); - _fileStream.Flush(flushToDisk: true); + _fileStream.Flush(true); _mappedFile = MemoryMappedFile.CreateFromFile( _fileStream, @@ -633,9 +654,9 @@ public sealed class PageFile : IDisposable newLengthBytes, _config.Access, HandleInheritability.None, - leaveOpen: true); + true); - _nextPageId = newPageCount; + NextPageId = newPageCount; return new TailTruncationResult( originalPageCount, @@ -646,7 +667,7 @@ public sealed class PageFile : IDisposable } /// - /// Trims excess physical file capacity beyond the current logical page count. + /// Trims excess physical file capacity beyond the current logical page count. /// /// The number of bytes removed from the file. public long TrimExcessCapacityToLogicalPageCount() @@ -655,8 +676,8 @@ public sealed class PageFile : IDisposable { EnsureFileOpen(); - var targetLengthBytes = (long)_nextPageId * _config.PageSize; - var currentLengthBytes = _fileStream!.Length; + long targetLengthBytes = NextPageId * _config.PageSize; + long currentLengthBytes = _fileStream!.Length; if (currentLengthBytes <= targetLengthBytes) return 0; @@ -664,7 +685,7 @@ public sealed class PageFile : IDisposable _mappedFile = null; _fileStream.SetLength(targetLengthBytes); - _fileStream.Flush(flushToDisk: true); + _fileStream.Flush(true); _mappedFile = MemoryMappedFile.CreateFromFile( _fileStream, @@ -672,18 +693,18 @@ public sealed class PageFile : IDisposable targetLengthBytes, _config.Access, HandleInheritability.None, - leaveOpen: true); + true); return currentLengthBytes - targetLengthBytes; } } /// - /// Defragments a slotted page in place by packing live slot payloads densely at the end of the page. + /// Defragments a slotted page in place by packing live slot payloads densely at the end of the page. /// /// The page identifier to defragment. /// The number of free bytes reclaimed by defragmentation. - /// when the page layout changed; otherwise, . + /// when the page layout changed; otherwise, . public bool DefragmentSlottedPage(uint pageId, out int reclaimedBytes) { var result = DefragmentSlottedPageWithStats(pageId); @@ -692,7 +713,7 @@ public sealed class PageFile : IDisposable } /// - /// Defragments a slotted page in place and returns detailed relocation stats. + /// Defragments a slotted page in place and returns detailed relocation stats. /// /// The page identifier to defragment. public SlottedPageDefragmentationResult DefragmentSlottedPageWithStats(uint pageId) @@ -701,7 +722,7 @@ public sealed class PageFile : IDisposable { EnsureFileOpen(); - if (pageId >= _nextPageId) + if (pageId >= NextPageId) throw new ArgumentOutOfRangeException(nameof(pageId)); var pageBuffer = new byte[_config.PageSize]; @@ -717,11 +738,11 @@ public sealed class PageFile : IDisposable } /// - /// Defragments a slotted-page buffer in memory by rewriting live slots densely. + /// Defragments a slotted-page buffer in memory by rewriting live slots densely. /// /// The page buffer to compact in place. /// The number of free bytes reclaimed by compaction. - /// when compaction modified the page; otherwise, . + /// when compaction modified the page; otherwise, . public static bool TryDefragmentSlottedPage(Span pageBuffer, out int reclaimedBytes) { var result = TryDefragmentSlottedPageWithStats(pageBuffer); @@ -730,7 +751,7 @@ public sealed class PageFile : IDisposable } /// - /// Defragments a slotted-page buffer in memory and returns detailed relocation stats. + /// Defragments a slotted-page buffer in memory and returns detailed relocation stats. /// /// The page buffer to compact in place. public static SlottedPageDefragmentationResult TryDefragmentSlottedPageWithStats(Span pageBuffer) @@ -742,7 +763,7 @@ public sealed class PageFile : IDisposable if (!IsSlottedPageType(header.PageType)) return SlottedPageDefragmentationResult.None; - var slotArrayEnd = SlottedPageHeader.Size + (header.SlotCount * SlotEntry.Size); + int slotArrayEnd = SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size; if (slotArrayEnd > pageBuffer.Length) return SlottedPageDefragmentationResult.None; @@ -750,29 +771,29 @@ public sealed class PageFile : IDisposable for (ushort i = 0; i < header.SlotCount; i++) { - var slotOffset = SlottedPageHeader.Size + (i * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + i * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.Slice(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0 || slot.Length == 0) continue; - var dataEnd = slot.Offset + slot.Length; + int dataEnd = slot.Offset + slot.Length; if (slot.Offset < slotArrayEnd || dataEnd > pageBuffer.Length) return SlottedPageDefragmentationResult.None; - var slotData = pageBuffer.Slice(slot.Offset, slot.Length).ToArray(); + byte[] slotData = pageBuffer.Slice(slot.Offset, slot.Length).ToArray(); activeSlots.Add((i, slot, slotData)); } var newFreeSpaceStart = (ushort)slotArrayEnd; - var writeCursor = pageBuffer.Length; + int writeCursor = pageBuffer.Length; var changed = false; var relocatedSlots = 0; - var oldFreeBytes = header.AvailableFreeSpace; + int oldFreeBytes = header.AvailableFreeSpace; for (var i = 0; i < activeSlots.Count; i++) { - var (slotIndex, slot, slotData) = activeSlots[i]; + (ushort slotIndex, var slot, byte[] slotData) = activeSlots[i]; writeCursor -= slotData.Length; if (writeCursor < newFreeSpaceStart) return SlottedPageDefragmentationResult.None; @@ -786,56 +807,23 @@ public sealed class PageFile : IDisposable changed = true; } - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; slot.WriteTo(pageBuffer.Slice(slotOffset, SlotEntry.Size)); } if (writeCursor > newFreeSpaceStart) - { pageBuffer.Slice(newFreeSpaceStart, writeCursor - newFreeSpaceStart).Clear(); - } - if (header.FreeSpaceStart != newFreeSpaceStart || header.FreeSpaceEnd != writeCursor) - { - changed = true; - } + if (header.FreeSpaceStart != newFreeSpaceStart || header.FreeSpaceEnd != writeCursor) changed = true; header.FreeSpaceStart = newFreeSpaceStart; header.FreeSpaceEnd = (ushort)writeCursor; header.WriteTo(pageBuffer); - var newFreeBytes = header.AvailableFreeSpace; - var reclaimedBytes = Math.Max(0, newFreeBytes - oldFreeBytes); + int newFreeBytes = header.AvailableFreeSpace; + int reclaimedBytes = Math.Max(0, newFreeBytes - oldFreeBytes); return new SlottedPageDefragmentationResult(changed, reclaimedBytes, relocatedSlots); } - - /// - /// Releases resources used by the page file. - /// - public void Dispose() - { - if (_disposed) - return; - - lock (_lock) - { - // 1. Flush any pending writes from memory-mapped file - if (_fileStream != null) - { - _fileStream.Flush(flushToDisk: true); - } - - // 2. Close memory-mapped file first - _mappedFile?.Dispose(); - - // 3. Then close file stream - _fileStream?.Dispose(); - - _disposed = true; - } - - GC.SuppressFinalize(this); - } private void EnsureFileOpen() { @@ -846,26 +834,23 @@ public sealed class PageFile : IDisposable private List CollectFreePageIds(bool includeEmptyPages) { var freePages = new HashSet(); - if (_nextPageId <= 2) + if (NextPageId <= 2) return []; var pageBuffer = new byte[_config.PageSize]; var seen = new HashSet(); - var current = _firstFreePageId; - while (current != 0 && current < _nextPageId && seen.Add(current)) + uint current = _firstFreePageId; + while (current != 0 && current < NextPageId && seen.Add(current)) { - if (current > 1) - { - freePages.Add(current); - } + if (current > 1) freePages.Add(current); ReadPage(current, pageBuffer); var header = PageHeader.ReadFrom(pageBuffer); current = header.NextPageId; } - for (uint pageId = 2; pageId < _nextPageId; pageId++) + for (uint pageId = 2; pageId < NextPageId; pageId++) { ReadPage(pageId, pageBuffer); var header = PageHeader.ReadFrom(pageBuffer); @@ -876,10 +861,7 @@ public sealed class PageFile : IDisposable continue; } - if (includeEmptyPages && IsTrulyEmptyPage(pageBuffer)) - { - freePages.Add(pageId); - } + if (includeEmptyPages && IsTrulyEmptyPage(pageBuffer)) freePages.Add(pageId); } var ordered = freePages.ToList(); @@ -900,8 +882,8 @@ public sealed class PageFile : IDisposable for (var i = 0; i < sortedFreePageIds.Count; i++) { - var pageId = sortedFreePageIds[i]; - var nextPageId = i + 1 < sortedFreePageIds.Count ? sortedFreePageIds[i + 1] : 0; + uint pageId = sortedFreePageIds[i]; + uint nextPageId = i + 1 < sortedFreePageIds.Count ? sortedFreePageIds[i + 1] : 0; Array.Clear(pageBuffer, 0, pageBuffer.Length); var freeHeader = new PageHeader @@ -928,7 +910,7 @@ public sealed class PageFile : IDisposable private bool IsReclaimableTailPage(uint pageId, HashSet explicitFreePages, byte[] pageBuffer) { - if (pageId <= 1 || pageId >= _nextPageId) + if (pageId <= 1 || pageId >= NextPageId) return false; if (explicitFreePages.Contains(pageId)) @@ -945,27 +927,25 @@ public sealed class PageFile : IDisposable private static bool IsTrulyEmptyPage(ReadOnlySpan pageBuffer) { for (var i = 0; i < pageBuffer.Length; i++) - { if (pageBuffer[i] != 0) return false; - } return true; } } /// -/// Detailed result from slotted-page defragmentation. +/// Detailed result from slotted-page defragmentation. /// public readonly struct SlottedPageDefragmentationResult { /// - /// No-op result for non-slotted or invalid buffers. + /// No-op result for non-slotted or invalid buffers. /// public static SlottedPageDefragmentationResult None => new(false, 0, 0); /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// Indicates whether the page layout changed. /// The number of bytes reclaimed. @@ -978,28 +958,28 @@ public readonly struct SlottedPageDefragmentationResult } /// - /// Gets a value indicating whether the page layout changed. + /// Gets a value indicating whether the page layout changed. /// public bool Changed { get; } /// - /// Gets reclaimed free bytes after defragmentation. + /// Gets reclaimed free bytes after defragmentation. /// public int ReclaimedBytes { get; } /// - /// Gets the number of live slots that were moved to a new offset. + /// Gets the number of live slots that were moved to a new offset. /// public int RelocatedSlotCount { get; } } /// -/// Result of a reclaimable tail truncation operation. +/// Result of a reclaimable tail truncation operation. /// public readonly struct TailTruncationResult { /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The page count before truncation. /// The page count after truncation. @@ -1014,31 +994,31 @@ public readonly struct TailTruncationResult } /// - /// Gets the page count before truncation. + /// Gets the page count before truncation. /// public uint PrePageCount { get; } /// - /// Gets the page count after truncation. + /// Gets the page count after truncation. /// public uint PostPageCount { get; } /// - /// Gets the number of truncated pages. + /// Gets the number of truncated pages. /// public uint TruncatedPages { get; } /// - /// Gets the number of truncated bytes. + /// Gets the number of truncated bytes. /// public long TruncatedBytes { get; } /// - /// Creates a no-op truncation result. + /// Creates a no-op truncation result. /// /// The page count to assign before and after truncation. public static TailTruncationResult None(uint pageCount) { return new TailTruncationResult(pageCount, pageCount, 0, 0); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/PageHeader.cs b/src/CBDD.Core/Storage/PageHeader.cs index 8acb70e..9c5ad8a 100755 --- a/src/CBDD.Core/Storage/PageHeader.cs +++ b/src/CBDD.Core/Storage/PageHeader.cs @@ -3,57 +3,45 @@ using System.Runtime.InteropServices; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Represents a page header in the database file. -/// Fixed 32-byte structure at the start of each page. -/// Implemented as struct for efficient memory layout. +/// Represents a page header in the database file. +/// Fixed 32-byte structure at the start of each page. +/// Implemented as struct for efficient memory layout. /// [StructLayout(LayoutKind.Explicit, Size = 32)] public struct PageHeader { /// Page ID (offset in pages from start of file) - [FieldOffset(0)] - public uint PageId; - + [FieldOffset(0)] public uint PageId; + /// Type of this page - [FieldOffset(4)] - public PageType PageType; - + [FieldOffset(4)] public PageType PageType; + /// Number of free bytes in this page - [FieldOffset(5)] - public ushort FreeBytes; - + [FieldOffset(5)] public ushort FreeBytes; + /// ID of next page in linked list (0 if none). For Page 0 (Header), this points to the First Free Page. - [FieldOffset(7)] - public uint NextPageId; - + [FieldOffset(7)] public uint NextPageId; + /// Transaction ID that last modified this page - [FieldOffset(11)] - public ulong TransactionId; - + [FieldOffset(11)] public ulong TransactionId; + /// Checksum for data integrity (CRC32) - [FieldOffset(19)] - public uint Checksum; - + [FieldOffset(19)] public uint Checksum; + /// Dictionary Root Page ID (Only used in Page 0 / File Header) - [FieldOffset(23)] - public uint DictionaryRootPageId; + [FieldOffset(23)] public uint DictionaryRootPageId; - [FieldOffset(27)] - private byte _reserved5; - [FieldOffset(28)] - private byte _reserved6; - [FieldOffset(29)] - private byte _reserved7; - [FieldOffset(30)] - private byte _reserved8; - [FieldOffset(31)] - private byte _reserved9; + [FieldOffset(27)] private byte _reserved5; + [FieldOffset(28)] private byte _reserved6; + [FieldOffset(29)] private byte _reserved7; + [FieldOffset(30)] private byte _reserved8; + [FieldOffset(31)] private byte _reserved9; - /// - /// Writes the header to a span - /// - /// The destination span that receives the serialized header. - public readonly void WriteTo(Span destination) + /// + /// Writes the header to a span + /// + /// The destination span that receives the serialized header. + public readonly void WriteTo(Span destination) { if (destination.Length < 32) throw new ArgumentException("Destination must be at least 32 bytes"); @@ -61,15 +49,15 @@ public struct PageHeader MemoryMarshal.Write(destination, in this); } - /// - /// Reads a header from a span - /// - /// The source span containing a serialized header. - public static PageHeader ReadFrom(ReadOnlySpan source) + /// + /// Reads a header from a span + /// + /// The source span containing a serialized header. + public static PageHeader ReadFrom(ReadOnlySpan source) { if (source.Length < 32) throw new ArgumentException("Source must be at least 32 bytes"); return MemoryMarshal.Read(source); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/PageType.cs b/src/CBDD.Core/Storage/PageType.cs index bbc2f4b..4b1555c 100755 --- a/src/CBDD.Core/Storage/PageType.cs +++ b/src/CBDD.Core/Storage/PageType.cs @@ -1,28 +1,28 @@ namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Page types in the database file +/// Page types in the database file /// public enum PageType : byte { /// Empty/free page - Empty = 0, - + Empty = 0, + /// File header page (page 0) - Header = 1, - + Header = 1, + /// Collection metadata page - Collection = 2, - + Collection = 2, + /// Data page containing documents - Data = 3, - + Data = 3, + /// Index B+Tree node page - Index = 4, - + Index = 4, + /// Free page list - FreeList = 5, - + FreeList = 5, + /// Overflow page for large documents Overflow = 6, @@ -40,4 +40,4 @@ public enum PageType : byte /// GEO Spatial index page Spatial = 11 -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/SlottedPage.cs b/src/CBDD.Core/Storage/SlottedPage.cs index 7ce45aa..3f35eb9 100755 --- a/src/CBDD.Core/Storage/SlottedPage.cs +++ b/src/CBDD.Core/Storage/SlottedPage.cs @@ -1,50 +1,43 @@ -using System.Runtime.InteropServices; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - -/// -/// Header for slotted pages supporting multiple variable-size documents per page. -/// Fixed 24-byte structure at start of each data page. -/// -[StructLayout(LayoutKind.Explicit, Size = 24)] +using System.Buffers.Binary; +using System.Runtime.InteropServices; + +namespace ZB.MOM.WW.CBDD.Core.Storage; + +/// +/// Header for slotted pages supporting multiple variable-size documents per page. +/// Fixed 24-byte structure at start of each data page. +/// +[StructLayout(LayoutKind.Explicit, Size = 24)] public struct SlottedPageHeader { - /// Page ID - [FieldOffset(0)] - public uint PageId; + /// Page ID + [FieldOffset(0)] public uint PageId; - /// Type of page (Data, Overflow, Index, Metadata) - [FieldOffset(4)] - public PageType PageType; + /// Type of page (Data, Overflow, Index, Metadata) + [FieldOffset(4)] public PageType PageType; - /// Number of slot entries in this page - [FieldOffset(8)] - public ushort SlotCount; + /// Number of slot entries in this page + [FieldOffset(8)] public ushort SlotCount; - /// Offset where free space starts (grows down with slots) - [FieldOffset(10)] - public ushort FreeSpaceStart; + /// Offset where free space starts (grows down with slots) + [FieldOffset(10)] public ushort FreeSpaceStart; - /// Offset where free space ends (grows up with data) - [FieldOffset(12)] - public ushort FreeSpaceEnd; + /// Offset where free space ends (grows up with data) + [FieldOffset(12)] public ushort FreeSpaceEnd; - /// Next overflow page ID (0 if none) - [FieldOffset(14)] - public uint NextOverflowPage; + /// Next overflow page ID (0 if none) + [FieldOffset(14)] public uint NextOverflowPage; - /// Transaction ID that last modified this page - [FieldOffset(18)] - public uint TransactionId; + /// Transaction ID that last modified this page + [FieldOffset(18)] public uint TransactionId; - /// Reserved for future use - [FieldOffset(22)] - public ushort Reserved; + /// Reserved for future use + [FieldOffset(22)] public ushort Reserved; public const int Size = 24; /// - /// Initializes a header with the current slotted-page format marker. + /// Initializes a header with the current slotted-page format marker. /// public SlottedPageHeader() { @@ -52,13 +45,13 @@ public struct SlottedPageHeader Reserved = StorageFormatConstants.SlottedPageFormatMarker; } - /// - /// Gets available free space in bytes - /// + /// + /// Gets available free space in bytes + /// public readonly int AvailableFreeSpace => FreeSpaceEnd - FreeSpaceStart; /// - /// Writes header to span + /// Writes header to span /// /// The destination span that receives the serialized header. public readonly void WriteTo(Span destination) @@ -66,11 +59,11 @@ public struct SlottedPageHeader if (destination.Length < Size) throw new ArgumentException($"Destination must be at least {Size} bytes"); - MemoryMarshal.Write(destination, in this); + MemoryMarshal.Write(destination, in this); } /// - /// Reads header from span + /// Reads header from span /// /// The source span containing the serialized header. public static SlottedPageHeader ReadFrom(ReadOnlySpan source) @@ -78,33 +71,30 @@ public struct SlottedPageHeader if (source.Length < Size) throw new ArgumentException($"Source must be at least {Size} bytes"); - return MemoryMarshal.Read(source); - } -} - -/// -/// Slot entry pointing to a document within a page. -/// Fixed 8-byte structure in slot array. -/// -[StructLayout(LayoutKind.Explicit, Size = 8)] -public struct SlotEntry -{ - /// Offset to document data within page - [FieldOffset(0)] - public ushort Offset; + return MemoryMarshal.Read(source); + } +} - /// Length of document data in bytes - [FieldOffset(2)] - public ushort Length; +/// +/// Slot entry pointing to a document within a page. +/// Fixed 8-byte structure in slot array. +/// +[StructLayout(LayoutKind.Explicit, Size = 8)] +public struct SlotEntry +{ + /// Offset to document data within page + [FieldOffset(0)] public ushort Offset; + + /// Length of document data in bytes + [FieldOffset(2)] public ushort Length; + + /// Slot flags (deleted, overflow, etc.) + [FieldOffset(4)] public SlotFlags Flags; - /// Slot flags (deleted, overflow, etc.) - [FieldOffset(4)] - public SlotFlags Flags; - public const int Size = 8; /// - /// Writes slot entry to span + /// Writes slot entry to span /// /// The destination span that receives the serialized slot entry. public readonly void WriteTo(Span destination) @@ -112,11 +102,11 @@ public struct SlotEntry if (destination.Length < Size) throw new ArgumentException($"Destination must be at least {Size} bytes"); - MemoryMarshal.Write(destination, in this); + MemoryMarshal.Write(destination, in this); } /// - /// Reads slot entry from span + /// Reads slot entry from span /// /// The source span containing the serialized slot entry. public static SlotEntry ReadFrom(ReadOnlySpan source) @@ -124,46 +114,47 @@ public struct SlotEntry if (source.Length < Size) throw new ArgumentException($"Source must be at least {Size} bytes"); - return MemoryMarshal.Read(source); - } -} - -/// -/// Flags for slot entries -/// -[Flags] -public enum SlotFlags : uint -{ - /// Slot is active and contains data + return MemoryMarshal.Read(source); + } +} + +/// +/// Flags for slot entries +/// +[Flags] +public enum SlotFlags : uint +{ + /// Slot is active and contains data None = 0, - /// Slot is marked as deleted (can be reused) + /// Slot is marked as deleted (can be reused) Deleted = 1 << 0, - /// Document continues in overflow pages + /// Document continues in overflow pages HasOverflow = 1 << 1, - /// Document data is compressed - Compressed = 1 << 2, -} - -/// -/// Location of a document within the database. -/// Maps ObjectId to specific page and slot. -/// + /// Document data is compressed + Compressed = 1 << 2 +} + +/// +/// Location of a document within the database. +/// Maps ObjectId to specific page and slot. +/// public readonly struct DocumentLocation { /// - /// Gets the page identifier containing the document. + /// Gets the page identifier containing the document. /// public uint PageId { get; init; } + /// - /// Gets the slot index within the page. + /// Gets the slot index within the page. /// public ushort SlotIndex { get; init; } /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The page identifier containing the document. /// The slot index within the page. @@ -174,7 +165,7 @@ public readonly struct DocumentLocation } /// - /// Serializes DocumentLocation to a byte span (6 bytes: 4 for PageId + 2 for SlotIndex) + /// Serializes DocumentLocation to a byte span (6 bytes: 4 for PageId + 2 for SlotIndex) /// /// The destination span that receives the serialized value. public void WriteTo(Span destination) @@ -182,12 +173,12 @@ public readonly struct DocumentLocation if (destination.Length < 6) throw new ArgumentException("Destination must be at least 6 bytes", nameof(destination)); - System.Buffers.Binary.BinaryPrimitives.WriteUInt32LittleEndian(destination, PageId); - System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(4), SlotIndex); + BinaryPrimitives.WriteUInt32LittleEndian(destination, PageId); + BinaryPrimitives.WriteUInt16LittleEndian(destination.Slice(4), SlotIndex); } /// - /// Deserializes DocumentLocation from a byte span (6 bytes) + /// Deserializes DocumentLocation from a byte span (6 bytes) /// /// The source span containing the serialized value. public static DocumentLocation ReadFrom(ReadOnlySpan source) @@ -195,14 +186,14 @@ public readonly struct DocumentLocation if (source.Length < 6) throw new ArgumentException("Source must be at least 6 bytes", nameof(source)); - var pageId = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(source); - var slotIndex = System.Buffers.Binary.BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(4)); + uint pageId = BinaryPrimitives.ReadUInt32LittleEndian(source); + ushort slotIndex = BinaryPrimitives.ReadUInt16LittleEndian(source.Slice(4)); - return new DocumentLocation(pageId, slotIndex); + return new DocumentLocation(pageId, slotIndex); } - /// - /// Size in bytes when serialized - /// - public const int SerializedSize = 6; -} + /// + /// Size in bytes when serialized + /// + public const int SerializedSize = 6; +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/SpatialPage.cs b/src/CBDD.Core/Storage/SpatialPage.cs index 6d8d255..631c686 100755 --- a/src/CBDD.Core/Storage/SpatialPage.cs +++ b/src/CBDD.Core/Storage/SpatialPage.cs @@ -1,12 +1,11 @@ using System.Buffers.Binary; using System.Runtime.InteropServices; -using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Core.Indexing.Internal; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Page for storing R-Tree nodes for Geospatial Indexing. +/// Page for storing R-Tree nodes for Geospatial Indexing. /// internal struct SpatialPage { @@ -29,14 +28,14 @@ internal struct SpatialPage public const int EntrySize = 38; // 32 (GeoBox) + 6 (Pointer) - /// - /// Initializes a spatial page. - /// - /// The page buffer to initialize. - /// The page identifier. - /// Whether this page is a leaf node. - /// The tree level for this page. - public static void Initialize(Span page, uint pageId, bool isLeaf, byte level) + /// + /// Initializes a spatial page. + /// + /// The page buffer to initialize. + /// The page identifier. + /// Whether this page is a leaf node. + /// The tree level for this page. + public static void Initialize(Span page, uint pageId, bool isLeaf, byte level) { var header = new PageHeader { @@ -54,65 +53,86 @@ internal struct SpatialPage BinaryPrimitives.WriteUInt32LittleEndian(page.Slice(ParentPageIdOffset), 0); } - /// - /// Gets a value indicating whether the page is a leaf node. - /// - /// The page buffer. - /// if the page is a leaf node; otherwise, . - public static bool GetIsLeaf(ReadOnlySpan page) => page[IsLeafOffset] == 1; - - /// - /// Gets the tree level stored in the page. - /// - /// The page buffer. - /// The level value. - public static byte GetLevel(ReadOnlySpan page) => page[LevelOffset]; - - /// - /// Gets the number of entries in the page. - /// - /// The page buffer. - /// The number of entries. - public static ushort GetEntryCount(ReadOnlySpan page) => BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(EntryCountOffset)); - - /// - /// Sets the number of entries in the page. - /// - /// The page buffer. - /// The entry count to set. - public static void SetEntryCount(Span page, ushort count) => BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(EntryCountOffset), count); - - /// - /// Gets the parent page identifier. - /// - /// The page buffer. - /// The parent page identifier. - public static uint GetParentPageId(ReadOnlySpan page) => BinaryPrimitives.ReadUInt32LittleEndian(page.Slice(ParentPageIdOffset)); - - /// - /// Sets the parent page identifier. - /// - /// The page buffer. - /// The parent page identifier. - public static void SetParentPageId(Span page, uint parentId) => BinaryPrimitives.WriteUInt32LittleEndian(page.Slice(ParentPageIdOffset), parentId); - - /// - /// Gets the maximum number of entries that can fit in a page. - /// - /// The page size in bytes. - /// The maximum number of entries. - public static int GetMaxEntries(int pageSize) => (pageSize - DataOffset) / EntrySize; - - /// - /// Writes an entry at the specified index. - /// - /// The page buffer. - /// The entry index. - /// The minimum bounding rectangle for the entry. - /// The document location pointer. - public static void WriteEntry(Span page, int index, GeoBox mbr, DocumentLocation pointer) + /// + /// Gets a value indicating whether the page is a leaf node. + /// + /// The page buffer. + /// if the page is a leaf node; otherwise, . + public static bool GetIsLeaf(ReadOnlySpan page) { - int offset = DataOffset + (index * EntrySize); + return page[IsLeafOffset] == 1; + } + + /// + /// Gets the tree level stored in the page. + /// + /// The page buffer. + /// The level value. + public static byte GetLevel(ReadOnlySpan page) + { + return page[LevelOffset]; + } + + /// + /// Gets the number of entries in the page. + /// + /// The page buffer. + /// The number of entries. + public static ushort GetEntryCount(ReadOnlySpan page) + { + return BinaryPrimitives.ReadUInt16LittleEndian(page.Slice(EntryCountOffset)); + } + + /// + /// Sets the number of entries in the page. + /// + /// The page buffer. + /// The entry count to set. + public static void SetEntryCount(Span page, ushort count) + { + BinaryPrimitives.WriteUInt16LittleEndian(page.Slice(EntryCountOffset), count); + } + + /// + /// Gets the parent page identifier. + /// + /// The page buffer. + /// The parent page identifier. + public static uint GetParentPageId(ReadOnlySpan page) + { + return BinaryPrimitives.ReadUInt32LittleEndian(page.Slice(ParentPageIdOffset)); + } + + /// + /// Sets the parent page identifier. + /// + /// The page buffer. + /// The parent page identifier. + public static void SetParentPageId(Span page, uint parentId) + { + BinaryPrimitives.WriteUInt32LittleEndian(page.Slice(ParentPageIdOffset), parentId); + } + + /// + /// Gets the maximum number of entries that can fit in a page. + /// + /// The page size in bytes. + /// The maximum number of entries. + public static int GetMaxEntries(int pageSize) + { + return (pageSize - DataOffset) / EntrySize; + } + + /// + /// Writes an entry at the specified index. + /// + /// The page buffer. + /// The entry index. + /// The minimum bounding rectangle for the entry. + /// The document location pointer. + public static void WriteEntry(Span page, int index, GeoBox mbr, DocumentLocation pointer) + { + int offset = DataOffset + index * EntrySize; var entrySpan = page.Slice(offset, EntrySize); // Write MBR (4 doubles) @@ -126,16 +146,16 @@ internal struct SpatialPage pointer.WriteTo(entrySpan.Slice(32, 6)); } - /// - /// Reads an entry at the specified index. - /// - /// The page buffer. - /// The entry index. - /// When this method returns, contains the entry MBR. - /// When this method returns, contains the entry document location. - public static void ReadEntry(ReadOnlySpan page, int index, out GeoBox mbr, out DocumentLocation pointer) + /// + /// Reads an entry at the specified index. + /// + /// The page buffer. + /// The entry index. + /// When this method returns, contains the entry MBR. + /// When this method returns, contains the entry document location. + public static void ReadEntry(ReadOnlySpan page, int index, out GeoBox mbr, out DocumentLocation pointer) { - int offset = DataOffset + (index * EntrySize); + int offset = DataOffset + index * EntrySize; var entrySpan = page.Slice(offset, EntrySize); var doubles = MemoryMarshal.Cast(entrySpan.Slice(0, 32)); @@ -143,23 +163,24 @@ internal struct SpatialPage pointer = DocumentLocation.ReadFrom(entrySpan.Slice(32, 6)); } - /// - /// Calculates the combined MBR of all entries in the page. - /// - /// The page buffer. - /// The combined MBR, or when the page has no entries. - public static GeoBox CalculateMBR(ReadOnlySpan page) + /// + /// Calculates the combined MBR of all entries in the page. + /// + /// The page buffer. + /// The combined MBR, or when the page has no entries. + public static GeoBox CalculateMBR(ReadOnlySpan page) { ushort count = GetEntryCount(page); if (count == 0) return GeoBox.Empty; - GeoBox result = GeoBox.Empty; - for (int i = 0; i < count; i++) + var result = GeoBox.Empty; + for (var i = 0; i < count; i++) { ReadEntry(page, i, out var mbr, out _); if (i == 0) result = mbr; else result = result.ExpandTo(mbr); } + return result; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Collections.cs b/src/CBDD.Core/Storage/StorageEngine.Collections.cs index 9d0a57c..f51a602 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Collections.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Collections.cs @@ -1,31 +1,27 @@ -using System; -using System.Collections.Generic; -using System.IO; -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Core.Collections; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - +using ZB.MOM.WW.CBDD.Core.Collections; +using ZB.MOM.WW.CBDD.Core.Indexing; + +namespace ZB.MOM.WW.CBDD.Core.Storage; + public class CollectionMetadata { /// - /// Gets or sets the collection name. + /// Gets or sets the collection name. /// public string Name { get; set; } = string.Empty; /// - /// Gets or sets the root page identifier of the primary index. + /// Gets or sets the root page identifier of the primary index. /// public uint PrimaryRootPageId { get; set; } /// - /// Gets or sets the root page identifier of the schema chain. + /// Gets or sets the root page identifier of the schema chain. /// public uint SchemaRootPageId { get; set; } /// - /// Gets the collection index metadata list. + /// Gets the collection index metadata list. /// public List Indexes { get; } = new(); } @@ -33,45 +29,45 @@ public class CollectionMetadata public class IndexMetadata { /// - /// Gets or sets the index name. + /// Gets or sets the index name. /// public string Name { get; set; } = string.Empty; /// - /// Gets or sets a value indicating whether this index enforces uniqueness. + /// Gets or sets a value indicating whether this index enforces uniqueness. /// public bool IsUnique { get; set; } /// - /// Gets or sets the index type. + /// Gets or sets the index type. /// public IndexType Type { get; set; } /// - /// Gets or sets indexed property paths. + /// Gets or sets indexed property paths. /// public string[] PropertyPaths { get; set; } = Array.Empty(); /// - /// Gets or sets vector dimensions for vector indexes. + /// Gets or sets vector dimensions for vector indexes. /// public int Dimensions { get; set; } /// - /// Gets or sets the vector similarity metric for vector indexes. + /// Gets or sets the vector similarity metric for vector indexes. /// public VectorMetric Metric { get; set; } /// - /// Gets or sets the root page identifier of the index structure. + /// Gets or sets the root page identifier of the index structure. /// public uint RootPageId { get; set; } } - -public sealed partial class StorageEngine -{ + +public sealed partial class StorageEngine +{ /// - /// Gets collection metadata by name. + /// Gets collection metadata by name. /// /// The collection name. /// The collection metadata if found; otherwise, null. @@ -82,7 +78,121 @@ public sealed partial class StorageEngine } /// - /// Returns all collection metadata entries currently registered in page 1. + /// Saves collection metadata to the metadata page. + /// + /// The metadata to save. + public void SaveCollectionMetadata(CollectionMetadata metadata) + { + using var stream = new MemoryStream(); + using var writer = new BinaryWriter(stream); + + writer.Write(metadata.Name); + writer.Write(metadata.PrimaryRootPageId); + writer.Write(metadata.SchemaRootPageId); + writer.Write(metadata.Indexes.Count); + foreach (var idx in metadata.Indexes) + { + writer.Write(idx.Name); + writer.Write(idx.IsUnique); + writer.Write((byte)idx.Type); + writer.Write(idx.RootPageId); + writer.Write(idx.PropertyPaths.Length); + foreach (string path in idx.PropertyPaths) writer.Write(path); + + if (idx.Type == IndexType.Vector) + { + writer.Write(idx.Dimensions); + writer.Write((byte)idx.Metric); + } + } + + byte[] newData = stream.ToArray(); + + var buffer = new byte[PageSize]; + ReadPage(1, null, buffer); + + var header = SlottedPageHeader.ReadFrom(buffer); + int existingSlotIndex = -1; + + for (ushort i = 0; i < header.SlotCount; i++) + { + int slotOffset = SlottedPageHeader.Size + i * SlotEntry.Size; + var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); + if ((slot.Flags & SlotFlags.Deleted) != 0) continue; + + try + { + using var ms = new MemoryStream(buffer, slot.Offset, slot.Length, false); + using var reader = new BinaryReader(ms); + string name = reader.ReadString(); + + if (string.Equals(name, metadata.Name, StringComparison.OrdinalIgnoreCase)) + { + existingSlotIndex = i; + break; + } + } + catch + { + } + } + + if (existingSlotIndex >= 0) + { + int slotOffset = SlottedPageHeader.Size + existingSlotIndex * SlotEntry.Size; + var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); + slot.Flags |= SlotFlags.Deleted; + slot.WriteTo(buffer.AsSpan(slotOffset)); + } + + if (header.AvailableFreeSpace < newData.Length + SlotEntry.Size) + // Compact logic omitted as per current architecture + throw new InvalidOperationException( + "Not enough space in Metadata Page (Page 1) to save collection metadata."); + + int docOffset = header.FreeSpaceEnd - newData.Length; + newData.CopyTo(buffer.AsSpan(docOffset)); + + ushort slotIndex; + if (existingSlotIndex >= 0) + { + slotIndex = (ushort)existingSlotIndex; + } + else + { + slotIndex = header.SlotCount; + header.SlotCount++; + } + + int newSlotEntryOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; + var newSlot = new SlotEntry + { + Offset = (ushort)docOffset, + Length = (ushort)newData.Length, + Flags = SlotFlags.None + }; + newSlot.WriteTo(buffer.AsSpan(newSlotEntryOffset)); + + header.FreeSpaceEnd = (ushort)docOffset; + if (existingSlotIndex == -1) + header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size); + + header.WriteTo(buffer); + WritePageImmediate(1, buffer); + } + + /// + /// Registers all BSON keys used by a set of mappers into the global dictionary. + /// + /// The mappers whose keys should be registered. + public void RegisterMappers(IEnumerable mappers) + { + var allKeys = mappers.SelectMany(m => m.UsedKeys).Distinct(); + RegisterKeys(allKeys); + } + + /// + /// Returns all collection metadata entries currently registered in page 1. /// public IReadOnlyList GetAllCollectionMetadata() { @@ -96,7 +206,7 @@ public sealed partial class StorageEngine for (ushort i = 0; i < header.SlotCount; i++) { - var slotOffset = SlottedPageHeader.Size + (i * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + i * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); if ((slot.Flags & SlotFlags.Deleted) != 0) continue; @@ -104,122 +214,12 @@ public sealed partial class StorageEngine if (slot.Offset < SlottedPageHeader.Size || slot.Offset + slot.Length > buffer.Length) continue; - if (TryDeserializeCollectionMetadata(buffer.AsSpan(slot.Offset, slot.Length), out var metadata) && metadata != null) - { - result.Add(metadata); - } + if (TryDeserializeCollectionMetadata(buffer.AsSpan(slot.Offset, slot.Length), out var metadata) && + metadata != null) result.Add(metadata); } return result; } - - /// - /// Saves collection metadata to the metadata page. - /// - /// The metadata to save. - public void SaveCollectionMetadata(CollectionMetadata metadata) - { - using var stream = new MemoryStream(); - using var writer = new BinaryWriter(stream); - - writer.Write(metadata.Name); - writer.Write(metadata.PrimaryRootPageId); - writer.Write(metadata.SchemaRootPageId); - writer.Write(metadata.Indexes.Count); - foreach (var idx in metadata.Indexes) - { - writer.Write(idx.Name); - writer.Write(idx.IsUnique); - writer.Write((byte)idx.Type); - writer.Write(idx.RootPageId); - writer.Write(idx.PropertyPaths.Length); - foreach (var path in idx.PropertyPaths) - { - writer.Write(path); - } - - if (idx.Type == IndexType.Vector) - { - writer.Write(idx.Dimensions); - writer.Write((byte)idx.Metric); - } - } - - var newData = stream.ToArray(); - - var buffer = new byte[PageSize]; - ReadPage(1, null, buffer); - - var header = SlottedPageHeader.ReadFrom(buffer); - int existingSlotIndex = -1; - - for (ushort i = 0; i < header.SlotCount; i++) - { - var slotOffset = SlottedPageHeader.Size + (i * SlotEntry.Size); - var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); - if ((slot.Flags & SlotFlags.Deleted) != 0) continue; - - try - { - using var ms = new MemoryStream(buffer, slot.Offset, slot.Length, false); - using var reader = new BinaryReader(ms); - var name = reader.ReadString(); - - if (string.Equals(name, metadata.Name, StringComparison.OrdinalIgnoreCase)) - { - existingSlotIndex = i; - break; - } - } - catch { } - } - - if (existingSlotIndex >= 0) - { - var slotOffset = SlottedPageHeader.Size + (existingSlotIndex * SlotEntry.Size); - var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset)); - slot.Flags |= SlotFlags.Deleted; - slot.WriteTo(buffer.AsSpan(slotOffset)); - } - - if (header.AvailableFreeSpace < newData.Length + SlotEntry.Size) - { - // Compact logic omitted as per current architecture - throw new InvalidOperationException("Not enough space in Metadata Page (Page 1) to save collection metadata."); - } - - int docOffset = header.FreeSpaceEnd - newData.Length; - newData.CopyTo(buffer.AsSpan(docOffset)); - - ushort slotIndex; - if (existingSlotIndex >= 0) - { - slotIndex = (ushort)existingSlotIndex; - } - else - { - slotIndex = header.SlotCount; - header.SlotCount++; - } - - var newSlotEntryOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); - var newSlot = new SlotEntry - { - Offset = (ushort)docOffset, - Length = (ushort)newData.Length, - Flags = SlotFlags.None - }; - newSlot.WriteTo(buffer.AsSpan(newSlotEntryOffset)); - - header.FreeSpaceEnd = (ushort)docOffset; - if (existingSlotIndex == -1) - { - header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + (header.SlotCount * SlotEntry.Size)); - } - - header.WriteTo(buffer); - WritePageImmediate(1, buffer); - } private static bool TryDeserializeCollectionMetadata(ReadOnlySpan rawBytes, out CollectionMetadata? metadata) { @@ -230,16 +230,16 @@ public sealed partial class StorageEngine using var ms = new MemoryStream(rawBytes.ToArray()); using var reader = new BinaryReader(ms); - var collName = reader.ReadString(); + string collName = reader.ReadString(); var parsed = new CollectionMetadata { Name = collName }; parsed.PrimaryRootPageId = reader.ReadUInt32(); parsed.SchemaRootPageId = reader.ReadUInt32(); - var indexCount = reader.ReadInt32(); + int indexCount = reader.ReadInt32(); if (indexCount < 0) return false; - for (int j = 0; j < indexCount; j++) + for (var j = 0; j < indexCount; j++) { var idx = new IndexMetadata { @@ -249,12 +249,12 @@ public sealed partial class StorageEngine RootPageId = reader.ReadUInt32() }; - var pathCount = reader.ReadInt32(); + int pathCount = reader.ReadInt32(); if (pathCount < 0) return false; idx.PropertyPaths = new string[pathCount]; - for (int k = 0; k < pathCount; k++) + for (var k = 0; k < pathCount; k++) idx.PropertyPaths[k] = reader.ReadString(); if (idx.Type == IndexType.Vector) @@ -274,14 +274,4 @@ public sealed partial class StorageEngine return false; } } - - /// - /// Registers all BSON keys used by a set of mappers into the global dictionary. - /// - /// The mappers whose keys should be registered. - public void RegisterMappers(IEnumerable mappers) - { - var allKeys = mappers.SelectMany(m => m.UsedKeys).Distinct(); - RegisterKeys(allKeys); - } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Diagnostics.cs b/src/CBDD.Core/Storage/StorageEngine.Diagnostics.cs index 2c7d37f..a234d4f 100644 --- a/src/CBDD.Core/Storage/StorageEngine.Diagnostics.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Diagnostics.cs @@ -5,172 +5,173 @@ using ZB.MOM.WW.CBDD.Core.Indexing; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Aggregated page counts grouped by page type. +/// Aggregated page counts grouped by page type. /// public sealed class PageTypeUsageEntry { /// - /// Gets the page type. + /// Gets the page type. /// public PageType PageType { get; init; } /// - /// Gets the number of pages of this type. + /// Gets the number of pages of this type. /// public int PageCount { get; init; } } /// -/// Per-collection page usage summary. +/// Per-collection page usage summary. /// public sealed class CollectionPageUsageEntry { /// - /// Gets the collection name. + /// Gets the collection name. /// public string CollectionName { get; init; } = string.Empty; /// - /// Gets the total number of distinct pages referenced by the collection. + /// Gets the total number of distinct pages referenced by the collection. /// public int TotalDistinctPages { get; init; } /// - /// Gets the number of data pages. + /// Gets the number of data pages. /// public int DataPages { get; init; } /// - /// Gets the number of overflow pages. + /// Gets the number of overflow pages. /// public int OverflowPages { get; init; } /// - /// Gets the number of index pages. + /// Gets the number of index pages. /// public int IndexPages { get; init; } /// - /// Gets the number of other page types. + /// Gets the number of other page types. /// public int OtherPages { get; init; } } /// -/// Per-collection compression ratio summary. +/// Per-collection compression ratio summary. /// public sealed class CollectionCompressionRatioEntry { /// - /// Gets the collection name. + /// Gets the collection name. /// public string CollectionName { get; init; } = string.Empty; /// - /// Gets the number of documents. + /// Gets the number of documents. /// public long DocumentCount { get; init; } /// - /// Gets the number of compressed documents. + /// Gets the number of compressed documents. /// public long CompressedDocumentCount { get; init; } /// - /// Gets the total uncompressed byte count. + /// Gets the total uncompressed byte count. /// public long BytesBeforeCompression { get; init; } /// - /// Gets the total stored byte count. + /// Gets the total stored byte count. /// public long BytesAfterCompression { get; init; } /// - /// Gets the compression ratio. + /// Gets the compression ratio. /// - public double CompressionRatio => BytesAfterCompression <= 0 ? 1.0 : (double)BytesBeforeCompression / BytesAfterCompression; + public double CompressionRatio => + BytesAfterCompression <= 0 ? 1.0 : (double)BytesBeforeCompression / BytesAfterCompression; } /// -/// Summary of free-list and reclaimable tail information. +/// Summary of free-list and reclaimable tail information. /// public sealed class FreeListSummary { /// - /// Gets the total page count. + /// Gets the total page count. /// public uint PageCount { get; init; } /// - /// Gets the free page count. + /// Gets the free page count. /// public int FreePageCount { get; init; } /// - /// Gets the total free bytes. + /// Gets the total free bytes. /// public long FreeBytes { get; init; } /// - /// Gets the fragmentation percentage. + /// Gets the fragmentation percentage. /// public double FragmentationPercent { get; init; } /// - /// Gets the number of reclaimable pages at the file tail. + /// Gets the number of reclaimable pages at the file tail. /// public uint TailReclaimablePages { get; init; } } /// -/// Single page entry in fragmentation reporting. +/// Single page entry in fragmentation reporting. /// public sealed class FragmentationPageEntry { /// - /// Gets the page identifier. + /// Gets the page identifier. /// public uint PageId { get; init; } /// - /// Gets the page type. + /// Gets the page type. /// public PageType PageType { get; init; } /// - /// Gets a value indicating whether this page is free. + /// Gets a value indicating whether this page is free. /// public bool IsFreePage { get; init; } /// - /// Gets the free bytes on the page. + /// Gets the free bytes on the page. /// public int FreeBytes { get; init; } } /// -/// Detailed fragmentation map and totals. +/// Detailed fragmentation map and totals. /// public sealed class FragmentationMapReport { /// - /// Gets the page entries. + /// Gets the page entries. /// public IReadOnlyList Pages { get; init; } = Array.Empty(); /// - /// Gets the total free bytes across all pages. + /// Gets the total free bytes across all pages. /// public long TotalFreeBytes { get; init; } /// - /// Gets the fragmentation percentage. + /// Gets the fragmentation percentage. /// public double FragmentationPercent { get; init; } /// - /// Gets the number of reclaimable pages at the file tail. + /// Gets the number of reclaimable pages at the file tail. /// public uint TailReclaimablePages { get; init; } } @@ -178,11 +179,11 @@ public sealed class FragmentationMapReport public sealed partial class StorageEngine { /// - /// Gets page usage grouped by page type. + /// Gets page usage grouped by page type. /// public IReadOnlyList GetPageUsageByPageType() { - var pageCount = _pageFile.NextPageId; + uint pageCount = _pageFile.NextPageId; var buffer = new byte[_pageFile.PageSize]; var counts = new Dictionary(); @@ -190,7 +191,7 @@ public sealed partial class StorageEngine { _pageFile.ReadPage(pageId, buffer); var pageType = PageHeader.ReadFrom(buffer).PageType; - counts[pageType] = counts.TryGetValue(pageType, out var count) ? count + 1 : 1; + counts[pageType] = counts.TryGetValue(pageType, out int count) ? count + 1 : 1; } return counts @@ -204,7 +205,7 @@ public sealed partial class StorageEngine } /// - /// Gets per-collection page usage by resolving primary-index locations and related index roots. + /// Gets per-collection page usage by resolving primary-index locations and related index roots. /// public IReadOnlyList GetPageUsageByCollection() { @@ -221,27 +222,23 @@ public sealed partial class StorageEngine pageIds.Add(metadata.SchemaRootPageId); foreach (var indexMetadata in metadata.Indexes) - { if (indexMetadata.RootPageId != 0) pageIds.Add(indexMetadata.RootPageId); - } foreach (var location in EnumeratePrimaryLocations(metadata)) { pageIds.Add(location.PageId); - if (TryReadFirstOverflowPage(location, out var firstOverflowPage)) - { + if (TryReadFirstOverflowPage(location, out uint firstOverflowPage)) AddOverflowChainPages(pageIds, firstOverflowPage); - } } - int data = 0; - int overflow = 0; - int indexPages = 0; - int other = 0; + var data = 0; + var overflow = 0; + var indexPages = 0; + var other = 0; var pageBuffer = new byte[_pageFile.PageSize]; - foreach (var pageId in pageIds) + foreach (uint pageId in pageIds) { if (pageId >= _pageFile.NextPageId) continue; @@ -250,21 +247,13 @@ public sealed partial class StorageEngine var pageType = PageHeader.ReadFrom(pageBuffer).PageType; if (pageType == PageType.Data) - { data++; - } else if (pageType == PageType.Overflow) - { overflow++; - } else if (pageType == PageType.Index || pageType == PageType.Vector || pageType == PageType.Spatial) - { indexPages++; - } else - { other++; - } } results.Add(new CollectionPageUsageEntry @@ -282,7 +271,7 @@ public sealed partial class StorageEngine } /// - /// Gets per-collection logical-vs-stored compression ratios. + /// Gets per-collection logical-vs-stored compression ratios. /// public IReadOnlyList GetCompressionRatioByCollection() { @@ -298,7 +287,8 @@ public sealed partial class StorageEngine foreach (var location in EnumeratePrimaryLocations(metadata)) { - if (!TryReadSlotPayloadStats(location, out var isCompressed, out var originalBytes, out var storedBytes)) + if (!TryReadSlotPayloadStats(location, out bool isCompressed, out int originalBytes, + out int storedBytes)) continue; docs++; @@ -323,7 +313,7 @@ public sealed partial class StorageEngine } /// - /// Gets free-list summary for diagnostics. + /// Gets free-list summary for diagnostics. /// public FreeListSummary GetFreeListSummary() { @@ -339,12 +329,12 @@ public sealed partial class StorageEngine } /// - /// Gets detailed page-level fragmentation diagnostics. + /// Gets detailed page-level fragmentation diagnostics. /// public FragmentationMapReport GetFragmentationMap() { - var freePageSet = new HashSet(_pageFile.EnumerateFreePages(includeEmptyPages: true)); - var pageCount = _pageFile.NextPageId; + var freePageSet = new HashSet(_pageFile.EnumerateFreePages()); + uint pageCount = _pageFile.NextPageId; var buffer = new byte[_pageFile.PageSize]; var pages = new List((int)pageCount); @@ -354,17 +344,12 @@ public sealed partial class StorageEngine { _pageFile.ReadPage(pageId, buffer); var pageHeader = PageHeader.ReadFrom(buffer); - var isFreePage = freePageSet.Contains(pageId); + bool isFreePage = freePageSet.Contains(pageId); - int freeBytes = 0; + var freeBytes = 0; if (isFreePage) - { freeBytes = _pageFile.PageSize; - } - else if (TryReadSlottedFreeSpace(buffer, out var slottedFreeBytes)) - { - freeBytes = slottedFreeBytes; - } + else if (TryReadSlottedFreeSpace(buffer, out int slottedFreeBytes)) freeBytes = slottedFreeBytes; totalFreeBytes += freeBytes; @@ -378,7 +363,7 @@ public sealed partial class StorageEngine } uint tailReclaimablePages = 0; - for (var i = pageCount; i > 2; i--) + for (uint i = pageCount; i > 2; i--) { if (!freePageSet.Contains(i - 1)) break; @@ -386,12 +371,12 @@ public sealed partial class StorageEngine tailReclaimablePages++; } - var fileBytes = Math.Max(1L, _pageFile.FileLengthBytes); + long fileBytes = Math.Max(1L, _pageFile.FileLengthBytes); return new FragmentationMapReport { Pages = pages, TotalFreeBytes = totalFreeBytes, - FragmentationPercent = (totalFreeBytes * 100d) / fileBytes, + FragmentationPercent = totalFreeBytes * 100d / fileBytes, TailReclaimablePages = tailReclaimablePages }; } @@ -403,10 +388,8 @@ public sealed partial class StorageEngine var index = new BTreeIndex(this, IndexOptions.CreateUnique("_id"), metadata.PrimaryRootPageId); - foreach (var entry in index.Range(IndexKey.MinKey, IndexKey.MaxKey, IndexDirection.Forward, transactionId: 0)) - { + foreach (var entry in index.Range(IndexKey.MinKey, IndexKey.MaxKey, IndexDirection.Forward, 0)) yield return entry.Location; - } } private bool TryReadFirstOverflowPage(in DocumentLocation location, out uint firstOverflowPage) @@ -419,7 +402,7 @@ public sealed partial class StorageEngine if (location.SlotIndex >= header.SlotCount) return false; - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return false; @@ -441,7 +424,7 @@ public sealed partial class StorageEngine var buffer = new byte[_pageFile.PageSize]; var visited = new HashSet(); - var current = firstOverflowPage; + uint current = firstOverflowPage; while (current != 0 && current < _pageFile.NextPageId && visited.Add(current)) { @@ -472,12 +455,12 @@ public sealed partial class StorageEngine if (location.SlotIndex >= header.SlotCount) return false; - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return false; - var hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; + bool hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; isCompressed = (slot.Flags & SlotFlags.Compressed) != 0; if (!hasOverflow) @@ -492,7 +475,8 @@ public sealed partial class StorageEngine if (slot.Length < CompressedPayloadHeader.Size) return false; - var compressedHeader = CompressedPayloadHeader.ReadFrom(pageBuffer.AsSpan(slot.Offset, CompressedPayloadHeader.Size)); + var compressedHeader = + CompressedPayloadHeader.ReadFrom(pageBuffer.AsSpan(slot.Offset, CompressedPayloadHeader.Size)); originalBytes = compressedHeader.OriginalLength; return true; } @@ -501,7 +485,7 @@ public sealed partial class StorageEngine return false; var primaryPayload = pageBuffer.AsSpan(slot.Offset, slot.Length); - var totalStoredBytes = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); + int totalStoredBytes = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); if (totalStoredBytes < 0) return false; @@ -522,8 +506,8 @@ public sealed partial class StorageEngine else { storedPrefix.CopyTo(headerBuffer); - var copied = storedPrefix.Length; - var nextOverflow = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); + int copied = storedPrefix.Length; + uint nextOverflow = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); var overflowBuffer = new byte[_pageFile.PageSize]; while (copied < CompressedPayloadHeader.Size && nextOverflow != 0 && nextOverflow < _pageFile.NextPageId) @@ -533,7 +517,8 @@ public sealed partial class StorageEngine if (overflowHeader.PageType != PageType.Overflow) return false; - var available = Math.Min(CompressedPayloadHeader.Size - copied, _pageFile.PageSize - SlottedPageHeader.Size); + int available = Math.Min(CompressedPayloadHeader.Size - copied, + _pageFile.PageSize - SlottedPageHeader.Size); overflowBuffer.AsSpan(SlottedPageHeader.Size, available).CopyTo(headerBuffer.Slice(copied)); copied += available; nextOverflow = overflowHeader.NextOverflowPage; @@ -547,4 +532,4 @@ public sealed partial class StorageEngine originalBytes = headerFromPayload.OriginalLength; return true; } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Dictionary.cs b/src/CBDD.Core/Storage/StorageEngine.Dictionary.cs index 8630192..e62935d 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Dictionary.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Dictionary.cs @@ -1,29 +1,104 @@ -using System.Collections.Concurrent; -using System.Text; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - -public sealed partial class StorageEngine -{ - private readonly ConcurrentDictionary _dictionaryCache = new(StringComparer.OrdinalIgnoreCase); - private readonly ConcurrentDictionary _dictionaryReverseCache = new(); - private uint _dictionaryRootPageId; - private ushort _nextDictionaryId; +using System.Collections.Concurrent; + +namespace ZB.MOM.WW.CBDD.Core.Storage; + +public sealed partial class StorageEngine +{ + private readonly ConcurrentDictionary _dictionaryCache = new(StringComparer.OrdinalIgnoreCase); // Lock for dictionary modifications (simple lock for now, could be RW lock) - private readonly object _dictionaryLock = new(); - - private void InitializeDictionary() - { - // 1. Read File Header (Page 0) to get Dictionary Root - var headerBuffer = new byte[PageSize]; - ReadPage(0, null, headerBuffer); + private readonly object _dictionaryLock = new(); + private readonly ConcurrentDictionary _dictionaryReverseCache = new(); + private uint _dictionaryRootPageId; + private ushort _nextDictionaryId; + + /// + /// Gets the key-to-id dictionary cache. + /// + /// The key-to-id map. + public ConcurrentDictionary GetKeyMap() + { + return _dictionaryCache; + } + + /// + /// Gets the id-to-key dictionary cache. + /// + /// The id-to-key map. + public ConcurrentDictionary GetKeyReverseMap() + { + return _dictionaryReverseCache; + } + + /// + /// Gets the ID for a dictionary key, creating it if it doesn't exist. + /// Thread-safe. + /// + /// The dictionary key. + /// The dictionary identifier for the key. + public ushort GetOrAddDictionaryEntry(string key) + { + key = key.ToLowerInvariant(); + if (_dictionaryCache.TryGetValue(key, out ushort id)) return id; + + lock (_dictionaryLock) + { + // Double checked locking + if (_dictionaryCache.TryGetValue(key, out id)) return id; + + // Try to find in storage (in case cache is incomplete or another process?) + // Note: FindAllGlobal loaded everything, so strict cache miss means it's not in DB. + // BUT if we support concurrent writers (multiple processed), we should re-check DB. + // Current CBDD seems to be single-process exclusive lock (FileShare.None). + // So in-memory cache is authoritative after load. + + // Generate New ID + ushort nextId = _nextDictionaryId; + if (nextId == 0) nextId = DictionaryPage.ReservedValuesEnd + 1; // Should be init, but safety + + // Insert into Page + // usage of default(ulong) or null transaction? + // Dictionary updates should ideally be transactional or immediate? + // "Immediate" for now to simplify, as dictionary is cross-collection. + // If we use transaction, we need to pass it in. For now, immediate write. + + // We need to support "Insert Global" which handles overflow. + // DictionaryPage.Insert only handles single page. + + // We need logic here to traverse chain and find space. + if (InsertDictionaryEntryGlobal(key, nextId)) + { + _dictionaryCache[key] = nextId; + _dictionaryReverseCache[nextId] = key; + _nextDictionaryId++; + return nextId; + } + + throw new InvalidOperationException("Failed to insert dictionary entry (Storage Full?)"); + } + } + + /// + /// Registers a set of keys in the global dictionary. + /// Ensures all keys are assigned an ID and persisted. + /// + /// The keys to register. + public void RegisterKeys(IEnumerable keys) + { + foreach (string key in keys) GetOrAddDictionaryEntry(key.ToLowerInvariant()); + } + + private void InitializeDictionary() + { + // 1. Read File Header (Page 0) to get Dictionary Root + var headerBuffer = new byte[PageSize]; + ReadPage(0, null, headerBuffer); var header = PageHeader.ReadFrom(headerBuffer); - if (header.DictionaryRootPageId == 0) - { - // Initialize new Dictionary - lock (_dictionaryLock) + if (header.DictionaryRootPageId == 0) + { + // Initialize new Dictionary + lock (_dictionaryLock) { // Double check ReadPage(0, null, headerBuffer); @@ -48,172 +123,92 @@ public sealed partial class StorageEngine else { _dictionaryRootPageId = header.DictionaryRootPageId; - } - } - } - else - { + } + } + } + else + { _dictionaryRootPageId = header.DictionaryRootPageId; // Warm cache - ushort maxId = DictionaryPage.ReservedValuesEnd; - foreach (var (key, val) in DictionaryPage.FindAllGlobal(this, _dictionaryRootPageId)) - { - var lowerKey = key.ToLowerInvariant(); - _dictionaryCache[lowerKey] = val; - _dictionaryReverseCache[val] = lowerKey; - if (val > maxId) maxId = val; - } - _nextDictionaryId = (ushort)(maxId + 1); - } - - // Pre-register internal keys used for Schema persistence + ushort maxId = DictionaryPage.ReservedValuesEnd; + foreach ((string key, ushort val) in DictionaryPage.FindAllGlobal(this, _dictionaryRootPageId)) + { + string lowerKey = key.ToLowerInvariant(); + _dictionaryCache[lowerKey] = val; + _dictionaryReverseCache[val] = lowerKey; + if (val > maxId) maxId = val; + } + + _nextDictionaryId = (ushort)(maxId + 1); + } + + // Pre-register internal keys used for Schema persistence RegisterKeys(new[] { "_id", "t", "_v", "f", "n", "b", "s", "a" }); // Pre-register common array indices to avoid mapping during high-frequency writes - var indices = new List(101); - for (int i = 0; i <= 100; i++) indices.Add(i.ToString()); - RegisterKeys(indices); - } - - /// - /// Gets the key-to-id dictionary cache. - /// - /// The key-to-id map. - public ConcurrentDictionary GetKeyMap() => _dictionaryCache; - - /// - /// Gets the id-to-key dictionary cache. - /// - /// The id-to-key map. - public ConcurrentDictionary GetKeyReverseMap() => _dictionaryReverseCache; - - /// - /// Gets the ID for a dictionary key, creating it if it doesn't exist. - /// Thread-safe. - /// - /// The dictionary key. - /// The dictionary identifier for the key. - public ushort GetOrAddDictionaryEntry(string key) - { - key = key.ToLowerInvariant(); - if (_dictionaryCache.TryGetValue(key, out var id)) - { - return id; - } - - lock (_dictionaryLock) - { - // Double checked locking - if (_dictionaryCache.TryGetValue(key, out id)) - { - return id; - } - - // Try to find in storage (in case cache is incomplete or another process?) - // Note: FindAllGlobal loaded everything, so strict cache miss means it's not in DB. - // BUT if we support concurrent writers (multiple processed), we should re-check DB. - // Current CBDD seems to be single-process exclusive lock (FileShare.None). - // So in-memory cache is authoritative after load. - - // Generate New ID - ushort nextId = _nextDictionaryId; - if (nextId == 0) nextId = DictionaryPage.ReservedValuesEnd + 1; // Should be init, but safety - - // Insert into Page - // usage of default(ulong) or null transaction? - // Dictionary updates should ideally be transactional or immediate? - // "Immediate" for now to simplify, as dictionary is cross-collection. - // If we use transaction, we need to pass it in. For now, immediate write. - - // We need to support "Insert Global" which handles overflow. - // DictionaryPage.Insert only handles single page. - - // We need logic here to traverse chain and find space. - if (InsertDictionaryEntryGlobal(key, nextId)) - { - _dictionaryCache[key] = nextId; - _dictionaryReverseCache[nextId] = key; - _nextDictionaryId++; - return nextId; - } - else - { - throw new InvalidOperationException("Failed to insert dictionary entry (Storage Full?)"); - } - } + var indices = new List(101); + for (var i = 0; i <= 100; i++) indices.Add(i.ToString()); + RegisterKeys(indices); } /// - /// Gets the dictionary key for an identifier. + /// Gets the dictionary key for an identifier. /// /// The dictionary identifier. - /// The dictionary key if found; otherwise, . + /// The dictionary key if found; otherwise, . public string? GetDictionaryKey(ushort id) { - if (_dictionaryReverseCache.TryGetValue(id, out var key)) + if (_dictionaryReverseCache.TryGetValue(id, out string? key)) return key; - return null; + return null; } - private bool InsertDictionaryEntryGlobal(string key, ushort value) - { - var pageId = _dictionaryRootPageId; + private bool InsertDictionaryEntryGlobal(string key, ushort value) + { + uint pageId = _dictionaryRootPageId; var pageBuffer = new byte[PageSize]; - while (true) - { + while (true) + { ReadPage(pageId, null, pageBuffer); // Try Insert - if (DictionaryPage.Insert(pageBuffer, key, value)) - { - // Success - Write Back - WritePageImmediate(pageId, pageBuffer); - return true; + if (DictionaryPage.Insert(pageBuffer, key, value)) + { + // Success - Write Back + WritePageImmediate(pageId, pageBuffer); + return true; } // Page Full - Check Next Page - var header = PageHeader.ReadFrom(pageBuffer); - if (header.NextPageId != 0) - { - pageId = header.NextPageId; - continue; + var header = PageHeader.ReadFrom(pageBuffer); + if (header.NextPageId != 0) + { + pageId = header.NextPageId; + continue; } // No Next Page - Allocate New - var newPageId = AllocatePage(); - var newPageBuffer = new byte[PageSize]; + uint newPageId = AllocatePage(); + var newPageBuffer = new byte[PageSize]; DictionaryPage.Initialize(newPageBuffer, newPageId); // Should likely insert into NEW page immediately to save I/O? // Or just link and loop? // Let's Insert into new page logic here to avoid re-reading. - if (!DictionaryPage.Insert(newPageBuffer, key, value)) + if (!DictionaryPage.Insert(newPageBuffer, key, value)) return false; // Should not happen on empty page unless key is huge > page // Write New Page WritePageImmediate(newPageId, newPageBuffer); // Update Previous Page Link - header.NextPageId = newPageId; - header.WriteTo(pageBuffer); + header.NextPageId = newPageId; + header.WriteTo(pageBuffer); WritePageImmediate(pageId, pageBuffer); - return true; - } - } - - /// - /// Registers a set of keys in the global dictionary. - /// Ensures all keys are assigned an ID and persisted. - /// - /// The keys to register. - public void RegisterKeys(IEnumerable keys) - { - foreach (var key in keys) - { - GetOrAddDictionaryEntry(key.ToLowerInvariant()); - } - } -} + return true; + } + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Format.cs b/src/CBDD.Core/Storage/StorageEngine.Format.cs index 7aea25c..207d920 100644 --- a/src/CBDD.Core/Storage/StorageEngine.Format.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Format.cs @@ -15,31 +15,32 @@ internal readonly struct StorageFormatMetadata internal const int WireSize = 16; /// - /// Gets a value indicating whether format metadata is present. + /// Gets a value indicating whether format metadata is present. /// public bool IsPresent { get; } /// - /// Gets the storage format version. + /// Gets the storage format version. /// public byte Version { get; } /// - /// Gets enabled storage feature flags. + /// Gets enabled storage feature flags. /// public StorageFeatureFlags FeatureFlags { get; } /// - /// Gets the default compression codec. + /// Gets the default compression codec. /// public CompressionCodec DefaultCodec { get; } /// - /// Gets a value indicating whether compression capability is enabled. + /// Gets a value indicating whether compression capability is enabled. /// public bool CompressionCapabilityEnabled => (FeatureFlags & StorageFeatureFlags.CompressionCapability) != 0; - private StorageFormatMetadata(bool isPresent, byte version, StorageFeatureFlags featureFlags, CompressionCodec defaultCodec) + private StorageFormatMetadata(bool isPresent, byte version, StorageFeatureFlags featureFlags, + CompressionCodec defaultCodec) { IsPresent = isPresent; Version = version; @@ -48,18 +49,19 @@ internal readonly struct StorageFormatMetadata } /// - /// Creates metadata representing a modern format-aware file. + /// Creates metadata representing a modern format-aware file. /// /// The storage format version. /// Enabled feature flags. /// The default compression codec. - public static StorageFormatMetadata Present(byte version, StorageFeatureFlags featureFlags, CompressionCodec defaultCodec) + public static StorageFormatMetadata Present(byte version, StorageFeatureFlags featureFlags, + CompressionCodec defaultCodec) { return new StorageFormatMetadata(true, version, featureFlags, defaultCodec); } /// - /// Creates metadata representing a legacy file without format metadata. + /// Creates metadata representing a legacy file without format metadata. /// /// The default compression codec. public static StorageFormatMetadata Legacy(CompressionCodec defaultCodec) @@ -88,12 +90,13 @@ public sealed partial class StorageEngine return metadata; if (!_pageFile.WasCreated) - return StorageFormatMetadata.Legacy(_compressionOptions.Codec); + return StorageFormatMetadata.Legacy(CompressionOptions.Codec); - var featureFlags = _compressionOptions.EnableCompression + var featureFlags = CompressionOptions.EnableCompression ? StorageFeatureFlags.CompressionCapability : StorageFeatureFlags.None; - var initialMetadata = StorageFormatMetadata.Present(CurrentStorageFormatVersion, featureFlags, _compressionOptions.Codec); + var initialMetadata = + StorageFormatMetadata.Present(CurrentStorageFormatVersion, featureFlags, CompressionOptions.Codec); WriteStorageFormatMetadata(initialMetadata); return initialMetadata; } @@ -104,11 +107,11 @@ public sealed partial class StorageEngine if (source.Length < StorageFormatMetadata.WireSize) return false; - var magic = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(0, 4)); + uint magic = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(0, 4)); if (magic != StorageFormatMagic) return false; - var version = source[4]; + byte version = source[4]; var featureFlags = (StorageFeatureFlags)source[5]; var codec = (CompressionCodec)source[6]; if (!Enum.IsDefined(codec)) @@ -128,4 +131,4 @@ public sealed partial class StorageEngine buffer[6] = (byte)metadata.DefaultCodec; _pageFile.WritePageZeroExtension(StorageHeaderExtensionOffset, buffer); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Maintenance.cs b/src/CBDD.Core/Storage/StorageEngine.Maintenance.cs index 83ecab0..bdc0a9b 100644 --- a/src/CBDD.Core/Storage/StorageEngine.Maintenance.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Maintenance.cs @@ -1,8 +1,8 @@ +using System.Buffers.Binary; +using System.IO.MemoryMappedFiles; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using System.Buffers.Binary; -using System.IO.MemoryMappedFiles; using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Core.Indexing.Internal; using ZB.MOM.WW.CBDD.Core.Transactions; @@ -10,62 +10,64 @@ using ZB.MOM.WW.CBDD.Core.Transactions; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Options that control compaction/vacuum behavior. +/// Options that control compaction/vacuum behavior. /// public sealed class CompactionOptions { /// - /// Gets or sets a value indicating whether to run a bounded online pass instead of full offline compaction. + /// Gets or sets a value indicating whether to run a bounded online pass instead of full offline compaction. /// public bool OnlineMode { get; init; } /// - /// Gets or sets a value indicating whether slotted-page defragmentation should be performed. + /// Gets or sets a value indicating whether slotted-page defragmentation should be performed. /// public bool DefragmentSlottedPages { get; init; } = true; /// - /// Gets or sets a value indicating whether free-list normalization should be performed. + /// Gets or sets a value indicating whether free-list normalization should be performed. /// public bool NormalizeFreeList { get; init; } = true; /// - /// Gets or sets a value indicating whether reclaimable tail pages should be truncated. + /// Gets or sets a value indicating whether reclaimable tail pages should be truncated. /// public bool EnableTailTruncation { get; init; } = true; /// - /// Gets or sets the minimum page count that must remain after truncation. + /// Gets or sets the minimum page count that must remain after truncation. /// public uint MinimumRetainedPages { get; init; } = 2; /// - /// Gets or sets the maximum number of pages processed per online batch. + /// Gets or sets the maximum number of pages processed per online batch. /// public int OnlineBatchPageLimit { get; init; } = 16; /// - /// Gets or sets the sleep delay inserted between online batches. + /// Gets or sets the sleep delay inserted between online batches. /// public TimeSpan OnlineBatchDelay { get; init; } = TimeSpan.FromMilliseconds(20); /// - /// Gets or sets the maximum duration of a single online pass. + /// Gets or sets the maximum duration of a single online pass. /// public TimeSpan MaxOnlineDuration { get; init; } = TimeSpan.FromSeconds(2); /// - /// Normalizes compaction options to safe runtime defaults. + /// Normalizes compaction options to safe runtime defaults. /// /// Optional compaction options. internal static CompactionOptions Normalize(CompactionOptions? options) { var normalized = options ?? new CompactionOptions(); - var minimumPages = normalized.MinimumRetainedPages < 2 ? 2u : normalized.MinimumRetainedPages; - var pageLimit = normalized.OnlineBatchPageLimit <= 0 ? 1 : normalized.OnlineBatchPageLimit; + uint minimumPages = normalized.MinimumRetainedPages < 2 ? 2u : normalized.MinimumRetainedPages; + int pageLimit = normalized.OnlineBatchPageLimit <= 0 ? 1 : normalized.OnlineBatchPageLimit; var batchDelay = normalized.OnlineBatchDelay < TimeSpan.Zero ? TimeSpan.Zero : normalized.OnlineBatchDelay; - var maxDuration = normalized.MaxOnlineDuration <= TimeSpan.Zero ? TimeSpan.FromMilliseconds(250) : normalized.MaxOnlineDuration; + var maxDuration = normalized.MaxOnlineDuration <= TimeSpan.Zero + ? TimeSpan.FromMilliseconds(250) + : normalized.MaxOnlineDuration; return new CompactionOptions { @@ -82,197 +84,197 @@ public sealed class CompactionOptions } /// -/// Scheduling and trigger options for maintenance orchestration. +/// Scheduling and trigger options for maintenance orchestration. /// public sealed class MaintenanceOptions { /// - /// Gets or sets a value indicating whether a maintenance pass should be attempted at startup. + /// Gets or sets a value indicating whether a maintenance pass should be attempted at startup. /// public bool RunAtStartup { get; init; } /// - /// Gets or sets the minimum fragmentation percentage required before maintenance should run. + /// Gets or sets the minimum fragmentation percentage required before maintenance should run. /// public double MinFragmentationPercent { get; init; } = 15.0; /// - /// Gets or sets the minimum reclaimable bytes threshold. + /// Gets or sets the minimum reclaimable bytes threshold. /// public long MinReclaimableBytes { get; init; } = 64 * 1024; /// - /// Gets or sets the maximum runtime for a maintenance pass. + /// Gets or sets the maximum runtime for a maintenance pass. /// public TimeSpan MaxRunDuration { get; init; } = TimeSpan.FromSeconds(5); /// - /// Gets or sets the online throttle value in pages per batch. + /// Gets or sets the online throttle value in pages per batch. /// public int OnlineThrottlePagesPerBatch { get; init; } = 16; /// - /// Gets or sets the delay inserted between online batches. + /// Gets or sets the delay inserted between online batches. /// public TimeSpan OnlineThrottleDelay { get; init; } = TimeSpan.FromMilliseconds(20); } /// -/// Space accounting and execution stats produced by compaction/vacuum operations. +/// Space accounting and execution stats produced by compaction/vacuum operations. /// public sealed class CompactionStats { /// - /// Gets or sets the operation start timestamp in UTC. + /// Gets or sets the operation start timestamp in UTC. /// public DateTimeOffset StartedAtUtc { get; init; } /// - /// Gets or sets the operation completion timestamp in UTC. + /// Gets or sets the operation completion timestamp in UTC. /// public DateTimeOffset CompletedAtUtc { get; init; } /// - /// Gets or sets a value indicating whether this run used online mode. + /// Gets or sets a value indicating whether this run used online mode. /// public bool OnlineMode { get; init; } /// - /// Gets or sets a value indicating whether this run resumed from an existing compaction marker. + /// Gets or sets a value indicating whether this run resumed from an existing compaction marker. /// public bool ResumedFromMarker { get; init; } /// - /// Gets or sets the pre-compaction file size in bytes. + /// Gets or sets the pre-compaction file size in bytes. /// public long PreFileSizeBytes { get; init; } /// - /// Gets or sets the post-compaction file size in bytes. + /// Gets or sets the post-compaction file size in bytes. /// public long PostFileSizeBytes { get; init; } /// - /// Gets or sets the pre-compaction page count. + /// Gets or sets the pre-compaction page count. /// public uint PrePageCount { get; init; } /// - /// Gets or sets the post-compaction page count. + /// Gets or sets the post-compaction page count. /// public uint PostPageCount { get; init; } /// - /// Gets or sets the pre-compaction free page count. + /// Gets or sets the pre-compaction free page count. /// public int PreFreePages { get; init; } /// - /// Gets or sets the post-compaction free page count. + /// Gets or sets the post-compaction free page count. /// public int PostFreePages { get; init; } /// - /// Gets or sets the pre-compaction estimated free bytes. + /// Gets or sets the pre-compaction estimated free bytes. /// public long PreFreeBytes { get; init; } /// - /// Gets or sets the post-compaction estimated free bytes. + /// Gets or sets the post-compaction estimated free bytes. /// public long PostFreeBytes { get; init; } /// - /// Gets or sets the pre-compaction estimated live bytes. + /// Gets or sets the pre-compaction estimated live bytes. /// public long PreLiveBytes { get; init; } /// - /// Gets or sets the post-compaction estimated live bytes. + /// Gets or sets the post-compaction estimated live bytes. /// public long PostLiveBytes { get; init; } /// - /// Gets or sets the pre-compaction fragmentation percentage. + /// Gets or sets the pre-compaction fragmentation percentage. /// public double PreFragmentationPercent { get; init; } /// - /// Gets or sets the post-compaction fragmentation percentage. + /// Gets or sets the post-compaction fragmentation percentage. /// public double PostFragmentationPercent { get; init; } /// - /// Gets or sets the number of scanned pages. + /// Gets or sets the number of scanned pages. /// public int PagesScanned { get; init; } /// - /// Gets or sets the number of defragmented pages. + /// Gets or sets the number of defragmented pages. /// public int PagesDefragmented { get; init; } /// - /// Gets or sets bytes reclaimed by slot compaction inside slotted pages. + /// Gets or sets bytes reclaimed by slot compaction inside slotted pages. /// public long BytesReclaimedByDefragmentation { get; init; } /// - /// Gets or sets the normalized free-list page count. + /// Gets or sets the normalized free-list page count. /// public int FreeListPagesNormalized { get; init; } /// - /// Gets or sets the number of pages truncated from EOF. + /// Gets or sets the number of pages truncated from EOF. /// public uint TailPagesTruncated { get; init; } /// - /// Gets or sets the number of bytes truncated from EOF. + /// Gets or sets the number of bytes truncated from EOF. /// public long TailBytesTruncated { get; init; } /// - /// Gets or sets the count of documents relocated during compaction. + /// Gets or sets the count of documents relocated during compaction. /// public long DocumentsRelocated { get; init; } /// - /// Gets or sets the count of pages where at least one document was relocated. + /// Gets or sets the count of pages where at least one document was relocated. /// public int PagesRelocated { get; init; } /// - /// Gets reclaimed file bytes (`PreFileSizeBytes - PostFileSizeBytes`). + /// Gets reclaimed file bytes (`PreFileSizeBytes - PostFileSizeBytes`). /// public long ReclaimedFileBytes => PreFileSizeBytes - PostFileSizeBytes; /// - /// Gets reclaimed pages (`PrePageCount - PostPageCount`). + /// Gets reclaimed pages (`PrePageCount - PostPageCount`). /// public long ReclaimedPages => (long)PrePageCount - PostPageCount; /// - /// Gets total runtime. + /// Gets total runtime. /// public TimeSpan Duration => CompletedAtUtc - StartedAtUtc; /// - /// Gets average processed bytes per second using pre-compaction file size and elapsed runtime. + /// Gets average processed bytes per second using pre-compaction file size and elapsed runtime. /// public double ThroughputBytesPerSecond => Duration.TotalSeconds <= 0 ? 0 : PreFileSizeBytes / Duration.TotalSeconds; /// - /// Gets average scanned pages per second during compaction. + /// Gets average scanned pages per second during compaction. /// public double ThroughputPagesPerSecond => Duration.TotalSeconds <= 0 ? 0 : PagesScanned / Duration.TotalSeconds; /// - /// Gets average relocated documents per second during compaction. + /// Gets average relocated documents per second during compaction. /// public double ThroughputDocumentsPerSecond => Duration.TotalSeconds <= 0 ? 0 @@ -281,130 +283,6 @@ public sealed class CompactionStats public sealed partial class StorageEngine { - private enum CompactionMarkerPhase - { - Started = 1, - Copied = 2, - Swapped = 3, - CleanupDone = 4 - } - - private sealed class CompactionMarkerState - { - /// - /// Gets or sets the compaction marker schema version. - /// - public int Version { get; set; } = 1; - - /// - /// Gets or sets the current compaction marker phase. - /// - public CompactionMarkerPhase Phase { get; set; } - - /// - /// Gets or sets the primary database path. - /// - public string DatabasePath { get; set; } = string.Empty; - - /// - /// Gets or sets the temporary database path. - /// - public string TempDatabasePath { get; set; } = string.Empty; - - /// - /// Gets or sets the backup database path. - /// - public string BackupDatabasePath { get; set; } = string.Empty; - - /// - /// Gets or sets the compaction start timestamp in UTC. - /// - public DateTimeOffset StartedAtUtc { get; set; } - - /// - /// Gets or sets the last marker update timestamp in UTC. - /// - public DateTimeOffset LastUpdatedUtc { get; set; } - - /// - /// Gets or sets a value indicating whether online mode was used. - /// - public bool OnlineMode { get; set; } - - /// - /// Gets or sets the compaction execution mode. - /// - public string Mode { get; set; } = "CopySwap"; - } - - private readonly struct CompactionSnapshot - { - /// - /// Initializes a new instance of the struct. - /// - /// The file size in bytes. - /// The total page count. - /// The free page count. - /// The total estimated free bytes. - /// The estimated fragmentation percentage. - /// The reclaimable tail page count. - public CompactionSnapshot( - long fileSizeBytes, - uint pageCount, - int freePageCount, - long totalFreeBytes, - double fragmentationPercent, - uint tailReclaimablePages) - { - FileSizeBytes = fileSizeBytes; - PageCount = pageCount; - FreePageCount = freePageCount; - TotalFreeBytes = totalFreeBytes; - FragmentationPercent = fragmentationPercent; - TailReclaimablePages = tailReclaimablePages; - } - - /// - /// Gets the file size in bytes. - /// - public long FileSizeBytes { get; } - - /// - /// Gets the total page count. - /// - public uint PageCount { get; } - - /// - /// Gets the free page count. - /// - public int FreePageCount { get; } - - /// - /// Gets the estimated total free bytes. - /// - public long TotalFreeBytes { get; } - - /// - /// Gets the estimated fragmentation percentage. - /// - public double FragmentationPercent { get; } - - /// - /// Gets the number of reclaimable tail pages. - /// - public uint TailReclaimablePages { get; } - } - - private struct CompactionWork - { - public int PagesScanned; - public int PagesDefragmented; - public long BytesReclaimedByDefragmentation; - public int FreeListPagesNormalized; - public long DocumentsRelocated; - public int PagesRelocated; - } - private static readonly JsonSerializerOptions CompactionMarkerSerializerOptions = new() { WriteIndented = false, @@ -416,12 +294,13 @@ public sealed partial class StorageEngine private uint _onlineCompactionCursor = 2; /// - /// Gets maintenance scheduling options configured for this engine instance. + /// Gets maintenance scheduling options configured for this engine instance. /// - public MaintenanceOptions MaintenanceOptions => _maintenanceOptions; + public MaintenanceOptions MaintenanceOptions { get; } /// - /// Runs compaction in offline mode by default. Set to run a bounded online pass. + /// Runs compaction in offline mode by default. Set to run a bounded online + /// pass. /// /// Optional compaction options. public CompactionStats Compact(CompactionOptions? options = null) @@ -430,23 +309,21 @@ public sealed partial class StorageEngine } /// - /// Runs compaction in offline mode by default. Set to run a bounded online pass. + /// Runs compaction in offline mode by default. Set to run a bounded online + /// pass. /// /// Optional compaction options. /// Cancellation token. public async Task CompactAsync(CompactionOptions? options = null, CancellationToken ct = default) { var normalizedOptions = CompactionOptions.Normalize(options); - if (normalizedOptions.OnlineMode) - { - return await RunOnlineCompactionPassAsync(normalizedOptions, ct); - } + if (normalizedOptions.OnlineMode) return await RunOnlineCompactionPassAsync(normalizedOptions, ct); - return await RunOfflineCompactionAsync(normalizedOptions, resumeOnly: false, ct); + return await RunOfflineCompactionAsync(normalizedOptions, false, ct); } /// - /// Alias for . + /// Alias for . /// /// Optional compaction options. public CompactionStats Vacuum(CompactionOptions? options = null) @@ -455,7 +332,7 @@ public sealed partial class StorageEngine } /// - /// Async alias for . + /// Async alias for . /// /// Optional compaction options. /// Cancellation token. @@ -465,14 +342,13 @@ public sealed partial class StorageEngine } /// - /// Runs a bounded, throttled online compaction pass. + /// Runs a bounded, throttled online compaction pass. /// /// Optional online compaction options. public CompactionStats RunOnlineCompactionPass(CompactionOptions? options = null) { var normalized = CompactionOptions.Normalize(options); if (!normalized.OnlineMode) - { normalized = new CompactionOptions { OnlineMode = true, @@ -484,21 +360,20 @@ public sealed partial class StorageEngine OnlineBatchDelay = normalized.OnlineBatchDelay, MaxOnlineDuration = normalized.MaxOnlineDuration }; - } return RunOnlineCompactionPassAsync(normalized).GetAwaiter().GetResult(); } /// - /// Runs a bounded, throttled online compaction pass. + /// Runs a bounded, throttled online compaction pass. /// /// Optional online compaction options. /// Cancellation token. - public async Task RunOnlineCompactionPassAsync(CompactionOptions? options = null, CancellationToken ct = default) + public async Task RunOnlineCompactionPassAsync(CompactionOptions? options = null, + CancellationToken ct = default) { var normalizedOptions = CompactionOptions.Normalize(options); if (!normalizedOptions.OnlineMode) - { normalizedOptions = new CompactionOptions { OnlineMode = true, @@ -510,13 +385,12 @@ public sealed partial class StorageEngine OnlineBatchDelay = normalizedOptions.OnlineBatchDelay, MaxOnlineDuration = normalizedOptions.MaxOnlineDuration }; - } return await ExecuteOnlinePassAsync(normalizedOptions, ct); } /// - /// If a compaction marker exists, resumes or finalizes the in-progress compaction idempotently. + /// If a compaction marker exists, resumes or finalizes the in-progress compaction idempotently. /// /// Optional compaction options. public CompactionStats? ResumeCompactionIfNeeded(CompactionOptions? options = null) @@ -525,15 +399,15 @@ public sealed partial class StorageEngine } /// - /// If a compaction marker exists, resumes or finalizes the in-progress compaction idempotently. + /// If a compaction marker exists, resumes or finalizes the in-progress compaction idempotently. /// /// Optional compaction options. /// Cancellation token. - public async Task ResumeCompactionIfNeededAsync(CompactionOptions? options = null, CancellationToken ct = default) + public async Task ResumeCompactionIfNeededAsync(CompactionOptions? options = null, + CancellationToken ct = default) { var normalizedOptions = CompactionOptions.Normalize(options); if (normalizedOptions.OnlineMode) - { normalizedOptions = new CompactionOptions { OnlineMode = false, @@ -545,16 +419,16 @@ public sealed partial class StorageEngine OnlineBatchDelay = normalizedOptions.OnlineBatchDelay, MaxOnlineDuration = normalizedOptions.MaxOnlineDuration }; - } - var markerPath = GetCompactionMarkerPath(); + string markerPath = GetCompactionMarkerPath(); if (!File.Exists(markerPath)) return null; - return await RunOfflineCompactionAsync(normalizedOptions, resumeOnly: true, ct); + return await RunOfflineCompactionAsync(normalizedOptions, true, ct); } - private async Task RunOfflineCompactionAsync(CompactionOptions options, bool resumeOnly, CancellationToken ct) + private async Task RunOfflineCompactionAsync(CompactionOptions options, bool resumeOnly, + CancellationToken ct) { await _maintenanceGate.WaitAsync(ct); try @@ -562,7 +436,7 @@ public sealed partial class StorageEngine await _commitLock.WaitAsync(ct); try { - var markerPath = GetCompactionMarkerPath(); + string markerPath = GetCompactionMarkerPath(); var markerState = TryReadCompactionMarker(markerPath, out var parsedMarker) ? parsedMarker : null; @@ -574,17 +448,11 @@ public sealed partial class StorageEngine var startedAt = markerState?.StartedAtUtc ?? DateTimeOffset.UtcNow; var preSnapshot = CaptureCompactionSnapshot(); - var tempPath = markerState?.TempDatabasePath; - if (string.IsNullOrWhiteSpace(tempPath)) - { - tempPath = GetCompactionTempPath(); - } + string? tempPath = markerState?.TempDatabasePath; + if (string.IsNullOrWhiteSpace(tempPath)) tempPath = GetCompactionTempPath(); - var backupPath = markerState?.BackupDatabasePath; - if (string.IsNullOrWhiteSpace(backupPath)) - { - backupPath = GetCompactionBackupPath(); - } + string? backupPath = markerState?.BackupDatabasePath; + if (string.IsNullOrWhiteSpace(backupPath)) backupPath = GetCompactionBackupPath(); markerState ??= new CompactionMarkerState { @@ -597,10 +465,7 @@ public sealed partial class StorageEngine Mode = "CopySwap" }; - if (!TryReadCompactionMarker(markerPath, out _)) - { - WriteCompactionMarker(markerPath, markerState); - } + if (!TryReadCompactionMarker(markerPath, out _)) WriteCompactionMarker(markerPath, markerState); var work = new CompactionWork(); TailTruncationResult tailResult = default; @@ -675,8 +540,8 @@ public sealed partial class StorageEngine tailResult, startedAt, DateTimeOffset.UtcNow, - onlineMode: false, - resumed: resumeOnly || parsedMarker != null); + false, + resumeOnly || parsedMarker != null); } finally { @@ -717,17 +582,14 @@ public sealed partial class StorageEngine await _commitLock.WaitAsync(ct); try { - if (!_activeTransactions.IsEmpty) - { - continue; - } + if (!_activeTransactions.IsEmpty) continue; CheckpointInternal(); var batch = RunInPlaceMaintenanceBatch( - pageStart: _onlineCompactionCursor, - maxPages: options.OnlineBatchPageLimit, + _onlineCompactionCursor, + options.OnlineBatchPageLimit, options, - updateCursor: true, + true, ct); work.PagesScanned += batch.PagesScanned; @@ -747,10 +609,8 @@ public sealed partial class StorageEngine _pageFile.Flush(); - if (batch.PagesScanned == 0 && currentTailNoProgress(currentTail: options.EnableTailTruncation ? tailResult : default)) - { - break; - } + if (batch.PagesScanned == 0 && + currentTailNoProgress(options.EnableTailTruncation ? tailResult : default)) break; } finally { @@ -779,8 +639,8 @@ public sealed partial class StorageEngine tailResult, startedAt, DateTimeOffset.UtcNow, - onlineMode: true, - resumed: false); + true, + false); } finally { @@ -793,17 +653,15 @@ public sealed partial class StorageEngine } } - private CompactionWork RunInPlaceMaintenanceBatch(uint pageStart, int maxPages, CompactionOptions options, bool updateCursor, CancellationToken ct) + private CompactionWork RunInPlaceMaintenanceBatch(uint pageStart, int maxPages, CompactionOptions options, + bool updateCursor, CancellationToken ct) { var work = new CompactionWork(); - var pageCount = _pageFile.NextPageId; + uint pageCount = _pageFile.NextPageId; if (pageCount <= 1 || maxPages <= 0) return work; - if (options.NormalizeFreeList) - { - work.FreeListPagesNormalized = _pageFile.NormalizeFreeList(includeEmptyPages: true); - } + if (options.NormalizeFreeList) work.FreeListPagesNormalized = _pageFile.NormalizeFreeList(); if (!options.DefragmentSlottedPages) return work; @@ -811,16 +669,13 @@ public sealed partial class StorageEngine if (pageStart < 1 || pageStart >= pageCount) pageStart = 1; - var remaining = Math.Min(maxPages, (int)Math.Max(0, pageCount - 1)); - var pageId = pageStart; + int remaining = Math.Min(maxPages, (int)Math.Max(0, pageCount - 1)); + uint pageId = pageStart; while (remaining > 0) { ct.ThrowIfCancellationRequested(); - if (pageId >= pageCount) - { - pageId = 1; - } + if (pageId >= pageCount) pageId = 1; var result = _pageFile.DefragmentSlottedPageWithStats(pageId); if (result.Changed) @@ -839,33 +694,26 @@ public sealed partial class StorageEngine pageId++; } - if (updateCursor) - { - _onlineCompactionCursor = pageId >= pageCount ? 1u : pageId; - } + if (updateCursor) _onlineCompactionCursor = pageId >= pageCount ? 1u : pageId; return work; } private CompactionSnapshot CaptureCompactionSnapshot() { - var fileSizeBytes = _pageFile.FileLengthBytes; - var logicalPageCount = _pageFile.NextPageId; - var pageSize = _pageFile.PageSize; + long fileSizeBytes = _pageFile.FileLengthBytes; + uint logicalPageCount = _pageFile.NextPageId; + int pageSize = _pageFile.PageSize; var pageCount = (uint)(fileSizeBytes / pageSize); - var freePages = _pageFile.EnumerateFreePages(includeEmptyPages: true); + var freePages = _pageFile.EnumerateFreePages(); var freePageSet = new HashSet(freePages); if (pageCount > logicalPageCount) - { - for (var pageId = logicalPageCount; pageId < pageCount; pageId++) - { + for (uint pageId = logicalPageCount; pageId < pageCount; pageId++) freePageSet.Add(pageId); - } - } - var freePageCount = freePageSet.Count; + int freePageCount = freePageSet.Count; long slottedFreeBytes = 0; if (logicalPageCount > 1) @@ -874,23 +722,23 @@ public sealed partial class StorageEngine for (uint pageId = 1; pageId < logicalPageCount; pageId++) { _pageFile.ReadPage(pageId, pageBuffer); - if (!TryReadSlottedFreeSpace(pageBuffer, out var availableFreeSpace)) + if (!TryReadSlottedFreeSpace(pageBuffer, out int availableFreeSpace)) continue; slottedFreeBytes += availableFreeSpace; } } - var freePageBytes = (long)freePageCount * pageSize; - var totalFreeBytes = freePageBytes + slottedFreeBytes; - var fragmentationPercent = fileSizeBytes <= 0 + long freePageBytes = (long)freePageCount * pageSize; + long totalFreeBytes = freePageBytes + slottedFreeBytes; + double fragmentationPercent = fileSizeBytes <= 0 ? 0 - : (totalFreeBytes * 100d) / fileSizeBytes; + : totalFreeBytes * 100d / fileSizeBytes; uint tailReclaimablePages = 0; - for (var pageId = pageCount; pageId > 2; pageId--) + for (uint pageId = pageCount; pageId > 2; pageId--) { - var candidate = pageId - 1; + uint candidate = pageId - 1; if (!freePageSet.Contains(candidate)) break; @@ -916,11 +764,12 @@ public sealed partial class StorageEngine if (!IsSlottedPageType(header.PageType)) return false; - var slotArrayEnd = SlottedPageHeader.Size + (header.SlotCount * SlotEntry.Size); + int slotArrayEnd = SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size; if (slotArrayEnd > pageBuffer.Length) return false; - if (header.FreeSpaceStart < slotArrayEnd || header.FreeSpaceEnd > pageBuffer.Length || header.FreeSpaceEnd < header.FreeSpaceStart) + if (header.FreeSpaceStart < slotArrayEnd || header.FreeSpaceEnd > pageBuffer.Length || + header.FreeSpaceEnd < header.FreeSpaceStart) return false; freeSpace = header.AvailableFreeSpace; @@ -993,19 +842,13 @@ public sealed partial class StorageEngine private static void EnsureCompactionArtifactDirectory(string path) { - var directory = Path.GetDirectoryName(path); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } + string? directory = Path.GetDirectoryName(path); + if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); } private static void DeleteFileIfExists(string path) { - if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) - { - File.Delete(path); - } + if (!string.IsNullOrWhiteSpace(path) && File.Exists(path)) File.Delete(path); } private void ValidateCollectionMetadataAndPrimaryIndexPointers(CancellationToken ct) @@ -1021,13 +864,13 @@ public sealed partial class StorageEngine metadata.Name, "primary index", metadata.PrimaryRootPageId, - expectedPageType: PageType.Index, + PageType.Index, pageBuffer); ValidateRootPage( metadata.Name, "schema", metadata.SchemaRootPageId, - expectedPageType: PageType.Schema, + PageType.Schema, pageBuffer); foreach (var indexMetadata in metadata.Indexes) @@ -1037,7 +880,7 @@ public sealed partial class StorageEngine metadata.Name, $"index '{indexMetadata.Name}'", indexMetadata.RootPageId, - expectedPageType: GetExpectedIndexRootPageType(indexMetadata.Type), + GetExpectedIndexRootPageType(indexMetadata.Type), pageBuffer); } @@ -1045,11 +888,9 @@ public sealed partial class StorageEngine { ct.ThrowIfCancellationRequested(); if (!TryReadStoredPayload(location, out _, out _)) - { throw new InvalidDataException( $"Compaction validation failed for collection '{metadata.Name}': " + $"primary index location (page {location.PageId}, slot {location.SlotIndex}) is invalid or deleted."); - } } } } @@ -1061,26 +902,19 @@ public sealed partial class StorageEngine PageType expectedPageType, byte[] pageBuffer) { - if (rootPageId == 0) - { - return; - } + if (rootPageId == 0) return; if (rootPageId >= _pageFile.NextPageId) - { throw new InvalidDataException( $"Compaction validation failed for collection '{collectionName}': " + $"{rootLabel} root page id {rootPageId} is out of range."); - } _pageFile.ReadPage(rootPageId, pageBuffer); var pageHeader = PageHeader.ReadFrom(pageBuffer); if (pageHeader.PageType != expectedPageType) - { throw new InvalidDataException( $"Compaction validation failed for collection '{collectionName}': " + $"{rootLabel} root page id {rootPageId} has type {pageHeader.PageType}, expected {expectedPageType}."); - } } private static PageType GetExpectedIndexRootPageType(IndexType indexType) @@ -1103,17 +937,17 @@ public sealed partial class StorageEngine var work = new CompactionWork(); TailTruncationResult tailResult = default; - var logicalSourceSizeBytes = Math.Max((long)_pageFile.NextPageId * _pageFile.PageSize, _pageFile.PageSize * 2L); + long logicalSourceSizeBytes = Math.Max(_pageFile.NextPageId * _pageFile.PageSize, _pageFile.PageSize * 2L); var tempConfig = CreateWritableCompactionPageFileConfig(_pageFile.Config, logicalSourceSizeBytes); var tempMaintenance = CreateCompactionTempMaintenanceOptions(); using var tempEngine = new StorageEngine( tempPath, tempConfig, - _compressionOptions, + CompressionOptions, tempMaintenance); - tempEngine.WriteStorageFormatMetadata(_storageFormatMetadata); - _ = tempEngine._pageFile.NormalizeFreeList(includeEmptyPages: true); + tempEngine.WriteStorageFormatMetadata(StorageFormatMetadata); + _ = tempEngine._pageFile.NormalizeFreeList(); CopyDictionaryMappingDeterministically(tempEngine, ct); var sourceCollections = GetAllCollectionMetadata(); @@ -1127,24 +961,17 @@ public sealed partial class StorageEngine tempEngine.SaveCollectionMetadata(collectionResult.Metadata); work.DocumentsRelocated += collectionResult.DocumentsRelocated; - foreach (var relocatedPageId in collectionResult.RelocatedSourcePageIds) - { + foreach (uint relocatedPageId in collectionResult.RelocatedSourcePageIds) relocatedSourcePages.Add(relocatedPageId); - } } work.PagesRelocated = relocatedSourcePages.Count; work.PagesScanned = Math.Max(0, (int)Math.Max(0, _pageFile.NextPageId - 1)); - if (options.NormalizeFreeList) - { - work.FreeListPagesNormalized = tempEngine._pageFile.NormalizeFreeList(includeEmptyPages: true); - } + if (options.NormalizeFreeList) work.FreeListPagesNormalized = tempEngine._pageFile.NormalizeFreeList(); if (options.EnableTailTruncation) - { tailResult = tempEngine._pageFile.TruncateReclaimableTailPages(options.MinimumRetainedPages); - } _ = tempEngine._pageFile.TrimExcessCapacityToLogicalPageCount(); tempEngine.CheckpointInternal(); @@ -1152,20 +979,19 @@ public sealed partial class StorageEngine return (work, tailResult); } - private static PageFileConfig CreateWritableCompactionPageFileConfig(PageFileConfig currentConfig, long sourceFileSize) + private static PageFileConfig CreateWritableCompactionPageFileConfig(PageFileConfig currentConfig, + long sourceFileSize) { - var minimumFileSize = Math.Max(currentConfig.PageSize * 2L, 0L); - var initialFileSize = Math.Max(sourceFileSize, minimumFileSize); + long minimumFileSize = Math.Max(currentConfig.PageSize * 2L, 0L); + long initialFileSize = Math.Max(sourceFileSize, minimumFileSize); if (currentConfig.Access == MemoryMappedFileAccess.ReadWrite) - { return new PageFileConfig { PageSize = currentConfig.PageSize, InitialFileSize = initialFileSize, Access = currentConfig.Access }; - } return new PageFileConfig { @@ -1180,99 +1006,14 @@ public sealed partial class StorageEngine return new MaintenanceOptions { RunAtStartup = false, - MinFragmentationPercent = _maintenanceOptions.MinFragmentationPercent, - MinReclaimableBytes = _maintenanceOptions.MinReclaimableBytes, - MaxRunDuration = _maintenanceOptions.MaxRunDuration, - OnlineThrottlePagesPerBatch = _maintenanceOptions.OnlineThrottlePagesPerBatch, - OnlineThrottleDelay = _maintenanceOptions.OnlineThrottleDelay + MinFragmentationPercent = MaintenanceOptions.MinFragmentationPercent, + MinReclaimableBytes = MaintenanceOptions.MinReclaimableBytes, + MaxRunDuration = MaintenanceOptions.MaxRunDuration, + OnlineThrottlePagesPerBatch = MaintenanceOptions.OnlineThrottlePagesPerBatch, + OnlineThrottleDelay = MaintenanceOptions.OnlineThrottleDelay }; } - private readonly struct CompactionCollectionRebuildResult - { - /// - /// Initializes a new instance of the struct. - /// - /// The rebuilt collection metadata. - /// The number of relocated documents. - /// The relocated source page identifiers. - public CompactionCollectionRebuildResult( - CollectionMetadata metadata, - long documentsRelocated, - IReadOnlyCollection relocatedSourcePageIds) - { - Metadata = metadata; - DocumentsRelocated = documentsRelocated; - RelocatedSourcePageIds = relocatedSourcePageIds; - } - - /// - /// Gets rebuilt collection metadata. - /// - public CollectionMetadata Metadata { get; } - - /// - /// Gets the number of relocated documents. - /// - public long DocumentsRelocated { get; } - - /// - /// Gets relocated source page identifiers. - /// - public IReadOnlyCollection RelocatedSourcePageIds { get; } - } - - private sealed class CompactionDataWriterState - { - /// - /// Gets free space tracked by page identifier. - /// - public Dictionary FreeSpaceByPage { get; } = new(); - - /// - /// Gets or sets the current data page identifier. - /// - public uint CurrentDataPageId { get; set; } - } - - private readonly struct CompactionVectorNode - { - /// - /// Initializes a new instance of the struct. - /// - /// The page identifier. - /// The node index within the page. - /// The mapped document location. - /// The vector payload. - public CompactionVectorNode(uint pageId, int nodeIndex, DocumentLocation location, float[] vector) - { - PageId = pageId; - NodeIndex = nodeIndex; - Location = location; - Vector = vector; - } - - /// - /// Gets the page identifier containing the vector node. - /// - public uint PageId { get; } - - /// - /// Gets the node index within the page. - /// - public int NodeIndex { get; } - - /// - /// Gets the document location associated with the node. - /// - public DocumentLocation Location { get; } - - /// - /// Gets the vector payload. - /// - public float[] Vector { get; } - } - private CompactionCollectionRebuildResult RebuildCollectionForCompaction( StorageEngine tempEngine, CollectionMetadata sourceMetadata, @@ -1290,13 +1031,11 @@ public sealed partial class StorageEngine foreach (var sourceIndexMetadata in sourceMetadata.Indexes) { if (sourceIndexMetadata.RootPageId != 0) - { throw new InvalidDataException( $"Compaction validation failed for collection '{sourceMetadata.Name}': " + $"secondary index '{sourceIndexMetadata.Name}' has a root but primary index root is missing."); - } - targetMetadata.Indexes.Add(CloneIndexMetadata(sourceIndexMetadata, rootPageId: 0)); + targetMetadata.Indexes.Add(CloneIndexMetadata(sourceIndexMetadata, 0)); } return new CompactionCollectionRebuildResult(targetMetadata, 0, Array.Empty()); @@ -1306,7 +1045,7 @@ public sealed partial class StorageEngine var relocatedSourcePages = new HashSet(); var locationRemap = new Dictionary(); - var resolvedPrimaryRoot = ResolveCurrentBTreeRootPageId( + uint resolvedPrimaryRoot = ResolveCurrentBTreeRootPageId( sourceMetadata.PrimaryRootPageId, sourceMetadata.Name, "primary index"); @@ -1314,28 +1053,27 @@ public sealed partial class StorageEngine var targetPrimary = CreateCompactionBTreeAtRoot( tempEngine, IndexOptions.CreateUnique("_id"), - rootPageId: 0); + 0); targetMetadata.PrimaryRootPageId = targetPrimary.RootPageId; long relocatedDocuments = 0; using (var transaction = tempEngine.BeginTransaction()) { foreach (var primaryEntry in sourcePrimary.Range( - IndexKey.MinKey, - IndexKey.MaxKey, - IndexDirection.Forward, - transactionId: 0)) + IndexKey.MinKey, + IndexKey.MaxKey, + IndexDirection.Forward, + 0)) { ct.ThrowIfCancellationRequested(); - if (!TryReadStoredPayload(primaryEntry.Location, out var storedPayload, out var isCompressed)) - { + if (!TryReadStoredPayload(primaryEntry.Location, out byte[] storedPayload, out bool isCompressed)) throw new InvalidDataException( $"Compaction rebuild failed for collection '{sourceMetadata.Name}': " + $"cannot read source payload at (page {primaryEntry.Location.PageId}, slot {primaryEntry.Location.SlotIndex})."); - } - var rebuiltLocation = InsertStoredPayloadForCompaction(tempEngine, storedPayload, isCompressed, dataWriter); + var rebuiltLocation = + InsertStoredPayloadForCompaction(tempEngine, storedPayload, isCompressed, dataWriter); targetPrimary.Insert(primaryEntry.Key, rebuiltLocation, transaction.TransactionId); locationRemap[primaryEntry.Location] = rebuiltLocation; relocatedSourcePages.Add(primaryEntry.Location.PageId); @@ -1361,7 +1099,8 @@ public sealed partial class StorageEngine tempEngine.CheckpointInternal(); tempEngine._pageFile.Flush(); - return new CompactionCollectionRebuildResult(targetMetadata, relocatedDocuments, relocatedSourcePages.ToArray()); + return new CompactionCollectionRebuildResult(targetMetadata, relocatedDocuments, + relocatedSourcePages.ToArray()); } private static IndexMetadata CloneIndexMetadata(IndexMetadata source, uint rootPageId) @@ -1378,7 +1117,8 @@ public sealed partial class StorageEngine }; } - private uint CopySchemaChainForCompaction(StorageEngine tempEngine, uint sourceSchemaRootPageId, CancellationToken ct) + private uint CopySchemaChainForCompaction(StorageEngine tempEngine, uint sourceSchemaRootPageId, + CancellationToken ct) { if (sourceSchemaRootPageId == 0) return 0; @@ -1403,74 +1143,76 @@ public sealed partial class StorageEngine CancellationToken ct) { if (sourceIndexMetadata.RootPageId == 0) - return CloneIndexMetadata(sourceIndexMetadata, rootPageId: 0); + return CloneIndexMetadata(sourceIndexMetadata, 0); switch (sourceIndexMetadata.Type) { case IndexType.BTree: case IndexType.Unique: case IndexType.Hash: + { + var sourceOptions = BuildIndexOptionsForCompaction(sourceIndexMetadata); + uint resolvedSourceRoot = ResolveCurrentBTreeRootPageId( + sourceIndexMetadata.RootPageId, + collectionName, + $"index '{sourceIndexMetadata.Name}'"); + var sourceIndex = new BTreeIndex(this, sourceOptions, resolvedSourceRoot); + var rebuiltIndex = CreateCompactionBTreeAtRoot( + tempEngine, + sourceOptions, + 0); + + foreach (var entry in sourceIndex.Range( + IndexKey.MinKey, + IndexKey.MaxKey, + IndexDirection.Forward, + 0)) { - var sourceOptions = BuildIndexOptionsForCompaction(sourceIndexMetadata); - var resolvedSourceRoot = ResolveCurrentBTreeRootPageId( - sourceIndexMetadata.RootPageId, + ct.ThrowIfCancellationRequested(); + var mapped = RemapLocationOrThrow( + locationRemap, + entry.Location, collectionName, - $"index '{sourceIndexMetadata.Name}'"); - var sourceIndex = new BTreeIndex(this, sourceOptions, resolvedSourceRoot); - var rebuiltIndex = CreateCompactionBTreeAtRoot( - tempEngine, - sourceOptions, - rootPageId: 0); - - foreach (var entry in sourceIndex.Range( - IndexKey.MinKey, - IndexKey.MaxKey, - IndexDirection.Forward, - transactionId: 0)) - { - ct.ThrowIfCancellationRequested(); - var mapped = RemapLocationOrThrow( - locationRemap, - entry.Location, - collectionName, - sourceIndexMetadata.Name); - rebuiltIndex.Insert(entry.Key, mapped, tempTransaction.TransactionId); - } - - return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId); + sourceIndexMetadata.Name); + rebuiltIndex.Insert(entry.Key, mapped, tempTransaction.TransactionId); } + + return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId); + } case IndexType.Spatial: + { + var options = BuildIndexOptionsForCompaction(sourceIndexMetadata); + using var rebuiltIndex = new RTreeIndex(tempEngine, options, 0); + foreach (var spatialEntry in EnumerateSpatialLeafEntriesForCompaction(sourceIndexMetadata.RootPageId, + ct)) { - var options = BuildIndexOptionsForCompaction(sourceIndexMetadata); - using var rebuiltIndex = new RTreeIndex(tempEngine, options, rootPageId: 0); - foreach (var spatialEntry in EnumerateSpatialLeafEntriesForCompaction(sourceIndexMetadata.RootPageId, ct)) - { - var mapped = RemapLocationOrThrow( - locationRemap, - spatialEntry.Location, - collectionName, - sourceIndexMetadata.Name); - rebuiltIndex.Insert(spatialEntry.Mbr, mapped, tempTransaction); - } - - return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId); + var mapped = RemapLocationOrThrow( + locationRemap, + spatialEntry.Location, + collectionName, + sourceIndexMetadata.Name); + rebuiltIndex.Insert(spatialEntry.Mbr, mapped, tempTransaction); } + + return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId); + } case IndexType.Vector: + { + var options = ResolveVectorIndexOptionsForCompaction(sourceIndexMetadata); + var rebuiltIndex = new VectorSearchIndex(tempEngine, options); + foreach (var vectorNode in EnumerateVectorNodesForCompaction(sourceIndexMetadata.RootPageId, options, + ct)) { - var options = ResolveVectorIndexOptionsForCompaction(sourceIndexMetadata); - var rebuiltIndex = new VectorSearchIndex(tempEngine, options, rootPageId: 0); - foreach (var vectorNode in EnumerateVectorNodesForCompaction(sourceIndexMetadata.RootPageId, options, ct)) - { - var mapped = RemapLocationOrThrow( - locationRemap, - vectorNode.Location, - collectionName, - sourceIndexMetadata.Name); - rebuiltIndex.Insert(vectorNode.Vector, mapped, tempTransaction); - } - - return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId); + var mapped = RemapLocationOrThrow( + locationRemap, + vectorNode.Location, + collectionName, + sourceIndexMetadata.Name); + rebuiltIndex.Insert(vectorNode.Vector, mapped, tempTransaction); } + + return CloneIndexMetadata(sourceIndexMetadata, rebuiltIndex.RootPageId); + } default: throw new InvalidDataException( $"Compaction rebuild failed for collection '{collectionName}', index '{sourceIndexMetadata.Name}': " + @@ -1497,27 +1239,23 @@ public sealed partial class StorageEngine if (configuredRootPageId == 0) return 0; - var current = configuredRootPageId; + uint current = configuredRootPageId; var visited = new HashSet(); var pageBuffer = new byte[_pageFile.PageSize]; while (visited.Add(current)) { if (current >= _pageFile.NextPageId) - { throw new InvalidDataException( $"Compaction validation failed for collection '{collectionName}': " + $"{rootLabel} root page id {current} is out of range."); - } _pageFile.ReadPage(current, pageBuffer); var pageHeader = PageHeader.ReadFrom(pageBuffer); if (pageHeader.PageType != PageType.Index) - { throw new InvalidDataException( $"Compaction validation failed for collection '{collectionName}': " + $"{rootLabel} root page id {current} has type {pageHeader.PageType}, expected {PageType.Index}."); - } var nodeHeader = BTreeNodeHeader.ReadFrom(pageBuffer.AsSpan(32)); if (nodeHeader.ParentPageId == 0) @@ -1533,26 +1271,21 @@ public sealed partial class StorageEngine private static void EnsureRootPagesAllocatedForCompaction(StorageEngine target, IEnumerable rootPageIds) { - var maxRootPageId = rootPageIds + uint maxRootPageId = rootPageIds .Where(pageId => pageId > 0) .DefaultIfEmpty(0u) .Max(); - while (maxRootPageId > 0 && target._pageFile.NextPageId <= maxRootPageId) - { - target.AllocatePage(); - } + while (maxRootPageId > 0 && target._pageFile.NextPageId <= maxRootPageId) target.AllocatePage(); } private static BTreeIndex CreateCompactionBTreeAtRoot(StorageEngine target, IndexOptions options, uint rootPageId) { if (rootPageId == 0) - return new BTreeIndex(target, options, rootPageId: 0); + return new BTreeIndex(target, options); if (target._pageFile.NextPageId <= rootPageId) - { EnsureRootPagesAllocatedForCompaction(target, new[] { rootPageId }); - } InitializeBTreeRootPageForCompaction(target, rootPageId); return new BTreeIndex(target, options, rootPageId); @@ -1587,17 +1320,17 @@ public sealed partial class StorageEngine private static IndexOptions BuildIndexOptionsForCompaction(IndexMetadata metadata) { - var fields = metadata.PropertyPaths ?? Array.Empty(); + string[] fields = metadata.PropertyPaths ?? Array.Empty(); return metadata.Type switch { IndexType.Unique => IndexOptions.CreateUnique(fields), IndexType.Hash => IndexOptions.CreateHash(fields), IndexType.Spatial => IndexOptions.CreateSpatial(fields), IndexType.Vector => IndexOptions.CreateVector( - dimensions: Math.Max(1, metadata.Dimensions), - metric: metadata.Metric, - m: 16, - ef: 200, + Math.Max(1, metadata.Dimensions), + metadata.Metric, + 16, + 200, fields), _ => metadata.IsUnique ? IndexOptions.CreateUnique(fields) : IndexOptions.CreateBTree(fields) }; @@ -1605,10 +1338,10 @@ public sealed partial class StorageEngine private IndexOptions ResolveVectorIndexOptionsForCompaction(IndexMetadata metadata) { - var dimensions = metadata.Dimensions; + int dimensions = metadata.Dimensions; var maxM = 16; - if (TryReadVectorPageLayout(metadata.RootPageId, out var pageDimensions, out var pageMaxM)) + if (TryReadVectorPageLayout(metadata.RootPageId, out int pageDimensions, out int pageMaxM)) { if (pageDimensions > 0) dimensions = pageDimensions; @@ -1617,16 +1350,14 @@ public sealed partial class StorageEngine } if (dimensions <= 0) - { throw new InvalidDataException( $"Compaction rebuild failed for index '{metadata.Name}': vector dimensions are invalid."); - } return IndexOptions.CreateVector( dimensions, - metric: metadata.Metric, - m: maxM, - ef: 200, + metadata.Metric, + maxM, + 200, metadata.PropertyPaths ?? Array.Empty()); } @@ -1662,30 +1393,26 @@ public sealed partial class StorageEngine while (pending.Count > 0) { ct.ThrowIfCancellationRequested(); - var pageId = pending.Min; + uint pageId = pending.Min; pending.Remove(pageId); if (!visitedPages.Add(pageId)) continue; if (pageId >= _pageFile.NextPageId) - { throw new InvalidDataException( $"Compaction rebuild failed: spatial page id {pageId} is out of range."); - } _pageFile.ReadPage(pageId, pageBuffer); var pageHeader = PageHeader.ReadFrom(pageBuffer); if (pageHeader.PageType != PageType.Spatial) - { throw new InvalidDataException( $"Compaction rebuild failed: page {pageId} in spatial index chain is not a spatial page."); - } - var isLeaf = SpatialPage.GetIsLeaf(pageBuffer); - var entryCount = SpatialPage.GetEntryCount(pageBuffer); + bool isLeaf = SpatialPage.GetIsLeaf(pageBuffer); + ushort entryCount = SpatialPage.GetEntryCount(pageBuffer); - for (int i = 0; i < entryCount; i++) + for (var i = 0; i < entryCount; i++) { SpatialPage.ReadEntry(pageBuffer, i, out var mbr, out var pointer); if (isLeaf) @@ -1695,15 +1422,10 @@ public sealed partial class StorageEngine else { if (pointer.PageId == 0 || pointer.PageId >= _pageFile.NextPageId) - { throw new InvalidDataException( $"Compaction rebuild failed: spatial child pointer page {pointer.PageId} is invalid."); - } - if (!visitedPages.Contains(pointer.PageId)) - { - pending.Add(pointer.PageId); - } + if (!visitedPages.Contains(pointer.PageId)) pending.Add(pointer.PageId); } } } @@ -1717,8 +1439,8 @@ public sealed partial class StorageEngine if (rootPageId == 0) yield break; - var dimensions = vectorOptions.Dimensions; - var maxM = Math.Max(1, vectorOptions.M); + int dimensions = vectorOptions.Dimensions; + int maxM = Math.Max(1, vectorOptions.M); var pending = new SortedSet { rootPageId }; var visitedPages = new HashSet(); var pageBuffer = new byte[_pageFile.PageSize]; @@ -1726,59 +1448,50 @@ public sealed partial class StorageEngine while (pending.Count > 0) { ct.ThrowIfCancellationRequested(); - var pageId = pending.Min; + uint pageId = pending.Min; pending.Remove(pageId); if (!visitedPages.Add(pageId)) continue; if (pageId >= _pageFile.NextPageId) - { throw new InvalidDataException( $"Compaction rebuild failed: vector page id {pageId} is out of range."); - } _pageFile.ReadPage(pageId, pageBuffer); var pageHeader = PageHeader.ReadFrom(pageBuffer); if (pageHeader.PageType != PageType.Vector) - { throw new InvalidDataException( $"Compaction rebuild failed: page {pageId} in vector index chain is not a vector page."); - } - var nodeCount = VectorPage.GetNodeCount(pageBuffer); + int nodeCount = VectorPage.GetNodeCount(pageBuffer); if (nodeCount < 0 || nodeCount > VectorPage.GetMaxNodes(pageBuffer)) - { throw new InvalidDataException( $"Compaction rebuild failed: vector page {pageId} has an invalid node count ({nodeCount})."); - } for (var nodeIndex = 0; nodeIndex < nodeCount; nodeIndex++) { var vector = new float[dimensions]; - VectorPage.ReadNodeData(pageBuffer, nodeIndex, out var location, out var maxLevel, vector); + VectorPage.ReadNodeData(pageBuffer, nodeIndex, out var location, out int maxLevel, vector); yield return new CompactionVectorNode(pageId, nodeIndex, location, vector); - var levelUpperBound = Math.Min(15, Math.Max(0, maxLevel)); + int levelUpperBound = Math.Min(15, Math.Max(0, maxLevel)); for (var level = 0; level <= levelUpperBound; level++) { var links = VectorPage.GetLinksSpan(pageBuffer, nodeIndex, level, dimensions, maxM); - for (var offset = 0; offset + DocumentLocation.SerializedSize <= links.Length; offset += DocumentLocation.SerializedSize) + for (var offset = 0; + offset + DocumentLocation.SerializedSize <= links.Length; + offset += DocumentLocation.SerializedSize) { var link = DocumentLocation.ReadFrom(links.Slice(offset, DocumentLocation.SerializedSize)); if (link.PageId == 0) break; if (link.PageId >= _pageFile.NextPageId) - { throw new InvalidDataException( $"Compaction rebuild failed: vector link points to out-of-range page {link.PageId}."); - } - if (!visitedPages.Contains(link.PageId)) - { - pending.Add(link.PageId); - } + if (!visitedPages.Contains(link.PageId)) pending.Add(link.PageId); } } } @@ -1792,41 +1505,40 @@ public sealed partial class StorageEngine CompactionDataWriterState writerState) { var baseFlags = isCompressed ? SlotFlags.Compressed : SlotFlags.None; - var maxSinglePagePayload = target.PageSize - SlottedPageHeader.Size - SlotEntry.Size; + int maxSinglePagePayload = target.PageSize - SlottedPageHeader.Size - SlotEntry.Size; if (storedPayload.Length + SlotEntry.Size <= maxSinglePagePayload) { - var pageId = FindDataPageWithSpaceForCompaction(target, writerState, storedPayload.Length + SlotEntry.Size); - if (pageId == 0) - { - pageId = AllocateDataPageForCompaction(target, writerState); - } + uint pageId = + FindDataPageWithSpaceForCompaction(target, writerState, storedPayload.Length + SlotEntry.Size); + if (pageId == 0) pageId = AllocateDataPageForCompaction(target, writerState); - var slotIndex = InsertPayloadIntoDataPageForCompaction(target, pageId, storedPayload, baseFlags, writerState); + ushort slotIndex = + InsertPayloadIntoDataPageForCompaction(target, pageId, storedPayload, baseFlags, writerState); return new DocumentLocation(pageId, slotIndex); } const int overflowMetadataSize = 8; - var primaryChunkSize = maxSinglePagePayload - overflowMetadataSize; + int primaryChunkSize = maxSinglePagePayload - overflowMetadataSize; if (primaryChunkSize <= 0) - { - throw new InvalidOperationException("Compaction rebuild cannot allocate overflow payload on current page size."); - } + throw new InvalidOperationException( + "Compaction rebuild cannot allocate overflow payload on current page size."); uint nextOverflowPageId = 0; - var overflowChunkSize = target.PageSize - SlottedPageHeader.Size; - var overflowBytes = storedPayload.Length - primaryChunkSize; - var fullPages = overflowBytes / overflowChunkSize; - var tailBytes = overflowBytes % overflowChunkSize; + int overflowChunkSize = target.PageSize - SlottedPageHeader.Size; + int overflowBytes = storedPayload.Length - primaryChunkSize; + int fullPages = overflowBytes / overflowChunkSize; + int tailBytes = overflowBytes % overflowChunkSize; if (tailBytes > 0) { - var tailOffset = primaryChunkSize + (fullPages * overflowChunkSize); - nextOverflowPageId = AllocateOverflowPageForCompaction(target, storedPayload.Slice(tailOffset, tailBytes), nextOverflowPageId); + int tailOffset = primaryChunkSize + fullPages * overflowChunkSize; + nextOverflowPageId = AllocateOverflowPageForCompaction(target, storedPayload.Slice(tailOffset, tailBytes), + nextOverflowPageId); } - for (var i = fullPages - 1; i >= 0; i--) + for (int i = fullPages - 1; i >= 0; i--) { - var chunkOffset = primaryChunkSize + (i * overflowChunkSize); + int chunkOffset = primaryChunkSize + i * overflowChunkSize; nextOverflowPageId = AllocateOverflowPageForCompaction( target, storedPayload.Slice(chunkOffset, overflowChunkSize), @@ -1838,20 +1550,20 @@ public sealed partial class StorageEngine BinaryPrimitives.WriteUInt32LittleEndian(primaryPayload.AsSpan(4, 4), nextOverflowPageId); storedPayload.Slice(0, primaryChunkSize).CopyTo(primaryPayload.AsSpan(8)); - var primaryPageId = FindDataPageWithSpaceForCompaction(target, writerState, primaryPayload.Length + SlotEntry.Size); - if (primaryPageId == 0) - { - primaryPageId = AllocateDataPageForCompaction(target, writerState); - } + uint primaryPageId = + FindDataPageWithSpaceForCompaction(target, writerState, primaryPayload.Length + SlotEntry.Size); + if (primaryPageId == 0) primaryPageId = AllocateDataPageForCompaction(target, writerState); var slotFlags = baseFlags | SlotFlags.HasOverflow; - var primarySlotIndex = InsertPayloadIntoDataPageForCompaction(target, primaryPageId, primaryPayload, slotFlags, writerState); + ushort primarySlotIndex = + InsertPayloadIntoDataPageForCompaction(target, primaryPageId, primaryPayload, slotFlags, writerState); return new DocumentLocation(primaryPageId, primarySlotIndex); } - private static uint AllocateOverflowPageForCompaction(StorageEngine target, ReadOnlySpan payloadChunk, uint nextOverflowPageId) + private static uint AllocateOverflowPageForCompaction(StorageEngine target, ReadOnlySpan payloadChunk, + uint nextOverflowPageId) { - var overflowPageId = target.AllocatePage(); + uint overflowPageId = target.AllocatePage(); var pageBuffer = new byte[target.PageSize]; var header = new SlottedPageHeader { @@ -1869,27 +1581,24 @@ public sealed partial class StorageEngine return overflowPageId; } - private static uint FindDataPageWithSpaceForCompaction(StorageEngine target, CompactionDataWriterState writerState, int requiredBytes) + private static uint FindDataPageWithSpaceForCompaction(StorageEngine target, CompactionDataWriterState writerState, + int requiredBytes) { if (writerState.CurrentDataPageId != 0 && - writerState.FreeSpaceByPage.TryGetValue(writerState.CurrentDataPageId, out var currentFree) && + writerState.FreeSpaceByPage.TryGetValue(writerState.CurrentDataPageId, out ushort currentFree) && currentFree >= requiredBytes) - { return writerState.CurrentDataPageId; - } - foreach (var (pageId, freeBytes) in writerState.FreeSpaceByPage) - { + foreach ((uint pageId, ushort freeBytes) in writerState.FreeSpaceByPage) if (freeBytes >= requiredBytes) return pageId; - } return 0; } private static uint AllocateDataPageForCompaction(StorageEngine target, CompactionDataWriterState writerState) { - var pageId = target.AllocatePage(); + uint pageId = target.AllocatePage(); var pageBuffer = new byte[target.PageSize]; var header = new SlottedPageHeader { @@ -1916,7 +1625,7 @@ public sealed partial class StorageEngine CompactionDataWriterState writerState) { var pageBuffer = new byte[target.PageSize]; - target.ReadPage(pageId, transactionId: null, pageBuffer); + target.ReadPage(pageId, null, pageBuffer); var header = SlottedPageHeader.ReadFrom(pageBuffer); if (header.PageType == PageType.Empty && header.FreeSpaceEnd == 0) @@ -1934,18 +1643,16 @@ public sealed partial class StorageEngine header.WriteTo(pageBuffer); } - var requiredSpace = payload.Length + SlotEntry.Size; + int requiredSpace = payload.Length + SlotEntry.Size; if (header.AvailableFreeSpace < requiredSpace) - { throw new InvalidOperationException( $"Compaction rebuild failed: not enough free space on page {pageId} (required {requiredSpace}, available {header.AvailableFreeSpace})."); - } - var slotIndex = FindReusableSlotForCompaction(pageBuffer, ref header); - var payloadOffset = header.FreeSpaceEnd - payload.Length; + ushort slotIndex = FindReusableSlotForCompaction(pageBuffer, ref header); + int payloadOffset = header.FreeSpaceEnd - payload.Length; payload.CopyTo(pageBuffer.AsSpan(payloadOffset, payload.Length)); - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; var slot = new SlotEntry { Offset = (ushort)payloadOffset, @@ -1957,7 +1664,7 @@ public sealed partial class StorageEngine if (slotIndex >= header.SlotCount) header.SlotCount = (ushort)(slotIndex + 1); - header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + (header.SlotCount * SlotEntry.Size)); + header.FreeSpaceStart = (ushort)(SlottedPageHeader.Size + header.SlotCount * SlotEntry.Size); header.FreeSpaceEnd = (ushort)payloadOffset; header.WriteTo(pageBuffer); @@ -1971,7 +1678,7 @@ public sealed partial class StorageEngine { for (ushort i = 0; i < header.SlotCount; i++) { - var slotOffset = SlottedPageHeader.Size + (i * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + i * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.Slice(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return i; @@ -1992,20 +1699,15 @@ public sealed partial class StorageEngine foreach (var entry in sourceEntries) { ct.ThrowIfCancellationRequested(); - var key = entry.Value.ToLowerInvariant(); - var id = entry.Key; + string key = entry.Value.ToLowerInvariant(); + ushort id = entry.Key; if (!target.InsertDictionaryEntryGlobal(key, id)) - { throw new InvalidOperationException( $"Compaction rebuild failed: unable to copy dictionary entry '{key}' with id {id}."); - } target._dictionaryCache[key] = id; target._dictionaryReverseCache[id] = key; - if (id > maxId) - { - maxId = id; - } + if (id > maxId) maxId = id; } target._nextDictionaryId = (ushort)(maxId + 1); @@ -2014,18 +1716,15 @@ public sealed partial class StorageEngine private static void ReinitializeDictionaryForCompaction(StorageEngine target, CancellationToken ct) { var chainPages = EnumerateDictionaryChainPages(target, target._dictionaryRootPageId, ct); - foreach (var pageId in chainPages) - { - target.FreePage(pageId); - } + foreach (uint pageId in chainPages) target.FreePage(pageId); - var newRootPageId = target.AllocatePage(); + uint newRootPageId = target.AllocatePage(); var dictionaryRootBuffer = new byte[target.PageSize]; DictionaryPage.Initialize(dictionaryRootBuffer, newRootPageId); target.WritePageImmediate(newRootPageId, dictionaryRootBuffer); var fileHeaderBuffer = new byte[target.PageSize]; - target.ReadPage(0, transactionId: null, fileHeaderBuffer); + target.ReadPage(0, null, fileHeaderBuffer); var fileHeader = PageHeader.ReadFrom(fileHeaderBuffer); fileHeader.DictionaryRootPageId = newRootPageId; fileHeader.WriteTo(fileHeaderBuffer); @@ -2036,7 +1735,8 @@ public sealed partial class StorageEngine target._dictionaryRootPageId = newRootPageId; } - private static IReadOnlyList EnumerateDictionaryChainPages(StorageEngine target, uint rootPageId, CancellationToken ct) + private static IReadOnlyList EnumerateDictionaryChainPages(StorageEngine target, uint rootPageId, + CancellationToken ct) { var result = new List(); if (rootPageId == 0) @@ -2044,7 +1744,7 @@ public sealed partial class StorageEngine var visited = new HashSet(); var pageBuffer = new byte[target.PageSize]; - var current = rootPageId; + uint current = rootPageId; while (current != 0 && visited.Add(current)) { @@ -2082,7 +1782,7 @@ public sealed partial class StorageEngine try { - var json = File.ReadAllText(markerPath); + string json = File.ReadAllText(markerPath); markerState = JsonSerializer.Deserialize(json, CompactionMarkerSerializerOptions); if (markerState == null) @@ -2129,60 +1829,48 @@ public sealed partial class StorageEngine private static void WriteCompactionMarker(string markerPath, CompactionMarkerState markerState) { markerState.LastUpdatedUtc = DateTimeOffset.UtcNow; - var json = JsonSerializer.Serialize(markerState, CompactionMarkerSerializerOptions); + string json = JsonSerializer.Serialize(markerState, CompactionMarkerSerializerOptions); - var directory = Path.GetDirectoryName(markerPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } + string? directory = Path.GetDirectoryName(markerPath); + if (!string.IsNullOrWhiteSpace(directory)) Directory.CreateDirectory(directory); using var stream = new FileStream( markerPath, FileMode.Create, FileAccess.Write, FileShare.None, - bufferSize: 4096, + 4096, FileOptions.WriteThrough); - using var writer = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); + using var writer = new StreamWriter(stream, new UTF8Encoding(false)); writer.Write(json); writer.Flush(); - stream.Flush(flushToDisk: true); + stream.Flush(true); } private static void DeleteCompactionMarkerIfExists(string markerPath) { - if (File.Exists(markerPath)) - { - File.Delete(markerPath); - } + if (File.Exists(markerPath)) File.Delete(markerPath); } private void EnsureNoActiveTransactions() { - foreach (var transactionId in _activeTransactions.Keys) - { + foreach (ulong transactionId in _activeTransactions.Keys) if (_walCache.TryGetValue(transactionId, out var pendingPages) && !pendingPages.IsEmpty) - { throw new InvalidOperationException("Compaction requires no active write transactions."); - } - } } private void TryRunStartupMaintenance() { - if (!_maintenanceOptions.RunAtStartup) + if (!MaintenanceOptions.RunAtStartup) return; try { var snapshot = CaptureCompactionSnapshot(); - if (snapshot.FragmentationPercent < _maintenanceOptions.MinFragmentationPercent && - snapshot.TailReclaimablePages * (long)_pageFile.PageSize < _maintenanceOptions.MinReclaimableBytes) - { + if (snapshot.FragmentationPercent < MaintenanceOptions.MinFragmentationPercent && + snapshot.TailReclaimablePages * _pageFile.PageSize < MaintenanceOptions.MinReclaimableBytes) return; - } var options = new CompactionOptions { @@ -2191,13 +1879,13 @@ public sealed partial class StorageEngine NormalizeFreeList = true, EnableTailTruncation = true, MinimumRetainedPages = 2, - OnlineBatchPageLimit = Math.Max(1, _maintenanceOptions.OnlineThrottlePagesPerBatch), - OnlineBatchDelay = _maintenanceOptions.OnlineThrottleDelay < TimeSpan.Zero + OnlineBatchPageLimit = Math.Max(1, MaintenanceOptions.OnlineThrottlePagesPerBatch), + OnlineBatchDelay = MaintenanceOptions.OnlineThrottleDelay < TimeSpan.Zero ? TimeSpan.Zero - : _maintenanceOptions.OnlineThrottleDelay, - MaxOnlineDuration = _maintenanceOptions.MaxRunDuration <= TimeSpan.Zero + : MaintenanceOptions.OnlineThrottleDelay, + MaxOnlineDuration = MaintenanceOptions.MaxRunDuration <= TimeSpan.Zero ? TimeSpan.FromSeconds(1) - : _maintenanceOptions.MaxRunDuration + : MaintenanceOptions.MaxRunDuration }; _ = RunOnlineCompactionPass(options); @@ -2207,4 +1895,213 @@ public sealed partial class StorageEngine // Startup maintenance is best-effort only. } } -} + + private enum CompactionMarkerPhase + { + Started = 1, + Copied = 2, + Swapped = 3, + CleanupDone = 4 + } + + private sealed class CompactionMarkerState + { + /// + /// Gets or sets the compaction marker schema version. + /// + public int Version { get; set; } = 1; + + /// + /// Gets or sets the current compaction marker phase. + /// + public CompactionMarkerPhase Phase { get; set; } + + /// + /// Gets or sets the primary database path. + /// + public string DatabasePath { get; set; } = string.Empty; + + /// + /// Gets or sets the temporary database path. + /// + public string TempDatabasePath { get; set; } = string.Empty; + + /// + /// Gets or sets the backup database path. + /// + public string BackupDatabasePath { get; set; } = string.Empty; + + /// + /// Gets or sets the compaction start timestamp in UTC. + /// + public DateTimeOffset StartedAtUtc { get; set; } + + /// + /// Gets or sets the last marker update timestamp in UTC. + /// + public DateTimeOffset LastUpdatedUtc { get; set; } + + /// + /// Gets or sets a value indicating whether online mode was used. + /// + public bool OnlineMode { get; set; } + + /// + /// Gets or sets the compaction execution mode. + /// + public string Mode { get; set; } = "CopySwap"; + } + + private readonly struct CompactionSnapshot + { + /// + /// Initializes a new instance of the struct. + /// + /// The file size in bytes. + /// The total page count. + /// The free page count. + /// The total estimated free bytes. + /// The estimated fragmentation percentage. + /// The reclaimable tail page count. + public CompactionSnapshot( + long fileSizeBytes, + uint pageCount, + int freePageCount, + long totalFreeBytes, + double fragmentationPercent, + uint tailReclaimablePages) + { + FileSizeBytes = fileSizeBytes; + PageCount = pageCount; + FreePageCount = freePageCount; + TotalFreeBytes = totalFreeBytes; + FragmentationPercent = fragmentationPercent; + TailReclaimablePages = tailReclaimablePages; + } + + /// + /// Gets the file size in bytes. + /// + public long FileSizeBytes { get; } + + /// + /// Gets the total page count. + /// + public uint PageCount { get; } + + /// + /// Gets the free page count. + /// + public int FreePageCount { get; } + + /// + /// Gets the estimated total free bytes. + /// + public long TotalFreeBytes { get; } + + /// + /// Gets the estimated fragmentation percentage. + /// + public double FragmentationPercent { get; } + + /// + /// Gets the number of reclaimable tail pages. + /// + public uint TailReclaimablePages { get; } + } + + private struct CompactionWork + { + public int PagesScanned; + public int PagesDefragmented; + public long BytesReclaimedByDefragmentation; + public int FreeListPagesNormalized; + public long DocumentsRelocated; + public int PagesRelocated; + } + + private readonly struct CompactionCollectionRebuildResult + { + /// + /// Initializes a new instance of the struct. + /// + /// The rebuilt collection metadata. + /// The number of relocated documents. + /// The relocated source page identifiers. + public CompactionCollectionRebuildResult( + CollectionMetadata metadata, + long documentsRelocated, + IReadOnlyCollection relocatedSourcePageIds) + { + Metadata = metadata; + DocumentsRelocated = documentsRelocated; + RelocatedSourcePageIds = relocatedSourcePageIds; + } + + /// + /// Gets rebuilt collection metadata. + /// + public CollectionMetadata Metadata { get; } + + /// + /// Gets the number of relocated documents. + /// + public long DocumentsRelocated { get; } + + /// + /// Gets relocated source page identifiers. + /// + public IReadOnlyCollection RelocatedSourcePageIds { get; } + } + + private sealed class CompactionDataWriterState + { + /// + /// Gets free space tracked by page identifier. + /// + public Dictionary FreeSpaceByPage { get; } = new(); + + /// + /// Gets or sets the current data page identifier. + /// + public uint CurrentDataPageId { get; set; } + } + + private readonly struct CompactionVectorNode + { + /// + /// Initializes a new instance of the struct. + /// + /// The page identifier. + /// The node index within the page. + /// The mapped document location. + /// The vector payload. + public CompactionVectorNode(uint pageId, int nodeIndex, DocumentLocation location, float[] vector) + { + PageId = pageId; + NodeIndex = nodeIndex; + Location = location; + Vector = vector; + } + + /// + /// Gets the page identifier containing the vector node. + /// + public uint PageId { get; } + + /// + /// Gets the node index within the page. + /// + public int NodeIndex { get; } + + /// + /// Gets the document location associated with the node. + /// + public DocumentLocation Location { get; } + + /// + /// Gets the vector payload. + /// + public float[] Vector { get; } + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Memory.cs b/src/CBDD.Core/Storage/StorageEngine.Memory.cs index 70373c9..543d764 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Memory.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Memory.cs @@ -1,11 +1,9 @@ -using ZB.MOM.WW.CBDD.Core.Transactions; - namespace ZB.MOM.WW.CBDD.Core.Storage; public sealed partial class StorageEngine { /// - /// Allocates a new page. + /// Allocates a new page. /// /// Page ID of the allocated page public uint AllocatePage() @@ -14,11 +12,11 @@ public sealed partial class StorageEngine } /// - /// Frees a page. + /// Frees a page. /// /// Page to free public void FreePage(uint pageId) { _pageFile.FreePage(pageId); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Migration.cs b/src/CBDD.Core/Storage/StorageEngine.Migration.cs index fbbac15..847b384 100644 --- a/src/CBDD.Core/Storage/StorageEngine.Migration.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Migration.cs @@ -5,98 +5,98 @@ using ZB.MOM.WW.CBDD.Core.Compression; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Options controlling compression migration. +/// Options controlling compression migration. /// public sealed class CompressionMigrationOptions { /// - /// Enables dry-run estimation without mutating database contents. + /// Enables dry-run estimation without mutating database contents. /// public bool DryRun { get; init; } = true; /// - /// Target codec for migrated payloads. + /// Target codec for migrated payloads. /// public CompressionCodec Codec { get; init; } = CompressionCodec.Brotli; /// - /// Target compression level. + /// Target compression level. /// public CompressionLevel Level { get; init; } = CompressionLevel.Fastest; /// - /// Minimum logical payload size required before compression is attempted. + /// Minimum logical payload size required before compression is attempted. /// public int MinSizeBytes { get; init; } = 1024; /// - /// Minimum savings percent required to keep compressed output. + /// Minimum savings percent required to keep compressed output. /// public int MinSavingsPercent { get; init; } = 10; /// - /// Optional include-only collection list (case-insensitive). + /// Optional include-only collection list (case-insensitive). /// public IReadOnlyList? IncludeCollections { get; init; } /// - /// Optional exclusion collection list (case-insensitive). + /// Optional exclusion collection list (case-insensitive). /// public IReadOnlyList? ExcludeCollections { get; init; } } /// -/// Result of a compression migration run. +/// Result of a compression migration run. /// public sealed class CompressionMigrationResult { /// - /// Gets a value indicating whether this run was executed in dry-run mode. + /// Gets a value indicating whether this run was executed in dry-run mode. /// public bool DryRun { get; init; } /// - /// Gets the target codec used for migration output. + /// Gets the target codec used for migration output. /// public CompressionCodec Codec { get; init; } /// - /// Gets the target compression level used for migration output. + /// Gets the target compression level used for migration output. /// public CompressionLevel Level { get; init; } /// - /// Gets the number of collections processed. + /// Gets the number of collections processed. /// public int CollectionsProcessed { get; init; } /// - /// Gets the number of documents scanned. + /// Gets the number of documents scanned. /// public long DocumentsScanned { get; init; } /// - /// Gets the number of documents rewritten. + /// Gets the number of documents rewritten. /// public long DocumentsRewritten { get; init; } /// - /// Gets the number of documents skipped. + /// Gets the number of documents skipped. /// public long DocumentsSkipped { get; init; } /// - /// Gets the total logical bytes observed before migration decisions. + /// Gets the total logical bytes observed before migration decisions. /// public long BytesBefore { get; init; } /// - /// Gets the estimated total stored bytes after migration. + /// Gets the estimated total stored bytes after migration. /// public long BytesEstimatedAfter { get; init; } /// - /// Gets the actual total stored bytes after migration when not in dry-run mode. + /// Gets the actual total stored bytes after migration when not in dry-run mode. /// public long BytesActualAfter { get; init; } } @@ -104,7 +104,7 @@ public sealed class CompressionMigrationResult public sealed partial class StorageEngine { /// - /// Estimates or applies a one-time compression migration. + /// Estimates or applies a one-time compression migration. /// /// Optional compression migration options. public CompressionMigrationResult MigrateCompression(CompressionMigrationOptions? options = null) @@ -113,11 +113,12 @@ public sealed partial class StorageEngine } /// - /// Estimates or applies a one-time compression migration. + /// Estimates or applies a one-time compression migration. /// /// Optional compression migration options. /// A token used to cancel the operation. - public async Task MigrateCompressionAsync(CompressionMigrationOptions? options = null, CancellationToken ct = default) + public async Task MigrateCompressionAsync(CompressionMigrationOptions? options = null, + CancellationToken ct = default) { var normalized = NormalizeMigrationOptions(options); @@ -147,13 +148,13 @@ public sealed partial class StorageEngine { ct.ThrowIfCancellationRequested(); - if (!TryReadStoredPayload(location, out var storedPayload, out var isCompressed)) + if (!TryReadStoredPayload(location, out byte[] storedPayload, out bool isCompressed)) { docsSkipped++; continue; } - if (!TryGetLogicalPayload(storedPayload, isCompressed, out var logicalPayload)) + if (!TryGetLogicalPayload(storedPayload, isCompressed, out byte[] logicalPayload)) { docsSkipped++; continue; @@ -162,15 +163,14 @@ public sealed partial class StorageEngine docsScanned++; bytesBefore += logicalPayload.Length; - var targetStored = BuildTargetStoredPayload(logicalPayload, normalized, out var targetCompressed); + byte[] targetStored = + BuildTargetStoredPayload(logicalPayload, normalized, out bool targetCompressed); bytesEstimatedAfter += targetStored.Length; - if (normalized.DryRun) - { - continue; - } + if (normalized.DryRun) continue; - if (!TryRewriteStoredPayloadAtLocation(location, targetStored, targetCompressed, out var actualStoredBytes)) + if (!TryRewriteStoredPayloadAtLocation(location, targetStored, targetCompressed, + out int actualStoredBytes)) { docsSkipped++; continue; @@ -184,9 +184,9 @@ public sealed partial class StorageEngine if (!normalized.DryRun) { var metadata = StorageFormatMetadata.Present( - version: 1, - featureFlags: StorageFeatureFlags.CompressionCapability, - defaultCodec: normalized.Codec); + 1, + StorageFeatureFlags.CompressionCapability, + normalized.Codec); WriteStorageFormatMetadata(metadata); _pageFile.Flush(); } @@ -221,7 +221,8 @@ public sealed partial class StorageEngine var normalized = options ?? new CompressionMigrationOptions(); if (!Enum.IsDefined(normalized.Codec) || normalized.Codec == CompressionCodec.None) - throw new ArgumentOutOfRangeException(nameof(options), "Migration codec must be a supported non-None codec."); + throw new ArgumentOutOfRangeException(nameof(options), + "Migration codec must be a supported non-None codec."); if (normalized.MinSizeBytes < 0) throw new ArgumentOutOfRangeException(nameof(options), "MinSizeBytes must be non-negative."); @@ -250,7 +251,8 @@ public sealed partial class StorageEngine .ToList(); } - private byte[] BuildTargetStoredPayload(ReadOnlySpan logicalPayload, CompressionMigrationOptions options, out bool compressed) + private byte[] BuildTargetStoredPayload(ReadOnlySpan logicalPayload, CompressionMigrationOptions options, + out bool compressed) { compressed = false; @@ -259,10 +261,10 @@ public sealed partial class StorageEngine try { - var compressedPayload = _compressionService.Compress(logicalPayload, options.Codec, options.Level); - var storedLength = CompressedPayloadHeader.Size + compressedPayload.Length; - var savings = logicalPayload.Length - storedLength; - var savingsPercent = logicalPayload.Length == 0 ? 0 : (int)((savings * 100L) / logicalPayload.Length); + byte[] compressedPayload = CompressionService.Compress(logicalPayload, options.Codec, options.Level); + int storedLength = CompressedPayloadHeader.Size + compressedPayload.Length; + int savings = logicalPayload.Length - storedLength; + int savingsPercent = logicalPayload.Length == 0 ? 0 : (int)(savings * 100L / logicalPayload.Length); if (savings <= 0 || savingsPercent < options.MinSavingsPercent) return logicalPayload.ToArray(); @@ -308,11 +310,11 @@ public sealed partial class StorageEngine try { - logicalPayload = _compressionService.Decompress( + logicalPayload = CompressionService.Decompress( compressedPayload, header.Codec, header.OriginalLength, - Math.Max(header.OriginalLength, _compressionOptions.MaxDecompressedSizeBytes)); + Math.Max(header.OriginalLength, CompressionOptions.MaxDecompressedSizeBytes)); return true; } catch @@ -336,13 +338,13 @@ public sealed partial class StorageEngine if (location.SlotIndex >= header.SlotCount) return false; - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return false; isCompressed = (slot.Flags & SlotFlags.Compressed) != 0; - var hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; + bool hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; if (!hasOverflow) { @@ -354,14 +356,14 @@ public sealed partial class StorageEngine return false; var primaryPayload = pageBuffer.AsSpan(slot.Offset, slot.Length); - var totalStoredLength = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); - var nextOverflow = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); + int totalStoredLength = BinaryPrimitives.ReadInt32LittleEndian(primaryPayload.Slice(0, 4)); + uint nextOverflow = BinaryPrimitives.ReadUInt32LittleEndian(primaryPayload.Slice(4, 4)); if (totalStoredLength < 0) return false; var output = new byte[totalStoredLength]; var primaryChunk = primaryPayload.Slice(8); - var copied = Math.Min(primaryChunk.Length, output.Length); + int copied = Math.Min(primaryChunk.Length, output.Length); primaryChunk.Slice(0, copied).CopyTo(output); var overflowBuffer = new byte[_pageFile.PageSize]; @@ -372,7 +374,7 @@ public sealed partial class StorageEngine if (overflowHeader.PageType != PageType.Overflow) return false; - var chunk = Math.Min(output.Length - copied, _pageFile.PageSize - SlottedPageHeader.Size); + int chunk = Math.Min(output.Length - copied, _pageFile.PageSize - SlottedPageHeader.Size); overflowBuffer.AsSpan(SlottedPageHeader.Size, chunk).CopyTo(output.AsSpan(copied)); copied += chunk; nextOverflow = overflowHeader.NextOverflowPage; @@ -403,12 +405,12 @@ public sealed partial class StorageEngine if (location.SlotIndex >= pageHeader.SlotCount) return false; - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) return false; - var oldHasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; + bool oldHasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; uint oldOverflowHead = 0; if (oldHasOverflow) { @@ -442,12 +444,12 @@ public sealed partial class StorageEngine if (slot.Length < 8) return false; - var primaryChunkSize = slot.Length - 8; + int primaryChunkSize = slot.Length - 8; if (primaryChunkSize < 0) return false; var remainder = newStoredPayload.Slice(primaryChunkSize); - var newOverflowHead = BuildOverflowChainForMigration(remainder); + uint newOverflowHead = BuildOverflowChainForMigration(remainder); var slotPayload = pageBuffer.AsSpan(slot.Offset, slot.Length); slotPayload.Clear(); @@ -475,22 +477,22 @@ public sealed partial class StorageEngine if (overflowPayload.IsEmpty) return 0; - var chunkSize = _pageFile.PageSize - SlottedPageHeader.Size; + int chunkSize = _pageFile.PageSize - SlottedPageHeader.Size; uint nextOverflowPageId = 0; - var tailSize = overflowPayload.Length % chunkSize; - var fullPages = overflowPayload.Length / chunkSize; + int tailSize = overflowPayload.Length % chunkSize; + int fullPages = overflowPayload.Length / chunkSize; if (tailSize > 0) { - var tailOffset = fullPages * chunkSize; + int tailOffset = fullPages * chunkSize; var tailSlice = overflowPayload.Slice(tailOffset, tailSize); nextOverflowPageId = AllocateOverflowPageForMigration(tailSlice, nextOverflowPageId); } - for (var i = fullPages - 1; i >= 0; i--) + for (int i = fullPages - 1; i >= 0; i--) { - var chunkOffset = i * chunkSize; + int chunkOffset = i * chunkSize; var chunk = overflowPayload.Slice(chunkOffset, chunkSize); nextOverflowPageId = AllocateOverflowPageForMigration(chunk, nextOverflowPageId); } @@ -500,7 +502,7 @@ public sealed partial class StorageEngine private uint AllocateOverflowPageForMigration(ReadOnlySpan payloadChunk, uint nextOverflowPageId) { - var pageId = _pageFile.AllocatePage(); + uint pageId = _pageFile.AllocatePage(); var buffer = new byte[_pageFile.PageSize]; var header = new SlottedPageHeader @@ -524,15 +526,15 @@ public sealed partial class StorageEngine { var buffer = new byte[_pageFile.PageSize]; var visited = new HashSet(); - var current = firstOverflowPage; + uint current = firstOverflowPage; while (current != 0 && current < _pageFile.NextPageId && visited.Add(current)) { _pageFile.ReadPage(current, buffer); var header = SlottedPageHeader.ReadFrom(buffer); - var next = header.NextOverflowPage; + uint next = header.NextOverflowPage; _pageFile.FreePage(current); current = next; } } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Pages.cs b/src/CBDD.Core/Storage/StorageEngine.Pages.cs index d06305d..9f7f682 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Pages.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Pages.cs @@ -1,14 +1,14 @@ -using ZB.MOM.WW.CBDD.Core.Transactions; +using System.Collections.Concurrent; namespace ZB.MOM.WW.CBDD.Core.Storage; public sealed partial class StorageEngine { /// - /// Reads a page with transaction isolation. - /// 1. Check WAL cache for uncommitted writes (Read Your Own Writes) - /// 2. Check WAL index for committed writes (lazy replay) - /// 3. Read from PageFile (committed baseline) + /// Reads a page with transaction isolation. + /// 1. Check WAL cache for uncommitted writes (Read Your Own Writes) + /// 2. Check WAL index for committed writes (lazy replay) + /// 3. Read from PageFile (committed baseline) /// /// Page to read /// Optional transaction ID for isolation @@ -17,32 +17,32 @@ public sealed partial class StorageEngine { // 1. Check transaction-local WAL cache (Read Your Own Writes) // transactionId=0 or null means "no active transaction, read committed only" - if (transactionId.HasValue && + if (transactionId.HasValue && transactionId.Value != 0 && _walCache.TryGetValue(transactionId.Value, out var txnPages) && - txnPages.TryGetValue(pageId, out var uncommittedData)) + txnPages.TryGetValue(pageId, out byte[]? uncommittedData)) { - var length = Math.Min(uncommittedData.Length, destination.Length); + int length = Math.Min(uncommittedData.Length, destination.Length); uncommittedData.AsSpan(0, length).CopyTo(destination); return; - } - - // 2. Check WAL index (committed but not checkpointed) - if (_walIndex.TryGetValue(pageId, out var committedData)) + } + + // 2. Check WAL index (committed but not checkpointed) + if (_walIndex.TryGetValue(pageId, out byte[]? committedData)) { - var length = Math.Min(committedData.Length, destination.Length); + int length = Math.Min(committedData.Length, destination.Length); committedData.AsSpan(0, length).CopyTo(destination); return; - } - - // 3. Read committed baseline from PageFile + } + + // 3. Read committed baseline from PageFile _pageFile.ReadPage(pageId, destination); } /// - /// Writes a page within a transaction. - /// Data goes to WAL cache immediately and becomes visible to that transaction only. - /// Will be written to WAL on commit. + /// Writes a page within a transaction. + /// Data goes to WAL cache immediately and becomes visible to that transaction only. + /// Will be written to WAL on commit. /// /// Page to write /// Transaction ID owning this write @@ -50,20 +50,20 @@ public sealed partial class StorageEngine public void WritePage(uint pageId, ulong transactionId, ReadOnlySpan data) { if (transactionId == 0) - throw new InvalidOperationException("Cannot write without a transaction (transactionId=0 is reserved)"); - - // Get or create transaction-local cache - var txnPages = _walCache.GetOrAdd(transactionId, - _ => new System.Collections.Concurrent.ConcurrentDictionary()); - - // Store defensive copy - var copy = data.ToArray(); + throw new InvalidOperationException("Cannot write without a transaction (transactionId=0 is reserved)"); + + // Get or create transaction-local cache + var txnPages = _walCache.GetOrAdd(transactionId, + _ => new ConcurrentDictionary()); + + // Store defensive copy + byte[] copy = data.ToArray(); txnPages[pageId] = copy; } /// - /// Writes a page immediately to disk (non-transactional). - /// Used for initialization and metadata updates outside of transactions. + /// Writes a page immediately to disk (non-transactional). + /// Used for initialization and metadata updates outside of transactions. /// /// Page to write /// Page data @@ -73,8 +73,8 @@ public sealed partial class StorageEngine } /// - /// Gets the number of pages currently allocated in the page file. - /// Useful for full database scans. + /// Gets the number of pages currently allocated in the page file. + /// Useful for full database scans. /// public uint PageCount => _pageFile.NextPageId; -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Recovery.cs b/src/CBDD.Core/Storage/StorageEngine.Recovery.cs index ba3a7d2..c4f6d1d 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Recovery.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Recovery.cs @@ -1,36 +1,36 @@ -using ZB.MOM.WW.CBDD.Core.Transactions; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - +using ZB.MOM.WW.CBDD.Core.Transactions; + +namespace ZB.MOM.WW.CBDD.Core.Storage; + public sealed partial class StorageEngine { - /// - /// Gets the current size of the WAL file. - /// - public long GetWalSize() - { - return _wal.GetCurrentSize(); - } - - /// - /// Truncates the WAL file. - /// Should only be called after a successful checkpoint. - /// - public void TruncateWal() - { - _wal.Truncate(); - } - - /// - /// Flushes the WAL to disk. - /// - public void FlushWal() - { - _wal.Flush(); + /// + /// Gets the current size of the WAL file. + /// + public long GetWalSize() + { + return _wal.GetCurrentSize(); } /// - /// Performs a truncate checkpoint by default. + /// Truncates the WAL file. + /// Should only be called after a successful checkpoint. + /// + public void TruncateWal() + { + _wal.Truncate(); + } + + /// + /// Flushes the WAL to disk. + /// + public void FlushWal() + { + _wal.Flush(); + } + + /// + /// Performs a truncate checkpoint by default. /// public void Checkpoint() { @@ -38,7 +38,7 @@ public sealed partial class StorageEngine } /// - /// Performs a checkpoint using the requested mode. + /// Performs a checkpoint using the requested mode. /// /// Checkpoint mode to execute. /// The checkpoint execution result. @@ -50,7 +50,7 @@ public sealed partial class StorageEngine lockAcquired = _commitLock.Wait(0); if (!lockAcquired) { - var walSize = _wal.GetCurrentSize(); + long walSize = _wal.GetCurrentSize(); return new CheckpointResult(mode, false, 0, walSize, walSize, false, false); } } @@ -66,19 +66,18 @@ public sealed partial class StorageEngine } finally { - if (lockAcquired) - { - _commitLock.Release(); - } + if (lockAcquired) _commitLock.Release(); } } private void CheckpointInternal() - => _ = CheckpointInternal(CheckpointMode.Truncate); + { + _ = CheckpointInternal(CheckpointMode.Truncate); + } private CheckpointResult CheckpointInternal(CheckpointMode mode) { - var walBytesBefore = _wal.GetCurrentSize(); + long walBytesBefore = _wal.GetCurrentSize(); var appliedPages = 0; var truncated = false; var restarted = false; @@ -91,10 +90,7 @@ public sealed partial class StorageEngine } // 2. Flush PageFile to ensure durability. - if (appliedPages > 0) - { - _pageFile.Flush(); - } + if (appliedPages > 0) _pageFile.Flush(); // 3. Clear in-memory WAL index (now persisted). _walIndex.Clear(); @@ -109,6 +105,7 @@ public sealed partial class StorageEngine _wal.WriteCheckpointRecord(); _wal.Flush(); } + break; case CheckpointMode.Truncate: if (walBytesBefore > 0) @@ -116,6 +113,7 @@ public sealed partial class StorageEngine _wal.Truncate(); truncated = true; } + break; case CheckpointMode.Restart: _wal.Restart(); @@ -126,12 +124,12 @@ public sealed partial class StorageEngine throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported checkpoint mode."); } - var walBytesAfter = _wal.GetCurrentSize(); + long walBytesAfter = _wal.GetCurrentSize(); return new CheckpointResult(mode, true, appliedPages, walBytesBefore, walBytesAfter, truncated, restarted); } /// - /// Performs a truncate checkpoint asynchronously by default. + /// Performs a truncate checkpoint asynchronously by default. /// /// The cancellation token. public async Task CheckpointAsync(CancellationToken ct = default) @@ -140,7 +138,7 @@ public sealed partial class StorageEngine } /// - /// Performs a checkpoint asynchronously using the requested mode. + /// Performs a checkpoint asynchronously using the requested mode. /// /// Checkpoint mode to execute. /// The cancellation token. @@ -153,7 +151,7 @@ public sealed partial class StorageEngine lockAcquired = await _commitLock.WaitAsync(0, ct); if (!lockAcquired) { - var walSize = _wal.GetCurrentSize(); + long walSize = _wal.GetCurrentSize(); return new CheckpointResult(mode, false, 0, walSize, walSize, false, false); } } @@ -170,16 +168,13 @@ public sealed partial class StorageEngine } finally { - if (lockAcquired) - { - _commitLock.Release(); - } + if (lockAcquired) _commitLock.Release(); } } /// - /// Recovers from crash by replaying WAL. - /// Applies committed transactions to PageFile in deterministic WAL order, then truncates WAL. + /// Recovers from crash by replaying WAL. + /// Applies committed transactions to PageFile in deterministic WAL order, then truncates WAL. /// public void Recover() { @@ -189,35 +184,28 @@ public sealed partial class StorageEngine // 1. Read WAL and locate the latest checkpoint boundary. var records = _wal.ReadAll(); var startIndex = 0; - for (var i = records.Count - 1; i >= 0; i--) - { + for (int i = records.Count - 1; i >= 0; i--) if (records[i].Type == WalRecordType.Checkpoint) { startIndex = i + 1; break; } - } // 2. Replay WAL in source order with deterministic commit application. var pendingWrites = new Dictionary>(); var appliedAny = false; - for (var i = startIndex; i < records.Count; i++) + for (int i = startIndex; i < records.Count; i++) { var record = records[i]; switch (record.Type) { case WalRecordType.Begin: if (!pendingWrites.ContainsKey(record.TransactionId)) - { pendingWrites[record.TransactionId] = new List<(uint, byte[])>(); - } break; case WalRecordType.Write: - if (record.AfterImage == null) - { - break; - } + if (record.AfterImage == null) break; if (!pendingWrites.TryGetValue(record.TransactionId, out var writes)) { @@ -228,12 +216,9 @@ public sealed partial class StorageEngine writes.Add((record.PageId, record.AfterImage)); break; case WalRecordType.Commit: - if (!pendingWrites.TryGetValue(record.TransactionId, out var committedWrites)) - { - break; - } + if (!pendingWrites.TryGetValue(record.TransactionId, out var committedWrites)) break; - foreach (var (pageId, data) in committedWrites) + foreach ((uint pageId, byte[] data) in committedWrites) { _pageFile.WritePage(pageId, data); appliedAny = true; @@ -251,23 +236,17 @@ public sealed partial class StorageEngine } // 3. Flush PageFile to ensure durability. - if (appliedAny) - { - _pageFile.Flush(); - } + if (appliedAny) _pageFile.Flush(); // 4. Clear in-memory WAL index (redundant since we just recovered). _walIndex.Clear(); // 5. Truncate WAL (all changes now in PageFile). - if (_wal.GetCurrentSize() > 0) - { - _wal.Truncate(); - } + if (_wal.GetCurrentSize() > 0) _wal.Truncate(); } finally { - _commitLock.Release(); - } - } -} + _commitLock.Release(); + } + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Schema.cs b/src/CBDD.Core/Storage/StorageEngine.Schema.cs index 5685c91..d1b0322 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Schema.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Schema.cs @@ -1,30 +1,28 @@ -using System; -using System.Collections.Generic; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Bson.Schema; namespace ZB.MOM.WW.CBDD.Core.Storage; -public sealed partial class StorageEngine -{ - /// - /// Reads all schemas from the schema page chain. - /// - /// The root page identifier of the schema chain. - /// The list of schemas in chain order. - public List GetSchemas(uint rootPageId) +public sealed partial class StorageEngine +{ + /// + /// Reads all schemas from the schema page chain. + /// + /// The root page identifier of the schema chain. + /// The list of schemas in chain order. + public List GetSchemas(uint rootPageId) { var schemas = new List(); if (rootPageId == 0) return schemas; - var pageId = rootPageId; + uint pageId = rootPageId; var buffer = new byte[PageSize]; while (pageId != 0) { ReadPage(pageId, null, buffer); - var header = PageHeader.ReadFrom(buffer); - + var header = PageHeader.ReadFrom(buffer); + if (header.PageType != PageType.Schema) break; int used = PageSize - 32 - header.FreeBytes; @@ -33,7 +31,7 @@ public sealed partial class StorageEngine var reader = new BsonSpanReader(buffer.AsSpan(32, used), GetKeyReverseMap()); while (reader.Remaining >= 4) { - var docSize = reader.PeekInt32(); + int docSize = reader.PeekInt32(); if (docSize <= 0 || docSize > reader.Remaining) break; var schema = BsonSchema.FromBson(ref reader); @@ -47,27 +45,27 @@ public sealed partial class StorageEngine return schemas; } - /// - /// Appends a new schema version. Returns the root page ID (which might be new if it was 0 initially). - /// - /// The root page identifier of the schema chain. - /// The schema to append. - public uint AppendSchema(uint rootPageId, BsonSchema schema) + /// + /// Appends a new schema version. Returns the root page ID (which might be new if it was 0 initially). + /// + /// The root page identifier of the schema chain. + /// The schema to append. + public uint AppendSchema(uint rootPageId, BsonSchema schema) { - var buffer = new byte[PageSize]; - - // Serialize schema to temporary buffer to calculate size + var buffer = new byte[PageSize]; + + // Serialize schema to temporary buffer to calculate size var tempBuffer = new byte[PageSize]; var tempWriter = new BsonSpanWriter(tempBuffer, GetKeyMap()); schema.ToBson(ref tempWriter); - var schemaSize = tempWriter.Position; + int schemaSize = tempWriter.Position; if (rootPageId == 0) { rootPageId = AllocatePage(); InitializeSchemaPage(buffer, rootPageId); - tempBuffer.AsSpan(0, schemaSize).CopyTo(buffer.AsSpan(32)); - + tempBuffer.AsSpan(0, schemaSize).CopyTo(buffer.AsSpan(32)); + var header = PageHeader.ReadFrom(buffer); header.FreeBytes = (ushort)(PageSize - 32 - schemaSize); header.WriteTo(buffer); @@ -91,13 +89,13 @@ public sealed partial class StorageEngine // Buffer now contains the last page var lastHeader = PageHeader.ReadFrom(buffer); int currentUsed = PageSize - 32 - lastHeader.FreeBytes; - int lastOffset = 32 + currentUsed; - + int lastOffset = 32 + currentUsed; + if (lastHeader.FreeBytes >= schemaSize) { // Fits in current page - tempBuffer.AsSpan(0, schemaSize).CopyTo(buffer.AsSpan(lastOffset)); - + tempBuffer.AsSpan(0, schemaSize).CopyTo(buffer.AsSpan(lastOffset)); + lastHeader.FreeBytes -= (ushort)schemaSize; lastHeader.WriteTo(buffer); @@ -106,14 +104,14 @@ public sealed partial class StorageEngine else { // Allocate new page - var newPageId = AllocatePage(); + uint newPageId = AllocatePage(); lastHeader.NextPageId = newPageId; lastHeader.WriteTo(buffer); WritePageImmediate(lastPageId, buffer); InitializeSchemaPage(buffer, newPageId); - tempBuffer.AsSpan(0, schemaSize).CopyTo(buffer.AsSpan(32)); - + tempBuffer.AsSpan(0, schemaSize).CopyTo(buffer.AsSpan(32)); + var newHeader = PageHeader.ReadFrom(buffer); newHeader.FreeBytes = (ushort)(PageSize - 32 - schemaSize); newHeader.WriteTo(buffer); @@ -145,4 +143,4 @@ public sealed partial class StorageEngine var doc = reader.RemainingBytes(); doc.CopyTo(page.Slice(32)); } -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.Transactions.cs b/src/CBDD.Core/Storage/StorageEngine.Transactions.cs index 27d4c2f..ceaeb7c 100755 --- a/src/CBDD.Core/Storage/StorageEngine.Transactions.cs +++ b/src/CBDD.Core/Storage/StorageEngine.Transactions.cs @@ -1,194 +1,75 @@ -using ZB.MOM.WW.CBDD.Core.Transactions; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - +using ZB.MOM.WW.CBDD.Core.Transactions; + +namespace ZB.MOM.WW.CBDD.Core.Storage; + public sealed partial class StorageEngine { - #region Transaction Management + /// + /// Gets the number of active transactions (diagnostics). + /// + public int ActiveTransactionCount => _walCache.Count; /// - /// Begins a new transaction. + /// Prepares a transaction: writes all changes to WAL but doesn't commit yet. + /// Part of 2-Phase Commit protocol. /// - /// The transaction isolation level. - /// The started transaction. - public Transaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) + /// Transaction ID + /// All writes to record in WAL + /// True if preparation succeeded + public bool PrepareTransaction(ulong transactionId) { - _commitLock.Wait(); - try - { - var txnId = _nextTransactionId++; - var transaction = new Transaction(txnId, this, isolationLevel); - _activeTransactions[txnId] = transaction; - return transaction; - } - finally - { - _commitLock.Release(); - } - } - - /// - /// Begins a new transaction asynchronously. - /// - /// The transaction isolation level. - /// The cancellation token. - /// The started transaction. - public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, CancellationToken ct = default) - { - await _commitLock.WaitAsync(ct); - try - { - var txnId = _nextTransactionId++; - var transaction = new Transaction(txnId, this, isolationLevel); - _activeTransactions[txnId] = transaction; - return transaction; - } - finally - { - _commitLock.Release(); - } - } - - /// - /// Commits the specified transaction. - /// - /// The transaction to commit. - public void CommitTransaction(Transaction transaction) - { - _commitLock.Wait(); try { - if (!_activeTransactions.ContainsKey(transaction.TransactionId)) - throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not active."); - - // 1. Prepare (Write to WAL) - // In a fuller 2PC, this would be separate. Here we do it as part of commit. - if (!PrepareTransaction(transaction.TransactionId)) - throw new IOException("Failed to write transaction to WAL"); + _wal.WriteBeginRecord(transactionId); - // 2. Commit (Write commit record, flush, move to cache) - // Use core commit path to avoid re-entering _commitLock. - CommitTransactionCore(transaction.TransactionId); + foreach (var walEntry in _walCache[transactionId]) + _wal.WriteDataRecord(transactionId, walEntry.Key, walEntry.Value); - _activeTransactions.TryRemove(transaction.TransactionId, out _); + _wal.Flush(); // Ensure WAL is persisted + return true; } - finally + catch { - _commitLock.Release(); - } - } - - /// - /// Commits the specified transaction asynchronously. - /// - /// The transaction to commit. - /// The cancellation token. - public async Task CommitTransactionAsync(Transaction transaction, CancellationToken ct = default) - { - await _commitLock.WaitAsync(ct); - try - { - if (!_activeTransactions.ContainsKey(transaction.TransactionId)) - throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not active."); - - // 1. Prepare (Write to WAL) - if (!await PrepareTransactionAsync(transaction.TransactionId, ct)) - throw new IOException("Failed to write transaction to WAL"); - - // 2. Commit (Write commit record, flush, move to cache) - // Use core commit path to avoid re-entering _commitLock. - await CommitTransactionCoreAsync(transaction.TransactionId, ct); - - _activeTransactions.TryRemove(transaction.TransactionId, out _); - } - finally - { - _commitLock.Release(); - } - } - - /// - /// Rolls back the specified transaction. - /// - /// The transaction to roll back. - public void RollbackTransaction(Transaction transaction) - { - RollbackTransaction(transaction.TransactionId); - _activeTransactions.TryRemove(transaction.TransactionId, out _); - } - - // Rollback doesn't usually require async logic unless logging abort record is async, - // but for consistency we might consider it. For now, sync is fine as it's not the happy path bottleneck. - - #endregion - - /// - /// Prepares a transaction: writes all changes to WAL but doesn't commit yet. - /// Part of 2-Phase Commit protocol. - /// - /// Transaction ID - /// All writes to record in WAL - /// True if preparation succeeded - public bool PrepareTransaction(ulong transactionId) - { - try - { - _wal.WriteBeginRecord(transactionId); - - foreach (var walEntry in _walCache[transactionId]) - { - _wal.WriteDataRecord(transactionId, walEntry.Key, walEntry.Value); - } - - _wal.Flush(); // Ensure WAL is persisted - return true; - } - catch - { - // TODO: Log error? + // TODO: Log error? return false; } } /// - /// Prepares a transaction asynchronously by writing pending changes to the WAL. + /// Prepares a transaction asynchronously by writing pending changes to the WAL. /// /// The transaction identifier. /// The cancellation token. - /// if preparation succeeds; otherwise, . + /// if preparation succeeds; otherwise, . public async Task PrepareTransactionAsync(ulong transactionId, CancellationToken ct = default) { try { - await _wal.WriteBeginRecordAsync(transactionId, ct); - - if (_walCache.TryGetValue(transactionId, out var changes)) - { - foreach (var walEntry in changes) - { - await _wal.WriteDataRecordAsync(transactionId, walEntry.Key, walEntry.Value, ct); - } - } + await _wal.WriteBeginRecordAsync(transactionId, ct); + + if (_walCache.TryGetValue(transactionId, out var changes)) + foreach (var walEntry in changes) + await _wal.WriteDataRecordAsync(transactionId, walEntry.Key, walEntry.Value, ct); + + await _wal.FlushAsync(ct); // Ensure WAL is persisted + return true; + } + catch + { + return false; + } + } - await _wal.FlushAsync(ct); // Ensure WAL is persisted - return true; - } - catch - { - return false; - } - } - /// - /// Commits a transaction: - /// 1. Writes all changes to WAL (for durability) - /// 2. Writes commit record - /// 3. Flushes WAL to disk - /// 4. Moves pages from cache to WAL index (for future reads) - /// 5. Clears WAL cache - /// - /// Transaction to commit - /// All writes performed in this transaction (unused, kept for compatibility) + /// Commits a transaction: + /// 1. Writes all changes to WAL (for durability) + /// 2. Writes commit record + /// 3. Flushes WAL to disk + /// 4. Moves pages from cache to WAL index (for future reads) + /// 5. Clears WAL cache + /// + /// Transaction to commit + /// All writes performed in this transaction (unused, kept for compatibility) public void CommitTransaction(ulong transactionId) { _commitLock.Wait(); @@ -216,10 +97,7 @@ public sealed partial class StorageEngine // 1. Write all changes to WAL (from cache, not writeSet!) _wal.WriteBeginRecord(transactionId); - foreach (var (pageId, data) in pages) - { - _wal.WriteDataRecord(transactionId, pageId, data); - } + foreach ((uint pageId, byte[] data) in pages) _wal.WriteDataRecord(transactionId, pageId, data); // 2. Write commit record and flush _wal.WriteCommitRecord(transactionId); @@ -227,20 +105,14 @@ public sealed partial class StorageEngine // 3. Move pages from cache to WAL index (for reads) _walCache.TryRemove(transactionId, out _); - foreach (var kvp in pages) - { - _walIndex[kvp.Key] = kvp.Value; - } + foreach (var kvp in pages) _walIndex[kvp.Key] = kvp.Value; // Auto-checkpoint if WAL grows too large - if (_wal.GetCurrentSize() > MaxWalSize) - { - CheckpointInternal(); - } + if (_wal.GetCurrentSize() > MaxWalSize) CheckpointInternal(); } /// - /// Commits a prepared transaction asynchronously by identifier. + /// Commits a prepared transaction asynchronously by identifier. /// /// The transaction identifier. /// The cancellation token. @@ -271,10 +143,7 @@ public sealed partial class StorageEngine // 1. Write all changes to WAL (from cache, not writeSet!) await _wal.WriteBeginRecordAsync(transactionId, ct); - foreach (var (pageId, data) in pages) - { - await _wal.WriteDataRecordAsync(transactionId, pageId, data, ct); - } + foreach ((uint pageId, byte[] data) in pages) await _wal.WriteDataRecordAsync(transactionId, pageId, data, ct); // 2. Write commit record and flush await _wal.WriteCommitRecordAsync(transactionId, ct); @@ -282,75 +151,177 @@ public sealed partial class StorageEngine // 3. Move pages from cache to WAL index (for reads) _walCache.TryRemove(transactionId, out _); - foreach (var kvp in pages) - { - _walIndex[kvp.Key] = kvp.Value; - } + foreach (var kvp in pages) _walIndex[kvp.Key] = kvp.Value; // Auto-checkpoint if WAL grows too large if (_wal.GetCurrentSize() > MaxWalSize) - { // Checkpoint might be sync or async. For now sync inside the lock is "safe" but blocking. // Ideally this should be async too. CheckpointInternal(); - } } - /// - /// Marks a transaction as committed after WAL writes. - /// Used for 2PC: after Prepare() writes to WAL, this finalizes the commit. - /// - /// Transaction to mark committed - public void MarkTransactionCommitted(ulong transactionId) - { - _commitLock.Wait(); - try - { - _wal.WriteCommitRecord(transactionId); + /// + /// Marks a transaction as committed after WAL writes. + /// Used for 2PC: after Prepare() writes to WAL, this finalizes the commit. + /// + /// Transaction to mark committed + public void MarkTransactionCommitted(ulong transactionId) + { + _commitLock.Wait(); + try + { + _wal.WriteCommitRecord(transactionId); _wal.Flush(); // Move from cache to WAL index - if (_walCache.TryRemove(transactionId, out var pages)) - { - foreach (var kvp in pages) - { - _walIndex[kvp.Key] = kvp.Value; - } - } - - // Auto-checkpoint if WAL grows too large - if (_wal.GetCurrentSize() > MaxWalSize) - { - CheckpointInternal(); - } - } - finally - { - _commitLock.Release(); - } - } - - /// - /// Rolls back a transaction: discards all uncommitted changes. - /// - /// Transaction to rollback - public void RollbackTransaction(ulong transactionId) - { - _walCache.TryRemove(transactionId, out _); + if (_walCache.TryRemove(transactionId, out var pages)) + foreach (var kvp in pages) + _walIndex[kvp.Key] = kvp.Value; + + // Auto-checkpoint if WAL grows too large + if (_wal.GetCurrentSize() > MaxWalSize) CheckpointInternal(); + } + finally + { + _commitLock.Release(); + } + } + + /// + /// Rolls back a transaction: discards all uncommitted changes. + /// + /// Transaction to rollback + public void RollbackTransaction(ulong transactionId) + { + _walCache.TryRemove(transactionId, out _); _wal.WriteAbortRecord(transactionId); } /// - /// Writes an abort record for the specified transaction. + /// Writes an abort record for the specified transaction. /// /// The transaction identifier. internal void WriteAbortRecord(ulong transactionId) { _wal.WriteAbortRecord(transactionId); } - - /// - /// Gets the number of active transactions (diagnostics). - /// - public int ActiveTransactionCount => _walCache.Count; -} + + #region Transaction Management + + /// + /// Begins a new transaction. + /// + /// The transaction isolation level. + /// The started transaction. + public Transaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) + { + _commitLock.Wait(); + try + { + ulong txnId = _nextTransactionId++; + var transaction = new Transaction(txnId, this, isolationLevel); + _activeTransactions[txnId] = transaction; + return transaction; + } + finally + { + _commitLock.Release(); + } + } + + /// + /// Begins a new transaction asynchronously. + /// + /// The transaction isolation level. + /// The cancellation token. + /// The started transaction. + public async Task BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.ReadCommitted, + CancellationToken ct = default) + { + await _commitLock.WaitAsync(ct); + try + { + ulong txnId = _nextTransactionId++; + var transaction = new Transaction(txnId, this, isolationLevel); + _activeTransactions[txnId] = transaction; + return transaction; + } + finally + { + _commitLock.Release(); + } + } + + /// + /// Commits the specified transaction. + /// + /// The transaction to commit. + public void CommitTransaction(Transaction transaction) + { + _commitLock.Wait(); + try + { + if (!_activeTransactions.ContainsKey(transaction.TransactionId)) + throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not active."); + + // 1. Prepare (Write to WAL) + // In a fuller 2PC, this would be separate. Here we do it as part of commit. + if (!PrepareTransaction(transaction.TransactionId)) + throw new IOException("Failed to write transaction to WAL"); + + // 2. Commit (Write commit record, flush, move to cache) + // Use core commit path to avoid re-entering _commitLock. + CommitTransactionCore(transaction.TransactionId); + + _activeTransactions.TryRemove(transaction.TransactionId, out _); + } + finally + { + _commitLock.Release(); + } + } + + /// + /// Commits the specified transaction asynchronously. + /// + /// The transaction to commit. + /// The cancellation token. + public async Task CommitTransactionAsync(Transaction transaction, CancellationToken ct = default) + { + await _commitLock.WaitAsync(ct); + try + { + if (!_activeTransactions.ContainsKey(transaction.TransactionId)) + throw new InvalidOperationException($"Transaction {transaction.TransactionId} is not active."); + + // 1. Prepare (Write to WAL) + if (!await PrepareTransactionAsync(transaction.TransactionId, ct)) + throw new IOException("Failed to write transaction to WAL"); + + // 2. Commit (Write commit record, flush, move to cache) + // Use core commit path to avoid re-entering _commitLock. + await CommitTransactionCoreAsync(transaction.TransactionId, ct); + + _activeTransactions.TryRemove(transaction.TransactionId, out _); + } + finally + { + _commitLock.Release(); + } + } + + /// + /// Rolls back the specified transaction. + /// + /// The transaction to roll back. + public void RollbackTransaction(Transaction transaction) + { + RollbackTransaction(transaction.TransactionId); + _activeTransactions.TryRemove(transaction.TransactionId, out _); + } + + // Rollback doesn't usually require async logic unless logging abort record is async, + // but for consistency we might consider it. For now, sync is fine as it's not the happy path bottleneck. + + #endregion +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/StorageEngine.cs b/src/CBDD.Core/Storage/StorageEngine.cs index 38adbf4..4438c8c 100755 --- a/src/CBDD.Core/Storage/StorageEngine.cs +++ b/src/CBDD.Core/Storage/StorageEngine.cs @@ -1,29 +1,30 @@ using System.Collections.Concurrent; +using ZB.MOM.WW.CBDD.Core.CDC; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Transactions; namespace ZB.MOM.WW.CBDD.Core.Storage; /// -/// Central storage engine managing page-based storage with WAL for durability. -/// -/// Architecture (WAL-based like SQLite/PostgreSQL): -/// - PageFile: Committed baseline (persistent on disk) -/// - WAL Cache: Uncommitted transaction writes (in-memory) -/// - Read: PageFile + WAL cache overlay (for Read Your Own Writes) -/// - Commit: Flush to WAL, clear cache -/// - Checkpoint: Merge WAL ? PageFile periodically +/// Central storage engine managing page-based storage with WAL for durability. +/// Architecture (WAL-based like SQLite/PostgreSQL): +/// - PageFile: Committed baseline (persistent on disk) +/// - WAL Cache: Uncommitted transaction writes (in-memory) +/// - Read: PageFile + WAL cache overlay (for Read Your Own Writes) +/// - Commit: Flush to WAL, clear cache +/// - Checkpoint: Merge WAL ? PageFile periodically /// public sealed partial class StorageEngine : IStorageEngine, IDisposable { + private const long MaxWalSize = 4 * 1024 * 1024; // 4MB + + // Transaction Management + private readonly ConcurrentDictionary _activeTransactions; + + // Global lock for commit/checkpoint synchronization + private readonly SemaphoreSlim _commitLock = new(1, 1); private readonly PageFile _pageFile; private readonly WriteAheadLog _wal; - private readonly CompressionOptions _compressionOptions; - private readonly CompressionService _compressionService; - private readonly CompressionTelemetry _compressionTelemetry; - private readonly StorageFormatMetadata _storageFormatMetadata; - private readonly MaintenanceOptions _maintenanceOptions; - private CDC.ChangeStreamDispatcher? _cdc; // WAL cache: TransactionId → (PageId → PageData) // Stores uncommitted writes for "Read Your Own Writes" isolation @@ -32,18 +33,10 @@ public sealed partial class StorageEngine : IStorageEngine, IDisposable // WAL index cache: PageId → PageData (from latest committed transaction) // Lazily populated on first read after commit private readonly ConcurrentDictionary _walIndex; - - // Global lock for commit/checkpoint synchronization - private readonly SemaphoreSlim _commitLock = new(1, 1); - - // Transaction Management - private readonly ConcurrentDictionary _activeTransactions; private ulong _nextTransactionId; - private const long MaxWalSize = 4 * 1024 * 1024; // 4MB - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The database file path. /// The page file configuration. @@ -55,13 +48,13 @@ public sealed partial class StorageEngine : IStorageEngine, IDisposable CompressionOptions? compressionOptions = null, MaintenanceOptions? maintenanceOptions = null) { - _compressionOptions = CompressionOptions.Normalize(compressionOptions); - _compressionService = new CompressionService(); - _compressionTelemetry = new CompressionTelemetry(); - _maintenanceOptions = maintenanceOptions ?? new MaintenanceOptions(); + CompressionOptions = CompressionOptions.Normalize(compressionOptions); + CompressionService = new CompressionService(); + CompressionTelemetry = new CompressionTelemetry(); + MaintenanceOptions = maintenanceOptions ?? new MaintenanceOptions(); // Auto-derive WAL path - var walPath = Path.ChangeExtension(databasePath, ".wal"); + string walPath = Path.ChangeExtension(databasePath, ".wal"); // Initialize storage infrastructure _pageFile = new PageFile(databasePath, config); @@ -72,14 +65,11 @@ public sealed partial class StorageEngine : IStorageEngine, IDisposable _walIndex = new ConcurrentDictionary(); _activeTransactions = new ConcurrentDictionary(); _nextTransactionId = 1; - _storageFormatMetadata = InitializeStorageFormatMetadata(); + StorageFormatMetadata = InitializeStorageFormatMetadata(); // Recover from WAL if exists (crash recovery or resume after close) // This replays any committed transactions not yet checkpointed - if (_wal.GetCurrentSize() > 0) - { - Recover(); - } + if (_wal.GetCurrentSize() > 0) Recover(); _ = ResumeCompactionIfNeeded(); @@ -92,58 +82,59 @@ public sealed partial class StorageEngine : IStorageEngine, IDisposable } /// - /// Page size for this storage engine + /// Compression options for this engine instance. + /// + public CompressionOptions CompressionOptions { get; } + + /// + /// Compression codec service for payload roundtrip operations. + /// + public CompressionService CompressionService { get; } + + /// + /// Compression telemetry counters for this engine instance. + /// + public CompressionTelemetry CompressionTelemetry { get; } + + /// + /// Gets storage format metadata associated with the current database. + /// + internal StorageFormatMetadata StorageFormatMetadata { get; } + + /// + /// Gets the registered change stream dispatcher, if available. + /// + internal ChangeStreamDispatcher? Cdc { get; private set; } + + /// + /// Page size for this storage engine /// public int PageSize => _pageFile.PageSize; /// - /// Compression options for this engine instance. - /// - public CompressionOptions CompressionOptions => _compressionOptions; - - /// - /// Compression codec service for payload roundtrip operations. - /// - public CompressionService CompressionService => _compressionService; - - /// - /// Compression telemetry counters for this engine instance. - /// - public CompressionTelemetry CompressionTelemetry => _compressionTelemetry; - - /// - /// Returns a point-in-time snapshot of compression telemetry counters. - /// - public CompressionStats GetCompressionStats() => _compressionTelemetry.GetSnapshot(); - - /// - /// Gets storage format metadata associated with the current database. - /// - internal StorageFormatMetadata StorageFormatMetadata => _storageFormatMetadata; - - /// - /// Checks if a page is currently being modified by another active transaction. - /// This is used to implement pessimistic locking for page allocation/selection. + /// Checks if a page is currently being modified by another active transaction. + /// This is used to implement pessimistic locking for page allocation/selection. /// /// The page identifier to check. /// The transaction identifier to exclude from the check. - /// if another transaction holds the page; otherwise, . + /// if another transaction holds the page; otherwise, . public bool IsPageLocked(uint pageId, ulong excludingTxId) { foreach (var kvp in _walCache) { - var txId = kvp.Key; + ulong txId = kvp.Key; if (txId == excludingTxId) continue; var txnPages = kvp.Value; if (txnPages.ContainsKey(pageId)) return true; } + return false; } /// - /// Disposes the storage engine and closes WAL. + /// Disposes the storage engine and closes WAL. /// public void Dispose() { @@ -151,13 +142,15 @@ public sealed partial class StorageEngine : IStorageEngine, IDisposable if (_activeTransactions != null) { foreach (var txn in _activeTransactions.Values) - { try { RollbackTransaction(txn.TransactionId); } - catch { /* Ignore errors during dispose */ } - } + catch + { + /* Ignore errors during dispose */ + } + _activeTransactions.Clear(); } @@ -168,32 +161,38 @@ public sealed partial class StorageEngine : IStorageEngine, IDisposable _commitLock?.Dispose(); } - /// - /// Registers the change stream dispatcher used for CDC notifications. - /// - /// The change stream dispatcher instance. - internal void RegisterCdc(CDC.ChangeStreamDispatcher cdc) + /// + void IStorageEngine.RegisterCdc(ChangeStreamDispatcher cdc) { - _cdc = cdc; + RegisterCdc(cdc); + } + + /// + ChangeStreamDispatcher? IStorageEngine.Cdc => Cdc; + + /// + CompressionOptions IStorageEngine.CompressionOptions => CompressionOptions; + + /// + CompressionService IStorageEngine.CompressionService => CompressionService; + + /// + CompressionTelemetry IStorageEngine.CompressionTelemetry => CompressionTelemetry; + + /// + /// Returns a point-in-time snapshot of compression telemetry counters. + /// + public CompressionStats GetCompressionStats() + { + return CompressionTelemetry.GetSnapshot(); } /// - /// Gets the registered change stream dispatcher, if available. + /// Registers the change stream dispatcher used for CDC notifications. /// - internal CDC.ChangeStreamDispatcher? Cdc => _cdc; - - /// - void IStorageEngine.RegisterCdc(CDC.ChangeStreamDispatcher cdc) => RegisterCdc(cdc); - - /// - CDC.ChangeStreamDispatcher? IStorageEngine.Cdc => _cdc; - - /// - CompressionOptions IStorageEngine.CompressionOptions => _compressionOptions; - - /// - CompressionService IStorageEngine.CompressionService => _compressionService; - - /// - CompressionTelemetry IStorageEngine.CompressionTelemetry => _compressionTelemetry; -} + /// The change stream dispatcher instance. + internal void RegisterCdc(ChangeStreamDispatcher cdc) + { + Cdc = cdc; + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Storage/VectorPage.cs b/src/CBDD.Core/Storage/VectorPage.cs index e590356..03978a6 100755 --- a/src/CBDD.Core/Storage/VectorPage.cs +++ b/src/CBDD.Core/Storage/VectorPage.cs @@ -1,40 +1,40 @@ -using System.Runtime.InteropServices; -using ZB.MOM.WW.CBDD.Core.Indexing; - -namespace ZB.MOM.WW.CBDD.Core.Storage; - -/// -/// Page for storing HNSW Vector Index nodes. -/// Each page stores a fixed number of nodes based on vector dimensions and M. -/// -public struct VectorPage -{ - // Layout: - // [PageHeader (32)] - // [Dimensions (4)] - // [MaxM (4)] - // [NodeSize (4)] - // [NodeCount (4)] - // [Nodes Data (Contiguous)...] - - private const int DimensionsOffset = 32; - private const int MaxMOffset = 36; - private const int NodeSizeOffset = 40; - private const int NodeCountOffset = 44; - private const int DataOffset = 48; - +using System.Buffers.Binary; +using System.Runtime.InteropServices; + +namespace ZB.MOM.WW.CBDD.Core.Storage; + +/// +/// Page for storing HNSW Vector Index nodes. +/// Each page stores a fixed number of nodes based on vector dimensions and M. +/// +public struct VectorPage +{ + // Layout: + // [PageHeader (32)] + // [Dimensions (4)] + // [MaxM (4)] + // [NodeSize (4)] + // [NodeCount (4)] + // [Nodes Data (Contiguous)...] + + private const int DimensionsOffset = 32; + private const int MaxMOffset = 36; + private const int NodeSizeOffset = 40; + private const int NodeCountOffset = 44; + private const int DataOffset = 48; + /// - /// Increments the node count stored in the vector page header. + /// Increments the node count stored in the vector page header. /// /// The page buffer. public static void IncrementNodeCount(Span page) { int count = GetNodeCount(page); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(page.Slice(NodeCountOffset), count + 1); + BinaryPrimitives.WriteInt32LittleEndian(page.Slice(NodeCountOffset), count + 1); } /// - /// Initializes a vector page with header metadata and sizing information. + /// Initializes a vector page with header metadata and sizing information. /// /// The page buffer. /// The page identifier. @@ -43,54 +43,60 @@ public struct VectorPage public static void Initialize(Span page, uint pageId, int dimensions, int maxM) { var header = new PageHeader - { - PageId = pageId, - PageType = PageType.Vector, - FreeBytes = (ushort)(page.Length - DataOffset), - NextPageId = 0, - TransactionId = 0 - }; - header.WriteTo(page); - - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(page.Slice(DimensionsOffset), dimensions); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(page.Slice(MaxMOffset), maxM); + { + PageId = pageId, + PageType = PageType.Vector, + FreeBytes = (ushort)(page.Length - DataOffset), + NextPageId = 0, + TransactionId = 0 + }; + header.WriteTo(page); + + BinaryPrimitives.WriteInt32LittleEndian(page.Slice(DimensionsOffset), dimensions); + BinaryPrimitives.WriteInt32LittleEndian(page.Slice(MaxMOffset), maxM); // Node Size Calculation: // Location (6) + MaxLevel (1) + Vector (dim * 4) + Links (maxM * 10 * 6) -- estimating 10 levels for simplicity // Better: Node size is variable? No, let's keep it fixed per index configuration to avoid fragmentation. // HNSW standard: level 0 has 2*M links, levels > 0 have M links. // Max level is typically < 16. Let's reserve space for 16 levels. - int nodeSize = 6 + 1 + (dimensions * 4) + (maxM * (2 + 15) * 6); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(page.Slice(NodeSizeOffset), nodeSize); - System.Buffers.Binary.BinaryPrimitives.WriteInt32LittleEndian(page.Slice(NodeCountOffset), 0); + int nodeSize = 6 + 1 + dimensions * 4 + maxM * (2 + 15) * 6; + BinaryPrimitives.WriteInt32LittleEndian(page.Slice(NodeSizeOffset), nodeSize); + BinaryPrimitives.WriteInt32LittleEndian(page.Slice(NodeCountOffset), 0); } /// - /// Gets the number of nodes currently stored in the page. + /// Gets the number of nodes currently stored in the page. /// /// The page buffer. /// The node count. - public static int GetNodeCount(ReadOnlySpan page) => - System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(page.Slice(NodeCountOffset)); + public static int GetNodeCount(ReadOnlySpan page) + { + return BinaryPrimitives.ReadInt32LittleEndian(page.Slice(NodeCountOffset)); + } /// - /// Gets the configured node size for the page. + /// Gets the configured node size for the page. /// /// The page buffer. /// The node size in bytes. - public static int GetNodeSize(ReadOnlySpan page) => - System.Buffers.Binary.BinaryPrimitives.ReadInt32LittleEndian(page.Slice(NodeSizeOffset)); + public static int GetNodeSize(ReadOnlySpan page) + { + return BinaryPrimitives.ReadInt32LittleEndian(page.Slice(NodeSizeOffset)); + } /// - /// Gets the maximum number of nodes that can fit in the page. + /// Gets the maximum number of nodes that can fit in the page. /// /// The page buffer. /// The maximum node count. - public static int GetMaxNodes(ReadOnlySpan page) => - (page.Length - DataOffset) / GetNodeSize(page); - + public static int GetMaxNodes(ReadOnlySpan page) + { + return (page.Length - DataOffset) / GetNodeSize(page); + } + /// - /// Writes a node to the page at the specified index. + /// Writes a node to the page at the specified index. /// /// The page buffer. /// The zero-based node index. @@ -98,50 +104,52 @@ public struct VectorPage /// The maximum graph level for the node. /// The vector values to store. /// The vector dimensionality. - public static void WriteNode(Span page, int nodeIndex, DocumentLocation loc, int maxLevel, ReadOnlySpan vector, int dimensions) + public static void WriteNode(Span page, int nodeIndex, DocumentLocation loc, int maxLevel, + ReadOnlySpan vector, int dimensions) { int nodeSize = GetNodeSize(page); - int offset = DataOffset + (nodeIndex * nodeSize); - var nodeSpan = page.Slice(offset, nodeSize); - - // 1. Document Location - loc.WriteTo(nodeSpan.Slice(0, 6)); - - // 2. Max Level - nodeSpan[6] = (byte)maxLevel; - - // 3. Vector - var vectorSpan = MemoryMarshal.Cast(nodeSpan.Slice(7, dimensions * 4)); - vector.CopyTo(vectorSpan); - - // 4. Links (initialize with 0/empty) - // Links follow the vector. Level 0: 2*M links, Level 1..15: M links. + int offset = DataOffset + nodeIndex * nodeSize; + var nodeSpan = page.Slice(offset, nodeSize); + + // 1. Document Location + loc.WriteTo(nodeSpan.Slice(0, 6)); + + // 2. Max Level + nodeSpan[6] = (byte)maxLevel; + + // 3. Vector + var vectorSpan = MemoryMarshal.Cast(nodeSpan.Slice(7, dimensions * 4)); + vector.CopyTo(vectorSpan); + + // 4. Links (initialize with 0/empty) + // Links follow the vector. Level 0: 2*M links, Level 1..15: M links. // For now, just ensure it's cleared or handled by the indexer. } /// - /// Reads node metadata and vector data from the page. + /// Reads node metadata and vector data from the page. /// /// The page buffer. /// The zero-based node index. /// When this method returns, contains the node document location. /// When this method returns, contains the node max level. /// The destination span for vector values. - public static void ReadNodeData(ReadOnlySpan page, int nodeIndex, out DocumentLocation loc, out int maxLevel, Span vector) + public static void ReadNodeData(ReadOnlySpan page, int nodeIndex, out DocumentLocation loc, out int maxLevel, + Span vector) { int nodeSize = GetNodeSize(page); - int offset = DataOffset + (nodeIndex * nodeSize); - var nodeSpan = page.Slice(offset, nodeSize); - - loc = DocumentLocation.ReadFrom(nodeSpan.Slice(0, 6)); + int offset = DataOffset + nodeIndex * nodeSize; + var nodeSpan = page.Slice(offset, nodeSize); + + loc = DocumentLocation.ReadFrom(nodeSpan.Slice(0, 6)); maxLevel = nodeSpan[6]; - var vectorSource = MemoryMarshal.Cast(nodeSpan.Slice(7, vector.Length * 4)); + var vectorSource = MemoryMarshal.Cast(nodeSpan.Slice(7, vector.Length * 4)); vectorSource.CopyTo(vector); } /// - /// Gets the span that stores links for a node at a specific level. + /// Gets the span that stores links for a node at a specific level. /// /// The page buffer. /// The zero-based node index. @@ -152,23 +160,19 @@ public struct VectorPage public static Span GetLinksSpan(Span page, int nodeIndex, int level, int dimensions, int maxM) { int nodeSize = GetNodeSize(page); - int nodeOffset = DataOffset + (nodeIndex * nodeSize); + int nodeOffset = DataOffset + nodeIndex * nodeSize; // Link offset: Location(6) + MaxLevel(1) + Vector(dim*4) - int linkBaseOffset = nodeOffset + 7 + (dimensions * 4); + int linkBaseOffset = nodeOffset + 7 + dimensions * 4; - int levelOffset; - if (level == 0) - { - levelOffset = 0; - } - else - { - // Level 0 has 2*M links - levelOffset = (2 * maxM * 6) + ((level - 1) * maxM * 6); - } - - int count = (level == 0) ? (2 * maxM) : maxM; - return page.Slice(linkBaseOffset + levelOffset, count * 6); - } -} + int levelOffset; + if (level == 0) + levelOffset = 0; + else + // Level 0 has 2*M links + levelOffset = 2 * maxM * 6 + (level - 1) * maxM * 6; + + int count = level == 0 ? 2 * maxM : maxM; + return page.Slice(linkBaseOffset + levelOffset, count * 6); + } +} \ No newline at end of file diff --git a/src/CBDD.Core/Transactions/CheckpointMode.cs b/src/CBDD.Core/Transactions/CheckpointMode.cs index 2855919..324829e 100755 --- a/src/CBDD.Core/Transactions/CheckpointMode.cs +++ b/src/CBDD.Core/Transactions/CheckpointMode.cs @@ -1,39 +1,39 @@ -namespace ZB.MOM.WW.CBDD.Core.Transactions; - +namespace ZB.MOM.WW.CBDD.Core.Transactions; + /// -/// Defines checkpoint modes for WAL (Write-Ahead Log) checkpointing. -/// Similar to SQLite's checkpoint strategies. +/// Defines checkpoint modes for WAL (Write-Ahead Log) checkpointing. +/// Similar to SQLite's checkpoint strategies. /// public enum CheckpointMode { /// - /// Passive checkpoint: non-blocking, best-effort transfer from WAL to database. - /// If the checkpoint lock is busy, the operation is skipped. - /// WAL content is preserved and a checkpoint marker is appended when work is applied. + /// Passive checkpoint: non-blocking, best-effort transfer from WAL to database. + /// If the checkpoint lock is busy, the operation is skipped. + /// WAL content is preserved and a checkpoint marker is appended when work is applied. /// Passive = 0, /// - /// Full checkpoint: waits for the checkpoint lock, transfers committed pages to - /// the page file, and preserves WAL content by appending a checkpoint marker. + /// Full checkpoint: waits for the checkpoint lock, transfers committed pages to + /// the page file, and preserves WAL content by appending a checkpoint marker. /// Full = 1, /// - /// Truncate checkpoint: same as but truncates WAL after - /// successfully applying committed pages. Use this to reclaim disk space. + /// Truncate checkpoint: same as but truncates WAL after + /// successfully applying committed pages. Use this to reclaim disk space. /// Truncate = 2, /// - /// Restart checkpoint: same as and then reinitializes - /// the WAL stream for a fresh writer session. + /// Restart checkpoint: same as and then reinitializes + /// the WAL stream for a fresh writer session. /// Restart = 3 } /// -/// Result of a checkpoint execution. +/// Result of a checkpoint execution. /// /// Requested checkpoint mode. /// True when checkpoint logic ran; false when skipped (for passive mode contention). @@ -49,4 +49,4 @@ public readonly record struct CheckpointResult( long WalBytesBefore, long WalBytesAfter, bool Truncated, - bool Restarted); + bool Restarted); \ No newline at end of file diff --git a/src/CBDD.Core/Transactions/ITransaction.cs b/src/CBDD.Core/Transactions/ITransaction.cs index a339d72..a052eea 100755 --- a/src/CBDD.Core/Transactions/ITransaction.cs +++ b/src/CBDD.Core/Transactions/ITransaction.cs @@ -1,66 +1,62 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - namespace ZB.MOM.WW.CBDD.Core.Transactions; /// -/// Public interface for database transactions. -/// Allows user-controlled transaction boundaries for batch operations. +/// Public interface for database transactions. +/// Allows user-controlled transaction boundaries for batch operations. /// /// -/// using (var txn = collection.BeginTransaction()) -/// { +/// using (var txn = collection.BeginTransaction()) +/// { /// collection.Insert(entity1, txn); /// collection.Insert(entity2, txn); /// txn.Commit(); -/// } +/// } /// public interface ITransaction : IDisposable { /// - /// Unique transaction identifier + /// Unique transaction identifier /// ulong TransactionId { get; } /// - /// Current state of the transaction + /// Current state of the transaction /// TransactionState State { get; } /// - /// Commits the transaction, making all changes permanent. - /// Must be called before Dispose() to persist changes. + /// Commits the transaction, making all changes permanent. + /// Must be called before Dispose() to persist changes. /// void Commit(); - /// - /// Asynchronously commits the transaction, making all changes permanent. - /// - /// The cancellation token. - Task CommitAsync(CancellationToken ct = default); + /// + /// Asynchronously commits the transaction, making all changes permanent. + /// + /// The cancellation token. + Task CommitAsync(CancellationToken ct = default); /// - /// Rolls back the transaction, discarding all changes. - /// Called automatically on Dispose() if Commit() was not called. + /// Rolls back the transaction, discarding all changes. + /// Called automatically on Dispose() if Commit() was not called. /// void Rollback(); /// - /// Adds a write operation to the current batch or transaction. + /// Adds a write operation to the current batch or transaction. /// /// The write operation to add. Cannot be null. void AddWrite(WriteOperation operation); /// - /// Prepares the object for use by performing any necessary initialization or setup. + /// Prepares the object for use by performing any necessary initialization or setup. /// /// true if the preparation was successful; otherwise, false. bool Prepare(); /// - /// Event triggered when the transaction acts rollback. - /// Useful for restoring in-memory state (like ID maps). + /// Event triggered when the transaction acts rollback. + /// Useful for restoring in-memory state (like ID maps). /// event Action? OnRollback; -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Transactions/ITransactionHolder.cs b/src/CBDD.Core/Transactions/ITransactionHolder.cs index 9a57a66..92d395a 100755 --- a/src/CBDD.Core/Transactions/ITransactionHolder.cs +++ b/src/CBDD.Core/Transactions/ITransactionHolder.cs @@ -1,26 +1,32 @@ namespace ZB.MOM.WW.CBDD.Core.Transactions; /// -/// Defines a contract for managing and providing access to the current transaction context. +/// Defines a contract for managing and providing access to the current transaction context. /// -/// Implementations of this interface are responsible for tracking the current transaction and starting a -/// new one if none exists. This is typically used in scenarios where transactional consistency is required across -/// multiple operations. +/// +/// Implementations of this interface are responsible for tracking the current transaction and starting a +/// new one if none exists. This is typically used in scenarios where transactional consistency is required across +/// multiple operations. +/// public interface ITransactionHolder { /// - /// Gets the current transaction if one exists; otherwise, starts a new transaction. + /// Gets the current transaction if one exists; otherwise, starts a new transaction. /// - /// Use this method to ensure that a transaction context is available for the current operation. - /// If a transaction is already in progress, it is returned; otherwise, a new transaction is started and returned. - /// The caller is responsible for managing the transaction's lifetime as appropriate. - /// An representing the current transaction, or a new transaction if none is active. + /// + /// Use this method to ensure that a transaction context is available for the current operation. + /// If a transaction is already in progress, it is returned; otherwise, a new transaction is started and returned. + /// The caller is responsible for managing the transaction's lifetime as appropriate. + /// + /// An representing the current transaction, or a new transaction if none is active. ITransaction GetCurrentTransactionOrStart(); /// - /// Gets the current transaction if one exists; otherwise, starts a new transaction asynchronously. + /// Gets the current transaction if one exists; otherwise, starts a new transaction asynchronously. /// - /// A task that represents the asynchronous operation. The task result contains an - /// representing the current or newly started transaction. + /// + /// A task that represents the asynchronous operation. The task result contains an + /// representing the current or newly started transaction. + /// Task GetCurrentTransactionOrStartAsync(); -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Transactions/Transaction.cs b/src/CBDD.Core/Transactions/Transaction.cs index 2c47166..0e7df02 100755 --- a/src/CBDD.Core/Transactions/Transaction.cs +++ b/src/CBDD.Core/Transactions/Transaction.cs @@ -1,257 +1,241 @@ using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.CDC; using ZB.MOM.WW.CBDD.Core.Storage; -using System; -using System.Threading; -using System.Threading.Tasks; namespace ZB.MOM.WW.CBDD.Core.Transactions; /// -/// Represents a transaction with ACID properties. -/// Uses MVCC (Multi-Version Concurrency Control) for isolation. +/// Represents a transaction with ACID properties. +/// Uses MVCC (Multi-Version Concurrency Control) for isolation. /// public sealed class Transaction : ITransaction { - private readonly ulong _transactionId; - private readonly IsolationLevel _isolationLevel; - private readonly DateTime _startTime; + private readonly List _pendingChanges = new(); private readonly StorageEngine _storage; - private readonly List _pendingChanges = new(); - private TransactionState _state; private bool _disposed; - /// - /// Initializes a new transaction. - /// - /// The unique transaction identifier. - /// The storage engine used by this transaction. - /// The transaction isolation level. - public Transaction(ulong transactionId, - StorageEngine storage, - IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) + /// + /// Initializes a new transaction. + /// + /// The unique transaction identifier. + /// The storage engine used by this transaction. + /// The transaction isolation level. + public Transaction(ulong transactionId, + StorageEngine storage, + IsolationLevel isolationLevel = IsolationLevel.ReadCommitted) { - _transactionId = transactionId; + TransactionId = transactionId; _storage = storage ?? throw new ArgumentNullException(nameof(storage)); - _isolationLevel = isolationLevel; - _startTime = DateTime.UtcNow; - _state = TransactionState.Active; + IsolationLevel = isolationLevel; + StartTime = DateTime.UtcNow; + State = TransactionState.Active; } - /// - /// Adds a pending CDC change to be published after commit. - /// - /// The change event to buffer. - internal void AddChange(CDC.InternalChangeEvent change) - { - _pendingChanges.Add(change); - } - - /// - /// Gets the unique transaction identifier. - /// - public ulong TransactionId => _transactionId; - - /// - /// Gets the current transaction state. - /// - public TransactionState State => _state; - - /// - /// Gets the configured transaction isolation level. - /// - public IsolationLevel IsolationLevel => _isolationLevel; - - /// - /// Gets the UTC start time of the transaction. - /// - public DateTime StartTime => _startTime; + /// + /// Gets the configured transaction isolation level. + /// + public IsolationLevel IsolationLevel { get; } /// - /// Adds a write operation to the transaction's write set. - /// NOTE: Makes a defensive copy of the data to ensure memory safety. - /// This allocation is necessary because the caller may return the buffer to a pool. - /// - /// The write operation to add. - public void AddWrite(WriteOperation operation) + /// Gets the UTC start time of the transaction. + /// + public DateTime StartTime { get; } + + /// + /// Gets the unique transaction identifier. + /// + public ulong TransactionId { get; } + + /// + /// Gets the current transaction state. + /// + public TransactionState State { get; private set; } + + /// + /// Adds a write operation to the transaction's write set. + /// NOTE: Makes a defensive copy of the data to ensure memory safety. + /// This allocation is necessary because the caller may return the buffer to a pool. + /// + /// The write operation to add. + public void AddWrite(WriteOperation operation) { - if (_state != TransactionState.Active) - throw new InvalidOperationException($"Cannot add writes to transaction in state {_state}"); + if (State != TransactionState.Active) + throw new InvalidOperationException($"Cannot add writes to transaction in state {State}"); // Defensive copy: necessary to prevent use-after-return if caller uses pooled buffers byte[] ownedCopy = operation.NewValue.ToArray(); // StorageEngine gestisce tutte le scritture transazionali - _storage.WritePage(operation.PageId, _transactionId, ownedCopy); + _storage.WritePage(operation.PageId, TransactionId, ownedCopy); } /// - /// Prepares the transaction for commit (2PC first phase) + /// Prepares the transaction for commit (2PC first phase) /// public bool Prepare() { - if (_state != TransactionState.Active) + if (State != TransactionState.Active) return false; - _state = TransactionState.Preparing; - - // StorageEngine handles WAL writes - return _storage.PrepareTransaction(_transactionId); + State = TransactionState.Preparing; + + // StorageEngine handles WAL writes + return _storage.PrepareTransaction(TransactionId); } /// - /// Commits the transaction. - /// Writes to WAL for durability and moves data to committed buffer. - /// Pages remain in memory until CheckpointManager writes them to disk. + /// Commits the transaction. + /// Writes to WAL for durability and moves data to committed buffer. + /// Pages remain in memory until CheckpointManager writes them to disk. /// public void Commit() { - if (_state != TransactionState.Preparing && _state != TransactionState.Active) - throw new InvalidOperationException($"Cannot commit transaction in state {_state}"); - - // StorageEngine handles WAL writes and buffer management - _storage.CommitTransaction(_transactionId); + if (State != TransactionState.Preparing && State != TransactionState.Active) + throw new InvalidOperationException($"Cannot commit transaction in state {State}"); - _state = TransactionState.Committed; + // StorageEngine handles WAL writes and buffer management + _storage.CommitTransaction(TransactionId); + + State = TransactionState.Committed; // Publish CDC events after successful commit if (_pendingChanges.Count > 0 && _storage.Cdc != null) - { foreach (var change in _pendingChanges) - { _storage.Cdc.Publish(change); - } - } - } - - /// - /// Asynchronously commits the transaction. - /// - /// A cancellation token. - public async Task CommitAsync(CancellationToken ct = default) - { - if (_state != TransactionState.Preparing && _state != TransactionState.Active) - throw new InvalidOperationException($"Cannot commit transaction in state {_state}"); - - // StorageEngine handles WAL writes and buffer management - await _storage.CommitTransactionAsync(_transactionId, ct); - - _state = TransactionState.Committed; - - // Publish CDC events after successful commit - if (_pendingChanges.Count > 0 && _storage.Cdc != null) - { - foreach (var change in _pendingChanges) - { - _storage.Cdc.Publish(change); - } - } } /// - /// Marks the transaction as committed without writing to PageFile. - /// Used by TransactionManager with lazy checkpointing. + /// Asynchronously commits the transaction. /// - internal void MarkCommitted() + /// A cancellation token. + public async Task CommitAsync(CancellationToken ct = default) { - if (_state != TransactionState.Preparing && _state != TransactionState.Active) - throw new InvalidOperationException($"Cannot commit transaction in state {_state}"); + if (State != TransactionState.Preparing && State != TransactionState.Active) + throw new InvalidOperationException($"Cannot commit transaction in state {State}"); - // StorageEngine marks transaction as committed and moves to committed buffer - _storage.MarkTransactionCommitted(_transactionId); - - _state = TransactionState.Committed; + // StorageEngine handles WAL writes and buffer management + await _storage.CommitTransactionAsync(TransactionId, ct); + + State = TransactionState.Committed; + + // Publish CDC events after successful commit + if (_pendingChanges.Count > 0 && _storage.Cdc != null) + foreach (var change in _pendingChanges) + _storage.Cdc.Publish(change); } /// - /// Rolls back the transaction (discards all writes) + /// Rolls back the transaction (discards all writes) /// public event Action? OnRollback; - /// - /// Rolls back the transaction and discards pending writes. - /// - public void Rollback() + /// + /// Rolls back the transaction and discards pending writes. + /// + public void Rollback() { - if (_state == TransactionState.Committed) + if (State == TransactionState.Committed) throw new InvalidOperationException("Cannot rollback committed transaction"); _pendingChanges.Clear(); - _storage.RollbackTransaction(_transactionId); - _state = TransactionState.Aborted; - + _storage.RollbackTransaction(TransactionId); + State = TransactionState.Aborted; + OnRollback?.Invoke(); } - /// - /// Releases transaction resources and rolls back if still active. - /// - public void Dispose() + /// + /// Releases transaction resources and rolls back if still active. + /// + public void Dispose() { if (_disposed) return; - if (_state == TransactionState.Active || _state == TransactionState.Preparing) - { + if (State == TransactionState.Active || State == TransactionState.Preparing) // Auto-rollback if not committed Rollback(); - } _disposed = true; GC.SuppressFinalize(this); } + + /// + /// Adds a pending CDC change to be published after commit. + /// + /// The change event to buffer. + internal void AddChange(InternalChangeEvent change) + { + _pendingChanges.Add(change); + } + + /// + /// Marks the transaction as committed without writing to PageFile. + /// Used by TransactionManager with lazy checkpointing. + /// + internal void MarkCommitted() + { + if (State != TransactionState.Preparing && State != TransactionState.Active) + throw new InvalidOperationException($"Cannot commit transaction in state {State}"); + + // StorageEngine marks transaction as committed and moves to committed buffer + _storage.MarkTransactionCommitted(TransactionId); + + State = TransactionState.Committed; + } } /// -/// Represents a write operation in a transaction. -/// Optimized to avoid allocations by using ReadOnlyMemory instead of byte[]. +/// Represents a write operation in a transaction. +/// Optimized to avoid allocations by using ReadOnlyMemory instead of byte[]. /// -public struct WriteOperation -{ - /// - /// Gets or sets the identifier of the affected document. - /// - public ObjectId DocumentId { get; set; } - - /// - /// Gets or sets the new serialized value. - /// - public ReadOnlyMemory NewValue { get; set; } - - /// - /// Gets or sets the target page identifier. - /// - public uint PageId { get; set; } - - /// - /// Gets or sets the operation type. - /// - public OperationType Type { get; set; } - - /// - /// Initializes a new write operation. - /// - /// The identifier of the affected document. - /// The new serialized value. - /// The target page identifier. - /// The operation type. - public WriteOperation(ObjectId documentId, ReadOnlyMemory newValue, uint pageId, OperationType type) - { - DocumentId = documentId; +public struct WriteOperation +{ + /// + /// Gets or sets the identifier of the affected document. + /// + public ObjectId DocumentId { get; set; } + + /// + /// Gets or sets the new serialized value. + /// + public ReadOnlyMemory NewValue { get; set; } + + /// + /// Gets or sets the target page identifier. + /// + public uint PageId { get; set; } + + /// + /// Gets or sets the operation type. + /// + public OperationType Type { get; set; } + + /// + /// Initializes a new write operation. + /// + /// The identifier of the affected document. + /// The new serialized value. + /// The target page identifier. + /// The operation type. + public WriteOperation(ObjectId documentId, ReadOnlyMemory newValue, uint pageId, OperationType type) + { + DocumentId = documentId; NewValue = newValue; PageId = pageId; Type = type; - } - - // Backward compatibility constructor - /// - /// Initializes a new write operation from a byte array payload. - /// - /// The identifier of the affected document. - /// The new serialized value. - /// The target page identifier. - /// The operation type. - public WriteOperation(ObjectId documentId, byte[] newValue, uint pageId, OperationType type) - { - DocumentId = documentId; + } + + // Backward compatibility constructor + /// + /// Initializes a new write operation from a byte array payload. + /// + /// The identifier of the affected document. + /// The new serialized value. + /// The target page identifier. + /// The operation type. + public WriteOperation(ObjectId documentId, byte[] newValue, uint pageId, OperationType type) + { + DocumentId = documentId; NewValue = newValue; PageId = pageId; Type = type; @@ -259,7 +243,7 @@ public struct WriteOperation } /// -/// Type of write operation +/// Type of write operation /// public enum OperationType : byte { @@ -267,4 +251,4 @@ public enum OperationType : byte Update = 2, Delete = 3, AllocatePage = 4 -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Transactions/TransactionState.cs b/src/CBDD.Core/Transactions/TransactionState.cs index 5a64472..0be0164 100755 --- a/src/CBDD.Core/Transactions/TransactionState.cs +++ b/src/CBDD.Core/Transactions/TransactionState.cs @@ -1,37 +1,37 @@ namespace ZB.MOM.WW.CBDD.Core.Transactions; /// -/// Transaction states +/// Transaction states /// public enum TransactionState : byte { /// Transaction is active and can accept operations - Active = 1, - + Active = 1, + /// Transaction is preparing to commit - Preparing = 2, - + Preparing = 2, + /// Transaction committed successfully - Committed = 3, - + Committed = 3, + /// Transaction was rolled back Aborted = 4 } /// -/// Transaction isolation levels +/// Transaction isolation levels /// public enum IsolationLevel : byte { /// Read uncommitted data - ReadUncommitted = 0, - + ReadUncommitted = 0, + /// Read only committed data (default) - ReadCommitted = 1, - + ReadCommitted = 1, + /// Repeatable reads - RepeatableRead = 2, - + RepeatableRead = 2, + /// Serializable (full isolation) Serializable = 3 -} +} \ No newline at end of file diff --git a/src/CBDD.Core/Transactions/WriteAheadLog.cs b/src/CBDD.Core/Transactions/WriteAheadLog.cs index 4ede234..3f3e5ed 100755 --- a/src/CBDD.Core/Transactions/WriteAheadLog.cs +++ b/src/CBDD.Core/Transactions/WriteAheadLog.cs @@ -1,7 +1,9 @@ +using System.Buffers; + namespace ZB.MOM.WW.CBDD.Core.Transactions; /// -/// WAL record types +/// WAL record types /// public enum WalRecordType : byte { @@ -13,590 +15,38 @@ public enum WalRecordType : byte } /// -/// Write-Ahead Log (WAL) for durability and recovery. -/// All changes are logged before being applied. +/// Write-Ahead Log (WAL) for durability and recovery. +/// All changes are logged before being applied. /// -public sealed class WriteAheadLog : IDisposable -{ +public sealed class WriteAheadLog : IDisposable +{ + private readonly SemaphoreSlim _lock = new(1, 1); private readonly string _walPath; + private bool _disposed; private FileStream? _walStream; - private readonly SemaphoreSlim _lock = new(1, 1); - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - /// The file path of the write-ahead log. - public WriteAheadLog(string walPath) - { - _walPath = walPath ?? throw new ArgumentNullException(nameof(walPath)); - + + /// + /// Initializes a new instance of the class. + /// + /// The file path of the write-ahead log. + public WriteAheadLog(string walPath) + { + _walPath = walPath ?? throw new ArgumentNullException(nameof(walPath)); + _walStream = new FileStream( _walPath, FileMode.OpenOrCreate, FileAccess.ReadWrite, - FileShare.None, // Exclusive access like PageFile - bufferSize: 64 * 1024); // 64KB buffer for better sequential write performance + FileShare.None, // Exclusive access like PageFile + 64 * 1024); // 64KB buffer for better sequential write performance // REMOVED FileOptions.WriteThrough for SQLite-style lazy checkpointing // Durability is ensured by explicit Flush() calls } - /// - /// Writes a begin transaction record - /// - /// The transaction identifier. - public void WriteBeginRecord(ulong transactionId) - { - _lock.Wait(); - try - { - WriteBeginRecordInternal(transactionId); - } - finally - { - _lock.Release(); - } - } - - /// - /// Writes a begin transaction record asynchronously. - /// - /// The transaction identifier. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public async ValueTask WriteBeginRecordAsync(ulong transactionId, CancellationToken ct = default) - { - await _lock.WaitAsync(ct); - try - { - // Use ArrayPool for async I/O compatibility (cannot use stackalloc with async) - var buffer = System.Buffers.ArrayPool.Shared.Rent(17); - try - { - buffer[0] = (byte)WalRecordType.Begin; - BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); - BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - - await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(buffer); - } - } - finally - { - _lock.Release(); - } - } - - private void WriteBeginRecordInternal(ulong transactionId) - { - Span buffer = stackalloc byte[17]; // type(1) + txnId(8) + timestamp(8) - buffer[0] = (byte)WalRecordType.Begin; - BitConverter.TryWriteBytes(buffer[1..9], transactionId); - BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - - _walStream!.Write(buffer); - } - - - /// - /// Writes a commit record - /// - /// - /// Writes a commit record - /// - /// The transaction identifier. - public void WriteCommitRecord(ulong transactionId) - { - _lock.Wait(); - try - { - WriteCommitRecordInternal(transactionId); - } - finally - { - _lock.Release(); - } - } - - /// - /// Writes a commit record asynchronously. - /// - /// The transaction identifier. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public async ValueTask WriteCommitRecordAsync(ulong transactionId, CancellationToken ct = default) - { - await _lock.WaitAsync(ct); - try - { - var buffer = System.Buffers.ArrayPool.Shared.Rent(17); - try - { - buffer[0] = (byte)WalRecordType.Commit; - BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); - BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - - await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(buffer); - } - } - finally - { - _lock.Release(); - } - } - - private void WriteCommitRecordInternal(ulong transactionId) - { - Span buffer = stackalloc byte[17]; // type(1) + txnId(8) + timestamp(8) - buffer[0] = (byte)WalRecordType.Commit; - BitConverter.TryWriteBytes(buffer[1..9], transactionId); - BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - - _walStream!.Write(buffer); - } - - - /// - /// Writes an abort record - /// - /// - /// Writes an abort record - /// - /// The transaction identifier. - public void WriteAbortRecord(ulong transactionId) - { - _lock.Wait(); - try - { - WriteAbortRecordInternal(transactionId); - } - finally - { - _lock.Release(); - } - } - - /// - /// Writes an abort record asynchronously. - /// - /// The transaction identifier. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public async ValueTask WriteAbortRecordAsync(ulong transactionId, CancellationToken ct = default) - { - await _lock.WaitAsync(ct); - try - { - var buffer = System.Buffers.ArrayPool.Shared.Rent(17); - try - { - buffer[0] = (byte)WalRecordType.Abort; - BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); - BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - - await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(buffer); - } - } - finally - { - _lock.Release(); - } - } - - private void WriteAbortRecordInternal(ulong transactionId) - { - Span buffer = stackalloc byte[17]; // type(1) + txnId(8) + timestamp(8) - buffer[0] = (byte)WalRecordType.Abort; - BitConverter.TryWriteBytes(buffer[1..9], transactionId); - BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - - _walStream!.Write(buffer); - } - - - /// - /// Writes a checkpoint marker record. - /// - public void WriteCheckpointRecord() - { - _lock.Wait(); - try - { - WriteCheckpointRecordInternal(); - } - finally - { - _lock.Release(); - } - } - - /// - /// Writes a checkpoint marker record asynchronously. - /// - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public async ValueTask WriteCheckpointRecordAsync(CancellationToken ct = default) - { - await _lock.WaitAsync(ct); - try - { - var buffer = System.Buffers.ArrayPool.Shared.Rent(17); - try - { - buffer[0] = (byte)WalRecordType.Checkpoint; - BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), 0UL); - BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(buffer); - } - } - finally - { - _lock.Release(); - } - } - - private void WriteCheckpointRecordInternal() - { - Span buffer = stackalloc byte[17]; // type(1) + reserved(8) + timestamp(8) - buffer[0] = (byte)WalRecordType.Checkpoint; - BitConverter.TryWriteBytes(buffer[1..9], 0UL); - BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); - _walStream!.Write(buffer); - } - - - /// - /// Writes a data modification record - /// - /// - /// Writes a data modification record - /// - /// The transaction identifier. - /// The page identifier of the modified page. - /// The page contents after modification. - public void WriteDataRecord(ulong transactionId, uint pageId, ReadOnlySpan afterImage) - { - _lock.Wait(); - try - { - WriteDataRecordInternal(transactionId, pageId, afterImage); - } - finally - { - _lock.Release(); - } - } - - /// - /// Writes a data modification record asynchronously. - /// - /// The transaction identifier. - /// The page identifier of the modified page. - /// The page contents after modification. - /// The cancellation token. - /// A task that represents the asynchronous write operation. - public async ValueTask WriteDataRecordAsync(ulong transactionId, uint pageId, ReadOnlyMemory afterImage, CancellationToken ct = default) - { - await _lock.WaitAsync(ct); - try - { - var headerSize = 17; - var totalSize = headerSize + afterImage.Length; - - var buffer = System.Buffers.ArrayPool.Shared.Rent(totalSize); - try - { - buffer[0] = (byte)WalRecordType.Write; - BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); - BitConverter.TryWriteBytes(buffer.AsSpan(9, 4), pageId); - BitConverter.TryWriteBytes(buffer.AsSpan(13, 4), afterImage.Length); - - afterImage.Span.CopyTo(buffer.AsSpan(headerSize)); - - await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, totalSize), ct); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(buffer); - } - } - finally - { - _lock.Release(); - } - } - - private void WriteDataRecordInternal(ulong transactionId, uint pageId, ReadOnlySpan afterImage) - { - // Header: type(1) + txnId(8) + pageId(4) + afterSize(4) = 17 bytes - var headerSize = 17; - var totalSize = headerSize + afterImage.Length; - - var buffer = System.Buffers.ArrayPool.Shared.Rent(totalSize); - try - { - buffer[0] = (byte)WalRecordType.Write; - BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); - BitConverter.TryWriteBytes(buffer.AsSpan(9, 4), pageId); - BitConverter.TryWriteBytes(buffer.AsSpan(13, 4), afterImage.Length); - - afterImage.CopyTo(buffer.AsSpan(headerSize)); - - _walStream!.Write(buffer.AsSpan(0, totalSize)); - } - finally - { - System.Buffers.ArrayPool.Shared.Return(buffer); - } - } - - /// - /// Flushes all buffered writes to disk + /// Releases resources used by the write-ahead log. /// - /// - /// Flushes all buffered writes to disk - /// - public void Flush() - { - _lock.Wait(); - try - { - _walStream?.Flush(flushToDisk: true); - } - finally - { - _lock.Release(); - } - } - - /// - /// Flushes all buffered writes to disk asynchronously. - /// - /// The cancellation token. - /// A task that represents the asynchronous flush operation. - public async Task FlushAsync(CancellationToken ct = default) - { - await _lock.WaitAsync(ct); - try - { - if (_walStream != null) - { - await _walStream.FlushAsync(ct); - // FlushAsync doesn't guarantee flushToDisk on all platforms/implementations in the same way as Flush(true) - // but FileStream in .NET 6+ handles this reasonable well. - // For strict durability, we might still want to invoke a sync flush or check platform specifics, - // but typically FlushAsync(ct) is sufficient for "Async" pattern. - // However, FileStream.FlushAsync() acts like flushToDisk=false by default in older .NET? - // Actually, FileStream.Flush() has flushToDisk arg, FlushAsync does not but implementation usually does buffer flush. - // To be safe for WAL, we might care about fsync. - // For now, just FlushAsync(); - } - } - finally - { - _lock.Release(); - } - } - - - /// - /// Gets the current size of the WAL file in bytes - /// - public long GetCurrentSize() - { - _lock.Wait(); - try - { - return _walStream?.Length ?? 0; - } - finally - { - _lock.Release(); - } - } - - - /// - /// Truncates the WAL file (removes all content). - /// Should only be called after successful checkpoint. - /// - public void Truncate() - { - _lock.Wait(); - try - { - if (_walStream != null) - { - _walStream.SetLength(0); - _walStream.Position = 0; - _walStream.Flush(flushToDisk: true); - } - } - finally - { - _lock.Release(); - } - } - - /// - /// Truncates and reopens the WAL stream to start a fresh writer session. - /// - public void Restart() - { - _lock.Wait(); - try - { - _walStream?.Dispose(); - _walStream = new FileStream( - _walPath, - FileMode.Create, - FileAccess.ReadWrite, - FileShare.None, - bufferSize: 64 * 1024); - } - finally - { - _lock.Release(); - } - } - - - /// - /// Reads all WAL records (for recovery) - /// - public List ReadAll() - { - _lock.Wait(); - try - { - var records = new List(); - - if (_walStream == null || _walStream.Length == 0) - return records; - - _walStream.Position = 0; - - // Allocate buffers outside loop to avoid CA2014 warning - Span headerBuf = stackalloc byte[16]; - Span dataBuf = stackalloc byte[12]; - - while (_walStream.Position < _walStream.Length) - { - var typeByte = _walStream.ReadByte(); - if (typeByte == -1) break; - - var type = (WalRecordType)typeByte; - - // Check for invalid record type (file padding or corruption) - if (typeByte == 0 || !Enum.IsDefined(typeof(WalRecordType), type)) - { - // Reached end of valid records (file may have padding) - break; - } - - WalRecord record; - - switch (type) - { - case WalRecordType.Begin: - case WalRecordType.Commit: - case WalRecordType.Abort: - case WalRecordType.Checkpoint: - // Read common fields (txnId + timestamp = 16 bytes) - var bytesRead = _walStream.Read(headerBuf); - if (bytesRead < 16) - { - // Incomplete record, stop reading - return records; - } - - var txnId = BitConverter.ToUInt64(headerBuf[0..8]); - var timestamp = BitConverter.ToInt64(headerBuf[8..16]); - - record = new WalRecord - { - Type = type, - TransactionId = txnId, - Timestamp = timestamp - }; - break; - - case WalRecordType.Write: - // Write records have different format: txnId(8) + pageId(4) + afterSize(4) - // Read txnId + pageId + afterSize = 16 bytes - bytesRead = _walStream.Read(headerBuf); - if (bytesRead < 16) - { - // Incomplete write record header, stop reading - return records; - } - - txnId = BitConverter.ToUInt64(headerBuf[0..8]); - var pageId = BitConverter.ToUInt32(headerBuf[8..12]); - var afterSize = BitConverter.ToInt32(headerBuf[12..16]); - - // Validate afterSize to prevent overflow or corruption - if (afterSize < 0 || afterSize > 100 * 1024 * 1024) // Max 100MB per record - { - // Corrupted size, stop reading - return records; - } - - var afterImage = new byte[afterSize]; - - // Read afterImage - if (_walStream.Read(afterImage) < afterSize) - { - // Incomplete after image, stop reading - return records; - } - - record = new WalRecord - { - Type = type, - TransactionId = txnId, - Timestamp = 0, // Write records don't have timestamp - PageId = pageId, - AfterImage = afterImage - }; - break; - - default: - // Unknown record type, stop reading - return records; - } - - records.Add(record); - } - - return records; - } - finally - { - _lock.Release(); - } - } - - /// - /// Releases resources used by the write-ahead log. - /// - public void Dispose() + public void Dispose() { if (_disposed) return; @@ -615,36 +65,576 @@ public sealed class WriteAheadLog : IDisposable GC.SuppressFinalize(this); } + + /// + /// Writes a begin transaction record + /// + /// The transaction identifier. + public void WriteBeginRecord(ulong transactionId) + { + _lock.Wait(); + try + { + WriteBeginRecordInternal(transactionId); + } + finally + { + _lock.Release(); + } + } + + /// + /// Writes a begin transaction record asynchronously. + /// + /// The transaction identifier. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async ValueTask WriteBeginRecordAsync(ulong transactionId, CancellationToken ct = default) + { + await _lock.WaitAsync(ct); + try + { + // Use ArrayPool for async I/O compatibility (cannot use stackalloc with async) + byte[] buffer = ArrayPool.Shared.Rent(17); + try + { + buffer[0] = (byte)WalRecordType.Begin; + BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); + BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + + await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + _lock.Release(); + } + } + + private void WriteBeginRecordInternal(ulong transactionId) + { + Span buffer = stackalloc byte[17]; // type(1) + txnId(8) + timestamp(8) + buffer[0] = (byte)WalRecordType.Begin; + BitConverter.TryWriteBytes(buffer[1..9], transactionId); + BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + + _walStream!.Write(buffer); + } + + + /// + /// Writes a commit record + /// + /// + /// Writes a commit record + /// + /// The transaction identifier. + public void WriteCommitRecord(ulong transactionId) + { + _lock.Wait(); + try + { + WriteCommitRecordInternal(transactionId); + } + finally + { + _lock.Release(); + } + } + + /// + /// Writes a commit record asynchronously. + /// + /// The transaction identifier. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async ValueTask WriteCommitRecordAsync(ulong transactionId, CancellationToken ct = default) + { + await _lock.WaitAsync(ct); + try + { + byte[] buffer = ArrayPool.Shared.Rent(17); + try + { + buffer[0] = (byte)WalRecordType.Commit; + BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); + BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + + await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + _lock.Release(); + } + } + + private void WriteCommitRecordInternal(ulong transactionId) + { + Span buffer = stackalloc byte[17]; // type(1) + txnId(8) + timestamp(8) + buffer[0] = (byte)WalRecordType.Commit; + BitConverter.TryWriteBytes(buffer[1..9], transactionId); + BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + + _walStream!.Write(buffer); + } + + + /// + /// Writes an abort record + /// + /// + /// Writes an abort record + /// + /// The transaction identifier. + public void WriteAbortRecord(ulong transactionId) + { + _lock.Wait(); + try + { + WriteAbortRecordInternal(transactionId); + } + finally + { + _lock.Release(); + } + } + + /// + /// Writes an abort record asynchronously. + /// + /// The transaction identifier. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async ValueTask WriteAbortRecordAsync(ulong transactionId, CancellationToken ct = default) + { + await _lock.WaitAsync(ct); + try + { + byte[] buffer = ArrayPool.Shared.Rent(17); + try + { + buffer[0] = (byte)WalRecordType.Abort; + BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); + BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + + await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + _lock.Release(); + } + } + + private void WriteAbortRecordInternal(ulong transactionId) + { + Span buffer = stackalloc byte[17]; // type(1) + txnId(8) + timestamp(8) + buffer[0] = (byte)WalRecordType.Abort; + BitConverter.TryWriteBytes(buffer[1..9], transactionId); + BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + + _walStream!.Write(buffer); + } + + + /// + /// Writes a checkpoint marker record. + /// + public void WriteCheckpointRecord() + { + _lock.Wait(); + try + { + WriteCheckpointRecordInternal(); + } + finally + { + _lock.Release(); + } + } + + /// + /// Writes a checkpoint marker record asynchronously. + /// + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async ValueTask WriteCheckpointRecordAsync(CancellationToken ct = default) + { + await _lock.WaitAsync(ct); + try + { + byte[] buffer = ArrayPool.Shared.Rent(17); + try + { + buffer[0] = (byte)WalRecordType.Checkpoint; + BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), 0UL); + BitConverter.TryWriteBytes(buffer.AsSpan(9, 8), DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, 17), ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + _lock.Release(); + } + } + + private void WriteCheckpointRecordInternal() + { + Span buffer = stackalloc byte[17]; // type(1) + reserved(8) + timestamp(8) + buffer[0] = (byte)WalRecordType.Checkpoint; + BitConverter.TryWriteBytes(buffer[1..9], 0UL); + BitConverter.TryWriteBytes(buffer[9..17], DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()); + _walStream!.Write(buffer); + } + + + /// + /// Writes a data modification record + /// + /// + /// Writes a data modification record + /// + /// The transaction identifier. + /// The page identifier of the modified page. + /// The page contents after modification. + public void WriteDataRecord(ulong transactionId, uint pageId, ReadOnlySpan afterImage) + { + _lock.Wait(); + try + { + WriteDataRecordInternal(transactionId, pageId, afterImage); + } + finally + { + _lock.Release(); + } + } + + /// + /// Writes a data modification record asynchronously. + /// + /// The transaction identifier. + /// The page identifier of the modified page. + /// The page contents after modification. + /// The cancellation token. + /// A task that represents the asynchronous write operation. + public async ValueTask WriteDataRecordAsync(ulong transactionId, uint pageId, ReadOnlyMemory afterImage, + CancellationToken ct = default) + { + await _lock.WaitAsync(ct); + try + { + var headerSize = 17; + int totalSize = headerSize + afterImage.Length; + + byte[] buffer = ArrayPool.Shared.Rent(totalSize); + try + { + buffer[0] = (byte)WalRecordType.Write; + BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); + BitConverter.TryWriteBytes(buffer.AsSpan(9, 4), pageId); + BitConverter.TryWriteBytes(buffer.AsSpan(13, 4), afterImage.Length); + + afterImage.Span.CopyTo(buffer.AsSpan(headerSize)); + + await _walStream!.WriteAsync(new ReadOnlyMemory(buffer, 0, totalSize), ct); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + finally + { + _lock.Release(); + } + } + + private void WriteDataRecordInternal(ulong transactionId, uint pageId, ReadOnlySpan afterImage) + { + // Header: type(1) + txnId(8) + pageId(4) + afterSize(4) = 17 bytes + var headerSize = 17; + int totalSize = headerSize + afterImage.Length; + + byte[] buffer = ArrayPool.Shared.Rent(totalSize); + try + { + buffer[0] = (byte)WalRecordType.Write; + BitConverter.TryWriteBytes(buffer.AsSpan(1, 8), transactionId); + BitConverter.TryWriteBytes(buffer.AsSpan(9, 4), pageId); + BitConverter.TryWriteBytes(buffer.AsSpan(13, 4), afterImage.Length); + + afterImage.CopyTo(buffer.AsSpan(headerSize)); + + _walStream!.Write(buffer.AsSpan(0, totalSize)); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + + /// + /// Flushes all buffered writes to disk + /// + /// + /// Flushes all buffered writes to disk + /// + public void Flush() + { + _lock.Wait(); + try + { + _walStream?.Flush(true); + } + finally + { + _lock.Release(); + } + } + + /// + /// Flushes all buffered writes to disk asynchronously. + /// + /// The cancellation token. + /// A task that represents the asynchronous flush operation. + public async Task FlushAsync(CancellationToken ct = default) + { + await _lock.WaitAsync(ct); + try + { + if (_walStream != null) await _walStream.FlushAsync(ct); + // FlushAsync doesn't guarantee flushToDisk on all platforms/implementations in the same way as Flush(true) + // but FileStream in .NET 6+ handles this reasonable well. + // For strict durability, we might still want to invoke a sync flush or check platform specifics, + // but typically FlushAsync(ct) is sufficient for "Async" pattern. + // However, FileStream.FlushAsync() acts like flushToDisk=false by default in older .NET? + // Actually, FileStream.Flush() has flushToDisk arg, FlushAsync does not but implementation usually does buffer flush. + // To be safe for WAL, we might care about fsync. + // For now, just FlushAsync(); + } + finally + { + _lock.Release(); + } + } + + + /// + /// Gets the current size of the WAL file in bytes + /// + public long GetCurrentSize() + { + _lock.Wait(); + try + { + return _walStream?.Length ?? 0; + } + finally + { + _lock.Release(); + } + } + + + /// + /// Truncates the WAL file (removes all content). + /// Should only be called after successful checkpoint. + /// + public void Truncate() + { + _lock.Wait(); + try + { + if (_walStream != null) + { + _walStream.SetLength(0); + _walStream.Position = 0; + _walStream.Flush(true); + } + } + finally + { + _lock.Release(); + } + } + + /// + /// Truncates and reopens the WAL stream to start a fresh writer session. + /// + public void Restart() + { + _lock.Wait(); + try + { + _walStream?.Dispose(); + _walStream = new FileStream( + _walPath, + FileMode.Create, + FileAccess.ReadWrite, + FileShare.None, + 64 * 1024); + } + finally + { + _lock.Release(); + } + } + + + /// + /// Reads all WAL records (for recovery) + /// + public List ReadAll() + { + _lock.Wait(); + try + { + var records = new List(); + + if (_walStream == null || _walStream.Length == 0) + return records; + + _walStream.Position = 0; + + // Allocate buffers outside loop to avoid CA2014 warning + Span headerBuf = stackalloc byte[16]; + Span dataBuf = stackalloc byte[12]; + + while (_walStream.Position < _walStream.Length) + { + int typeByte = _walStream.ReadByte(); + if (typeByte == -1) break; + + var type = (WalRecordType)typeByte; + + // Check for invalid record type (file padding or corruption) + if (typeByte == 0 || !Enum.IsDefined(typeof(WalRecordType), type)) + // Reached end of valid records (file may have padding) + break; + + WalRecord record; + + switch (type) + { + case WalRecordType.Begin: + case WalRecordType.Commit: + case WalRecordType.Abort: + case WalRecordType.Checkpoint: + // Read common fields (txnId + timestamp = 16 bytes) + int bytesRead = _walStream.Read(headerBuf); + if (bytesRead < 16) + // Incomplete record, stop reading + return records; + + var txnId = BitConverter.ToUInt64(headerBuf[..8]); + var timestamp = BitConverter.ToInt64(headerBuf[8..16]); + + record = new WalRecord + { + Type = type, + TransactionId = txnId, + Timestamp = timestamp + }; + break; + + case WalRecordType.Write: + // Write records have different format: txnId(8) + pageId(4) + afterSize(4) + // Read txnId + pageId + afterSize = 16 bytes + bytesRead = _walStream.Read(headerBuf); + if (bytesRead < 16) + // Incomplete write record header, stop reading + return records; + + txnId = BitConverter.ToUInt64(headerBuf[..8]); + var pageId = BitConverter.ToUInt32(headerBuf[8..12]); + var afterSize = BitConverter.ToInt32(headerBuf[12..16]); + + // Validate afterSize to prevent overflow or corruption + if (afterSize < 0 || afterSize > 100 * 1024 * 1024) // Max 100MB per record + // Corrupted size, stop reading + return records; + + var afterImage = new byte[afterSize]; + + // Read afterImage + if (_walStream.Read(afterImage) < afterSize) + // Incomplete after image, stop reading + return records; + + record = new WalRecord + { + Type = type, + TransactionId = txnId, + Timestamp = 0, // Write records don't have timestamp + PageId = pageId, + AfterImage = afterImage + }; + break; + + default: + // Unknown record type, stop reading + return records; + } + + records.Add(record); + } + + return records; + } + finally + { + _lock.Release(); + } + } } /// -/// Represents a WAL record. -/// Implemented as struct for memory efficiency. +/// Represents a WAL record. +/// Implemented as struct for memory efficiency. /// -public struct WalRecord -{ - /// - /// Gets or sets the WAL record type. - /// - public WalRecordType Type { get; set; } - - /// - /// Gets or sets the transaction identifier. - /// - public ulong TransactionId { get; set; } - - /// - /// Gets or sets the record timestamp in Unix milliseconds. - /// - public long Timestamp { get; set; } - - /// - /// Gets or sets the page identifier for write records. - /// - public uint PageId { get; set; } - - /// - /// Gets or sets the after-image payload for write records. - /// - public byte[]? AfterImage { get; set; } -} +public struct WalRecord +{ + /// + /// Gets or sets the WAL record type. + /// + public WalRecordType Type { get; set; } + + /// + /// Gets or sets the transaction identifier. + /// + public ulong TransactionId { get; set; } + + /// + /// Gets or sets the record timestamp in Unix milliseconds. + /// + public long Timestamp { get; set; } + + /// + /// Gets or sets the page identifier for write records. + /// + public uint PageId { get; set; } + + /// + /// Gets or sets the after-image payload for write records. + /// + public byte[]? AfterImage { get; set; } +} \ No newline at end of file diff --git a/src/CBDD.Core/ZB.MOM.WW.CBDD.Core.csproj b/src/CBDD.Core/ZB.MOM.WW.CBDD.Core.csproj index d032abe..24f0a44 100755 --- a/src/CBDD.Core/ZB.MOM.WW.CBDD.Core.csproj +++ b/src/CBDD.Core/ZB.MOM.WW.CBDD.Core.csproj @@ -1,38 +1,38 @@ - - net10.0 - ZB.MOM.WW.CBDD.Core - ZB.MOM.WW.CBDD.Core - latest - enable - enable - true - true - - ZB.MOM.WW.CBDD.Core - 1.3.1 - CBDD Team - High-Performance BSON Database Engine Core Library for .NET 10 - MIT - README.md - https://github.com/EntglDb/CBDD - database;embedded;bson;nosql;net10;zero-allocation - True - + + net10.0 + ZB.MOM.WW.CBDD.Core + ZB.MOM.WW.CBDD.Core + latest + enable + enable + true + true - - - + ZB.MOM.WW.CBDD.Core + 1.3.1 + CBDD Team + High-Performance BSON Database Engine Core Library for .NET 10 + MIT + README.md + https://github.com/EntglDb/CBDD + database;embedded;bson;nosql;net10;zero-allocation + True + - - - <_Parameter1>ZB.MOM.WW.CBDD.Tests - - + + + - - - + + + <_Parameter1>ZB.MOM.WW.CBDD.Tests + + + + + + diff --git a/src/CBDD.SourceGenerators/Analysis/EntityAnalyzer.cs b/src/CBDD.SourceGenerators/Analysis/EntityAnalyzer.cs index cf11119..9fb7a27 100755 --- a/src/CBDD.SourceGenerators/Analysis/EntityAnalyzer.cs +++ b/src/CBDD.SourceGenerators/Analysis/EntityAnalyzer.cs @@ -1,261 +1,257 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using ZB.MOM.WW.CBDD.SourceGenerators.Helpers; -using ZB.MOM.WW.CBDD.SourceGenerators.Models; - -namespace ZB.MOM.WW.CBDD.SourceGenerators -{ - public static class EntityAnalyzer +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using ZB.MOM.WW.CBDD.SourceGenerators.Helpers; +using ZB.MOM.WW.CBDD.SourceGenerators.Models; + +namespace ZB.MOM.WW.CBDD.SourceGenerators; + +public static class EntityAnalyzer +{ + /// + /// Analyzes an entity symbol and builds source-generation metadata. + /// + /// The entity type symbol to analyze. + /// The semantic model for symbol and syntax analysis. + /// The analyzed entity metadata. + public static EntityInfo Analyze(INamedTypeSymbol entityType, SemanticModel semanticModel) { - /// - /// Analyzes an entity symbol and builds source-generation metadata. - /// - /// The entity type symbol to analyze. - /// The semantic model for symbol and syntax analysis. - /// The analyzed entity metadata. - public static EntityInfo Analyze(INamedTypeSymbol entityType, SemanticModel semanticModel) + var entityInfo = new EntityInfo { - var entityInfo = new EntityInfo - { - Name = entityType.Name, - Namespace = entityType.ContainingNamespace.ToDisplayString(), - FullTypeName = SyntaxHelper.GetFullName(entityType), - CollectionName = entityType.Name.ToLowerInvariant() + "s" - }; - - var tableAttr = AttributeHelper.GetAttribute(entityType, "Table"); - if (tableAttr != null) - { - var tableName = tableAttr.ConstructorArguments.Length > 0 ? tableAttr.ConstructorArguments[0].Value?.ToString() : null; - var schema = AttributeHelper.GetNamedArgumentValue(tableAttr, "Schema"); + Name = entityType.Name, + Namespace = entityType.ContainingNamespace.ToDisplayString(), + FullTypeName = SyntaxHelper.GetFullName(entityType), + CollectionName = entityType.Name.ToLowerInvariant() + "s" + }; - var collectionName = !string.IsNullOrEmpty(tableName) ? tableName! : entityInfo.Name; - if (!string.IsNullOrEmpty(schema)) - { - collectionName = $"{schema}.{collectionName}"; - } - entityInfo.CollectionName = collectionName; - } + var tableAttr = AttributeHelper.GetAttribute(entityType, "Table"); + if (tableAttr != null) + { + string? tableName = tableAttr.ConstructorArguments.Length > 0 + ? tableAttr.ConstructorArguments[0].Value?.ToString() + : null; + string? schema = AttributeHelper.GetNamedArgumentValue(tableAttr, "Schema"); - // Analyze properties of the root entity - AnalyzeProperties(entityType, entityInfo.Properties); - - // Check if entity needs reflection-based deserialization - // Include properties with private setters or init-only setters (which can't be set outside initializers) - entityInfo.HasPrivateSetters = entityInfo.Properties.Any(p => (!p.HasPublicSetter && p.HasAnySetter) || p.HasInitOnlySetter); + string collectionName = !string.IsNullOrEmpty(tableName) ? tableName! : entityInfo.Name; + if (!string.IsNullOrEmpty(schema)) collectionName = $"{schema}.{collectionName}"; + entityInfo.CollectionName = collectionName; + } - // Check if entity has public parameterless constructor - var hasPublicParameterlessConstructor = entityType.Constructors - .Any(c => c.DeclaredAccessibility == Accessibility.Public && c.Parameters.Length == 0); - entityInfo.HasPrivateOrNoConstructor = !hasPublicParameterlessConstructor; - - // Analyze nested types recursively - // We use a dictionary for nested types to ensure uniqueness by name - var analyzedTypes = new HashSet(); - AnalyzeNestedTypesRecursive(entityInfo.Properties, entityInfo.NestedTypes, semanticModel, analyzedTypes, 1, 3); + // Analyze properties of the root entity + AnalyzeProperties(entityType, entityInfo.Properties); - // Determine ID property - // entityInfo.IdProperty is computed from Properties.FirstOrDefault(p => p.IsKey) + // Check if entity needs reflection-based deserialization + // Include properties with private setters or init-only setters (which can't be set outside initializers) + entityInfo.HasPrivateSetters = + entityInfo.Properties.Any(p => (!p.HasPublicSetter && p.HasAnySetter) || p.HasInitOnlySetter); - if (entityInfo.IdProperty == null) - { - // Fallback to convention: property named "Id" - var idProp = entityInfo.Properties.FirstOrDefault(p => p.Name == "Id"); - if (idProp != null) - { - idProp.IsKey = true; - } - } + // Check if entity has public parameterless constructor + bool hasPublicParameterlessConstructor = entityType.Constructors + .Any(c => c.DeclaredAccessibility == Accessibility.Public && c.Parameters.Length == 0); + entityInfo.HasPrivateOrNoConstructor = !hasPublicParameterlessConstructor; - // Check for AutoId (int/long keys) - if (entityInfo.IdProperty != null) - { - var idType = entityInfo.IdProperty.TypeName.TrimEnd('?'); - if (idType == "int" || idType == "Int32" || idType == "long" || idType == "Int64") - { - entityInfo.AutoId = true; - } - } + // Analyze nested types recursively + // We use a dictionary for nested types to ensure uniqueness by name + var analyzedTypes = new HashSet(); + AnalyzeNestedTypesRecursive(entityInfo.Properties, entityInfo.NestedTypes, semanticModel, analyzedTypes, 1, 3); - return entityInfo; - } - - private static void AnalyzeProperties(INamedTypeSymbol typeSymbol, List properties) - { - // Collect properties from the entire inheritance hierarchy - var seenProperties = new HashSet(); - var currentType = typeSymbol; + // Determine ID property + // entityInfo.IdProperty is computed from Properties.FirstOrDefault(p => p.IsKey) - while (currentType != null && currentType.SpecialType != SpecialType.System_Object) - { - var sourceProps = currentType.GetMembers() - .OfType() - .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic); + if (entityInfo.IdProperty == null) + { + // Fallback to convention: property named "Id" + var idProp = entityInfo.Properties.FirstOrDefault(p => p.Name == "Id"); + if (idProp != null) idProp.IsKey = true; + } - foreach (var prop in sourceProps) - { - // Skip if already seen (overridden property in derived class takes precedence) - if (!seenProperties.Add(prop.Name)) - continue; + // Check for AutoId (int/long keys) + if (entityInfo.IdProperty != null) + { + string idType = entityInfo.IdProperty.TypeName.TrimEnd('?'); + if (idType == "int" || idType == "Int32" || idType == "long" || idType == "Int64") entityInfo.AutoId = true; + } - if (AttributeHelper.ShouldIgnore(prop)) - continue; + return entityInfo; + } - // Skip computed getter-only properties (no setter, no backing field) - bool isReadOnlyGetter = prop.SetMethod == null && !SyntaxHelper.HasBackingField(prop); - if (isReadOnlyGetter) - continue; + private static void AnalyzeProperties(INamedTypeSymbol typeSymbol, List properties) + { + // Collect properties from the entire inheritance hierarchy + var seenProperties = new HashSet(); + var currentType = typeSymbol; - var columnAttr = AttributeHelper.GetAttribute(prop, "Column"); - var bsonFieldName = AttributeHelper.GetAttributeStringValue(prop, "BsonProperty") ?? + while (currentType != null && currentType.SpecialType != SpecialType.System_Object) + { + var sourceProps = currentType.GetMembers() + .OfType() + .Where(p => p.DeclaredAccessibility == Accessibility.Public && !p.IsStatic); + + foreach (var prop in sourceProps) + { + // Skip if already seen (overridden property in derived class takes precedence) + if (!seenProperties.Add(prop.Name)) + continue; + + if (AttributeHelper.ShouldIgnore(prop)) + continue; + + // Skip computed getter-only properties (no setter, no backing field) + bool isReadOnlyGetter = prop.SetMethod == null && !SyntaxHelper.HasBackingField(prop); + if (isReadOnlyGetter) + continue; + + var columnAttr = AttributeHelper.GetAttribute(prop, "Column"); + string? bsonFieldName = AttributeHelper.GetAttributeStringValue(prop, "BsonProperty") ?? AttributeHelper.GetAttributeStringValue(prop, "JsonPropertyName"); - if (bsonFieldName == null && columnAttr != null) - { - bsonFieldName = columnAttr.ConstructorArguments.Length > 0 ? columnAttr.ConstructorArguments[0].Value?.ToString() : null; - } + if (bsonFieldName == null && columnAttr != null) + bsonFieldName = columnAttr.ConstructorArguments.Length > 0 + ? columnAttr.ConstructorArguments[0].Value?.ToString() + : null; - var propInfo = new PropertyInfo - { - Name = prop.Name, - TypeName = SyntaxHelper.GetTypeName(prop.Type), - BsonFieldName = bsonFieldName ?? prop.Name.ToLowerInvariant(), - ColumnTypeName = columnAttr != null ? AttributeHelper.GetNamedArgumentValue(columnAttr, "TypeName") : null, - IsNullable = SyntaxHelper.IsNullableType(prop.Type), - IsKey = AttributeHelper.IsKey(prop), - IsRequired = AttributeHelper.HasAttribute(prop, "Required"), - - HasPublicSetter = prop.SetMethod?.DeclaredAccessibility == Accessibility.Public, - HasInitOnlySetter = prop.SetMethod?.IsInitOnly == true, - HasAnySetter = prop.SetMethod != null, - IsReadOnlyGetter = isReadOnlyGetter, - BackingFieldName = (prop.SetMethod?.DeclaredAccessibility != Accessibility.Public) - ? $"<{prop.Name}>k__BackingField" - : null - }; - - // MaxLength / MinLength - propInfo.MaxLength = AttributeHelper.GetAttributeIntValue(prop, "MaxLength"); - propInfo.MinLength = AttributeHelper.GetAttributeIntValue(prop, "MinLength"); - - var stringLengthAttr = AttributeHelper.GetAttribute(prop, "StringLength"); - if (stringLengthAttr != null) - { - if (stringLengthAttr.ConstructorArguments.Length > 0 && stringLengthAttr.ConstructorArguments[0].Value is int max) - propInfo.MaxLength = max; - - var minLenStr = AttributeHelper.GetNamedArgumentValue(stringLengthAttr, "MinimumLength"); - if (int.TryParse(minLenStr, out var min)) - propInfo.MinLength = min; - } - - // Range - var rangeAttr = AttributeHelper.GetAttribute(prop, "Range"); - if (rangeAttr != null && rangeAttr.ConstructorArguments.Length >= 2) - { - if (rangeAttr.ConstructorArguments[0].Value is double dmin) propInfo.RangeMin = dmin; - else if (rangeAttr.ConstructorArguments[0].Value is int imin) propInfo.RangeMin = (double)imin; - - if (rangeAttr.ConstructorArguments[1].Value is double dmax) propInfo.RangeMax = dmax; - else if (rangeAttr.ConstructorArguments[1].Value is int imax) propInfo.RangeMax = (double)imax; - } - - if (SyntaxHelper.IsCollectionType(prop.Type, out var itemType)) - { - propInfo.IsCollection = true; - propInfo.IsArray = prop.Type is IArrayTypeSymbol; - - // Determine concrete collection type name - propInfo.CollectionConcreteTypeName = SyntaxHelper.GetTypeName(prop.Type); - - if (itemType != null) - { - propInfo.CollectionItemType = SyntaxHelper.GetTypeName(itemType); - - // Check if collection item is nested object - if (SyntaxHelper.IsNestedObjectType(itemType)) - { - propInfo.IsCollectionItemNested = true; - propInfo.NestedTypeName = itemType.Name; - propInfo.NestedTypeFullName = SyntaxHelper.GetFullName((INamedTypeSymbol)itemType); - } - } - } - // Check for Nested Object - else if (SyntaxHelper.IsNestedObjectType(prop.Type)) - { - propInfo.IsNestedObject = true; - propInfo.NestedTypeName = prop.Type.Name; - propInfo.NestedTypeFullName = SyntaxHelper.GetFullName((INamedTypeSymbol)prop.Type); - } - - properties.Add(propInfo); - } - - currentType = currentType.BaseType; - } - } - - private static void AnalyzeNestedTypesRecursive( - List properties, - Dictionary targetNestedTypes, - SemanticModel semanticModel, - HashSet analyzedTypes, - int currentDepth, - int maxDepth) - { - if (currentDepth > maxDepth) return; - - // Identify properties that reference nested types (either directly or via collection) - var nestedProps = properties - .Where(p => (p.IsNestedObject || p.IsCollectionItemNested) && !string.IsNullOrEmpty(p.NestedTypeFullName)) - .ToList(); - - foreach (var prop in nestedProps) - { - var fullTypeName = prop.NestedTypeFullName!; - var simpleName = prop.NestedTypeName!; - - // Avoid cycles - if (analyzedTypes.Contains(fullTypeName)) continue; - - // If already in target list, skip - if (targetNestedTypes.ContainsKey(fullTypeName)) continue; - - // Try to find the symbol - INamedTypeSymbol? nestedTypeSymbol = null; - - // Try by full name - nestedTypeSymbol = semanticModel.Compilation.GetTypeByMetadataName(fullTypeName); - - // If not found, try to resolve via semantic model (might be in the same compilation) - if (nestedTypeSymbol == null) + var propInfo = new PropertyInfo { - // This is more complex, but usually fullTypeName from ToDisplayString() is traceable. - // For now, let's assume GetTypeByMetadataName works for fully qualified names. - } + Name = prop.Name, + TypeName = SyntaxHelper.GetTypeName(prop.Type), + BsonFieldName = bsonFieldName ?? prop.Name.ToLowerInvariant(), + ColumnTypeName = columnAttr != null + ? AttributeHelper.GetNamedArgumentValue(columnAttr, "TypeName") + : null, + IsNullable = SyntaxHelper.IsNullableType(prop.Type), + IsKey = AttributeHelper.IsKey(prop), + IsRequired = AttributeHelper.HasAttribute(prop, "Required"), - if (nestedTypeSymbol == null) continue; - - analyzedTypes.Add(fullTypeName); - - var nestedInfo = new NestedTypeInfo - { - Name = simpleName, - Namespace = nestedTypeSymbol.ContainingNamespace.ToDisplayString(), - FullTypeName = fullTypeName, - Depth = currentDepth + HasPublicSetter = prop.SetMethod?.DeclaredAccessibility == Accessibility.Public, + HasInitOnlySetter = prop.SetMethod?.IsInitOnly == true, + HasAnySetter = prop.SetMethod != null, + IsReadOnlyGetter = isReadOnlyGetter, + BackingFieldName = prop.SetMethod?.DeclaredAccessibility != Accessibility.Public + ? $"<{prop.Name}>k__BackingField" + : null }; - // Analyze properties of this nested type - AnalyzeProperties(nestedTypeSymbol, nestedInfo.Properties); - targetNestedTypes[fullTypeName] = nestedInfo; + // MaxLength / MinLength + propInfo.MaxLength = AttributeHelper.GetAttributeIntValue(prop, "MaxLength"); + propInfo.MinLength = AttributeHelper.GetAttributeIntValue(prop, "MinLength"); - // Recurse - AnalyzeNestedTypesRecursive(nestedInfo.Properties, nestedInfo.NestedTypes, semanticModel, analyzedTypes, currentDepth + 1, maxDepth); - } - } - } -} + var stringLengthAttr = AttributeHelper.GetAttribute(prop, "StringLength"); + if (stringLengthAttr != null) + { + if (stringLengthAttr.ConstructorArguments.Length > 0 && + stringLengthAttr.ConstructorArguments[0].Value is int max) + propInfo.MaxLength = max; + + string? minLenStr = AttributeHelper.GetNamedArgumentValue(stringLengthAttr, "MinimumLength"); + if (int.TryParse(minLenStr, out int min)) + propInfo.MinLength = min; + } + + // Range + var rangeAttr = AttributeHelper.GetAttribute(prop, "Range"); + if (rangeAttr != null && rangeAttr.ConstructorArguments.Length >= 2) + { + if (rangeAttr.ConstructorArguments[0].Value is double dmin) propInfo.RangeMin = dmin; + else if (rangeAttr.ConstructorArguments[0].Value is int imin) propInfo.RangeMin = (double)imin; + + if (rangeAttr.ConstructorArguments[1].Value is double dmax) propInfo.RangeMax = dmax; + else if (rangeAttr.ConstructorArguments[1].Value is int imax) propInfo.RangeMax = (double)imax; + } + + if (SyntaxHelper.IsCollectionType(prop.Type, out var itemType)) + { + propInfo.IsCollection = true; + propInfo.IsArray = prop.Type is IArrayTypeSymbol; + + // Determine concrete collection type name + propInfo.CollectionConcreteTypeName = SyntaxHelper.GetTypeName(prop.Type); + + if (itemType != null) + { + propInfo.CollectionItemType = SyntaxHelper.GetTypeName(itemType); + + // Check if collection item is nested object + if (SyntaxHelper.IsNestedObjectType(itemType)) + { + propInfo.IsCollectionItemNested = true; + propInfo.NestedTypeName = itemType.Name; + propInfo.NestedTypeFullName = SyntaxHelper.GetFullName((INamedTypeSymbol)itemType); + } + } + } + // Check for Nested Object + else if (SyntaxHelper.IsNestedObjectType(prop.Type)) + { + propInfo.IsNestedObject = true; + propInfo.NestedTypeName = prop.Type.Name; + propInfo.NestedTypeFullName = SyntaxHelper.GetFullName((INamedTypeSymbol)prop.Type); + } + + properties.Add(propInfo); + } + + currentType = currentType.BaseType; + } + } + + private static void AnalyzeNestedTypesRecursive( + List properties, + Dictionary targetNestedTypes, + SemanticModel semanticModel, + HashSet analyzedTypes, + int currentDepth, + int maxDepth) + { + if (currentDepth > maxDepth) return; + + // Identify properties that reference nested types (either directly or via collection) + var nestedProps = properties + .Where(p => (p.IsNestedObject || p.IsCollectionItemNested) && !string.IsNullOrEmpty(p.NestedTypeFullName)) + .ToList(); + + foreach (var prop in nestedProps) + { + string fullTypeName = prop.NestedTypeFullName!; + string simpleName = prop.NestedTypeName!; + + // Avoid cycles + if (analyzedTypes.Contains(fullTypeName)) continue; + + // If already in target list, skip + if (targetNestedTypes.ContainsKey(fullTypeName)) continue; + + // Try to find the symbol + INamedTypeSymbol? nestedTypeSymbol = null; + + // Try by full name + nestedTypeSymbol = semanticModel.Compilation.GetTypeByMetadataName(fullTypeName); + + // If not found, try to resolve via semantic model (might be in the same compilation) + if (nestedTypeSymbol == null) + { + // This is more complex, but usually fullTypeName from ToDisplayString() is traceable. + // For now, let's assume GetTypeByMetadataName works for fully qualified names. + } + + if (nestedTypeSymbol == null) continue; + + analyzedTypes.Add(fullTypeName); + + var nestedInfo = new NestedTypeInfo + { + Name = simpleName, + Namespace = nestedTypeSymbol.ContainingNamespace.ToDisplayString(), + FullTypeName = fullTypeName, + Depth = currentDepth + }; + + // Analyze properties of this nested type + AnalyzeProperties(nestedTypeSymbol, nestedInfo.Properties); + targetNestedTypes[fullTypeName] = nestedInfo; + + // Recurse + AnalyzeNestedTypesRecursive(nestedInfo.Properties, nestedInfo.NestedTypes, semanticModel, analyzedTypes, + currentDepth + 1, maxDepth); + } + } +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/Generators/CodeGenerator.cs b/src/CBDD.SourceGenerators/Generators/CodeGenerator.cs index c2d8345..2c857f6 100755 --- a/src/CBDD.SourceGenerators/Generators/CodeGenerator.cs +++ b/src/CBDD.SourceGenerators/Generators/CodeGenerator.cs @@ -1,898 +1,901 @@ -using System; -using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Text; using ZB.MOM.WW.CBDD.SourceGenerators.Models; -using ZB.MOM.WW.CBDD.SourceGenerators.Helpers; -namespace ZB.MOM.WW.CBDD.SourceGenerators +namespace ZB.MOM.WW.CBDD.SourceGenerators; + +public static class CodeGenerator { - public static class CodeGenerator - { - /// - /// Generates the mapper class source code for an entity. - /// - /// The entity metadata used for generation. - /// The namespace where the mapper class is generated. - /// The generated mapper class source code. - public static string GenerateMapperClass(EntityInfo entity, string mapperNamespace) - { - var sb = new StringBuilder(); - var mapperName = GetMapperName(entity.FullTypeName); - var keyProp = entity.Properties.FirstOrDefault(p => p.IsKey); - var isRoot = entity.IdProperty != null; - - sb.AppendLine("#pragma warning disable CS8604"); - - // Class Declaration - if (isRoot) - { - var baseClass = GetBaseMapperClass(keyProp, entity); - // Ensure FullTypeName has global:: prefix if not already present (assuming FullTypeName is fully qualified) - var entityType = $"global::{entity.FullTypeName}"; - sb.AppendLine($" public class {mapperName} : global::ZB.MOM.WW.CBDD.Core.Collections.{baseClass}{entityType}>"); - } - else - { - sb.AppendLine($" public class {mapperName}"); - } - - sb.AppendLine($" {{"); - - // Converter instance - if (keyProp?.ConverterTypeName != null) - { - sb.AppendLine($" private readonly global::{keyProp.ConverterTypeName} _idConverter = new();"); - sb.AppendLine(); - } + /// + /// Generates the mapper class source code for an entity. + /// + /// The entity metadata used for generation. + /// The namespace where the mapper class is generated. + /// The generated mapper class source code. + public static string GenerateMapperClass(EntityInfo entity, string mapperNamespace) + { + var sb = new StringBuilder(); + string mapperName = GetMapperName(entity.FullTypeName); + var keyProp = entity.Properties.FirstOrDefault(p => p.IsKey); + bool isRoot = entity.IdProperty != null; - // Generate static setters for private properties (Expression Trees) - var privateSetterProps = entity.Properties.Where(p => (!p.HasPublicSetter && p.HasAnySetter) || p.HasInitOnlySetter).ToList(); - if (privateSetterProps.Any()) - { - sb.AppendLine($" // Cached Expression Tree setters for private properties"); - foreach (var prop in privateSetterProps) - { - var entityType = $"global::{entity.FullTypeName}"; - var propType = QualifyType(prop.TypeName); - sb.AppendLine($" private static readonly global::System.Action<{entityType}, {propType}> _setter_{prop.Name} = CreateSetter<{entityType}, {propType}>(\"{prop.Name}\");"); - } - sb.AppendLine(); - - sb.AppendLine($" private static global::System.Action CreateSetter(string propertyName)"); - sb.AppendLine($" {{"); - sb.AppendLine($" var param = global::System.Linq.Expressions.Expression.Parameter(typeof(TObj), \"obj\");"); - sb.AppendLine($" var value = global::System.Linq.Expressions.Expression.Parameter(typeof(TVal), \"val\");"); - sb.AppendLine($" var prop = global::System.Linq.Expressions.Expression.Property(param, propertyName);"); - sb.AppendLine($" var assign = global::System.Linq.Expressions.Expression.Assign(prop, value);"); - sb.AppendLine($" return global::System.Linq.Expressions.Expression.Lambda>(assign, param, value).Compile();"); - sb.AppendLine($" }}"); - sb.AppendLine(); - } + sb.AppendLine("#pragma warning disable CS8604"); - // Collection Name (only for root) - if (isRoot) - { - sb.AppendLine($" public override string CollectionName => \"{entity.CollectionName}\";"); - sb.AppendLine(); - } - else if (entity.Properties.All(p => !p.IsKey)) - { - sb.AppendLine($"// #warning Entity '{entity.Name}' has no defined primary key. Mapper may not support all features."); - } - - // Serialize Method - GenerateSerializeMethod(sb, entity, isRoot, mapperNamespace); - - sb.AppendLine(); - - // Deserialize Method - GenerateDeserializeMethod(sb, entity, isRoot, mapperNamespace); - - if (isRoot) - { - sb.AppendLine(); - GenerateIdAccessors(sb, entity); - } - - sb.AppendLine($" }}"); - sb.AppendLine("#pragma warning restore CS8604"); - - return sb.ToString(); - } - - private static void GenerateSerializeMethod(StringBuilder sb, EntityInfo entity, bool isRoot, string mapperNamespace) + // Class Declaration + if (isRoot) { - var entityType = $"global::{entity.FullTypeName}"; - - // Always generate SerializeFields (writes only fields, no document wrapper) - // This is needed even for root entities, as they may be used as nested objects - // Note: BsonSpanWriter is a ref struct, so it must be passed by ref - sb.AppendLine($" public void SerializeFields({entityType} entity, ref global::ZB.MOM.WW.CBDD.Bson.BsonSpanWriter writer)"); - sb.AppendLine($" {{"); - GenerateFieldWritesCore(sb, entity, mapperNamespace); - sb.AppendLine($" }}"); - sb.AppendLine(); - - // Generate Serialize method (with document wrapper) - var methodSig = isRoot - ? $"public override int Serialize({entityType} entity, global::ZB.MOM.WW.CBDD.Bson.BsonSpanWriter writer)" - : $"public int Serialize({entityType} entity, global::ZB.MOM.WW.CBDD.Bson.BsonSpanWriter writer)"; - - sb.AppendLine($" {methodSig}"); - sb.AppendLine($" {{"); - sb.AppendLine($" var startingPos = writer.BeginDocument();"); - sb.AppendLine(); - GenerateFieldWritesCore(sb, entity, mapperNamespace); - sb.AppendLine(); - sb.AppendLine($" writer.EndDocument(startingPos);"); - sb.AppendLine($" return writer.Position;"); - sb.AppendLine($" }}"); - } - - private static void GenerateFieldWritesCore(StringBuilder sb, EntityInfo entity, string mapperNamespace) + string baseClass = GetBaseMapperClass(keyProp, entity); + // Ensure FullTypeName has global:: prefix if not already present (assuming FullTypeName is fully qualified) + var entityType = $"global::{entity.FullTypeName}"; + sb.AppendLine( + $" public class {mapperName} : global::ZB.MOM.WW.CBDD.Core.Collections.{baseClass}{entityType}>"); + } + else { - foreach (var prop in entity.Properties) - { - // Handle key property - serialize as "_id" regardless of property name - if (prop.IsKey) - { - if (prop.ConverterTypeName != null) - { - var providerProp = new PropertyInfo { TypeName = prop.ProviderTypeName ?? "string" }; - var idWriteMethod = GetPrimitiveWriteMethod(providerProp, allowKey: true); - if (idWriteMethod == "WriteString") - { - sb.AppendLine($" var convertedId = _idConverter.ConvertToProvider(entity.{prop.Name});"); - sb.AppendLine($" if (convertedId != null)"); - sb.AppendLine($" {{"); - sb.AppendLine($" writer.WriteString(\"_id\", convertedId);"); - sb.AppendLine($" }}"); - sb.AppendLine($" else"); - sb.AppendLine($" {{"); - sb.AppendLine($" writer.WriteNull(\"_id\");"); - sb.AppendLine($" }}"); - } - else - { - sb.AppendLine($" writer.{idWriteMethod}(\"_id\", _idConverter.ConvertToProvider(entity.{prop.Name}));"); - } - } - else - { - var idWriteMethod = GetPrimitiveWriteMethod(prop, allowKey: true); - if (idWriteMethod != null) - { - sb.AppendLine($" writer.{idWriteMethod}(\"_id\", entity.{prop.Name});"); - } - else - { - sb.AppendLine($"#warning Unsupported Id type for '{prop.Name}': {prop.TypeName}. Serialization of '_id' will fail."); - sb.AppendLine($" // Unsupported Id type: {prop.TypeName}"); - } - } - continue; - } - - GenerateValidation(sb, prop); - GenerateWriteProperty(sb, prop, mapperNamespace); - } - } - - private static void GenerateValidation(StringBuilder sb, PropertyInfo prop) - { - var isString = prop.TypeName == "string" || prop.TypeName == "String"; - - if (prop.IsRequired) - { - if (isString) - { - sb.AppendLine($" if (string.IsNullOrEmpty(entity.{prop.Name})) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is required.\");"); - } - else if (prop.IsNullable) - { - sb.AppendLine($" if (entity.{prop.Name} == null) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is required.\");"); - } - } - - if (prop.MaxLength.HasValue && isString) - { - sb.AppendLine($" if ((entity.{prop.Name}?.Length ?? 0) > {prop.MaxLength}) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} exceeds max length {prop.MaxLength}.\");"); - } - if (prop.MinLength.HasValue && isString) - { - sb.AppendLine($" if ((entity.{prop.Name}?.Length ?? 0) < {prop.MinLength}) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is below min length {prop.MinLength}.\");"); - } - - if (prop.RangeMin.HasValue || prop.RangeMax.HasValue) - { - var minStr = prop.RangeMin?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "double.MinValue"; - var maxStr = prop.RangeMax?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "double.MaxValue"; - sb.AppendLine($" if ((double)entity.{prop.Name} < {minStr} || (double)entity.{prop.Name} > {maxStr}) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is outside range [{minStr}, {maxStr}].\");"); - } + sb.AppendLine($" public class {mapperName}"); } - private static void GenerateWriteProperty(StringBuilder sb, PropertyInfo prop, string mapperNamespace) + sb.AppendLine(" {"); + + // Converter instance + if (keyProp?.ConverterTypeName != null) { - var fieldName = prop.BsonFieldName; - - if (prop.IsCollection) + sb.AppendLine($" private readonly global::{keyProp.ConverterTypeName} _idConverter = new();"); + sb.AppendLine(); + } + + // Generate static setters for private properties (Expression Trees) + var privateSetterProps = entity.Properties + .Where(p => (!p.HasPublicSetter && p.HasAnySetter) || p.HasInitOnlySetter).ToList(); + if (privateSetterProps.Any()) + { + sb.AppendLine(" // Cached Expression Tree setters for private properties"); + foreach (var prop in privateSetterProps) { - // Add null check for nullable collections - if (prop.IsNullable) - { - sb.AppendLine($" if (entity.{prop.Name} != null)"); - sb.AppendLine($" {{"); - } - - var arrayVar = $"{prop.Name.ToLower()}Array"; - var indent = prop.IsNullable ? " " : ""; - sb.AppendLine($" {indent}var {arrayVar}Pos = writer.BeginArray(\"{fieldName}\");"); - sb.AppendLine($" {indent}var {prop.Name.ToLower()}Index = 0;"); - sb.AppendLine($" {indent}foreach (var item in entity.{prop.Name})"); - sb.AppendLine($" {indent}{{"); - - - if (prop.IsCollectionItemNested) - { - sb.AppendLine($" {indent} // Nested Object in List"); - var nestedMapperTypes = GetMapperName(prop.NestedTypeFullName!); - sb.AppendLine($" {indent} var {prop.Name.ToLower()}ItemMapper = new global::{mapperNamespace}.{nestedMapperTypes}();"); - - sb.AppendLine($" {indent} var itemStartPos = writer.BeginDocument({prop.Name.ToLower()}Index.ToString());"); - sb.AppendLine($" {indent} {prop.Name.ToLower()}ItemMapper.SerializeFields(item, ref writer);"); - sb.AppendLine($" {indent} writer.EndDocument(itemStartPos);"); - } - else - { - // Simplified: pass a dummy PropertyInfo with the item type for primitive collection items - var dummyProp = new PropertyInfo { TypeName = prop.CollectionItemType! }; - var writeMethod = GetPrimitiveWriteMethod(dummyProp); - if (writeMethod != null) - { - sb.AppendLine($" {indent} writer.{writeMethod}({prop.Name.ToLower()}Index.ToString(), item);"); - } - } - sb.AppendLine($" {indent} {prop.Name.ToLower()}Index++;"); - - sb.AppendLine($" {indent}}}"); - sb.AppendLine($" {indent}writer.EndArray({arrayVar}Pos);"); - - // Close the null check if block - if (prop.IsNullable) - { - sb.AppendLine($" }}"); - sb.AppendLine($" else"); - sb.AppendLine($" {{"); - sb.AppendLine($" writer.WriteNull(\"{fieldName}\");"); - sb.AppendLine($" }}"); - } + var entityType = $"global::{entity.FullTypeName}"; + string propType = QualifyType(prop.TypeName); + sb.AppendLine( + $" private static readonly global::System.Action<{entityType}, {propType}> _setter_{prop.Name} = CreateSetter<{entityType}, {propType}>(\"{prop.Name}\");"); } - else if (prop.IsNestedObject) + + sb.AppendLine(); + + sb.AppendLine( + " private static global::System.Action CreateSetter(string propertyName)"); + sb.AppendLine(" {"); + sb.AppendLine( + " var param = global::System.Linq.Expressions.Expression.Parameter(typeof(TObj), \"obj\");"); + sb.AppendLine( + " var value = global::System.Linq.Expressions.Expression.Parameter(typeof(TVal), \"val\");"); + sb.AppendLine( + " var prop = global::System.Linq.Expressions.Expression.Property(param, propertyName);"); + sb.AppendLine(" var assign = global::System.Linq.Expressions.Expression.Assign(prop, value);"); + sb.AppendLine( + " return global::System.Linq.Expressions.Expression.Lambda>(assign, param, value).Compile();"); + sb.AppendLine(" }"); + sb.AppendLine(); + } + + // Collection Name (only for root) + if (isRoot) + { + sb.AppendLine($" public override string CollectionName => \"{entity.CollectionName}\";"); + sb.AppendLine(); + } + else if (entity.Properties.All(p => !p.IsKey)) + { + sb.AppendLine( + $"// #warning Entity '{entity.Name}' has no defined primary key. Mapper may not support all features."); + } + + // Serialize Method + GenerateSerializeMethod(sb, entity, isRoot, mapperNamespace); + + sb.AppendLine(); + + // Deserialize Method + GenerateDeserializeMethod(sb, entity, isRoot, mapperNamespace); + + if (isRoot) + { + sb.AppendLine(); + GenerateIdAccessors(sb, entity); + } + + sb.AppendLine(" }"); + sb.AppendLine("#pragma warning restore CS8604"); + + return sb.ToString(); + } + + private static void GenerateSerializeMethod(StringBuilder sb, EntityInfo entity, bool isRoot, + string mapperNamespace) + { + var entityType = $"global::{entity.FullTypeName}"; + + // Always generate SerializeFields (writes only fields, no document wrapper) + // This is needed even for root entities, as they may be used as nested objects + // Note: BsonSpanWriter is a ref struct, so it must be passed by ref + sb.AppendLine( + $" public void SerializeFields({entityType} entity, ref global::ZB.MOM.WW.CBDD.Bson.BsonSpanWriter writer)"); + sb.AppendLine(" {"); + GenerateFieldWritesCore(sb, entity, mapperNamespace); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Generate Serialize method (with document wrapper) + string methodSig = isRoot + ? $"public override int Serialize({entityType} entity, global::ZB.MOM.WW.CBDD.Bson.BsonSpanWriter writer)" + : $"public int Serialize({entityType} entity, global::ZB.MOM.WW.CBDD.Bson.BsonSpanWriter writer)"; + + sb.AppendLine($" {methodSig}"); + sb.AppendLine(" {"); + sb.AppendLine(" var startingPos = writer.BeginDocument();"); + sb.AppendLine(); + GenerateFieldWritesCore(sb, entity, mapperNamespace); + sb.AppendLine(); + sb.AppendLine(" writer.EndDocument(startingPos);"); + sb.AppendLine(" return writer.Position;"); + sb.AppendLine(" }"); + } + + private static void GenerateFieldWritesCore(StringBuilder sb, EntityInfo entity, string mapperNamespace) + { + foreach (var prop in entity.Properties) + { + // Handle key property - serialize as "_id" regardless of property name + if (prop.IsKey) + { + if (prop.ConverterTypeName != null) + { + var providerProp = new PropertyInfo { TypeName = prop.ProviderTypeName ?? "string" }; + string? idWriteMethod = GetPrimitiveWriteMethod(providerProp, true); + if (idWriteMethod == "WriteString") + { + sb.AppendLine( + $" var convertedId = _idConverter.ConvertToProvider(entity.{prop.Name});"); + sb.AppendLine(" if (convertedId != null)"); + sb.AppendLine(" {"); + sb.AppendLine(" writer.WriteString(\"_id\", convertedId);"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine(" writer.WriteNull(\"_id\");"); + sb.AppendLine(" }"); + } + else + { + sb.AppendLine( + $" writer.{idWriteMethod}(\"_id\", _idConverter.ConvertToProvider(entity.{prop.Name}));"); + } + } + else + { + string? idWriteMethod = GetPrimitiveWriteMethod(prop, true); + if (idWriteMethod != null) + { + sb.AppendLine($" writer.{idWriteMethod}(\"_id\", entity.{prop.Name});"); + } + else + { + sb.AppendLine( + $"#warning Unsupported Id type for '{prop.Name}': {prop.TypeName}. Serialization of '_id' will fail."); + sb.AppendLine($" // Unsupported Id type: {prop.TypeName}"); + } + } + + continue; + } + + GenerateValidation(sb, prop); + GenerateWriteProperty(sb, prop, mapperNamespace); + } + } + + private static void GenerateValidation(StringBuilder sb, PropertyInfo prop) + { + bool isString = prop.TypeName == "string" || prop.TypeName == "String"; + + if (prop.IsRequired) + { + if (isString) + sb.AppendLine( + $" if (string.IsNullOrEmpty(entity.{prop.Name})) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is required.\");"); + else if (prop.IsNullable) + sb.AppendLine( + $" if (entity.{prop.Name} == null) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is required.\");"); + } + + if (prop.MaxLength.HasValue && isString) + sb.AppendLine( + $" if ((entity.{prop.Name}?.Length ?? 0) > {prop.MaxLength}) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} exceeds max length {prop.MaxLength}.\");"); + if (prop.MinLength.HasValue && isString) + sb.AppendLine( + $" if ((entity.{prop.Name}?.Length ?? 0) < {prop.MinLength}) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is below min length {prop.MinLength}.\");"); + + if (prop.RangeMin.HasValue || prop.RangeMax.HasValue) + { + string minStr = prop.RangeMin?.ToString(CultureInfo.InvariantCulture) ?? "double.MinValue"; + string maxStr = prop.RangeMax?.ToString(CultureInfo.InvariantCulture) ?? "double.MaxValue"; + sb.AppendLine( + $" if ((double)entity.{prop.Name} < {minStr} || (double)entity.{prop.Name} > {maxStr}) throw new global::System.ComponentModel.DataAnnotations.ValidationException(\"Property {prop.Name} is outside range [{minStr}, {maxStr}].\");"); + } + } + + private static void GenerateWriteProperty(StringBuilder sb, PropertyInfo prop, string mapperNamespace) + { + string fieldName = prop.BsonFieldName; + + if (prop.IsCollection) + { + // Add null check for nullable collections + if (prop.IsNullable) { sb.AppendLine($" if (entity.{prop.Name} != null)"); - sb.AppendLine($" {{"); - sb.AppendLine($" var {prop.Name.ToLower()}Pos = writer.BeginDocument(\"{fieldName}\");"); - var nestedMapperType = GetMapperName(prop.NestedTypeFullName!); - sb.AppendLine($" var {prop.Name.ToLower()}Mapper = new global::{mapperNamespace}.{nestedMapperType}();"); - sb.AppendLine($" {prop.Name.ToLower()}Mapper.SerializeFields(entity.{prop.Name}, ref writer);"); - sb.AppendLine($" writer.EndDocument({prop.Name.ToLower()}Pos);"); - sb.AppendLine($" }}"); - sb.AppendLine($" else"); - sb.AppendLine($" {{"); - sb.AppendLine($" writer.WriteNull(\"{fieldName}\");"); - sb.AppendLine($" }}"); + sb.AppendLine(" {"); + } + + var arrayVar = $"{prop.Name.ToLower()}Array"; + string indent = prop.IsNullable ? " " : ""; + sb.AppendLine($" {indent}var {arrayVar}Pos = writer.BeginArray(\"{fieldName}\");"); + sb.AppendLine($" {indent}var {prop.Name.ToLower()}Index = 0;"); + sb.AppendLine($" {indent}foreach (var item in entity.{prop.Name})"); + sb.AppendLine($" {indent}{{"); + + + if (prop.IsCollectionItemNested) + { + sb.AppendLine($" {indent} // Nested Object in List"); + string nestedMapperTypes = GetMapperName(prop.NestedTypeFullName!); + sb.AppendLine( + $" {indent} var {prop.Name.ToLower()}ItemMapper = new global::{mapperNamespace}.{nestedMapperTypes}();"); + + sb.AppendLine( + $" {indent} var itemStartPos = writer.BeginDocument({prop.Name.ToLower()}Index.ToString());"); + sb.AppendLine( + $" {indent} {prop.Name.ToLower()}ItemMapper.SerializeFields(item, ref writer);"); + sb.AppendLine($" {indent} writer.EndDocument(itemStartPos);"); } else { - var writeMethod = GetPrimitiveWriteMethod(prop, allowKey: false); + // Simplified: pass a dummy PropertyInfo with the item type for primitive collection items + var dummyProp = new PropertyInfo { TypeName = prop.CollectionItemType! }; + string? writeMethod = GetPrimitiveWriteMethod(dummyProp); if (writeMethod != null) + sb.AppendLine( + $" {indent} writer.{writeMethod}({prop.Name.ToLower()}Index.ToString(), item);"); + } + + sb.AppendLine($" {indent} {prop.Name.ToLower()}Index++;"); + + sb.AppendLine($" {indent}}}"); + sb.AppendLine($" {indent}writer.EndArray({arrayVar}Pos);"); + + // Close the null check if block + if (prop.IsNullable) + { + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine($" writer.WriteNull(\"{fieldName}\");"); + sb.AppendLine(" }"); + } + } + else if (prop.IsNestedObject) + { + sb.AppendLine($" if (entity.{prop.Name} != null)"); + sb.AppendLine(" {"); + sb.AppendLine($" var {prop.Name.ToLower()}Pos = writer.BeginDocument(\"{fieldName}\");"); + string nestedMapperType = GetMapperName(prop.NestedTypeFullName!); + sb.AppendLine( + $" var {prop.Name.ToLower()}Mapper = new global::{mapperNamespace}.{nestedMapperType}();"); + sb.AppendLine( + $" {prop.Name.ToLower()}Mapper.SerializeFields(entity.{prop.Name}, ref writer);"); + sb.AppendLine($" writer.EndDocument({prop.Name.ToLower()}Pos);"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine($" writer.WriteNull(\"{fieldName}\");"); + sb.AppendLine(" }"); + } + else + { + string? writeMethod = GetPrimitiveWriteMethod(prop); + if (writeMethod != null) + { + if (prop.IsNullable || prop.TypeName == "string" || prop.TypeName == "String") { - if (prop.IsNullable || prop.TypeName == "string" || prop.TypeName == "String") - { - sb.AppendLine($" if (entity.{prop.Name} != null)"); - sb.AppendLine($" {{"); - // For nullable value types, use .Value to unwrap - // String is a reference type and doesn't need .Value - var isValueTypeNullable = prop.IsNullable && IsValueType(prop.TypeName); - var valueAccess = isValueTypeNullable - ? $"entity.{prop.Name}.Value" - : $"entity.{prop.Name}"; - sb.AppendLine($" writer.{writeMethod}(\"{fieldName}\", {valueAccess});"); - sb.AppendLine($" }}"); - sb.AppendLine($" else"); - sb.AppendLine($" {{"); - sb.AppendLine($" writer.WriteNull(\"{fieldName}\");"); - sb.AppendLine($" }}"); - } - else - { - sb.AppendLine($" writer.{writeMethod}(\"{fieldName}\", entity.{prop.Name});"); - } + sb.AppendLine($" if (entity.{prop.Name} != null)"); + sb.AppendLine(" {"); + // For nullable value types, use .Value to unwrap + // String is a reference type and doesn't need .Value + bool isValueTypeNullable = prop.IsNullable && IsValueType(prop.TypeName); + string valueAccess = isValueTypeNullable + ? $"entity.{prop.Name}.Value" + : $"entity.{prop.Name}"; + sb.AppendLine($" writer.{writeMethod}(\"{fieldName}\", {valueAccess});"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine($" writer.WriteNull(\"{fieldName}\");"); + sb.AppendLine(" }"); } else { - sb.AppendLine($"#warning Property '{prop.Name}' of type '{prop.TypeName}' is not directly supported and has no converter. It will be skipped during serialization."); - sb.AppendLine($" // Unsupported type: {prop.TypeName} for {prop.Name}"); + sb.AppendLine($" writer.{writeMethod}(\"{fieldName}\", entity.{prop.Name});"); } } - } - - private static void GenerateDeserializeMethod(StringBuilder sb, EntityInfo entity, bool isRoot, string mapperNamespace) - { - var entityType = $"global::{entity.FullTypeName}"; - var needsReflection = entity.HasPrivateSetters || entity.HasPrivateOrNoConstructor; - - // Always generate a public Deserialize method that accepts ref (for nested/internal usage) - GenerateDeserializeCore(sb, entity, entityType, needsReflection, mapperNamespace); - - // For root entities, also generate the override without ref that calls the ref version - if (isRoot) - { - sb.AppendLine(); - sb.AppendLine($" public override {entityType} Deserialize(global::ZB.MOM.WW.CBDD.Bson.BsonSpanReader reader)"); - sb.AppendLine($" {{"); - sb.AppendLine($" return Deserialize(ref reader);"); - sb.AppendLine($" }}"); - } - } - - private static void GenerateDeserializeCore(StringBuilder sb, EntityInfo entity, string entityType, bool needsReflection, string mapperNamespace) - { - // Public method that always accepts ref for internal/nested usage - sb.AppendLine($" public {entityType} Deserialize(ref global::ZB.MOM.WW.CBDD.Bson.BsonSpanReader reader)"); - sb.AppendLine($" {{"); - // Use object initializer if possible or constructor, but for now standard new() - // To support required properties, we might need a different approach or verify if source generators can detect required. - // For now, let's assume standard creation and property setting. - // If required properties are present, compiling 'new T()' might fail if they aren't set in initializer. - // Alternative: Deserialize into temporary variables then construct. - - // Declare temp variables for all properties - foreach (var prop in entity.Properties) - { - var baseType = QualifyType(prop.TypeName.TrimEnd('?')); - - // Handle collections init - if (prop.IsCollection) - { - var itemType = prop.CollectionItemType; - if (prop.IsCollectionItemNested) itemType = $"global::{prop.NestedTypeFullName}"; // Use full name with global:: - sb.AppendLine($" var {prop.Name.ToLower()} = new global::System.Collections.Generic.List<{itemType}>();"); - } - else - { - sb.AppendLine($" {baseType}? {prop.Name.ToLower()} = default;"); - } - } - - - // Read document size and track boundaries - sb.AppendLine($" var docSize = reader.ReadDocumentSize();"); - sb.AppendLine($" var docEndPos = reader.Position + docSize - 4; // -4 because size includes itself"); - sb.AppendLine(); - sb.AppendLine($" while (reader.Position < docEndPos)"); - sb.AppendLine($" {{"); - sb.AppendLine($" var bsonType = reader.ReadBsonType();"); - sb.AppendLine($" if (bsonType == global::ZB.MOM.WW.CBDD.Bson.BsonType.EndOfDocument) break;"); - sb.AppendLine(); - sb.AppendLine($" var elementName = reader.ReadElementHeader();"); - sb.AppendLine($" switch (elementName)"); - sb.AppendLine($" {{"); - - foreach (var prop in entity.Properties) - { - var caseName = prop.IsKey ? "_id" : prop.BsonFieldName; - sb.AppendLine($" case \"{caseName}\":"); - - // Read Logic -> assign to local var - GenerateReadPropertyToLocal(sb, prop, "bsonType", mapperNamespace); - - sb.AppendLine($" break;"); - } - - sb.AppendLine($" default:"); - sb.AppendLine($" reader.SkipValue(bsonType);"); - sb.AppendLine($" break;"); - sb.AppendLine($" }}"); - sb.AppendLine($" }}"); - sb.AppendLine(); - - // Construct object - different approach if needs reflection - if (needsReflection) - { - // Use GetUninitializedObject + Expression Trees for private setters - sb.AppendLine($" // Creating instance without calling constructor (has private members)"); - sb.AppendLine($" var entity = (global::{entity.FullTypeName})global::System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(global::{entity.FullTypeName}));"); - sb.AppendLine(); - - // Set properties using setters (Expression Trees for private, direct for public) - foreach (var prop in entity.Properties) - { - var varName = prop.Name.ToLower(); - var propValue = varName; - - if (prop.IsCollection) - { - // Convert to appropriate collection type - if (prop.IsArray) - { - propValue += ".ToArray()"; - } - else if (prop.CollectionConcreteTypeName != null) - { - var concreteType = prop.CollectionConcreteTypeName; - var itemType = prop.IsCollectionItemNested ? $"global::{prop.NestedTypeFullName}" : prop.CollectionItemType; - - if (concreteType.Contains("HashSet")) - propValue = $"new global::System.Collections.Generic.HashSet<{itemType}>({propValue})"; - else if (concreteType.Contains("ISet")) - propValue = $"new global::System.Collections.Generic.HashSet<{itemType}>({propValue})"; - else if (concreteType.Contains("LinkedList")) - propValue = $"new global::System.Collections.Generic.LinkedList<{itemType}>({propValue})"; - else if (concreteType.Contains("Queue")) - propValue = $"new global::System.Collections.Generic.Queue<{itemType}>({propValue})"; - else if (concreteType.Contains("Stack")) - propValue = $"new global::System.Collections.Generic.Stack<{itemType}>({propValue})"; - else if (concreteType.Contains("IReadOnlyList") || concreteType.Contains("IReadOnlyCollection")) - propValue += ".AsReadOnly()"; - } - } - - // Use appropriate setter - if ((!prop.HasPublicSetter && prop.HasAnySetter) || prop.HasInitOnlySetter) - { - // Use Expression Tree setter (for private or init-only setters) - sb.AppendLine($" _setter_{prop.Name}(entity, {propValue} ?? default!);"); - } - else - { - // Direct property assignment - sb.AppendLine($" entity.{prop.Name} = {propValue} ?? default!;"); - } - } - sb.AppendLine(); - sb.AppendLine($" return entity;"); - } else { - // Standard object initializer approach - sb.AppendLine($" return new {entityType}"); - sb.AppendLine($" {{"); - foreach (var prop in entity.Properties) - { - var val = prop.Name.ToLower(); - if (prop.IsCollection) - { - // Convert to appropriate collection type - if (prop.IsArray) - { - val += ".ToArray()"; - } - else if (prop.CollectionConcreteTypeName != null) - { - var concreteType = prop.CollectionConcreteTypeName; - var itemType = prop.IsCollectionItemNested ? $"global::{prop.NestedTypeFullName}" : prop.CollectionItemType; - - // Check if it needs conversion from List - if (concreteType.Contains("HashSet")) - { - val = $"new global::System.Collections.Generic.HashSet<{itemType}>({val})"; - } - else if (concreteType.Contains("ISet")) - { - val = $"new global::System.Collections.Generic.HashSet<{itemType}>({val})"; - } - else if (concreteType.Contains("LinkedList")) - { - val = $"new global::System.Collections.Generic.LinkedList<{itemType}>({val})"; - } - else if (concreteType.Contains("Queue")) - { - val = $"new global::System.Collections.Generic.Queue<{itemType}>({val})"; - } - else if (concreteType.Contains("Stack")) - { - val = $"new global::System.Collections.Generic.Stack<{itemType}>({val})"; - } - else if (concreteType.Contains("IReadOnlyList") || concreteType.Contains("IReadOnlyCollection")) - { - val += ".AsReadOnly()"; - } - // Otherwise keep as List (works for List, IList, ICollection, IEnumerable) - } - } - // For nullable properties, don't use ?? default! since null is a valid value - if (prop.IsNullable) - { - sb.AppendLine($" {prop.Name} = {val},"); - } - else - { - sb.AppendLine($" {prop.Name} = {val} ?? default!,"); - } - } - sb.AppendLine($" }};"); - } - sb.AppendLine($" }}"); - } - - private static void GenerateReadPropertyToLocal(StringBuilder sb, PropertyInfo prop, string bsonTypeVar, string mapperNamespace) - { - var localVar = prop.Name.ToLower(); - - if (prop.IsCollection) - { - var arrVar = prop.Name.ToLower(); - sb.AppendLine($" // Read Array {prop.Name}"); - sb.AppendLine($" var {arrVar}ArrSize = reader.ReadDocumentSize();"); - sb.AppendLine($" var {arrVar}ArrEndPos = reader.Position + {arrVar}ArrSize - 4;"); - sb.AppendLine($" while (reader.Position < {arrVar}ArrEndPos)"); - sb.AppendLine($" {{"); - sb.AppendLine($" var itemType = reader.ReadBsonType();"); - sb.AppendLine($" if (itemType == global::ZB.MOM.WW.CBDD.Bson.BsonType.EndOfDocument) break;"); - sb.AppendLine($" reader.ReadElementHeader(); // Skip index key"); - - if (prop.IsCollectionItemNested) - { - var nestedMapperTypes = GetMapperName(prop.NestedTypeFullName!); - sb.AppendLine($" var {prop.Name.ToLower()}ItemMapper = new global::{mapperNamespace}.{nestedMapperTypes}();"); - sb.AppendLine($" var item = {prop.Name.ToLower()}ItemMapper.Deserialize(ref reader);"); - sb.AppendLine($" {localVar}.Add(item);"); - } - else - { - var readMethod = GetPrimitiveReadMethod(new PropertyInfo { TypeName = prop.CollectionItemType! }); - if (readMethod != null) - { - var cast = (prop.CollectionItemType == "float" || prop.CollectionItemType == "Single") ? "(float)" : ""; - sb.AppendLine($" var item = {cast}reader.{readMethod}();"); - sb.AppendLine($" {localVar}.Add(item);"); - } - else - { - sb.AppendLine($" reader.SkipValue(itemType);"); - } - } - sb.AppendLine($" }}"); - } - else if (prop.IsKey && prop.ConverterTypeName != null) - { - var providerProp = new PropertyInfo { TypeName = prop.ProviderTypeName ?? "string" }; - var readMethod = GetPrimitiveReadMethod(providerProp); - sb.AppendLine($" {localVar} = _idConverter.ConvertFromProvider(reader.{readMethod}());"); - } - else if (prop.IsNestedObject) - { - sb.AppendLine($" if ({bsonTypeVar} == global::ZB.MOM.WW.CBDD.Bson.BsonType.Null)"); - sb.AppendLine($" {{"); - sb.AppendLine($" {localVar} = null;"); - sb.AppendLine($" }}"); - sb.AppendLine($" else"); - sb.AppendLine($" {{"); - var nestedMapperType = GetMapperName(prop.NestedTypeFullName!); - sb.AppendLine($" var {prop.Name.ToLower()}Mapper = new global::{mapperNamespace}.{nestedMapperType}();"); - sb.AppendLine($" {localVar} = {prop.Name.ToLower()}Mapper.Deserialize(ref reader);"); - sb.AppendLine($" }}"); - } - else - { - var readMethod = GetPrimitiveReadMethod(prop); - if (readMethod != null) - { - var cast = (prop.TypeName == "float" || prop.TypeName == "Single") ? "(float)" : ""; - - // Handle nullable types - check for null in BSON stream - if (prop.IsNullable) - { - sb.AppendLine($" if ({bsonTypeVar} == global::ZB.MOM.WW.CBDD.Bson.BsonType.Null)"); - sb.AppendLine($" {{"); - sb.AppendLine($" {localVar} = null;"); - sb.AppendLine($" }}"); - sb.AppendLine($" else"); - sb.AppendLine($" {{"); - sb.AppendLine($" {localVar} = {cast}reader.{readMethod}();"); - sb.AppendLine($" }}"); - } - else - { - sb.AppendLine($" {localVar} = {cast}reader.{readMethod}();"); - } - } - else - { - sb.AppendLine($" reader.SkipValue({bsonTypeVar});"); - } - } - } - - /// - /// Gets a deterministic mapper type name from a fully qualified type name. - /// - /// The fully qualified entity type name. - /// The generated mapper type name. - public static string GetMapperName(string fullTypeName) - { - if (string.IsNullOrEmpty(fullTypeName)) return "UnknownMapper"; - // Remove global:: prefix - var cleanName = fullTypeName.Replace("global::", ""); - // Replace dots, plus (nested classes), and colons (global::) with underscores - return cleanName.Replace(".", "_").Replace("+", "_").Replace(":", "_") + "Mapper"; - } - - private static void GenerateIdAccessors(StringBuilder sb, EntityInfo entity) - { - var keyProp = entity.Properties.FirstOrDefault(p => p.IsKey); - - // Use CollectionIdTypeFullName if available (from DocumentCollection declaration) - string keyType; - if (!string.IsNullOrEmpty(entity.CollectionIdTypeFullName)) - { - // Remove "global::" prefix if present - keyType = entity.CollectionIdTypeFullName!.Replace("global::", ""); - } - else - { - keyType = keyProp?.TypeName ?? "ObjectId"; - } - - // Normalize keyType - remove nullable suffix for the methods - // We expect Id to have a value during serialization/deserialization - keyType = keyType.TrimEnd('?'); - - // Normalize keyType - switch (keyType) - { - case "Int32": keyType = "int"; break; - case "Int64": keyType = "long"; break; - case "String": keyType = "string"; break; - case "Double": keyType = "double"; break; - case "Boolean": keyType = "bool"; break; - case "Decimal": keyType = "decimal"; break; - case "Guid": keyType = "global::System.Guid"; break; - case "DateTime": keyType = "global::System.DateTime"; break; - case "ObjectId": keyType = "global::ZB.MOM.WW.CBDD.Bson.ObjectId"; break; - } - - var entityType = $"global::{entity.FullTypeName}"; - var qualifiedKeyType = keyType.StartsWith("global::") ? keyType : (keyProp?.ConverterTypeName != null ? $"global::{keyProp.TypeName.TrimEnd('?')}" : keyType); - - var propName = keyProp?.Name ?? "Id"; - - // GetId can return nullable if the property is nullable, but we add ! to assert non-null - // This helps catch bugs where entities are created without an Id - if (keyProp?.IsNullable == true) - { - sb.AppendLine($" public override {qualifiedKeyType} GetId({entityType} entity) => entity.{propName}!;"); - } - else - { - sb.AppendLine($" public override {qualifiedKeyType} GetId({entityType} entity) => entity.{propName};"); - } - - // If the ID property has a private or init-only setter, use the compiled setter - if (entity.HasPrivateSetters && keyProp != null && (!keyProp.HasPublicSetter || keyProp.HasInitOnlySetter)) - { - sb.AppendLine($" public override void SetId({entityType} entity, {qualifiedKeyType} id) => _setter_{propName}(entity, id);"); - } - else - { - sb.AppendLine($" public override void SetId({entityType} entity, {qualifiedKeyType} id) => entity.{propName} = id;"); - } - - if (keyProp?.ConverterTypeName != null) - { - var providerType = keyProp.ProviderTypeName ?? "string"; - // Normalize providerType - switch (providerType) - { - case "Int32": providerType = "int"; break; - case "Int64": providerType = "long"; break; - case "String": providerType = "string"; break; - case "Guid": providerType = "global::System.Guid"; break; - case "ObjectId": providerType = "global::ZB.MOM.WW.CBDD.Bson.ObjectId"; break; - } - - sb.AppendLine(); - sb.AppendLine($" public override global::ZB.MOM.WW.CBDD.Core.Indexing.IndexKey ToIndexKey({qualifiedKeyType} id) => "); - sb.AppendLine($" global::ZB.MOM.WW.CBDD.Core.Indexing.IndexKey.Create(_idConverter.ConvertToProvider(id));"); - sb.AppendLine(); - sb.AppendLine($" public override {qualifiedKeyType} FromIndexKey(global::ZB.MOM.WW.CBDD.Core.Indexing.IndexKey key) => "); - sb.AppendLine($" _idConverter.ConvertFromProvider(key.As<{providerType}>());"); - } - } - - private static string GetBaseMapperClass(PropertyInfo? keyProp, EntityInfo entity) - { - if (keyProp?.ConverterTypeName != null) - { - return $"DocumentMapperBase declaration) - string keyType; - if (!string.IsNullOrEmpty(entity.CollectionIdTypeFullName)) - { - // Remove "global::" prefix if present - keyType = entity.CollectionIdTypeFullName!.Replace("global::", ""); - } - else - { - keyType = keyProp?.TypeName ?? "ObjectId"; - } - - // Normalize type by removing nullable suffix (?) for comparison - // At serialization time, we expect the Id to always have a value - var normalizedKeyType = keyType.TrimEnd('?'); - - if (normalizedKeyType.EndsWith("Int32") || normalizedKeyType == "int") return "Int32MapperBase<"; - if (normalizedKeyType.EndsWith("Int64") || normalizedKeyType == "long") return "Int64MapperBase<"; - if (normalizedKeyType.EndsWith("String") || normalizedKeyType == "string") return "StringMapperBase<"; - if (normalizedKeyType.EndsWith("Guid")) return "GuidMapperBase<"; - if (normalizedKeyType.EndsWith("ObjectId")) return "ObjectIdMapperBase<"; - - return "ObjectIdMapperBase<"; - } - - private static string? GetPrimitiveWriteMethod(PropertyInfo prop, bool allowKey = false) - { - var typeName = prop.TypeName; - if (prop.ColumnTypeName == "point" || prop.ColumnTypeName == "coordinate" || prop.ColumnTypeName == "geopoint") - { - return "WriteCoordinates"; - } - - if (typeName.Contains("double") && typeName.Contains(",") && typeName.StartsWith("(") && typeName.EndsWith(")")) - { - return "WriteCoordinates"; - } - - var cleanType = typeName.TrimEnd('?').Trim(); - - if (cleanType.EndsWith("Int32") || cleanType == "int") return "WriteInt32"; - if (cleanType.EndsWith("Int64") || cleanType == "long") return "WriteInt64"; - if (cleanType.EndsWith("String") || cleanType == "string") return "WriteString"; - if (cleanType.EndsWith("Boolean") || cleanType == "bool") return "WriteBoolean"; - if (cleanType.EndsWith("Single") || cleanType == "float") return "WriteDouble"; - if (cleanType.EndsWith("Double") || cleanType == "double") return "WriteDouble"; - if (cleanType.EndsWith("Decimal") || cleanType == "decimal") return "WriteDecimal128"; - if (cleanType.EndsWith("DateTime") && !cleanType.EndsWith("DateTimeOffset")) return "WriteDateTime"; - if (cleanType.EndsWith("DateTimeOffset")) return "WriteDateTimeOffset"; - if (cleanType.EndsWith("TimeSpan")) return "WriteTimeSpan"; - if (cleanType.EndsWith("DateOnly")) return "WriteDateOnly"; - if (cleanType.EndsWith("TimeOnly")) return "WriteTimeOnly"; - if (cleanType.EndsWith("Guid")) return "WriteGuid"; - if (cleanType.EndsWith("ObjectId")) return "WriteObjectId"; - - return null; - } - - private static string? GetPrimitiveReadMethod(PropertyInfo prop) - { - var typeName = prop.TypeName; - if (prop.ColumnTypeName == "point" || prop.ColumnTypeName == "coordinate" || prop.ColumnTypeName == "geopoint") - { - return "ReadCoordinates"; - } - - if (typeName.Contains("double") && typeName.Contains(",") && typeName.StartsWith("(") && typeName.EndsWith(")")) - { - return "ReadCoordinates"; - } - - var cleanType = typeName.TrimEnd('?').Trim(); - - if (cleanType.EndsWith("Int32") || cleanType == "int") return "ReadInt32"; - if (cleanType.EndsWith("Int64") || cleanType == "long") return "ReadInt64"; - if (cleanType.EndsWith("String") || cleanType == "string") return "ReadString"; - if (cleanType.EndsWith("Boolean") || cleanType == "bool") return "ReadBoolean"; - if (cleanType.EndsWith("Single") || cleanType == "float") return "ReadDouble"; - if (cleanType.EndsWith("Double") || cleanType == "double") return "ReadDouble"; - if (cleanType.EndsWith("Decimal") || cleanType == "decimal") return "ReadDecimal128"; - if (cleanType.EndsWith("DateTime") && !cleanType.EndsWith("DateTimeOffset")) return "ReadDateTime"; - if (cleanType.EndsWith("DateTimeOffset")) return "ReadDateTimeOffset"; - if (cleanType.EndsWith("TimeSpan")) return "ReadTimeSpan"; - if (cleanType.EndsWith("DateOnly")) return "ReadDateOnly"; - if (cleanType.EndsWith("TimeOnly")) return "ReadTimeOnly"; - if (cleanType.EndsWith("Guid")) return "ReadGuid"; - if (cleanType.EndsWith("ObjectId")) return "ReadObjectId"; - - return null; - } - - private static bool IsValueType(string typeName) - { - // Check if the type is a value type (struct) that requires .Value unwrapping when nullable - // String is a reference type and doesn't need .Value - var cleanType = typeName.TrimEnd('?').Trim(); - - // Common value types - if (cleanType.EndsWith("Int32") || cleanType == "int") return true; - if (cleanType.EndsWith("Int64") || cleanType == "long") return true; - if (cleanType.EndsWith("Boolean") || cleanType == "bool") return true; - if (cleanType.EndsWith("Single") || cleanType == "float") return true; - if (cleanType.EndsWith("Double") || cleanType == "double") return true; - if (cleanType.EndsWith("Decimal") || cleanType == "decimal") return true; - if (cleanType.EndsWith("DateTime")) return true; - if (cleanType.EndsWith("DateTimeOffset")) return true; - if (cleanType.EndsWith("TimeSpan")) return true; - if (cleanType.EndsWith("DateOnly")) return true; - if (cleanType.EndsWith("TimeOnly")) return true; - if (cleanType.EndsWith("Guid")) return true; - if (cleanType.EndsWith("ObjectId")) return true; - - // String and other reference types - return false; - } - - private static string QualifyType(string typeName) - { - if (string.IsNullOrEmpty(typeName)) return "object"; - if (typeName.StartsWith("global::")) return typeName; - - var isNullable = typeName.EndsWith("?"); - var baseType = typeName.TrimEnd('?').Trim(); - - if (baseType.StartsWith("(") && baseType.EndsWith(")")) return typeName; // Tuple - - switch (baseType) - { - case "int": - case "long": - case "string": - case "bool": - case "double": - case "float": - case "decimal": - case "byte": - case "sbyte": - case "short": - case "ushort": - case "uint": - case "ulong": - case "char": - case "object": - case "dynamic": - case "void": - return baseType + (isNullable ? "?" : ""); - case "Guid": return "global::System.Guid" + (isNullable ? "?" : ""); - case "DateTime": return "global::System.DateTime" + (isNullable ? "?" : ""); - case "DateTimeOffset": return "global::System.DateTimeOffset" + (isNullable ? "?" : ""); - case "TimeSpan": return "global::System.TimeSpan" + (isNullable ? "?" : ""); - case "DateOnly": return "global::System.DateOnly" + (isNullable ? "?" : ""); - case "TimeOnly": return "global::System.TimeOnly" + (isNullable ? "?" : ""); - case "ObjectId": return "global::ZB.MOM.WW.CBDD.Bson.ObjectId" + (isNullable ? "?" : ""); - default: - return $"global::{typeName}"; - } - } - - private static bool IsPrimitive(string typeName) - { - var cleanType = typeName.TrimEnd('?').Trim(); - if (cleanType.StartsWith("(") && cleanType.EndsWith(")")) return true; - - switch (cleanType) - { - case "int": - case "long": - case "string": - case "bool": - case "double": - case "float": - case "decimal": - case "byte": - case "sbyte": - case "short": - case "ushort": - case "uint": - case "ulong": - case "char": - case "object": - return true; - default: - return false; + sb.AppendLine( + $"#warning Property '{prop.Name}' of type '{prop.TypeName}' is not directly supported and has no converter. It will be skipped during serialization."); + sb.AppendLine($" // Unsupported type: {prop.TypeName} for {prop.Name}"); } } } -} + + private static void GenerateDeserializeMethod(StringBuilder sb, EntityInfo entity, bool isRoot, + string mapperNamespace) + { + var entityType = $"global::{entity.FullTypeName}"; + bool needsReflection = entity.HasPrivateSetters || entity.HasPrivateOrNoConstructor; + + // Always generate a public Deserialize method that accepts ref (for nested/internal usage) + GenerateDeserializeCore(sb, entity, entityType, needsReflection, mapperNamespace); + + // For root entities, also generate the override without ref that calls the ref version + if (isRoot) + { + sb.AppendLine(); + sb.AppendLine( + $" public override {entityType} Deserialize(global::ZB.MOM.WW.CBDD.Bson.BsonSpanReader reader)"); + sb.AppendLine(" {"); + sb.AppendLine(" return Deserialize(ref reader);"); + sb.AppendLine(" }"); + } + } + + private static void GenerateDeserializeCore(StringBuilder sb, EntityInfo entity, string entityType, + bool needsReflection, string mapperNamespace) + { + // Public method that always accepts ref for internal/nested usage + sb.AppendLine( + $" public {entityType} Deserialize(ref global::ZB.MOM.WW.CBDD.Bson.BsonSpanReader reader)"); + sb.AppendLine(" {"); + // Use object initializer if possible or constructor, but for now standard new() + // To support required properties, we might need a different approach or verify if source generators can detect required. + // For now, let's assume standard creation and property setting. + // If required properties are present, compiling 'new T()' might fail if they aren't set in initializer. + // Alternative: Deserialize into temporary variables then construct. + + // Declare temp variables for all properties + foreach (var prop in entity.Properties) + { + string baseType = QualifyType(prop.TypeName.TrimEnd('?')); + + // Handle collections init + if (prop.IsCollection) + { + string? itemType = prop.CollectionItemType; + if (prop.IsCollectionItemNested) + itemType = $"global::{prop.NestedTypeFullName}"; // Use full name with global:: + sb.AppendLine( + $" var {prop.Name.ToLower()} = new global::System.Collections.Generic.List<{itemType}>();"); + } + else + { + sb.AppendLine($" {baseType}? {prop.Name.ToLower()} = default;"); + } + } + + + // Read document size and track boundaries + sb.AppendLine(" var docSize = reader.ReadDocumentSize();"); + sb.AppendLine(" var docEndPos = reader.Position + docSize - 4; // -4 because size includes itself"); + sb.AppendLine(); + sb.AppendLine(" while (reader.Position < docEndPos)"); + sb.AppendLine(" {"); + sb.AppendLine(" var bsonType = reader.ReadBsonType();"); + sb.AppendLine(" if (bsonType == global::ZB.MOM.WW.CBDD.Bson.BsonType.EndOfDocument) break;"); + sb.AppendLine(); + sb.AppendLine(" var elementName = reader.ReadElementHeader();"); + sb.AppendLine(" switch (elementName)"); + sb.AppendLine(" {"); + + foreach (var prop in entity.Properties) + { + string caseName = prop.IsKey ? "_id" : prop.BsonFieldName; + sb.AppendLine($" case \"{caseName}\":"); + + // Read Logic -> assign to local var + GenerateReadPropertyToLocal(sb, prop, "bsonType", mapperNamespace); + + sb.AppendLine(" break;"); + } + + sb.AppendLine(" default:"); + sb.AppendLine(" reader.SkipValue(bsonType);"); + sb.AppendLine(" break;"); + sb.AppendLine(" }"); + sb.AppendLine(" }"); + sb.AppendLine(); + + // Construct object - different approach if needs reflection + if (needsReflection) + { + // Use GetUninitializedObject + Expression Trees for private setters + sb.AppendLine(" // Creating instance without calling constructor (has private members)"); + sb.AppendLine( + $" var entity = (global::{entity.FullTypeName})global::System.Runtime.CompilerServices.RuntimeHelpers.GetUninitializedObject(typeof(global::{entity.FullTypeName}));"); + sb.AppendLine(); + + // Set properties using setters (Expression Trees for private, direct for public) + foreach (var prop in entity.Properties) + { + string varName = prop.Name.ToLower(); + string propValue = varName; + + if (prop.IsCollection) + { + // Convert to appropriate collection type + if (prop.IsArray) + { + propValue += ".ToArray()"; + } + else if (prop.CollectionConcreteTypeName != null) + { + string? concreteType = prop.CollectionConcreteTypeName; + string? itemType = prop.IsCollectionItemNested + ? $"global::{prop.NestedTypeFullName}" + : prop.CollectionItemType; + + if (concreteType.Contains("HashSet")) + propValue = $"new global::System.Collections.Generic.HashSet<{itemType}>({propValue})"; + else if (concreteType.Contains("ISet")) + propValue = $"new global::System.Collections.Generic.HashSet<{itemType}>({propValue})"; + else if (concreteType.Contains("LinkedList")) + propValue = $"new global::System.Collections.Generic.LinkedList<{itemType}>({propValue})"; + else if (concreteType.Contains("Queue")) + propValue = $"new global::System.Collections.Generic.Queue<{itemType}>({propValue})"; + else if (concreteType.Contains("Stack")) + propValue = $"new global::System.Collections.Generic.Stack<{itemType}>({propValue})"; + else if (concreteType.Contains("IReadOnlyList") || concreteType.Contains("IReadOnlyCollection")) + propValue += ".AsReadOnly()"; + } + } + + // Use appropriate setter + if ((!prop.HasPublicSetter && prop.HasAnySetter) || prop.HasInitOnlySetter) + // Use Expression Tree setter (for private or init-only setters) + sb.AppendLine($" _setter_{prop.Name}(entity, {propValue} ?? default!);"); + else + // Direct property assignment + sb.AppendLine($" entity.{prop.Name} = {propValue} ?? default!;"); + } + + sb.AppendLine(); + sb.AppendLine(" return entity;"); + } + else + { + // Standard object initializer approach + sb.AppendLine($" return new {entityType}"); + sb.AppendLine(" {"); + foreach (var prop in entity.Properties) + { + string val = prop.Name.ToLower(); + if (prop.IsCollection) + { + // Convert to appropriate collection type + if (prop.IsArray) + { + val += ".ToArray()"; + } + else if (prop.CollectionConcreteTypeName != null) + { + string? concreteType = prop.CollectionConcreteTypeName; + string? itemType = prop.IsCollectionItemNested + ? $"global::{prop.NestedTypeFullName}" + : prop.CollectionItemType; + + // Check if it needs conversion from List + if (concreteType.Contains("HashSet")) + val = $"new global::System.Collections.Generic.HashSet<{itemType}>({val})"; + else if (concreteType.Contains("ISet")) + val = $"new global::System.Collections.Generic.HashSet<{itemType}>({val})"; + else if (concreteType.Contains("LinkedList")) + val = $"new global::System.Collections.Generic.LinkedList<{itemType}>({val})"; + else if (concreteType.Contains("Queue")) + val = $"new global::System.Collections.Generic.Queue<{itemType}>({val})"; + else if (concreteType.Contains("Stack")) + val = $"new global::System.Collections.Generic.Stack<{itemType}>({val})"; + else if (concreteType.Contains("IReadOnlyList") || concreteType.Contains("IReadOnlyCollection")) + val += ".AsReadOnly()"; + // Otherwise keep as List (works for List, IList, ICollection, IEnumerable) + } + } + + // For nullable properties, don't use ?? default! since null is a valid value + if (prop.IsNullable) + sb.AppendLine($" {prop.Name} = {val},"); + else + sb.AppendLine($" {prop.Name} = {val} ?? default!,"); + } + + sb.AppendLine(" };"); + } + + sb.AppendLine(" }"); + } + + private static void GenerateReadPropertyToLocal(StringBuilder sb, PropertyInfo prop, string bsonTypeVar, + string mapperNamespace) + { + string localVar = prop.Name.ToLower(); + + if (prop.IsCollection) + { + string arrVar = prop.Name.ToLower(); + sb.AppendLine($" // Read Array {prop.Name}"); + sb.AppendLine($" var {arrVar}ArrSize = reader.ReadDocumentSize();"); + sb.AppendLine($" var {arrVar}ArrEndPos = reader.Position + {arrVar}ArrSize - 4;"); + sb.AppendLine($" while (reader.Position < {arrVar}ArrEndPos)"); + sb.AppendLine(" {"); + sb.AppendLine(" var itemType = reader.ReadBsonType();"); + sb.AppendLine( + " if (itemType == global::ZB.MOM.WW.CBDD.Bson.BsonType.EndOfDocument) break;"); + sb.AppendLine(" reader.ReadElementHeader(); // Skip index key"); + + if (prop.IsCollectionItemNested) + { + string nestedMapperTypes = GetMapperName(prop.NestedTypeFullName!); + sb.AppendLine( + $" var {prop.Name.ToLower()}ItemMapper = new global::{mapperNamespace}.{nestedMapperTypes}();"); + sb.AppendLine( + $" var item = {prop.Name.ToLower()}ItemMapper.Deserialize(ref reader);"); + sb.AppendLine($" {localVar}.Add(item);"); + } + else + { + string? readMethod = GetPrimitiveReadMethod(new PropertyInfo { TypeName = prop.CollectionItemType! }); + if (readMethod != null) + { + string cast = prop.CollectionItemType == "float" || prop.CollectionItemType == "Single" + ? "(float)" + : ""; + sb.AppendLine($" var item = {cast}reader.{readMethod}();"); + sb.AppendLine($" {localVar}.Add(item);"); + } + else + { + sb.AppendLine(" reader.SkipValue(itemType);"); + } + } + + sb.AppendLine(" }"); + } + else if (prop.IsKey && prop.ConverterTypeName != null) + { + var providerProp = new PropertyInfo { TypeName = prop.ProviderTypeName ?? "string" }; + string? readMethod = GetPrimitiveReadMethod(providerProp); + sb.AppendLine( + $" {localVar} = _idConverter.ConvertFromProvider(reader.{readMethod}());"); + } + else if (prop.IsNestedObject) + { + sb.AppendLine($" if ({bsonTypeVar} == global::ZB.MOM.WW.CBDD.Bson.BsonType.Null)"); + sb.AppendLine(" {"); + sb.AppendLine($" {localVar} = null;"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + string nestedMapperType = GetMapperName(prop.NestedTypeFullName!); + sb.AppendLine( + $" var {prop.Name.ToLower()}Mapper = new global::{mapperNamespace}.{nestedMapperType}();"); + sb.AppendLine( + $" {localVar} = {prop.Name.ToLower()}Mapper.Deserialize(ref reader);"); + sb.AppendLine(" }"); + } + else + { + string? readMethod = GetPrimitiveReadMethod(prop); + if (readMethod != null) + { + string cast = prop.TypeName == "float" || prop.TypeName == "Single" ? "(float)" : ""; + + // Handle nullable types - check for null in BSON stream + if (prop.IsNullable) + { + sb.AppendLine( + $" if ({bsonTypeVar} == global::ZB.MOM.WW.CBDD.Bson.BsonType.Null)"); + sb.AppendLine(" {"); + sb.AppendLine($" {localVar} = null;"); + sb.AppendLine(" }"); + sb.AppendLine(" else"); + sb.AppendLine(" {"); + sb.AppendLine($" {localVar} = {cast}reader.{readMethod}();"); + sb.AppendLine(" }"); + } + else + { + sb.AppendLine($" {localVar} = {cast}reader.{readMethod}();"); + } + } + else + { + sb.AppendLine($" reader.SkipValue({bsonTypeVar});"); + } + } + } + + /// + /// Gets a deterministic mapper type name from a fully qualified type name. + /// + /// The fully qualified entity type name. + /// The generated mapper type name. + public static string GetMapperName(string fullTypeName) + { + if (string.IsNullOrEmpty(fullTypeName)) return "UnknownMapper"; + // Remove global:: prefix + string cleanName = fullTypeName.Replace("global::", ""); + // Replace dots, plus (nested classes), and colons (global::) with underscores + return cleanName.Replace(".", "_").Replace("+", "_").Replace(":", "_") + "Mapper"; + } + + private static void GenerateIdAccessors(StringBuilder sb, EntityInfo entity) + { + var keyProp = entity.Properties.FirstOrDefault(p => p.IsKey); + + // Use CollectionIdTypeFullName if available (from DocumentCollection declaration) + string keyType; + if (!string.IsNullOrEmpty(entity.CollectionIdTypeFullName)) + // Remove "global::" prefix if present + keyType = entity.CollectionIdTypeFullName!.Replace("global::", ""); + else + keyType = keyProp?.TypeName ?? "ObjectId"; + + // Normalize keyType - remove nullable suffix for the methods + // We expect Id to have a value during serialization/deserialization + keyType = keyType.TrimEnd('?'); + + // Normalize keyType + switch (keyType) + { + case "Int32": keyType = "int"; break; + case "Int64": keyType = "long"; break; + case "String": keyType = "string"; break; + case "Double": keyType = "double"; break; + case "Boolean": keyType = "bool"; break; + case "Decimal": keyType = "decimal"; break; + case "Guid": keyType = "global::System.Guid"; break; + case "DateTime": keyType = "global::System.DateTime"; break; + case "ObjectId": keyType = "global::ZB.MOM.WW.CBDD.Bson.ObjectId"; break; + } + + var entityType = $"global::{entity.FullTypeName}"; + string qualifiedKeyType = keyType.StartsWith("global::") ? keyType : + keyProp?.ConverterTypeName != null ? $"global::{keyProp.TypeName.TrimEnd('?')}" : keyType; + + string propName = keyProp?.Name ?? "Id"; + + // GetId can return nullable if the property is nullable, but we add ! to assert non-null + // This helps catch bugs where entities are created without an Id + if (keyProp?.IsNullable == true) + sb.AppendLine( + $" public override {qualifiedKeyType} GetId({entityType} entity) => entity.{propName}!;"); + else + sb.AppendLine( + $" public override {qualifiedKeyType} GetId({entityType} entity) => entity.{propName};"); + + // If the ID property has a private or init-only setter, use the compiled setter + if (entity.HasPrivateSetters && keyProp != null && (!keyProp.HasPublicSetter || keyProp.HasInitOnlySetter)) + sb.AppendLine( + $" public override void SetId({entityType} entity, {qualifiedKeyType} id) => _setter_{propName}(entity, id);"); + else + sb.AppendLine( + $" public override void SetId({entityType} entity, {qualifiedKeyType} id) => entity.{propName} = id;"); + + if (keyProp?.ConverterTypeName != null) + { + string providerType = keyProp.ProviderTypeName ?? "string"; + // Normalize providerType + switch (providerType) + { + case "Int32": providerType = "int"; break; + case "Int64": providerType = "long"; break; + case "String": providerType = "string"; break; + case "Guid": providerType = "global::System.Guid"; break; + case "ObjectId": providerType = "global::ZB.MOM.WW.CBDD.Bson.ObjectId"; break; + } + + sb.AppendLine(); + sb.AppendLine( + $" public override global::ZB.MOM.WW.CBDD.Core.Indexing.IndexKey ToIndexKey({qualifiedKeyType} id) => "); + sb.AppendLine( + " global::ZB.MOM.WW.CBDD.Core.Indexing.IndexKey.Create(_idConverter.ConvertToProvider(id));"); + sb.AppendLine(); + sb.AppendLine( + $" public override {qualifiedKeyType} FromIndexKey(global::ZB.MOM.WW.CBDD.Core.Indexing.IndexKey key) => "); + sb.AppendLine($" _idConverter.ConvertFromProvider(key.As<{providerType}>());"); + } + } + + private static string GetBaseMapperClass(PropertyInfo? keyProp, EntityInfo entity) + { + if (keyProp?.ConverterTypeName != null) return $"DocumentMapperBase declaration) + string keyType; + if (!string.IsNullOrEmpty(entity.CollectionIdTypeFullName)) + // Remove "global::" prefix if present + keyType = entity.CollectionIdTypeFullName!.Replace("global::", ""); + else + keyType = keyProp?.TypeName ?? "ObjectId"; + + // Normalize type by removing nullable suffix (?) for comparison + // At serialization time, we expect the Id to always have a value + string normalizedKeyType = keyType.TrimEnd('?'); + + if (normalizedKeyType.EndsWith("Int32") || normalizedKeyType == "int") return "Int32MapperBase<"; + if (normalizedKeyType.EndsWith("Int64") || normalizedKeyType == "long") return "Int64MapperBase<"; + if (normalizedKeyType.EndsWith("String") || normalizedKeyType == "string") return "StringMapperBase<"; + if (normalizedKeyType.EndsWith("Guid")) return "GuidMapperBase<"; + if (normalizedKeyType.EndsWith("ObjectId")) return "ObjectIdMapperBase<"; + + return "ObjectIdMapperBase<"; + } + + private static string? GetPrimitiveWriteMethod(PropertyInfo prop, bool allowKey = false) + { + string typeName = prop.TypeName; + if (prop.ColumnTypeName == "point" || prop.ColumnTypeName == "coordinate" || prop.ColumnTypeName == "geopoint") + return "WriteCoordinates"; + + if (typeName.Contains("double") && typeName.Contains(",") && typeName.StartsWith("(") && typeName.EndsWith(")")) + return "WriteCoordinates"; + + string cleanType = typeName.TrimEnd('?').Trim(); + + if (cleanType.EndsWith("Int32") || cleanType == "int") return "WriteInt32"; + if (cleanType.EndsWith("Int64") || cleanType == "long") return "WriteInt64"; + if (cleanType.EndsWith("String") || cleanType == "string") return "WriteString"; + if (cleanType.EndsWith("Boolean") || cleanType == "bool") return "WriteBoolean"; + if (cleanType.EndsWith("Single") || cleanType == "float") return "WriteDouble"; + if (cleanType.EndsWith("Double") || cleanType == "double") return "WriteDouble"; + if (cleanType.EndsWith("Decimal") || cleanType == "decimal") return "WriteDecimal128"; + if (cleanType.EndsWith("DateTime") && !cleanType.EndsWith("DateTimeOffset")) return "WriteDateTime"; + if (cleanType.EndsWith("DateTimeOffset")) return "WriteDateTimeOffset"; + if (cleanType.EndsWith("TimeSpan")) return "WriteTimeSpan"; + if (cleanType.EndsWith("DateOnly")) return "WriteDateOnly"; + if (cleanType.EndsWith("TimeOnly")) return "WriteTimeOnly"; + if (cleanType.EndsWith("Guid")) return "WriteGuid"; + if (cleanType.EndsWith("ObjectId")) return "WriteObjectId"; + + return null; + } + + private static string? GetPrimitiveReadMethod(PropertyInfo prop) + { + string typeName = prop.TypeName; + if (prop.ColumnTypeName == "point" || prop.ColumnTypeName == "coordinate" || prop.ColumnTypeName == "geopoint") + return "ReadCoordinates"; + + if (typeName.Contains("double") && typeName.Contains(",") && typeName.StartsWith("(") && typeName.EndsWith(")")) + return "ReadCoordinates"; + + string cleanType = typeName.TrimEnd('?').Trim(); + + if (cleanType.EndsWith("Int32") || cleanType == "int") return "ReadInt32"; + if (cleanType.EndsWith("Int64") || cleanType == "long") return "ReadInt64"; + if (cleanType.EndsWith("String") || cleanType == "string") return "ReadString"; + if (cleanType.EndsWith("Boolean") || cleanType == "bool") return "ReadBoolean"; + if (cleanType.EndsWith("Single") || cleanType == "float") return "ReadDouble"; + if (cleanType.EndsWith("Double") || cleanType == "double") return "ReadDouble"; + if (cleanType.EndsWith("Decimal") || cleanType == "decimal") return "ReadDecimal128"; + if (cleanType.EndsWith("DateTime") && !cleanType.EndsWith("DateTimeOffset")) return "ReadDateTime"; + if (cleanType.EndsWith("DateTimeOffset")) return "ReadDateTimeOffset"; + if (cleanType.EndsWith("TimeSpan")) return "ReadTimeSpan"; + if (cleanType.EndsWith("DateOnly")) return "ReadDateOnly"; + if (cleanType.EndsWith("TimeOnly")) return "ReadTimeOnly"; + if (cleanType.EndsWith("Guid")) return "ReadGuid"; + if (cleanType.EndsWith("ObjectId")) return "ReadObjectId"; + + return null; + } + + private static bool IsValueType(string typeName) + { + // Check if the type is a value type (struct) that requires .Value unwrapping when nullable + // String is a reference type and doesn't need .Value + string cleanType = typeName.TrimEnd('?').Trim(); + + // Common value types + if (cleanType.EndsWith("Int32") || cleanType == "int") return true; + if (cleanType.EndsWith("Int64") || cleanType == "long") return true; + if (cleanType.EndsWith("Boolean") || cleanType == "bool") return true; + if (cleanType.EndsWith("Single") || cleanType == "float") return true; + if (cleanType.EndsWith("Double") || cleanType == "double") return true; + if (cleanType.EndsWith("Decimal") || cleanType == "decimal") return true; + if (cleanType.EndsWith("DateTime")) return true; + if (cleanType.EndsWith("DateTimeOffset")) return true; + if (cleanType.EndsWith("TimeSpan")) return true; + if (cleanType.EndsWith("DateOnly")) return true; + if (cleanType.EndsWith("TimeOnly")) return true; + if (cleanType.EndsWith("Guid")) return true; + if (cleanType.EndsWith("ObjectId")) return true; + + // String and other reference types + return false; + } + + private static string QualifyType(string typeName) + { + if (string.IsNullOrEmpty(typeName)) return "object"; + if (typeName.StartsWith("global::")) return typeName; + + bool isNullable = typeName.EndsWith("?"); + string baseType = typeName.TrimEnd('?').Trim(); + + if (baseType.StartsWith("(") && baseType.EndsWith(")")) return typeName; // Tuple + + switch (baseType) + { + case "int": + case "long": + case "string": + case "bool": + case "double": + case "float": + case "decimal": + case "byte": + case "sbyte": + case "short": + case "ushort": + case "uint": + case "ulong": + case "char": + case "object": + case "dynamic": + case "void": + return baseType + (isNullable ? "?" : ""); + case "Guid": return "global::System.Guid" + (isNullable ? "?" : ""); + case "DateTime": return "global::System.DateTime" + (isNullable ? "?" : ""); + case "DateTimeOffset": return "global::System.DateTimeOffset" + (isNullable ? "?" : ""); + case "TimeSpan": return "global::System.TimeSpan" + (isNullable ? "?" : ""); + case "DateOnly": return "global::System.DateOnly" + (isNullable ? "?" : ""); + case "TimeOnly": return "global::System.TimeOnly" + (isNullable ? "?" : ""); + case "ObjectId": return "global::ZB.MOM.WW.CBDD.Bson.ObjectId" + (isNullable ? "?" : ""); + default: + return $"global::{typeName}"; + } + } + + private static bool IsPrimitive(string typeName) + { + string cleanType = typeName.TrimEnd('?').Trim(); + if (cleanType.StartsWith("(") && cleanType.EndsWith(")")) return true; + + switch (cleanType) + { + case "int": + case "long": + case "string": + case "bool": + case "double": + case "float": + case "decimal": + case "byte": + case "sbyte": + case "short": + case "ushort": + case "uint": + case "ulong": + case "char": + case "object": + return true; + default: + return false; + } + } +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/Generators/MapperGenerator.cs b/src/CBDD.SourceGenerators/Generators/MapperGenerator.cs index 6b51df4..9fca85a 100755 --- a/src/CBDD.SourceGenerators/Generators/MapperGenerator.cs +++ b/src/CBDD.SourceGenerators/Generators/MapperGenerator.cs @@ -1,403 +1,401 @@ using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; using ZB.MOM.WW.CBDD.SourceGenerators.Helpers; using ZB.MOM.WW.CBDD.SourceGenerators.Models; -namespace ZB.MOM.WW.CBDD.SourceGenerators -{ - public class DbContextInfo - { - /// - /// Gets or sets the simple class name of the DbContext. - /// - public string ClassName { get; set; } = ""; - - /// - /// Gets the fully qualified class name of the DbContext. - /// - public string FullClassName => string.IsNullOrEmpty(Namespace) ? ClassName : $"{Namespace}.{ClassName}"; - - /// - /// Gets or sets the namespace that contains the DbContext. - /// - public string Namespace { get; set; } = ""; - - /// - /// Gets or sets the source file path where the DbContext was found. - /// - public string FilePath { get; set; } = ""; - - /// - /// Gets or sets a value indicating whether the DbContext is nested. - /// - public bool IsNested { get; set; } - - /// - /// Gets or sets a value indicating whether the DbContext is partial. - /// - public bool IsPartial { get; set; } - - /// - /// Gets or sets a value indicating whether the DbContext inherits from another DbContext. - /// - public bool HasBaseDbContext { get; set; } // True if inherits from another DbContext (not DocumentDbContext directly) - - /// - /// Gets or sets the entities discovered for this DbContext. - /// - public List Entities { get; set; } = new List(); - - /// - /// Gets or sets the collected nested types keyed by full type name. - /// - public Dictionary GlobalNestedTypes { get; set; } = new Dictionary(); - } +namespace ZB.MOM.WW.CBDD.SourceGenerators; - [Generator] - public class MapperGenerator : IIncrementalGenerator - { - /// - /// Initializes the mapper source generator pipeline. - /// - /// The incremental generator initialization context. - public void Initialize(IncrementalGeneratorInitializationContext context) - { - // Find all classes that inherit from DocumentDbContext - var dbContextClasses = context.SyntaxProvider - .CreateSyntaxProvider( - predicate: static (node, _) => IsPotentialDbContext(node), - transform: static (ctx, _) => GetDbContextInfo(ctx)) - .Where(static context => context is not null) - .Collect() - .SelectMany(static (contexts, _) => contexts.GroupBy(c => c!.FullClassName).Select(g => g.First())!); - - // Generate code for each DbContext - context.RegisterSourceOutput(dbContextClasses, static (spc, dbContext) => +public class DbContextInfo +{ + /// + /// Gets or sets the simple class name of the DbContext. + /// + public string ClassName { get; set; } = ""; + + /// + /// Gets the fully qualified class name of the DbContext. + /// + public string FullClassName => string.IsNullOrEmpty(Namespace) ? ClassName : $"{Namespace}.{ClassName}"; + + /// + /// Gets or sets the namespace that contains the DbContext. + /// + public string Namespace { get; set; } = ""; + + /// + /// Gets or sets the source file path where the DbContext was found. + /// + public string FilePath { get; set; } = ""; + + /// + /// Gets or sets a value indicating whether the DbContext is nested. + /// + public bool IsNested { get; set; } + + /// + /// Gets or sets a value indicating whether the DbContext is partial. + /// + public bool IsPartial { get; set; } + + /// + /// Gets or sets a value indicating whether the DbContext inherits from another DbContext. + /// + public bool + HasBaseDbContext { get; set; } // True if inherits from another DbContext (not DocumentDbContext directly) + + /// + /// Gets or sets the entities discovered for this DbContext. + /// + public List Entities { get; set; } = new(); + + /// + /// Gets or sets the collected nested types keyed by full type name. + /// + public Dictionary GlobalNestedTypes { get; set; } = new(); +} + +[Generator] +public class MapperGenerator : IIncrementalGenerator +{ + /// + /// Initializes the mapper source generator pipeline. + /// + /// The incremental generator initialization context. + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Find all classes that inherit from DocumentDbContext + var dbContextClasses = context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => IsPotentialDbContext(node), + static (ctx, _) => GetDbContextInfo(ctx)) + .Where(static context => context is not null) + .Collect() + .SelectMany(static (contexts, _) => contexts.GroupBy(c => c!.FullClassName).Select(g => g.First())!); + + // Generate code for each DbContext + context.RegisterSourceOutput(dbContextClasses, static (spc, dbContext) => + { + if (dbContext == null) return; + + var sb = new StringBuilder(); + sb.AppendLine($"// Found DbContext: {dbContext.ClassName}"); + sb.AppendLine( + $"// BaseType: {(dbContext.HasBaseDbContext ? "inherits from another DbContext" : "inherits from DocumentDbContext directly")}"); + + + foreach (var entity in dbContext.Entities) + // Aggregate nested types recursively + CollectNestedTypes(entity.NestedTypes, dbContext.GlobalNestedTypes); + + // Collect namespaces + var namespaces = new HashSet { - if (dbContext == null) return; + "System", + "System.Collections.Generic", + "ZB.MOM.WW.CBDD.Bson", + "ZB.MOM.WW.CBDD.Core.Collections" + }; + + // Add Entity namespaces + foreach (var entity in dbContext.Entities) + if (!string.IsNullOrEmpty(entity.Namespace)) + namespaces.Add(entity.Namespace); + foreach (var nested in dbContext.GlobalNestedTypes.Values) + if (!string.IsNullOrEmpty(nested.Namespace)) + namespaces.Add(nested.Namespace); + + // Sanitize file path for name uniqueness + string safeName = dbContext.ClassName; + if (!string.IsNullOrEmpty(dbContext.FilePath)) + { + string fileName = Path.GetFileNameWithoutExtension(dbContext.FilePath); + safeName += $"_{fileName}"; + } + + sb.AppendLine("// "); + sb.AppendLine("#nullable enable"); + foreach (string ns in namespaces.OrderBy(n => n)) sb.AppendLine($"using {ns};"); + sb.AppendLine(); + + // Use safeName (Context + Filename) to avoid collisions + var mapperNamespace = $"{dbContext.Namespace}.{safeName}_Mappers"; + sb.AppendLine($"namespace {mapperNamespace}"); + sb.AppendLine("{"); + + var generatedMappers = new HashSet(); + + // Generate Entity Mappers + foreach (var entity in dbContext.Entities) + if (generatedMappers.Add(entity.FullTypeName)) + sb.AppendLine(CodeGenerator.GenerateMapperClass(entity, mapperNamespace)); + + // Generate Nested Mappers + foreach (var nested in dbContext.GlobalNestedTypes.Values) + if (generatedMappers.Add(nested.FullTypeName)) + { + var nestedEntity = new EntityInfo + { + Name = nested.Name, + Namespace = nested.Namespace, + FullTypeName = nested.FullTypeName // Ensure FullTypeName is copied + // Helper to copy properties + }; + nestedEntity.Properties.AddRange(nested.Properties); + + sb.AppendLine(CodeGenerator.GenerateMapperClass(nestedEntity, mapperNamespace)); + } + + sb.AppendLine("}"); + sb.AppendLine(); + + // Partial DbContext for InitializeCollections (Only for top-level partial classes) + if (!dbContext.IsNested && dbContext.IsPartial) + { + sb.AppendLine($"namespace {dbContext.Namespace}"); + sb.AppendLine("{"); + sb.AppendLine($" public partial class {dbContext.ClassName}"); + sb.AppendLine(" {"); + sb.AppendLine(" protected override void InitializeCollections()"); + sb.AppendLine(" {"); + + // Call base.InitializeCollections() if this context inherits from another DbContext + if (dbContext.HasBaseDbContext) sb.AppendLine(" base.InitializeCollections();"); - var sb = new StringBuilder(); - sb.AppendLine($"// Found DbContext: {dbContext.ClassName}"); - sb.AppendLine($"// BaseType: {(dbContext.HasBaseDbContext ? "inherits from another DbContext" : "inherits from DocumentDbContext directly")}"); - - foreach (var entity in dbContext.Entities) - { - // Aggregate nested types recursively - CollectNestedTypes(entity.NestedTypes, dbContext.GlobalNestedTypes); - } + if (!string.IsNullOrEmpty(entity.CollectionPropertyName)) + { + var mapperName = + $"global::{mapperNamespace}.{CodeGenerator.GetMapperName(entity.FullTypeName)}"; + sb.AppendLine( + $" this.{entity.CollectionPropertyName} = CreateCollection(new {mapperName}());"); + } - // Collect namespaces - var namespaces = new HashSet - { - "System", - "System.Collections.Generic", - "ZB.MOM.WW.CBDD.Bson", - "ZB.MOM.WW.CBDD.Core.Collections" - }; - - // Add Entity namespaces - foreach (var entity in dbContext.Entities) - { - if (!string.IsNullOrEmpty(entity.Namespace)) - namespaces.Add(entity.Namespace); - } - foreach (var nested in dbContext.GlobalNestedTypes.Values) - { - if (!string.IsNullOrEmpty(nested.Namespace)) - namespaces.Add(nested.Namespace); - } - - // Sanitize file path for name uniqueness - var safeName = dbContext.ClassName; - if (!string.IsNullOrEmpty(dbContext.FilePath)) - { - var fileName = System.IO.Path.GetFileNameWithoutExtension(dbContext.FilePath); - safeName += $"_{fileName}"; - } - - sb.AppendLine("// "); - sb.AppendLine("#nullable enable"); - foreach (var ns in namespaces.OrderBy(n => n)) - { - sb.AppendLine($"using {ns};"); - } + sb.AppendLine(" }"); sb.AppendLine(); - // Use safeName (Context + Filename) to avoid collisions - var mapperNamespace = $"{dbContext.Namespace}.{safeName}_Mappers"; - sb.AppendLine($"namespace {mapperNamespace}"); - sb.AppendLine($"{{"); + // Generate Set() override + var collectionsWithProperties = dbContext.Entities + .Where(e => !string.IsNullOrEmpty(e.CollectionPropertyName) && + !string.IsNullOrEmpty(e.CollectionIdTypeFullName)) + .ToList(); - var generatedMappers = new HashSet(); - - // Generate Entity Mappers - foreach (var entity in dbContext.Entities) + if (collectionsWithProperties.Any()) { - if (generatedMappers.Add(entity.FullTypeName)) + sb.AppendLine( + " public override global::ZB.MOM.WW.CBDD.Core.Collections.DocumentCollection Set()"); + sb.AppendLine(" {"); + + foreach (var entity in collectionsWithProperties) { - sb.AppendLine(CodeGenerator.GenerateMapperClass(entity, mapperNamespace)); + var entityTypeStr = $"global::{entity.FullTypeName}"; + string? idTypeStr = entity.CollectionIdTypeFullName; + sb.AppendLine( + $" if (typeof(TId) == typeof({idTypeStr}) && typeof(T) == typeof({entityTypeStr}))"); + sb.AppendLine( + $" return (global::ZB.MOM.WW.CBDD.Core.Collections.DocumentCollection)(object)this.{entity.CollectionPropertyName};"); } - } - // Generate Nested Mappers - foreach (var nested in dbContext.GlobalNestedTypes.Values) - { - if (generatedMappers.Add(nested.FullTypeName)) - { - var nestedEntity = new EntityInfo - { - Name = nested.Name, - Namespace = nested.Namespace, - FullTypeName = nested.FullTypeName, // Ensure FullTypeName is copied - // Helper to copy properties - }; - nestedEntity.Properties.AddRange(nested.Properties); - - sb.AppendLine(CodeGenerator.GenerateMapperClass(nestedEntity, mapperNamespace)); - } - } - - sb.AppendLine($"}}"); - sb.AppendLine(); - - // Partial DbContext for InitializeCollections (Only for top-level partial classes) - if (!dbContext.IsNested && dbContext.IsPartial) - { - sb.AppendLine($"namespace {dbContext.Namespace}"); - sb.AppendLine($"{{"); - sb.AppendLine($" public partial class {dbContext.ClassName}"); - sb.AppendLine($" {{"); - sb.AppendLine($" protected override void InitializeCollections()"); - sb.AppendLine($" {{"); - - // Call base.InitializeCollections() if this context inherits from another DbContext if (dbContext.HasBaseDbContext) - { - sb.AppendLine($" base.InitializeCollections();"); - } - - foreach (var entity in dbContext.Entities) - { - if (!string.IsNullOrEmpty(entity.CollectionPropertyName)) - { - var mapperName = $"global::{mapperNamespace}.{CodeGenerator.GetMapperName(entity.FullTypeName)}"; - sb.AppendLine($" this.{entity.CollectionPropertyName} = CreateCollection(new {mapperName}());"); - } - } - - sb.AppendLine($" }}"); - sb.AppendLine(); + sb.AppendLine(" return base.Set();"); + else + sb.AppendLine( + " throw new global::System.InvalidOperationException($\"No collection registered for entity type '{typeof(T).Name}' with key type '{typeof(TId).Name}'.\");"); - // Generate Set() override - var collectionsWithProperties = dbContext.Entities - .Where(e => !string.IsNullOrEmpty(e.CollectionPropertyName) && !string.IsNullOrEmpty(e.CollectionIdTypeFullName)) - .ToList(); + sb.AppendLine(" }"); + } - if (collectionsWithProperties.Any()) - { - sb.AppendLine($" public override global::ZB.MOM.WW.CBDD.Core.Collections.DocumentCollection Set()"); - sb.AppendLine($" {{"); + sb.AppendLine(" }"); + sb.AppendLine("}"); + } - foreach (var entity in collectionsWithProperties) - { - var entityTypeStr = $"global::{entity.FullTypeName}"; - var idTypeStr = entity.CollectionIdTypeFullName; - sb.AppendLine($" if (typeof(TId) == typeof({idTypeStr}) && typeof(T) == typeof({entityTypeStr}))"); - sb.AppendLine($" return (global::ZB.MOM.WW.CBDD.Core.Collections.DocumentCollection)(object)this.{entity.CollectionPropertyName};"); - } + spc.AddSource($"{dbContext.Namespace}.{safeName}.Mappers.g.cs", sb.ToString()); + }); + } - if (dbContext.HasBaseDbContext) - { - sb.AppendLine($" return base.Set();"); - } - else - { - sb.AppendLine($" throw new global::System.InvalidOperationException($\"No collection registered for entity type '{{typeof(T).Name}}' with key type '{{typeof(TId).Name}}'.\");"); - } - - sb.AppendLine($" }}"); - } - - sb.AppendLine($" }}"); - sb.AppendLine($"}}"); - } - - spc.AddSource($"{dbContext.Namespace}.{safeName}.Mappers.g.cs", sb.ToString()); - }); - } - - private static void CollectNestedTypes(Dictionary source, Dictionary target) - { - foreach (var kvp in source) + private static void CollectNestedTypes(Dictionary source, + Dictionary target) + { + foreach (var kvp in source) + if (!target.ContainsKey(kvp.Value.FullTypeName)) { - if (!target.ContainsKey(kvp.Value.FullTypeName)) + target[kvp.Value.FullTypeName] = kvp.Value; + CollectNestedTypes(kvp.Value.NestedTypes, target); + } + } + + private static void PrintNestedTypes(StringBuilder sb, Dictionary nestedTypes, + string indent) + { + foreach (var nt in nestedTypes.Values) + { + sb.AppendLine($"//{indent}- {nt.Name} (Depth: {nt.Depth})"); + if (nt.Properties.Count > 0) + // Print properties for nested type to be sure + foreach (var p in nt.Properties) { - target[kvp.Value.FullTypeName] = kvp.Value; - CollectNestedTypes(kvp.Value.NestedTypes, target); + var flags = new List(); + if (p.IsCollection) flags.Add($"Collection<{p.CollectionItemType}>"); + if (p.IsNestedObject) flags.Add($"Nested<{p.NestedTypeName}>"); + string flagStr = flags.Any() ? $" [{string.Join(", ", flags)}]" : ""; + sb.AppendLine($"//{indent} - {p.Name}: {p.TypeName}{flagStr}"); + } + + if (nt.NestedTypes.Any()) PrintNestedTypes(sb, nt.NestedTypes, indent + " "); + } + } + + private static bool IsPotentialDbContext(SyntaxNode node) + { + if (node.SyntaxTree.FilePath.EndsWith(".g.cs")) return false; + + return node is ClassDeclarationSyntax classDecl && + classDecl.BaseList != null && + classDecl.Identifier.Text.EndsWith("Context"); + } + + private static DbContextInfo? GetDbContextInfo(GeneratorSyntaxContext context) + { + var classDecl = (ClassDeclarationSyntax)context.Node; + var semanticModel = context.SemanticModel; + + var classSymbol = ModelExtensions.GetDeclaredSymbol(semanticModel, classDecl) as INamedTypeSymbol; + if (classSymbol == null) return null; + + if (!SyntaxHelper.InheritsFrom(classSymbol, "DocumentDbContext")) + return null; + + // Check if this context inherits from another DbContext (not DocumentDbContext directly) + var baseType = classSymbol.BaseType; + bool hasBaseDbContext = baseType != null && + baseType.Name != "DocumentDbContext" && + SyntaxHelper.InheritsFrom(baseType, "DocumentDbContext"); + + var info = new DbContextInfo + { + ClassName = classSymbol.Name, + Namespace = classSymbol.ContainingNamespace.ToDisplayString(), + FilePath = classDecl.SyntaxTree.FilePath, + IsNested = classSymbol.ContainingType != null, + IsPartial = classDecl.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)), + HasBaseDbContext = hasBaseDbContext + }; + + // Analyze OnModelCreating to find entities + var onModelCreating = classDecl.Members + .OfType() + .FirstOrDefault(m => m.Identifier.Text == "OnModelCreating"); + + if (onModelCreating != null) + { + var entityCalls = SyntaxHelper.FindMethodInvocations(onModelCreating, "Entity"); + foreach (var call in entityCalls) + { + string? typeName = SyntaxHelper.GetGenericTypeArgument(call); + if (typeName != null) + { + // Try to find the symbol + INamedTypeSymbol? entityType = null; + + // 1. Try by name in current compilation (simple name) + var symbols = semanticModel.Compilation.GetSymbolsWithName(typeName); + entityType = symbols.OfType().FirstOrDefault(); + + // 2. Try by metadata name (if fully qualified) + if (entityType == null) entityType = semanticModel.Compilation.GetTypeByMetadataName(typeName); + + if (entityType != null) + { + // Check for duplicates + string fullTypeName = SyntaxHelper.GetFullName(entityType); + if (!info.Entities.Any(e => e.FullTypeName == fullTypeName)) + { + var entityInfo = EntityAnalyzer.Analyze(entityType, semanticModel); + info.Entities.Add(entityInfo); + } + } } } - } - - private static void PrintNestedTypes(StringBuilder sb, Dictionary nestedTypes, string indent) - { - foreach (var nt in nestedTypes.Values) - { - sb.AppendLine($"//{indent}- {nt.Name} (Depth: {nt.Depth})"); - if (nt.Properties.Count > 0) - { - // Print properties for nested type to be sure - foreach (var p in nt.Properties) - { - var flags = new List(); - if (p.IsCollection) flags.Add($"Collection<{p.CollectionItemType}>"); - if (p.IsNestedObject) flags.Add($"Nested<{p.NestedTypeName}>"); - var flagStr = flags.Any() ? $" [{string.Join(", ", flags)}]" : ""; - sb.AppendLine($"//{indent} - {p.Name}: {p.TypeName}{flagStr}"); - } - } - - if (nt.NestedTypes.Any()) - { - PrintNestedTypes(sb, nt.NestedTypes, indent + " "); - } - } - } - - private static bool IsPotentialDbContext(SyntaxNode node) - { - if (node.SyntaxTree.FilePath.EndsWith(".g.cs")) return false; + } - return node is ClassDeclarationSyntax classDecl && - classDecl.BaseList != null && - classDecl.Identifier.Text.EndsWith("Context"); - } - - private static DbContextInfo? GetDbContextInfo(GeneratorSyntaxContext context) + // Analyze OnModelCreating for HasConversion + if (onModelCreating != null) { - var classDecl = (ClassDeclarationSyntax)context.Node; - var semanticModel = context.SemanticModel; - - var classSymbol = semanticModel.GetDeclaredSymbol(classDecl) as INamedTypeSymbol; - if (classSymbol == null) return null; - - if (!SyntaxHelper.InheritsFrom(classSymbol, "DocumentDbContext")) - return null; - - // Check if this context inherits from another DbContext (not DocumentDbContext directly) - var baseType = classSymbol.BaseType; - bool hasBaseDbContext = baseType != null && - baseType.Name != "DocumentDbContext" && - SyntaxHelper.InheritsFrom(baseType, "DocumentDbContext"); - - var info = new DbContextInfo + var conversionCalls = SyntaxHelper.FindMethodInvocations(onModelCreating, "HasConversion"); + foreach (var call in conversionCalls) { - ClassName = classSymbol.Name, - Namespace = classSymbol.ContainingNamespace.ToDisplayString(), - FilePath = classDecl.SyntaxTree.FilePath, - IsNested = classSymbol.ContainingType != null, - IsPartial = classDecl.Modifiers.Any(m => m.IsKind(Microsoft.CodeAnalysis.CSharp.SyntaxKind.PartialKeyword)), - HasBaseDbContext = hasBaseDbContext - }; - - // Analyze OnModelCreating to find entities - var onModelCreating = classDecl.Members - .OfType() - .FirstOrDefault(m => m.Identifier.Text == "OnModelCreating"); - - if (onModelCreating != null) - { - var entityCalls = SyntaxHelper.FindMethodInvocations(onModelCreating, "Entity"); - foreach (var call in entityCalls) - { - var typeName = SyntaxHelper.GetGenericTypeArgument(call); - if (typeName != null) - { - // Try to find the symbol - INamedTypeSymbol? entityType = null; - - // 1. Try by name in current compilation (simple name) - var symbols = semanticModel.Compilation.GetSymbolsWithName(typeName); - entityType = symbols.OfType().FirstOrDefault(); - - // 2. Try by metadata name (if fully qualified) - if (entityType == null) - { - entityType = semanticModel.Compilation.GetTypeByMetadataName(typeName); - } + string? converterName = SyntaxHelper.GetGenericTypeArgument(call); + if (converterName == null) continue; - if (entityType != null) + // Trace back: .Property(x => x.Id).HasConversion() or .HasKey(x => x.Id).HasConversion() + if (call.Expression is MemberAccessExpressionSyntax + { + Expression: InvocationExpressionSyntax propertyCall + } && + propertyCall.Expression is MemberAccessExpressionSyntax + { + Name: IdentifierNameSyntax { Identifier: { Text: var propertyMethod } } + } && + (propertyMethod == "Property" || propertyMethod == "HasKey")) + { + string? propertyName = + SyntaxHelper.GetPropertyName(propertyCall.ArgumentList.Arguments.FirstOrDefault()?.Expression); + if (propertyName == null) continue; + + // Trace further back: Entity().Property(...) + if (propertyCall.Expression is MemberAccessExpressionSyntax { - // Check for duplicates - var fullTypeName = SyntaxHelper.GetFullName(entityType); - if (!info.Entities.Any(e => e.FullTypeName == fullTypeName)) + Expression: InvocationExpressionSyntax entityCall + } && + entityCall.Expression is MemberAccessExpressionSyntax + { + Name: GenericNameSyntax { Identifier: { Text: "Entity" } } + }) + { + string? entityTypeName = SyntaxHelper.GetGenericTypeArgument(entityCall); + if (entityTypeName != null) + { + var entity = info.Entities.FirstOrDefault(e => + e.Name == entityTypeName || e.FullTypeName.EndsWith("." + entityTypeName)); + if (entity != null) { - var entityInfo = EntityAnalyzer.Analyze(entityType, semanticModel); - info.Entities.Add(entityInfo); - } - } - } - } - } - - // Analyze OnModelCreating for HasConversion - if (onModelCreating != null) - { - var conversionCalls = SyntaxHelper.FindMethodInvocations(onModelCreating, "HasConversion"); - foreach (var call in conversionCalls) - { - var converterName = SyntaxHelper.GetGenericTypeArgument(call); - if (converterName == null) continue; - - // Trace back: .Property(x => x.Id).HasConversion() or .HasKey(x => x.Id).HasConversion() - if (call.Expression is MemberAccessExpressionSyntax { Expression: InvocationExpressionSyntax propertyCall } && - propertyCall.Expression is MemberAccessExpressionSyntax { Name: IdentifierNameSyntax { Identifier: { Text: var propertyMethod } } } && - (propertyMethod == "Property" || propertyMethod == "HasKey")) - { - var propertyName = SyntaxHelper.GetPropertyName(propertyCall.ArgumentList.Arguments.FirstOrDefault()?.Expression); - if (propertyName == null) continue; - - // Trace further back: Entity().Property(...) - if (propertyCall.Expression is MemberAccessExpressionSyntax { Expression: InvocationExpressionSyntax entityCall } && - entityCall.Expression is MemberAccessExpressionSyntax { Name: GenericNameSyntax { Identifier: { Text: "Entity" } } }) - { - var entityTypeName = SyntaxHelper.GetGenericTypeArgument(entityCall); - if (entityTypeName != null) - { - var entity = info.Entities.FirstOrDefault(e => e.Name == entityTypeName || e.FullTypeName.EndsWith("." + entityTypeName)); - if (entity != null) + var prop = entity.Properties.FirstOrDefault(p => p.Name == propertyName); + if (prop != null) { - var prop = entity.Properties.FirstOrDefault(p => p.Name == propertyName); - if (prop != null) + // Resolve TProvider from ValueConverter + var converterType = + semanticModel.Compilation.GetTypeByMetadataName(converterName) ?? + semanticModel.Compilation.GetSymbolsWithName(converterName) + .OfType().FirstOrDefault(); + + prop.ConverterTypeName = converterType != null + ? SyntaxHelper.GetFullName(converterType) + : converterName; + + if (converterType != null && converterType.BaseType != null && + converterType.BaseType.Name == "ValueConverter" && + converterType.BaseType.TypeArguments.Length == 2) { - // Resolve TProvider from ValueConverter - var converterType = semanticModel.Compilation.GetTypeByMetadataName(converterName) ?? - semanticModel.Compilation.GetSymbolsWithName(converterName).OfType().FirstOrDefault(); - - prop.ConverterTypeName = converterType != null ? SyntaxHelper.GetFullName(converterType) : converterName; - - if (converterType != null && converterType.BaseType != null && - converterType.BaseType.Name == "ValueConverter" && - converterType.BaseType.TypeArguments.Length == 2) + prop.ProviderTypeName = converterType.BaseType.TypeArguments[1].Name; + } + else if (converterType != null) + { + // Fallback: search deeper in base types + var converterBaseType = converterType.BaseType; + while (converterBaseType != null) { - prop.ProviderTypeName = converterType.BaseType.TypeArguments[1].Name; - } - else if (converterType != null) - { - // Fallback: search deeper in base types - var converterBaseType = converterType.BaseType; - while (converterBaseType != null) + if (converterBaseType.Name == "ValueConverter" && + converterBaseType.TypeArguments.Length == 2) { - if (converterBaseType.Name == "ValueConverter" && converterBaseType.TypeArguments.Length == 2) - { - prop.ProviderTypeName = converterBaseType.TypeArguments[1].Name; - break; - } - converterBaseType = converterBaseType.BaseType; + prop.ProviderTypeName = converterBaseType.TypeArguments[1].Name; + break; } + + converterBaseType = converterBaseType.BaseType; } } } @@ -406,31 +404,28 @@ namespace ZB.MOM.WW.CBDD.SourceGenerators } } } + } - // Analyze properties to find DocumentCollection - var properties = classSymbol.GetMembers().OfType(); - foreach (var prop in properties) - { - if (prop.Type is INamedTypeSymbol namedType && - namedType.OriginalDefinition.Name == "DocumentCollection") + // Analyze properties to find DocumentCollection + var properties = classSymbol.GetMembers().OfType(); + foreach (var prop in properties) + if (prop.Type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.Name == "DocumentCollection") + // Expecting 2 type arguments: TId, TEntity + if (namedType.TypeArguments.Length == 2) { - // Expecting 2 type arguments: TId, TEntity - if (namedType.TypeArguments.Length == 2) + var entityType = namedType.TypeArguments[1]; + var entityInfo = info.Entities.FirstOrDefault(e => e.FullTypeName == entityType.ToDisplayString()); + + // If found, update + if (entityInfo != null) { - var entityType = namedType.TypeArguments[1]; - var entityInfo = info.Entities.FirstOrDefault(e => e.FullTypeName == entityType.ToDisplayString()); - - // If found, update - if (entityInfo != null) - { - entityInfo.CollectionPropertyName = prop.Name; - entityInfo.CollectionIdTypeFullName = namedType.TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - } + entityInfo.CollectionPropertyName = prop.Name; + entityInfo.CollectionIdTypeFullName = namedType.TypeArguments[0] + .ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } } - } - - return info; - } + + return info; } -} +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/Helpers/AttributeHelper.cs b/src/CBDD.SourceGenerators/Helpers/AttributeHelper.cs index f7031ad..66e035f 100755 --- a/src/CBDD.SourceGenerators/Helpers/AttributeHelper.cs +++ b/src/CBDD.SourceGenerators/Helpers/AttributeHelper.cs @@ -1,121 +1,115 @@ -using System; -using System.Linq; -using Microsoft.CodeAnalysis; - -namespace ZB.MOM.WW.CBDD.SourceGenerators.Helpers -{ - public static class AttributeHelper +using System.Linq; +using Microsoft.CodeAnalysis; + +namespace ZB.MOM.WW.CBDD.SourceGenerators.Helpers; + +public static class AttributeHelper +{ + /// + /// Determines whether a property should be ignored during mapping. + /// + /// The property symbol to inspect. + /// when the property has an ignore attribute; otherwise, . + public static bool ShouldIgnore(IPropertySymbol property) { - /// - /// Determines whether a property should be ignored during mapping. - /// - /// The property symbol to inspect. - /// when the property has an ignore attribute; otherwise, . - public static bool ShouldIgnore(IPropertySymbol property) - { - return HasAttribute(property, "BsonIgnore") || - HasAttribute(property, "JsonIgnore") || - HasAttribute(property, "NotMapped"); - } - - /// - /// Determines whether a property is marked as a key. - /// - /// The property symbol to inspect. - /// when the property has a key attribute; otherwise, . - public static bool IsKey(IPropertySymbol property) - { - return HasAttribute(property, "Key") || - HasAttribute(property, "BsonId"); - } - - /// - /// Gets the first constructor argument value for the specified attribute as a string. - /// - /// The symbol to inspect. - /// The attribute name to match. - /// The attribute value if found; otherwise, . - public static string? GetAttributeStringValue(ISymbol symbol, string attributeName) - { - var attr = GetAttribute(symbol, attributeName); - if (attr != null && attr.ConstructorArguments.Length > 0) - { - return attr.ConstructorArguments[0].Value?.ToString(); - } - - return null; - } - - /// - /// Gets the first constructor argument value for the specified attribute as an integer. - /// - /// The symbol to inspect. - /// The attribute name to match. - /// The attribute value if found; otherwise, . - public static int? GetAttributeIntValue(ISymbol symbol, string attributeName) - { - var attr = GetAttribute(symbol, attributeName); - if (attr != null && attr.ConstructorArguments.Length > 0) - { - if (attr.ConstructorArguments[0].Value is int val) return val; - } - - return null; - } - - /// - /// Gets the first constructor argument value for the specified attribute as a double. - /// - /// The symbol to inspect. - /// The attribute name to match. - /// The attribute value if found; otherwise, . - public static double? GetAttributeDoubleValue(ISymbol symbol, string attributeName) - { - var attr = GetAttribute(symbol, attributeName); - if (attr != null && attr.ConstructorArguments.Length > 0) - { - if (attr.ConstructorArguments[0].Value is double val) return val; - if (attr.ConstructorArguments[0].Value is float fval) return (double)fval; - if (attr.ConstructorArguments[0].Value is int ival) return (double)ival; - } - - return null; - } - - /// - /// Gets a named argument value from an attribute. - /// - /// The attribute data. - /// The named argument key. - /// The named argument value if present; otherwise, . - public static string? GetNamedArgumentValue(AttributeData attr, string name) - { - return attr.NamedArguments.FirstOrDefault(a => a.Key == name).Value.Value?.ToString(); - } - - /// - /// Gets the first attribute that matches the specified name. - /// - /// The symbol to inspect. - /// The attribute name to match. - /// The matching attribute data if found; otherwise, . - public static AttributeData? GetAttribute(ISymbol symbol, string attributeName) - { - return symbol.GetAttributes().FirstOrDefault(a => - a.AttributeClass != null && - (a.AttributeClass.Name == attributeName || - a.AttributeClass.Name == attributeName + "Attribute")); - } - - /// - /// Determines whether a symbol has the specified attribute. - /// - /// The symbol to inspect. - /// The attribute name to match. - /// when a matching attribute exists; otherwise, . - public static bool HasAttribute(ISymbol symbol, string attributeName) - { - return GetAttribute(symbol, attributeName) != null; - } + return HasAttribute(property, "BsonIgnore") || + HasAttribute(property, "JsonIgnore") || + HasAttribute(property, "NotMapped"); } -} + + /// + /// Determines whether a property is marked as a key. + /// + /// The property symbol to inspect. + /// when the property has a key attribute; otherwise, . + public static bool IsKey(IPropertySymbol property) + { + return HasAttribute(property, "Key") || + HasAttribute(property, "BsonId"); + } + + /// + /// Gets the first constructor argument value for the specified attribute as a string. + /// + /// The symbol to inspect. + /// The attribute name to match. + /// The attribute value if found; otherwise, . + public static string? GetAttributeStringValue(ISymbol symbol, string attributeName) + { + var attr = GetAttribute(symbol, attributeName); + if (attr != null && attr.ConstructorArguments.Length > 0) return attr.ConstructorArguments[0].Value?.ToString(); + + return null; + } + + /// + /// Gets the first constructor argument value for the specified attribute as an integer. + /// + /// The symbol to inspect. + /// The attribute name to match. + /// The attribute value if found; otherwise, . + public static int? GetAttributeIntValue(ISymbol symbol, string attributeName) + { + var attr = GetAttribute(symbol, attributeName); + if (attr != null && attr.ConstructorArguments.Length > 0) + if (attr.ConstructorArguments[0].Value is int val) + return val; + + return null; + } + + /// + /// Gets the first constructor argument value for the specified attribute as a double. + /// + /// The symbol to inspect. + /// The attribute name to match. + /// The attribute value if found; otherwise, . + public static double? GetAttributeDoubleValue(ISymbol symbol, string attributeName) + { + var attr = GetAttribute(symbol, attributeName); + if (attr != null && attr.ConstructorArguments.Length > 0) + { + if (attr.ConstructorArguments[0].Value is double val) return val; + if (attr.ConstructorArguments[0].Value is float fval) return (double)fval; + if (attr.ConstructorArguments[0].Value is int ival) return (double)ival; + } + + return null; + } + + /// + /// Gets a named argument value from an attribute. + /// + /// The attribute data. + /// The named argument key. + /// The named argument value if present; otherwise, . + public static string? GetNamedArgumentValue(AttributeData attr, string name) + { + return attr.NamedArguments.FirstOrDefault(a => a.Key == name).Value.Value?.ToString(); + } + + /// + /// Gets the first attribute that matches the specified name. + /// + /// The symbol to inspect. + /// The attribute name to match. + /// The matching attribute data if found; otherwise, . + public static AttributeData? GetAttribute(ISymbol symbol, string attributeName) + { + return symbol.GetAttributes().FirstOrDefault(a => + a.AttributeClass != null && + (a.AttributeClass.Name == attributeName || + a.AttributeClass.Name == attributeName + "Attribute")); + } + + /// + /// Determines whether a symbol has the specified attribute. + /// + /// The symbol to inspect. + /// The attribute name to match. + /// when a matching attribute exists; otherwise, . + public static bool HasAttribute(ISymbol symbol, string attributeName) + { + return GetAttribute(symbol, attributeName) != null; + } +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/Helpers/SyntaxHelper.cs b/src/CBDD.SourceGenerators/Helpers/SyntaxHelper.cs index 6ace28e..be6317b 100755 --- a/src/CBDD.SourceGenerators/Helpers/SyntaxHelper.cs +++ b/src/CBDD.SourceGenerators/Helpers/SyntaxHelper.cs @@ -1,253 +1,229 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace ZB.MOM.WW.CBDD.SourceGenerators.Helpers -{ - public static class SyntaxHelper +using System.Collections.Generic; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace ZB.MOM.WW.CBDD.SourceGenerators.Helpers; + +public static class SyntaxHelper +{ + /// + /// Determines whether a symbol inherits from a base type with the specified name. + /// + /// The symbol to inspect. + /// The base type name to match. + /// if the symbol inherits from the base type; otherwise, . + public static bool InheritsFrom(INamedTypeSymbol symbol, string baseTypeName) { - /// - /// Determines whether a symbol inherits from a base type with the specified name. - /// - /// The symbol to inspect. - /// The base type name to match. - /// if the symbol inherits from the base type; otherwise, . - public static bool InheritsFrom(INamedTypeSymbol symbol, string baseTypeName) + var current = symbol.BaseType; + while (current != null) { - var current = symbol.BaseType; - while (current != null) - { - if (current.Name == baseTypeName) - return true; - current = current.BaseType; - } - return false; + if (current.Name == baseTypeName) + return true; + current = current.BaseType; } - /// - /// Finds method invocations with a matching method name under the provided syntax node. - /// - /// The root syntax node to search. - /// The method name to match. - /// A list of matching invocation expressions. - public static List FindMethodInvocations(SyntaxNode node, string methodName) - { - return node.DescendantNodes() - .OfType() - .Where(invocation => - { - if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) - { - return memberAccess.Name.Identifier.Text == methodName; - } - return false; - }) - .ToList(); - } + return false; + } - /// - /// Gets the first generic type argument from an invocation, if present. - /// - /// The invocation to inspect. - /// The generic type argument text, or when not available. - public static string? GetGenericTypeArgument(InvocationExpressionSyntax invocation) - { - if (invocation.Expression is MemberAccessExpressionSyntax memberAccess && - memberAccess.Name is GenericNameSyntax genericName && - genericName.TypeArgumentList.Arguments.Count > 0) - { - return genericName.TypeArgumentList.Arguments[0].ToString(); - } - return null; - } - - /// - /// Extracts a property name from an expression. - /// - /// The expression to analyze. - /// The property name when resolved; otherwise, . - public static string? GetPropertyName(ExpressionSyntax? expression) - { - if (expression == null) return null; - if (expression is LambdaExpressionSyntax lambda) - { - return GetPropertyName(lambda.Body as ExpressionSyntax); - } - if (expression is MemberAccessExpressionSyntax memberAccess) - { - return memberAccess.Name.Identifier.Text; - } - if (expression is PrefixUnaryExpressionSyntax prefixUnary && prefixUnary.Operand is MemberAccessExpressionSyntax prefixMember) - { - return prefixMember.Name.Identifier.Text; - } - if (expression is PostfixUnaryExpressionSyntax postfixUnary && postfixUnary.Operand is MemberAccessExpressionSyntax postfixMember) - { - return postfixMember.Name.Identifier.Text; - } - return null; - } - - /// - /// Gets the fully-qualified type name without the global prefix. - /// - /// The symbol to format. - /// The formatted full type name. - public static string GetFullName(INamedTypeSymbol symbol) - { - return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) - .Replace("global::", ""); - } - - /// - /// Gets a display name for a type symbol. - /// - /// The type symbol to format. - /// The display name. - public static string GetTypeName(ITypeSymbol type) - { - if (type is INamedTypeSymbol namedType && - namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - var underlyingType = namedType.TypeArguments[0]; - return GetTypeName(underlyingType) + "?"; - } - - if (type is IArrayTypeSymbol arrayType) - { - return GetTypeName(arrayType.ElementType) + "[]"; - } - - if (type is INamedTypeSymbol nt && nt.IsTupleType) - { - return type.ToDisplayString(); - } - - return type.ToDisplayString(); - } - - /// - /// Determines whether a type is nullable. - /// - /// The type to evaluate. - /// if the type is nullable; otherwise, . - public static bool IsNullableType(ITypeSymbol type) - { - if (type is INamedTypeSymbol namedType && - namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - return true; - } - return type.NullableAnnotation == NullableAnnotation.Annotated; - } - - /// - /// Determines whether a type is a collection and returns its item type when available. - /// - /// The type to evaluate. - /// When this method returns, contains the collection item type if the type is a collection. - /// if the type is a collection; otherwise, . - public static bool IsCollectionType(ITypeSymbol type, out ITypeSymbol? itemType) - { - itemType = null; - - // Exclude string (it's IEnumerable but not a collection for our purposes) - if (type.SpecialType == SpecialType.System_String) + /// + /// Finds method invocations with a matching method name under the provided syntax node. + /// + /// The root syntax node to search. + /// The method name to match. + /// A list of matching invocation expressions. + public static List FindMethodInvocations(SyntaxNode node, string methodName) + { + return node.DescendantNodes() + .OfType() + .Where(invocation => + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess) + return memberAccess.Name.Identifier.Text == methodName; return false; + }) + .ToList(); + } - // Handle arrays - if (type is IArrayTypeSymbol arrayType) - { - itemType = arrayType.ElementType; - return true; - } + /// + /// Gets the first generic type argument from an invocation, if present. + /// + /// The invocation to inspect. + /// The generic type argument text, or when not available. + public static string? GetGenericTypeArgument(InvocationExpressionSyntax invocation) + { + if (invocation.Expression is MemberAccessExpressionSyntax memberAccess && + memberAccess.Name is GenericNameSyntax genericName && + genericName.TypeArgumentList.Arguments.Count > 0) + return genericName.TypeArgumentList.Arguments[0].ToString(); + return null; + } - // Check if the type itself is IEnumerable - if (type is INamedTypeSymbol namedType && namedType.IsGenericType) - { - var typeDefName = namedType.OriginalDefinition.ToDisplayString(); - if (typeDefName == "System.Collections.Generic.IEnumerable" && namedType.TypeArguments.Length == 1) - { - itemType = namedType.TypeArguments[0]; - return true; - } - } + /// + /// Extracts a property name from an expression. + /// + /// The expression to analyze. + /// The property name when resolved; otherwise, . + public static string? GetPropertyName(ExpressionSyntax? expression) + { + if (expression == null) return null; + if (expression is LambdaExpressionSyntax lambda) return GetPropertyName(lambda.Body as ExpressionSyntax); + if (expression is MemberAccessExpressionSyntax memberAccess) return memberAccess.Name.Identifier.Text; + if (expression is PrefixUnaryExpressionSyntax prefixUnary && + prefixUnary.Operand is MemberAccessExpressionSyntax prefixMember) return prefixMember.Name.Identifier.Text; + if (expression is PostfixUnaryExpressionSyntax postfixUnary && + postfixUnary.Operand is MemberAccessExpressionSyntax postfixMember) + return postfixMember.Name.Identifier.Text; + return null; + } - // Check if the type implements IEnumerable by walking all interfaces - var enumerableInterface = type.AllInterfaces - .FirstOrDefault(i => i.IsGenericType && - i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable"); + /// + /// Gets the fully-qualified type name without the global prefix. + /// + /// The symbol to format. + /// The formatted full type name. + public static string GetFullName(INamedTypeSymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) + .Replace("global::", ""); + } - if (enumerableInterface != null && enumerableInterface.TypeArguments.Length == 1) - { - itemType = enumerableInterface.TypeArguments[0]; - return true; - } + /// + /// Gets a display name for a type symbol. + /// + /// The type symbol to format. + /// The display name. + public static string GetTypeName(ITypeSymbol type) + { + if (type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + { + var underlyingType = namedType.TypeArguments[0]; + return GetTypeName(underlyingType) + "?"; + } + if (type is IArrayTypeSymbol arrayType) return GetTypeName(arrayType.ElementType) + "[]"; + + if (type is INamedTypeSymbol nt && nt.IsTupleType) return type.ToDisplayString(); + + return type.ToDisplayString(); + } + + /// + /// Determines whether a type is nullable. + /// + /// The type to evaluate. + /// if the type is nullable; otherwise, . + public static bool IsNullableType(ITypeSymbol type) + { + if (type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + return true; + return type.NullableAnnotation == NullableAnnotation.Annotated; + } + + /// + /// Determines whether a type is a collection and returns its item type when available. + /// + /// The type to evaluate. + /// When this method returns, contains the collection item type if the type is a collection. + /// if the type is a collection; otherwise, . + public static bool IsCollectionType(ITypeSymbol type, out ITypeSymbol? itemType) + { + itemType = null; + + // Exclude string (it's IEnumerable but not a collection for our purposes) + if (type.SpecialType == SpecialType.System_String) return false; + + // Handle arrays + if (type is IArrayTypeSymbol arrayType) + { + itemType = arrayType.ElementType; + return true; } - /// - /// Determines whether a type should be treated as a primitive value. - /// - /// The type to evaluate. - /// if the type is primitive-like; otherwise, . - public static bool IsPrimitiveType(ITypeSymbol type) + // Check if the type itself is IEnumerable + if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { - if (type is INamedTypeSymbol namedType && - namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) - { - type = namedType.TypeArguments[0]; + string typeDefName = namedType.OriginalDefinition.ToDisplayString(); + if (typeDefName == "System.Collections.Generic.IEnumerable" && namedType.TypeArguments.Length == 1) + { + itemType = namedType.TypeArguments[0]; + return true; } - - if (type.SpecialType != SpecialType.None && type.SpecialType != SpecialType.System_Object) - return true; - - var typeName = type.Name; - if (typeName == "Guid" || typeName == "DateTime" || typeName == "DateTimeOffset" || - typeName == "TimeSpan" || typeName == "DateOnly" || typeName == "TimeOnly" || - typeName == "Decimal" || typeName == "ObjectId") - return true; - - if (type.TypeKind == TypeKind.Enum) - return true; - - if (type is INamedTypeSymbol nt && nt.IsTupleType) - return true; - - return false; } - /// - /// Determines whether a type should be treated as a nested object. - /// - /// The type to evaluate. - /// if the type is a nested object; otherwise, . - public static bool IsNestedObjectType(ITypeSymbol type) - { - if (IsPrimitiveType(type)) return false; - if (type.SpecialType == SpecialType.System_String) return false; - if (IsCollectionType(type, out _)) return false; - if (type.SpecialType == SpecialType.System_Object) return false; - if (type is INamedTypeSymbol nt && nt.IsTupleType) return false; + // Check if the type implements IEnumerable by walking all interfaces + var enumerableInterface = type.AllInterfaces + .FirstOrDefault(i => i.IsGenericType && + i.OriginalDefinition.ToDisplayString() == "System.Collections.Generic.IEnumerable"); - return type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct; + if (enumerableInterface != null && enumerableInterface.TypeArguments.Length == 1) + { + itemType = enumerableInterface.TypeArguments[0]; + return true; } - /// - /// Determines whether a property has an associated backing field. - /// - /// The property to inspect. - /// if a backing field is found; otherwise, . - public static bool HasBackingField(IPropertySymbol property) - { - // Auto-properties have compiler-generated backing fields - // Check if there's a field with the pattern k__BackingField - return property.ContainingType.GetMembers() - .OfType() - .Any(f => f.AssociatedSymbol?.Equals(property, SymbolEqualityComparer.Default) == true); - } - } -} + return false; + } + + /// + /// Determines whether a type should be treated as a primitive value. + /// + /// The type to evaluate. + /// if the type is primitive-like; otherwise, . + public static bool IsPrimitiveType(ITypeSymbol type) + { + if (type is INamedTypeSymbol namedType && + namedType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) + type = namedType.TypeArguments[0]; + + if (type.SpecialType != SpecialType.None && type.SpecialType != SpecialType.System_Object) + return true; + + string typeName = type.Name; + if (typeName == "Guid" || typeName == "DateTime" || typeName == "DateTimeOffset" || + typeName == "TimeSpan" || typeName == "DateOnly" || typeName == "TimeOnly" || + typeName == "Decimal" || typeName == "ObjectId") + return true; + + if (type.TypeKind == TypeKind.Enum) + return true; + + if (type is INamedTypeSymbol nt && nt.IsTupleType) + return true; + + return false; + } + + /// + /// Determines whether a type should be treated as a nested object. + /// + /// The type to evaluate. + /// if the type is a nested object; otherwise, . + public static bool IsNestedObjectType(ITypeSymbol type) + { + if (IsPrimitiveType(type)) return false; + if (type.SpecialType == SpecialType.System_String) return false; + if (IsCollectionType(type, out _)) return false; + if (type.SpecialType == SpecialType.System_Object) return false; + if (type is INamedTypeSymbol nt && nt.IsTupleType) return false; + + return type.TypeKind == TypeKind.Class || type.TypeKind == TypeKind.Struct; + } + + /// + /// Determines whether a property has an associated backing field. + /// + /// The property to inspect. + /// if a backing field is found; otherwise, . + public static bool HasBackingField(IPropertySymbol property) + { + // Auto-properties have compiler-generated backing fields + // Check if there's a field with the pattern k__BackingField + return property.ContainingType.GetMembers() + .OfType() + .Any(f => f.AssociatedSymbol?.Equals(property, SymbolEqualityComparer.Default) == true); + } +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/Models/DbContextInfo.cs b/src/CBDD.SourceGenerators/Models/DbContextInfo.cs index 5d03488..aaf6d74 100755 --- a/src/CBDD.SourceGenerators/Models/DbContextInfo.cs +++ b/src/CBDD.SourceGenerators/Models/DbContextInfo.cs @@ -1,32 +1,31 @@ -using System.Collections.Generic; - -namespace ZB.MOM.WW.CBDD.SourceGenerators.Models -{ - public class DbContextInfo - { - /// - /// Gets or sets the DbContext class name. - /// - public string ClassName { get; set; } = ""; +using System.Collections.Generic; - /// - /// Gets or sets the namespace containing the DbContext. - /// - public string Namespace { get; set; } = ""; +namespace ZB.MOM.WW.CBDD.SourceGenerators.Models; - /// - /// Gets or sets the source file path for the DbContext. - /// - public string FilePath { get; set; } = ""; +public class DbContextInfo +{ + /// + /// Gets or sets the DbContext class name. + /// + public string ClassName { get; set; } = ""; - /// - /// Gets the entity types discovered for the DbContext. - /// - public List Entities { get; } = new List(); + /// + /// Gets or sets the namespace containing the DbContext. + /// + public string Namespace { get; set; } = ""; - /// - /// Gets global nested types keyed by type name. - /// - public Dictionary GlobalNestedTypes { get; } = new Dictionary(); - } -} + /// + /// Gets or sets the source file path for the DbContext. + /// + public string FilePath { get; set; } = ""; + + /// + /// Gets the entity types discovered for the DbContext. + /// + public List Entities { get; } = new(); + + /// + /// Gets global nested types keyed by type name. + /// + public Dictionary GlobalNestedTypes { get; } = new(); +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/Models/EntityInfo.cs b/src/CBDD.SourceGenerators/Models/EntityInfo.cs index 2168ff0..d831d51 100755 --- a/src/CBDD.SourceGenerators/Models/EntityInfo.cs +++ b/src/CBDD.SourceGenerators/Models/EntityInfo.cs @@ -1,213 +1,247 @@ -using System.Collections.Generic; -using System.Linq; - -namespace ZB.MOM.WW.CBDD.SourceGenerators.Models +using System.Collections.Generic; +using System.Linq; + +namespace ZB.MOM.WW.CBDD.SourceGenerators.Models; + +/// +/// Contains metadata describing an entity discovered by source generation. +/// +public class EntityInfo { /// - /// Contains metadata describing an entity discovered by source generation. + /// Gets or sets the entity name. /// - public class EntityInfo - { - /// - /// Gets or sets the entity name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the entity namespace. - /// - public string Namespace { get; set; } = ""; - /// - /// Gets or sets the fully qualified entity type name. - /// - public string FullTypeName { get; set; } = ""; - /// - /// Gets or sets the collection name for the entity. - /// - public string CollectionName { get; set; } = ""; - /// - /// Gets or sets the collection property name. - /// - public string? CollectionPropertyName { get; set; } - /// - /// Gets or sets the fully qualified collection identifier type name. - /// - public string? CollectionIdTypeFullName { get; set; } - - /// - /// Gets the key property for the entity if one exists. - /// - public PropertyInfo? IdProperty => Properties.FirstOrDefault(p => p.IsKey); - /// - /// Gets or sets a value indicating whether IDs are automatically generated. - /// - public bool AutoId { get; set; } - /// - /// Gets or sets a value indicating whether the entity uses private setters. - /// - public bool HasPrivateSetters { get; set; } - /// - /// Gets or sets a value indicating whether the entity has a private or missing constructor. - /// - public bool HasPrivateOrNoConstructor { get; set; } - - /// - /// Gets the entity properties. - /// - public List Properties { get; } = new List(); - /// - /// Gets nested type metadata keyed by type name. - /// - public Dictionary NestedTypes { get; } = new Dictionary(); - /// - /// Gets property names that should be ignored by mapping. - /// - public HashSet IgnoredProperties { get; } = new HashSet(); - } + public string Name { get; set; } = ""; /// - /// Contains metadata describing a mapped property. + /// Gets or sets the entity namespace. /// - public class PropertyInfo - { - /// - /// Gets or sets the property name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the property type name. - /// - public string TypeName { get; set; } = ""; - /// - /// Gets or sets the BSON field name. - /// - public string BsonFieldName { get; set; } = ""; - /// - /// Gets or sets the database column type name. - /// - public string? ColumnTypeName { get; set; } - /// - /// Gets or sets a value indicating whether the property is nullable. - /// - public bool IsNullable { get; set; } - - /// - /// Gets or sets a value indicating whether the property has a public setter. - /// - public bool HasPublicSetter { get; set; } - /// - /// Gets or sets a value indicating whether the property uses an init-only setter. - /// - public bool HasInitOnlySetter { get; set; } - /// - /// Gets or sets a value indicating whether the property has any setter. - /// - public bool HasAnySetter { get; set; } - /// - /// Gets or sets a value indicating whether the getter is read-only. - /// - public bool IsReadOnlyGetter { get; set; } - /// - /// Gets or sets the backing field name if available. - /// - public string? BackingFieldName { get; set; } - - /// - /// Gets or sets a value indicating whether the property is the key. - /// - public bool IsKey { get; set; } - /// - /// Gets or sets a value indicating whether the property is required. - /// - public bool IsRequired { get; set; } - /// - /// Gets or sets the maximum allowed length. - /// - public int? MaxLength { get; set; } - /// - /// Gets or sets the minimum allowed length. - /// - public int? MinLength { get; set; } - /// - /// Gets or sets the minimum allowed range value. - /// - public double? RangeMin { get; set; } - /// - /// Gets or sets the maximum allowed range value. - /// - public double? RangeMax { get; set; } - - /// - /// Gets or sets a value indicating whether the property is a collection. - /// - public bool IsCollection { get; set; } - /// - /// Gets or sets a value indicating whether the property is an array. - /// - public bool IsArray { get; set; } - /// - /// Gets or sets the collection item type name. - /// - public string? CollectionItemType { get; set; } - /// - /// Gets or sets the concrete collection type name. - /// - public string? CollectionConcreteTypeName { get; set; } - - /// - /// Gets or sets a value indicating whether the property is a nested object. - /// - public bool IsNestedObject { get; set; } - /// - /// Gets or sets a value indicating whether collection items are nested objects. - /// - public bool IsCollectionItemNested { get; set; } - /// - /// Gets or sets the nested type name. - /// - public string? NestedTypeName { get; set; } - /// - /// Gets or sets the fully qualified nested type name. - /// - public string? NestedTypeFullName { get; set; } - /// - /// Gets or sets the converter type name. - /// - public string? ConverterTypeName { get; set; } - /// - /// Gets or sets the provider type name used by the converter. - /// - public string? ProviderTypeName { get; set; } - } + public string Namespace { get; set; } = ""; /// - /// Contains metadata describing a nested type. + /// Gets or sets the fully qualified entity type name. /// - public class NestedTypeInfo - { - /// - /// Gets or sets the nested type name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the nested type namespace. - /// - public string Namespace { get; set; } = ""; - /// - /// Gets or sets the fully qualified nested type name. - /// - public string FullTypeName { get; set; } = ""; - /// - /// Gets or sets the depth of the nested type. - /// - public int Depth { get; set; } + public string FullTypeName { get; set; } = ""; - /// - /// Gets the nested type properties. - /// - public List Properties { get; } = new List(); - /// - /// Gets nested type metadata keyed by type name. - /// - public Dictionary NestedTypes { get; } = new Dictionary(); - } + /// + /// Gets or sets the collection name for the entity. + /// + public string CollectionName { get; set; } = ""; + + /// + /// Gets or sets the collection property name. + /// + public string? CollectionPropertyName { get; set; } + + /// + /// Gets or sets the fully qualified collection identifier type name. + /// + public string? CollectionIdTypeFullName { get; set; } + + /// + /// Gets the key property for the entity if one exists. + /// + public PropertyInfo? IdProperty => Properties.FirstOrDefault(p => p.IsKey); + + /// + /// Gets or sets a value indicating whether IDs are automatically generated. + /// + public bool AutoId { get; set; } + + /// + /// Gets or sets a value indicating whether the entity uses private setters. + /// + public bool HasPrivateSetters { get; set; } + + /// + /// Gets or sets a value indicating whether the entity has a private or missing constructor. + /// + public bool HasPrivateOrNoConstructor { get; set; } + + /// + /// Gets the entity properties. + /// + public List Properties { get; } = new(); + + /// + /// Gets nested type metadata keyed by type name. + /// + public Dictionary NestedTypes { get; } = new(); + + /// + /// Gets property names that should be ignored by mapping. + /// + public HashSet IgnoredProperties { get; } = new(); } + +/// +/// Contains metadata describing a mapped property. +/// +public class PropertyInfo +{ + /// + /// Gets or sets the property name. + /// + public string Name { get; set; } = ""; + + /// + /// Gets or sets the property type name. + /// + public string TypeName { get; set; } = ""; + + /// + /// Gets or sets the BSON field name. + /// + public string BsonFieldName { get; set; } = ""; + + /// + /// Gets or sets the database column type name. + /// + public string? ColumnTypeName { get; set; } + + /// + /// Gets or sets a value indicating whether the property is nullable. + /// + public bool IsNullable { get; set; } + + /// + /// Gets or sets a value indicating whether the property has a public setter. + /// + public bool HasPublicSetter { get; set; } + + /// + /// Gets or sets a value indicating whether the property uses an init-only setter. + /// + public bool HasInitOnlySetter { get; set; } + + /// + /// Gets or sets a value indicating whether the property has any setter. + /// + public bool HasAnySetter { get; set; } + + /// + /// Gets or sets a value indicating whether the getter is read-only. + /// + public bool IsReadOnlyGetter { get; set; } + + /// + /// Gets or sets the backing field name if available. + /// + public string? BackingFieldName { get; set; } + + /// + /// Gets or sets a value indicating whether the property is the key. + /// + public bool IsKey { get; set; } + + /// + /// Gets or sets a value indicating whether the property is required. + /// + public bool IsRequired { get; set; } + + /// + /// Gets or sets the maximum allowed length. + /// + public int? MaxLength { get; set; } + + /// + /// Gets or sets the minimum allowed length. + /// + public int? MinLength { get; set; } + + /// + /// Gets or sets the minimum allowed range value. + /// + public double? RangeMin { get; set; } + + /// + /// Gets or sets the maximum allowed range value. + /// + public double? RangeMax { get; set; } + + /// + /// Gets or sets a value indicating whether the property is a collection. + /// + public bool IsCollection { get; set; } + + /// + /// Gets or sets a value indicating whether the property is an array. + /// + public bool IsArray { get; set; } + + /// + /// Gets or sets the collection item type name. + /// + public string? CollectionItemType { get; set; } + + /// + /// Gets or sets the concrete collection type name. + /// + public string? CollectionConcreteTypeName { get; set; } + + /// + /// Gets or sets a value indicating whether the property is a nested object. + /// + public bool IsNestedObject { get; set; } + + /// + /// Gets or sets a value indicating whether collection items are nested objects. + /// + public bool IsCollectionItemNested { get; set; } + + /// + /// Gets or sets the nested type name. + /// + public string? NestedTypeName { get; set; } + + /// + /// Gets or sets the fully qualified nested type name. + /// + public string? NestedTypeFullName { get; set; } + + /// + /// Gets or sets the converter type name. + /// + public string? ConverterTypeName { get; set; } + + /// + /// Gets or sets the provider type name used by the converter. + /// + public string? ProviderTypeName { get; set; } +} + +/// +/// Contains metadata describing a nested type. +/// +public class NestedTypeInfo +{ + /// + /// Gets or sets the nested type name. + /// + public string Name { get; set; } = ""; + + /// + /// Gets or sets the nested type namespace. + /// + public string Namespace { get; set; } = ""; + + /// + /// Gets or sets the fully qualified nested type name. + /// + public string FullTypeName { get; set; } = ""; + + /// + /// Gets or sets the depth of the nested type. + /// + public int Depth { get; set; } + + /// + /// Gets the nested type properties. + /// + public List Properties { get; } = new(); + + /// + /// Gets nested type metadata keyed by type name. + /// + public Dictionary NestedTypes { get; } = new(); +} \ No newline at end of file diff --git a/src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj b/src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj index 4214045..f756abc 100755 --- a/src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj +++ b/src/CBDD.SourceGenerators/ZB.MOM.WW.CBDD.SourceGenerators.csproj @@ -1,38 +1,38 @@ - - net10.0 - ZB.MOM.WW.CBDD.SourceGenerators - ZB.MOM.WW.CBDD.SourceGenerators - latest - enable - true - true - ZB.MOM.WW.CBDD.SourceGenerators - 1.3.1 - CBDD Team - Source Generators for CBDD High-Performance BSON Database Engine - MIT - README.md - https://github.com/EntglDb/CBDD - database;embedded;bson;nosql;net10;zero-allocation;source-generator - true - false - true - true - + + net10.0 + ZB.MOM.WW.CBDD.SourceGenerators + ZB.MOM.WW.CBDD.SourceGenerators + latest + enable + true + true + ZB.MOM.WW.CBDD.SourceGenerators + 1.3.1 + CBDD Team + Source Generators for CBDD High-Performance BSON Database Engine + MIT + README.md + https://github.com/EntglDb/CBDD + database;embedded;bson;nosql;net10;zero-allocation;source-generator + true + false + true + true + - - - - + + + + - - - + + + - - - + + + diff --git a/src/CBDD/ZB.MOM.WW.CBDD.csproj b/src/CBDD/ZB.MOM.WW.CBDD.csproj index bdf9525..38d66e6 100755 --- a/src/CBDD/ZB.MOM.WW.CBDD.csproj +++ b/src/CBDD/ZB.MOM.WW.CBDD.csproj @@ -1,34 +1,34 @@ - - net10.0 - ZB.MOM.WW.CBDD - ZB.MOM.WW.CBDD - latest - enable - enable - true - true - - ZB.MOM.WW.CBDD - 1.3.1 - CBDD Team - High-Performance BSON Database Engine for .NET 10 - MIT - README.md - https://github.com/EntglDb/CBDD - database;embedded;bson;nosql;net10;zero-allocation - True - + + net10.0 + ZB.MOM.WW.CBDD + ZB.MOM.WW.CBDD + latest + enable + enable + true + true - - - - - + ZB.MOM.WW.CBDD + 1.3.1 + CBDD Team + High-Performance BSON Database Engine for .NET 10 + MIT + README.md + https://github.com/EntglDb/CBDD + database;embedded;bson;nosql;net10;zero-allocation + True + - - - + + + + + + + + + diff --git a/tests/CBDD.Tests.Benchmark/Compaction/CompactionBenchmarks.cs b/tests/CBDD.Tests.Benchmark/Compaction/CompactionBenchmarks.cs index 7efe737..5e5982b 100644 --- a/tests/CBDD.Tests.Benchmark/Compaction/CompactionBenchmarks.cs +++ b/tests/CBDD.Tests.Benchmark/Compaction/CompactionBenchmarks.cs @@ -1,6 +1,6 @@ +using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Storage; @@ -15,21 +15,22 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark; [JsonExporterAttribute.Full] public class CompactionBenchmarks { + private readonly List _insertedIds = []; + private DocumentCollection _collection = null!; + + private string _dbPath = string.Empty; + private StorageEngine _storage = null!; + private BenchmarkTransactionHolder _transactionHolder = null!; + private string _walPath = string.Empty; + /// - /// Gets or sets the number of documents used per benchmark iteration. + /// Gets or sets the number of documents used per benchmark iteration. /// [Params(2_000)] public int DocumentCount { get; set; } - private string _dbPath = string.Empty; - private string _walPath = string.Empty; - private StorageEngine _storage = null!; - private BenchmarkTransactionHolder _transactionHolder = null!; - private DocumentCollection _collection = null!; - private List _insertedIds = []; - /// - /// Prepares benchmark state and seed data for each iteration. + /// Prepares benchmark state and seed data for each iteration. /// [IterationSetup] public void Setup() @@ -53,17 +54,14 @@ public class CompactionBenchmarks _transactionHolder.CommitAndReset(); _storage.Checkpoint(); - for (var i = _insertedIds.Count - 1; i >= _insertedIds.Count / 3; i--) - { - _collection.Delete(_insertedIds[i]); - } + for (int i = _insertedIds.Count - 1; i >= _insertedIds.Count / 3; i--) _collection.Delete(_insertedIds[i]); _transactionHolder.CommitAndReset(); _storage.Checkpoint(); } /// - /// Cleans up benchmark resources and temporary files after each iteration. + /// Cleans up benchmark resources and temporary files after each iteration. /// [IterationCleanup] public void Cleanup() @@ -76,7 +74,7 @@ public class CompactionBenchmarks } /// - /// Benchmarks reclaimed file bytes reported by offline compaction. + /// Benchmarks reclaimed file bytes reported by offline compaction. /// /// The reclaimed file byte count. [Benchmark(Baseline = true)] @@ -95,7 +93,7 @@ public class CompactionBenchmarks } /// - /// Benchmarks tail bytes truncated by offline compaction. + /// Benchmarks tail bytes truncated by offline compaction. /// /// The truncated tail byte count. [Benchmark] @@ -135,7 +133,7 @@ public class CompactionBenchmarks private static string BuildPayload(int seed) { - var builder = new System.Text.StringBuilder(2500); + var builder = new StringBuilder(2500); for (var i = 0; i < 80; i++) { builder.Append("compact-"); @@ -147,4 +145,4 @@ public class CompactionBenchmarks return builder.ToString(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Compression/CompressionBenchmarks.cs b/tests/CBDD.Tests.Benchmark/Compression/CompressionBenchmarks.cs index 1f5edc5..3054350 100644 --- a/tests/CBDD.Tests.Benchmark/Compression/CompressionBenchmarks.cs +++ b/tests/CBDD.Tests.Benchmark/Compression/CompressionBenchmarks.cs @@ -1,7 +1,7 @@ +using System.IO.Compression; +using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using System.IO.Compression; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Compression; @@ -19,36 +19,36 @@ public class CompressionBenchmarks { private const int SeedCount = 300; private const int WorkloadCount = 100; + private DocumentCollection _collection = null!; + + private string _dbPath = string.Empty; + + private Person[] _insertBatch = Array.Empty(); + private ObjectId[] _seedIds = Array.Empty(); + private StorageEngine _storage = null!; + private BenchmarkTransactionHolder _transactionHolder = null!; + private string _walPath = string.Empty; /// - /// Gets or sets whether compression is enabled for the benchmark run. + /// Gets or sets whether compression is enabled for the benchmark run. /// [Params(false, true)] public bool EnableCompression { get; set; } /// - /// Gets or sets the compression codec for the benchmark run. + /// Gets or sets the compression codec for the benchmark run. /// [Params(CompressionCodec.Brotli, CompressionCodec.Deflate)] public CompressionCodec Codec { get; set; } /// - /// Gets or sets the compression level for the benchmark run. + /// Gets or sets the compression level for the benchmark run. /// [Params(CompressionLevel.Fastest, CompressionLevel.Optimal)] public CompressionLevel Level { get; set; } - private string _dbPath = string.Empty; - private string _walPath = string.Empty; - private StorageEngine _storage = null!; - private BenchmarkTransactionHolder _transactionHolder = null!; - private DocumentCollection _collection = null!; - - private Person[] _insertBatch = Array.Empty(); - private ObjectId[] _seedIds = Array.Empty(); - /// - /// Prepares benchmark storage and seed data for each iteration. + /// Prepares benchmark storage and seed data for each iteration. /// [IterationSetup] public void Setup() @@ -73,19 +73,19 @@ public class CompressionBenchmarks _seedIds = new ObjectId[SeedCount]; for (var i = 0; i < SeedCount; i++) { - var doc = CreatePerson(i, includeLargeBio: true); + var doc = CreatePerson(i, true); _seedIds[i] = _collection.Insert(doc); } _transactionHolder.CommitAndReset(); _insertBatch = Enumerable.Range(SeedCount, WorkloadCount) - .Select(i => CreatePerson(i, includeLargeBio: true)) + .Select(i => CreatePerson(i, true)) .ToArray(); } /// - /// Cleans up benchmark resources for each iteration. + /// Cleans up benchmark resources for each iteration. /// [IterationCleanup] public void Cleanup() @@ -98,7 +98,7 @@ public class CompressionBenchmarks } /// - /// Benchmarks insert workload performance. + /// Benchmarks insert workload performance. /// [Benchmark(Baseline = true)] [BenchmarkCategory("Compression_InsertUpdateRead")] @@ -109,7 +109,7 @@ public class CompressionBenchmarks } /// - /// Benchmarks update workload performance. + /// Benchmarks update workload performance. /// [Benchmark] [BenchmarkCategory("Compression_InsertUpdateRead")] @@ -131,7 +131,7 @@ public class CompressionBenchmarks } /// - /// Benchmarks read workload performance. + /// Benchmarks read workload performance. /// [Benchmark] [BenchmarkCategory("Compression_InsertUpdateRead")] @@ -141,10 +141,7 @@ public class CompressionBenchmarks for (var i = 0; i < WorkloadCount; i++) { var person = _collection.FindById(_seedIds[i]); - if (person != null) - { - checksum += person.Age; - } + if (person != null) checksum += person.Age; } _transactionHolder.CommitAndReset(); @@ -158,7 +155,7 @@ public class CompressionBenchmarks Id = ObjectId.NewObjectId(), FirstName = $"First_{i}", LastName = $"Last_{i}", - Age = 20 + (i % 50), + Age = 20 + i % 50, Bio = includeLargeBio ? BuildBio(i) : $"bio-{i}", CreatedAt = DateTime.UnixEpoch.AddMinutes(i), Balance = 100 + i, @@ -183,7 +180,7 @@ public class CompressionBenchmarks private static string BuildBio(int seed) { - var builder = new System.Text.StringBuilder(4500); + var builder = new StringBuilder(4500); for (var i = 0; i < 150; i++) { builder.Append("bio-"); @@ -195,4 +192,4 @@ public class CompressionBenchmarks return builder.ToString(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Fixtures/Person.cs b/tests/CBDD.Tests.Benchmark/Fixtures/Person.cs index 11d0dc2..d32a9c7 100755 --- a/tests/CBDD.Tests.Benchmark/Fixtures/Person.cs +++ b/tests/CBDD.Tests.Benchmark/Fixtures/Person.cs @@ -1,21 +1,21 @@ using ZB.MOM.WW.CBDD.Bson; -using System; -using System.Collections.Generic; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; public class Address { /// - /// Gets or sets the Street. + /// Gets or sets the Street. /// public string Street { get; set; } = string.Empty; + /// - /// Gets or sets the City. + /// Gets or sets the City. /// public string City { get; set; } = string.Empty; + /// - /// Gets or sets the ZipCode. + /// Gets or sets the ZipCode. /// public string ZipCode { get; set; } = string.Empty; } @@ -23,19 +23,22 @@ public class Address public class WorkHistory { /// - /// Gets or sets the CompanyName. + /// Gets or sets the CompanyName. /// public string CompanyName { get; set; } = string.Empty; + /// - /// Gets or sets the Title. + /// Gets or sets the Title. /// public string Title { get; set; } = string.Empty; + /// - /// Gets or sets the DurationYears. + /// Gets or sets the DurationYears. /// public int DurationYears { get; set; } + /// - /// Gets or sets the Tags. + /// Gets or sets the Tags. /// public List Tags { get; set; } = new(); } @@ -43,41 +46,48 @@ public class WorkHistory public class Person { /// - /// Gets or sets the Id. + /// Gets or sets the Id. /// public ObjectId Id { get; set; } + /// - /// Gets or sets the FirstName. + /// Gets or sets the FirstName. /// public string FirstName { get; set; } = string.Empty; + /// - /// Gets or sets the LastName. + /// Gets or sets the LastName. /// public string LastName { get; set; } = string.Empty; + /// - /// Gets or sets the Age. + /// Gets or sets the Age. /// public int Age { get; set; } + /// - /// Gets or sets the Bio. + /// Gets or sets the Bio. /// public string? Bio { get; set; } = string.Empty; + /// - /// Gets or sets the CreatedAt. + /// Gets or sets the CreatedAt. /// public DateTime CreatedAt { get; set; } // Complex fields /// - /// Gets or sets the Balance. + /// Gets or sets the Balance. /// public decimal Balance { get; set; } + /// - /// Gets or sets the HomeAddress. + /// Gets or sets the HomeAddress. /// public Address HomeAddress { get; set; } = new(); + /// - /// Gets or sets the EmploymentHistory. + /// Gets or sets the EmploymentHistory. /// public List EmploymentHistory { get; set; } = new(); -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Fixtures/PersonMapper.cs b/tests/CBDD.Tests.Benchmark/Fixtures/PersonMapper.cs index bf48451..c9cbb57 100755 --- a/tests/CBDD.Tests.Benchmark/Fixtures/PersonMapper.cs +++ b/tests/CBDD.Tests.Benchmark/Fixtures/PersonMapper.cs @@ -1,26 +1,30 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; -using System.Buffers; -using System.Runtime.InteropServices; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; -public class PersonMapper : ObjectIdMapperBase -{ - /// - public override string CollectionName => "people"; - - /// - public override ObjectId GetId(Person entity) => entity.Id; - - /// - public override void SetId(Person entity, ObjectId id) => entity.Id = id; - - /// - public override int Serialize(Person entity, BsonSpanWriter writer) - { - var sizePos = writer.BeginDocument(); - +public class PersonMapper : ObjectIdMapperBase +{ + /// + public override string CollectionName => "people"; + + /// + public override ObjectId GetId(Person entity) + { + return entity.Id; + } + + /// + public override void SetId(Person entity, ObjectId id) + { + entity.Id = id; + } + + /// + public override int Serialize(Person entity, BsonSpanWriter writer) + { + int sizePos = writer.BeginDocument(); + writer.WriteObjectId("_id", entity.Id); writer.WriteString("firstname", entity.FirstName); writer.WriteString("lastname", entity.LastName); @@ -30,111 +34,119 @@ public class PersonMapper : ObjectIdMapperBase else writer.WriteNull("bio"); - writer.WriteInt64("createdat", entity.CreatedAt.Ticks); - - // Complex fields - writer.WriteDouble("balance", (double)entity.Balance); - - // Nested Object: Address - var addrPos = writer.BeginDocument("homeaddress"); + writer.WriteInt64("createdat", entity.CreatedAt.Ticks); + + // Complex fields + writer.WriteDouble("balance", (double)entity.Balance); + + // Nested Object: Address + int addrPos = writer.BeginDocument("homeaddress"); writer.WriteString("street", entity.HomeAddress.Street); writer.WriteString("city", entity.HomeAddress.City); writer.WriteString("zipcode", entity.HomeAddress.ZipCode); - writer.EndDocument(addrPos); - - // Collection: EmploymentHistory - var histPos = writer.BeginArray("employmenthistory"); - for (int i = 0; i < entity.EmploymentHistory.Count; i++) + writer.EndDocument(addrPos); + + // Collection: EmploymentHistory + int histPos = writer.BeginArray("employmenthistory"); + for (var i = 0; i < entity.EmploymentHistory.Count; i++) { var item = entity.EmploymentHistory[i]; // Array elements are keys "0", "1", "2"... - var itemPos = writer.BeginDocument(i.ToString()); - + int itemPos = writer.BeginDocument(i.ToString()); + writer.WriteString("companyname", item.CompanyName); writer.WriteString("title", item.Title); - writer.WriteInt32("durationyears", item.DurationYears); - - // Nested Collection: Tags - var tagsPos = writer.BeginArray("tags"); - for (int j = 0; j < item.Tags.Count; j++) - { - writer.WriteString(j.ToString(), item.Tags[j]); - } - writer.EndArray(tagsPos); - + writer.WriteInt32("durationyears", item.DurationYears); + + // Nested Collection: Tags + int tagsPos = writer.BeginArray("tags"); + for (var j = 0; j < item.Tags.Count; j++) writer.WriteString(j.ToString(), item.Tags[j]); + writer.EndArray(tagsPos); + writer.EndDocument(itemPos); } - writer.EndArray(histPos); - - writer.EndDocument(sizePos); - + + writer.EndArray(histPos); + + writer.EndDocument(sizePos); + return writer.Position; } - /// - public override Person Deserialize(BsonSpanReader reader) - { - var person = new Person(); - - reader.ReadDocumentSize(); - + /// + public override Person Deserialize(BsonSpanReader reader) + { + var person = new Person(); + + reader.ReadDocumentSize(); + while (reader.Remaining > 0) { var type = reader.ReadBsonType(); if (type == BsonType.EndOfDocument) - break; - - var name = reader.ReadElementHeader(); - + break; + + string name = reader.ReadElementHeader(); + switch (name) { case "_id": person.Id = reader.ReadObjectId(); break; case "firstname": person.FirstName = reader.ReadString(); break; case "lastname": person.LastName = reader.ReadString(); break; case "age": person.Age = reader.ReadInt32(); break; - case "bio": + case "bio": if (type == BsonType.Null) person.Bio = null; - else person.Bio = reader.ReadString(); + else person.Bio = reader.ReadString(); break; case "createdat": person.CreatedAt = new DateTime(reader.ReadInt64()); break; - case "balance": person.Balance = (decimal)reader.ReadDouble(); break; - + case "balance": person.Balance = (decimal)reader.ReadDouble(); break; + case "homeaddress": reader.ReadDocumentSize(); // Enter document while (reader.Remaining > 0) { var addrType = reader.ReadBsonType(); if (addrType == BsonType.EndOfDocument) break; - var addrName = reader.ReadElementHeader(); - - // We assume strict schema for benchmark speed, but should handle skipping + string addrName = reader.ReadElementHeader(); + + // We assume strict schema for benchmark speed, but should handle skipping if (addrName == "street") person.HomeAddress.Street = reader.ReadString(); else if (addrName == "city") person.HomeAddress.City = reader.ReadString(); else if (addrName == "zipcode") person.HomeAddress.ZipCode = reader.ReadString(); else reader.SkipValue(addrType); } - break; - + + break; + case "employmenthistory": reader.ReadDocumentSize(); // Enter Array while (reader.Remaining > 0) { var arrType = reader.ReadBsonType(); if (arrType == BsonType.EndOfDocument) break; - reader.ReadElementHeader(); // Array index "0", "1"... ignore - - // Read WorkHistory item + reader.ReadElementHeader(); // Array index "0", "1"... ignore + + // Read WorkHistory item var workItem = new WorkHistory(); reader.ReadDocumentSize(); // Enter Item Document while (reader.Remaining > 0) { var itemType = reader.ReadBsonType(); if (itemType == BsonType.EndOfDocument) break; - var itemName = reader.ReadElementHeader(); - - if (itemName == "companyname") workItem.CompanyName = reader.ReadString(); - else if (itemName == "title") workItem.Title = reader.ReadString(); - else if (itemName == "durationyears") workItem.DurationYears = reader.ReadInt32(); + string itemName = reader.ReadElementHeader(); + + if (itemName == "companyname") + { + workItem.CompanyName = reader.ReadString(); + } + else if (itemName == "title") + { + workItem.Title = reader.ReadString(); + } + else if (itemName == "durationyears") + { + workItem.DurationYears = reader.ReadInt32(); + } else if (itemName == "tags") { reader.ReadDocumentSize(); // Enter Tags Array @@ -149,18 +161,23 @@ public class PersonMapper : ObjectIdMapperBase reader.SkipValue(tagType); } } - else reader.SkipValue(itemType); + else + { + reader.SkipValue(itemType); + } } + person.EmploymentHistory.Add(workItem); } - break; - + + break; + default: reader.SkipValue(type); break; } - } - + } + return person; } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Infrastructure/BenchmarkTransactionHolder.cs b/tests/CBDD.Tests.Benchmark/Infrastructure/BenchmarkTransactionHolder.cs index 01441da..fa10f3d 100644 --- a/tests/CBDD.Tests.Benchmark/Infrastructure/BenchmarkTransactionHolder.cs +++ b/tests/CBDD.Tests.Benchmark/Infrastructure/BenchmarkTransactionHolder.cs @@ -1,4 +1,3 @@ -using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; @@ -11,7 +10,7 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab private ITransaction? _currentTransaction; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The storage engine used to create transactions. public BenchmarkTransactionHolder(StorageEngine storage) @@ -20,7 +19,15 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab } /// - /// Gets the current active transaction or starts a new one. + /// Disposes this holder and rolls back any outstanding transaction. + /// + public void Dispose() + { + RollbackAndReset(); + } + + /// + /// Gets the current active transaction or starts a new one. /// /// The current active transaction. public ITransaction GetCurrentTransactionOrStart() @@ -28,16 +35,14 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab lock (_sync) { if (_currentTransaction == null || _currentTransaction.State != TransactionState.Active) - { _currentTransaction = _storage.BeginTransaction(); - } return _currentTransaction; } } /// - /// Gets the current active transaction or starts a new one asynchronously. + /// Gets the current active transaction or starts a new one asynchronously. /// /// A task that returns the current active transaction. public Task GetCurrentTransactionOrStartAsync() @@ -46,22 +51,17 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab } /// - /// Commits the current transaction when active and clears the holder. + /// Commits the current transaction when active and clears the holder. /// public void CommitAndReset() { lock (_sync) { - if (_currentTransaction == null) - { - return; - } + if (_currentTransaction == null) return; if (_currentTransaction.State == TransactionState.Active || _currentTransaction.State == TransactionState.Preparing) - { _currentTransaction.Commit(); - } _currentTransaction.Dispose(); _currentTransaction = null; @@ -69,33 +69,20 @@ internal sealed class BenchmarkTransactionHolder : ITransactionHolder, IDisposab } /// - /// Rolls back the current transaction when active and clears the holder. + /// Rolls back the current transaction when active and clears the holder. /// public void RollbackAndReset() { lock (_sync) { - if (_currentTransaction == null) - { - return; - } + if (_currentTransaction == null) return; if (_currentTransaction.State == TransactionState.Active || _currentTransaction.State == TransactionState.Preparing) - { _currentTransaction.Rollback(); - } _currentTransaction.Dispose(); _currentTransaction = null; } } - - /// - /// Disposes this holder and rolls back any outstanding transaction. - /// - public void Dispose() - { - RollbackAndReset(); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Infrastructure/Logging.cs b/tests/CBDD.Tests.Benchmark/Infrastructure/Logging.cs index ce7413f..3f7f5bf 100644 --- a/tests/CBDD.Tests.Benchmark/Infrastructure/Logging.cs +++ b/tests/CBDD.Tests.Benchmark/Infrastructure/Logging.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Serilog; +using ILogger = Microsoft.Extensions.Logging.ILogger; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; @@ -8,16 +9,16 @@ internal static class Logging private static readonly Lazy LoggerFactoryInstance = new(CreateFactory); /// - /// Gets the shared logger factory for benchmarks. + /// Gets the shared logger factory for benchmarks. /// public static ILoggerFactory LoggerFactory => LoggerFactoryInstance.Value; /// - /// Creates a logger for the specified category type. + /// Creates a logger for the specified category type. /// /// The logger category type. - /// A logger for . - public static Microsoft.Extensions.Logging.ILogger CreateLogger() + /// A logger for . + public static ILogger CreateLogger() { return LoggerFactory.CreateLogger(); } @@ -32,7 +33,7 @@ internal static class Logging return Microsoft.Extensions.Logging.LoggerFactory.Create(builder => { builder.ClearProviders(); - builder.AddSerilog(serilogLogger, dispose: true); + builder.AddSerilog(serilogLogger, true); }); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Infrastructure/Program.cs b/tests/CBDD.Tests.Benchmark/Infrastructure/Program.cs index 9007f18..ea32ad4 100755 --- a/tests/CBDD.Tests.Benchmark/Infrastructure/Program.cs +++ b/tests/CBDD.Tests.Benchmark/Infrastructure/Program.cs @@ -3,17 +3,17 @@ using BenchmarkDotNet.Configs; using BenchmarkDotNet.Exporters; using BenchmarkDotNet.Reports; using BenchmarkDotNet.Running; -using Microsoft.Extensions.Logging; +using Perfolizer.Horology; using Serilog.Context; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; -class Program +internal class Program { - static void Main(string[] args) + private static void Main(string[] args) { var logger = Logging.CreateLogger(); - var mode = args.Length > 0 ? args[0].Trim().ToLowerInvariant() : string.Empty; + string mode = args.Length > 0 ? args[0].Trim().ToLowerInvariant() : string.Empty; if (mode == "manual") { @@ -84,6 +84,6 @@ class Program .AddExporter(HtmlExporter.Default) .WithSummaryStyle(SummaryStyle.Default .WithRatioStyle(RatioStyle.Trend) - .WithTimeUnit(Perfolizer.Horology.TimeUnit.Microsecond)); + .WithTimeUnit(TimeUnit.Microsecond)); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Insert/InsertBenchmarks.cs b/tests/CBDD.Tests.Benchmark/Insert/InsertBenchmarks.cs index 8c1c786..bbe6ccf 100755 --- a/tests/CBDD.Tests.Benchmark/Insert/InsertBenchmarks.cs +++ b/tests/CBDD.Tests.Benchmark/Insert/InsertBenchmarks.cs @@ -1,18 +1,13 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; using Microsoft.Extensions.Logging; using Serilog.Context; -using System.IO; +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.Collections; +using ZB.MOM.WW.CBDD.Core.Storage; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; - [InProcess] [MemoryDiagnoser] [GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] @@ -23,33 +18,30 @@ public class InsertBenchmarks private const int BatchSize = 1000; private static readonly ILogger Logger = Logging.CreateLogger(); + private Person[] _batchData = Array.Empty(); + private DocumentCollection? _collection; + private string _docDbPath = ""; private string _docDbWalPath = ""; + private Person? _singlePerson; - private StorageEngine? _storage = null; - private BenchmarkTransactionHolder? _transactionHolder = null; - private DocumentCollection? _collection = null; - - private Person[] _batchData = Array.Empty(); - private Person? _singlePerson = null; + private StorageEngine? _storage; + private BenchmarkTransactionHolder? _transactionHolder; /// - /// Tests setup. + /// Tests setup. /// [GlobalSetup] public void Setup() { - var temp = AppContext.BaseDirectory; + string temp = AppContext.BaseDirectory; var id = Guid.NewGuid().ToString("N"); _docDbPath = Path.Combine(temp, $"bench_docdb_{id}.db"); _docDbWalPath = Path.ChangeExtension(_docDbPath, ".wal"); _singlePerson = CreatePerson(0); _batchData = new Person[BatchSize]; - for (int i = 0; i < BatchSize; i++) - { - _batchData[i] = CreatePerson(i); - } + for (var i = 0; i < BatchSize; i++) _batchData[i] = CreatePerson(i); } private Person CreatePerson(int i) @@ -59,7 +51,7 @@ public class InsertBenchmarks Id = ObjectId.NewObjectId(), FirstName = $"First_{i}", LastName = $"Last_{i}", - Age = 20 + (i % 50), + Age = 20 + i % 50, Bio = null, // Removed large payload to focus on structure CreatedAt = DateTime.UtcNow, Balance = 1000.50m * (i + 1), @@ -72,8 +64,7 @@ public class InsertBenchmarks }; // Add 10 work history items to stress structure traversal - for (int j = 0; j < 10; j++) - { + for (var j = 0; j < 10; j++) p.EmploymentHistory.Add(new WorkHistory { CompanyName = $"TechCorp_{i}_{j}", @@ -81,13 +72,12 @@ public class InsertBenchmarks DurationYears = j, Tags = new List { "C#", "BSON", "Performance", "Database", "Complex" } }); - } return p; } /// - /// Tests iteration setup. + /// Tests iteration setup. /// [IterationSetup] public void IterationSetup() @@ -98,7 +88,7 @@ public class InsertBenchmarks } /// - /// Tests cleanup. + /// Tests cleanup. /// [IterationCleanup] public void Cleanup() @@ -111,7 +101,7 @@ public class InsertBenchmarks _storage?.Dispose(); _storage = null; - System.Threading.Thread.Sleep(100); + Thread.Sleep(100); if (File.Exists(_docDbPath)) File.Delete(_docDbPath); if (File.Exists(_docDbWalPath)) File.Delete(_docDbWalPath); @@ -125,7 +115,7 @@ public class InsertBenchmarks // --- Benchmarks --- /// - /// Tests document db insert single. + /// Tests document db insert single. /// [Benchmark(Baseline = true, Description = "CBDD Single Insert")] [BenchmarkCategory("Insert_Single")] @@ -136,7 +126,7 @@ public class InsertBenchmarks } /// - /// Tests document db insert batch. + /// Tests document db insert batch. /// [Benchmark(Description = "CBDD Batch Insert (1000 items, 1 Txn)")] [BenchmarkCategory("Insert_Batch")] @@ -145,4 +135,4 @@ public class InsertBenchmarks _collection?.InsertBulk(_batchData); _transactionHolder?.CommitAndReset(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Read/ReadBenchmarks.cs b/tests/CBDD.Tests.Benchmark/Read/ReadBenchmarks.cs index a5963ca..3f584aa 100755 --- a/tests/CBDD.Tests.Benchmark/Read/ReadBenchmarks.cs +++ b/tests/CBDD.Tests.Benchmark/Read/ReadBenchmarks.cs @@ -1,12 +1,8 @@ using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; -using System.IO; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; @@ -19,24 +15,24 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark; public class ReadBenchmarks { private const int DocCount = 1000; + private DocumentCollection _collection = null!; private string _docDbPath = null!; private string _docDbWalPath = null!; - private StorageEngine _storage = null!; - private BenchmarkTransactionHolder _transactionHolder = null!; - private DocumentCollection _collection = null!; - private ObjectId[] _ids = null!; + + private StorageEngine _storage = null!; private ObjectId _targetId; + private BenchmarkTransactionHolder _transactionHolder = null!; /// - /// Tests setup. + /// Tests setup. /// [GlobalSetup] public void Setup() { - var temp = AppContext.BaseDirectory; + string temp = AppContext.BaseDirectory; var id = Guid.NewGuid().ToString("N"); _docDbPath = Path.Combine(temp, $"bench_read_docdb_{id}.db"); _docDbWalPath = Path.ChangeExtension(_docDbPath, ".wal"); @@ -49,18 +45,19 @@ public class ReadBenchmarks _collection = new DocumentCollection(_storage, _transactionHolder, new PersonMapper()); _ids = new ObjectId[DocCount]; - for (int i = 0; i < DocCount; i++) + for (var i = 0; i < DocCount; i++) { var p = CreatePerson(i); _ids[i] = _collection.Insert(p); } + _transactionHolder.CommitAndReset(); _targetId = _ids[DocCount / 2]; } /// - /// Tests cleanup. + /// Tests cleanup. /// [GlobalCleanup] public void Cleanup() @@ -79,7 +76,7 @@ public class ReadBenchmarks Id = ObjectId.NewObjectId(), FirstName = $"First_{i}", LastName = $"Last_{i}", - Age = 20 + (i % 50), + Age = 20 + i % 50, Bio = null, CreatedAt = DateTime.UtcNow, Balance = 1000.50m * (i + 1), @@ -92,8 +89,7 @@ public class ReadBenchmarks }; // Add 10 work history items - for (int j = 0; j < 10; j++) - { + for (var j = 0; j < 10; j++) p.EmploymentHistory.Add(new WorkHistory { CompanyName = $"TechCorp_{i}_{j}", @@ -101,13 +97,12 @@ public class ReadBenchmarks DurationYears = j, Tags = new List { "C#", "BSON", "Performance", "Database", "Complex" } }); - } return p; } /// - /// Tests document db find by id. + /// Tests document db find by id. /// [Benchmark(Baseline = true, Description = "CBDD FindById")] [BenchmarkCategory("Read_Single")] @@ -115,4 +110,4 @@ public class ReadBenchmarks { return _collection.FindById(_targetId); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Serialization/SerializationBenchmarks.cs b/tests/CBDD.Tests.Benchmark/Serialization/SerializationBenchmarks.cs index 0dc79d5..6a89149 100755 --- a/tests/CBDD.Tests.Benchmark/Serialization/SerializationBenchmarks.cs +++ b/tests/CBDD.Tests.Benchmark/Serialization/SerializationBenchmarks.cs @@ -1,7 +1,8 @@ +using System.Collections.Concurrent; +using System.Text.Json; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; using ZB.MOM.WW.CBDD.Bson; -using System.Text.Json; namespace ZB.MOM.WW.CBDD.Tests.Benchmark; @@ -13,32 +14,37 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark; public class SerializationBenchmarks { private const int BatchSize = 10000; - private Person _person = null!; - private List _people = null!; - private PersonMapper _mapper = new PersonMapper(); + + private static readonly ConcurrentDictionary _keyMap = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _keys = new(); + + private readonly List _bsonDataList = new(); + private readonly List _jsonDataList = new(); + private readonly PersonMapper _mapper = new(); private byte[] _bsonData = Array.Empty(); - private byte[] _jsonData = Array.Empty(); - - private List _bsonDataList = new(); - private List _jsonDataList = new(); - + private byte[] _jsonData = Array.Empty(); + private List _people = null!; + private Person _person = null!; + private byte[] _serializeBuffer = Array.Empty(); - private static readonly System.Collections.Concurrent.ConcurrentDictionary _keyMap = new(StringComparer.OrdinalIgnoreCase); - private static readonly System.Collections.Concurrent.ConcurrentDictionary _keys = new(); - - static SerializationBenchmarks() + static SerializationBenchmarks() { ushort id = 1; - string[] initialKeys = { "_id", "firstname", "lastname", "age", "bio", "createdat", "balance", "homeaddress", "street", "city", "zipcode", "employmenthistory", "companyname", "title", "durationyears", "tags" }; - foreach (var key in initialKeys) + string[] initialKeys = + { + "_id", "firstname", "lastname", "age", "bio", "createdat", "balance", "homeaddress", "street", "city", + "zipcode", "employmenthistory", "companyname", "title", "durationyears", "tags" + }; + foreach (string key in initialKeys) { _keyMap[key] = id; _keys[id] = key; id++; } + // Add some indices for arrays - for (int i = 0; i < 100; i++) + for (var i = 0; i < 100; i++) { var s = i.ToString(); _keyMap[s] = id; @@ -47,26 +53,23 @@ public class SerializationBenchmarks } } - /// - /// Prepares benchmark data for serialization and deserialization scenarios. - /// - [GlobalSetup] - public void Setup() + /// + /// Prepares benchmark data for serialization and deserialization scenarios. + /// + [GlobalSetup] + public void Setup() { _person = CreatePerson(0); _people = new List(BatchSize); - for (int i = 0; i < BatchSize; i++) - { - _people.Add(CreatePerson(i)); - } - - // Pre-allocate buffer for BSON serialization + for (var i = 0; i < BatchSize; i++) _people.Add(CreatePerson(i)); + + // Pre-allocate buffer for BSON serialization _serializeBuffer = new byte[8192]; var writer = new BsonSpanWriter(_serializeBuffer, _keyMap); // Single item data - var len = _mapper.Serialize(_person, writer); + int len = _mapper.Serialize(_person, writer); _bsonData = _serializeBuffer.AsSpan(0, len).ToArray(); _jsonData = JsonSerializer.SerializeToUtf8Bytes(_person); @@ -87,10 +90,10 @@ public class SerializationBenchmarks FirstName = $"First_{i}", LastName = $"Last_{i}", Age = 25, - Bio = null, + Bio = null, CreatedAt = DateTime.UtcNow, Balance = 1000.50m, - HomeAddress = new Address + HomeAddress = new Address { Street = $"{i} Main St", City = "Tech City", @@ -98,8 +101,7 @@ public class SerializationBenchmarks } }; - for (int j = 0; j < 10; j++) - { + for (var j = 0; j < 10; j++) p.EmploymentHistory.Add(new WorkHistory { CompanyName = $"TechCorp_{i}_{j}", @@ -107,58 +109,57 @@ public class SerializationBenchmarks DurationYears = j, Tags = new List { "C#", "BSON", "Performance", "Database", "Complex" } }); - } return p; } - /// - /// Benchmarks BSON serialization for a single document. - /// - [Benchmark(Description = "Serialize Single (BSON)")] - [BenchmarkCategory("Single")] - public void Serialize_Bson() + /// + /// Benchmarks BSON serialization for a single document. + /// + [Benchmark(Description = "Serialize Single (BSON)")] + [BenchmarkCategory("Single")] + public void Serialize_Bson() { var writer = new BsonSpanWriter(_serializeBuffer, _keyMap); _mapper.Serialize(_person, writer); } - /// - /// Benchmarks JSON serialization for a single document. - /// - [Benchmark(Description = "Serialize Single (JSON)")] - [BenchmarkCategory("Single")] - public void Serialize_Json() + /// + /// Benchmarks JSON serialization for a single document. + /// + [Benchmark(Description = "Serialize Single (JSON)")] + [BenchmarkCategory("Single")] + public void Serialize_Json() { JsonSerializer.SerializeToUtf8Bytes(_person); - } - - /// - /// Benchmarks BSON deserialization for a single document. - /// - [Benchmark(Description = "Deserialize Single (BSON)")] - [BenchmarkCategory("Single")] - public Person Deserialize_Bson() + } + + /// + /// Benchmarks BSON deserialization for a single document. + /// + [Benchmark(Description = "Deserialize Single (BSON)")] + [BenchmarkCategory("Single")] + public Person Deserialize_Bson() { var reader = new BsonSpanReader(_bsonData, _keys); return _mapper.Deserialize(reader); } - /// - /// Benchmarks JSON deserialization for a single document. - /// - [Benchmark(Description = "Deserialize Single (JSON)")] - [BenchmarkCategory("Single")] - public Person? Deserialize_Json() + /// + /// Benchmarks JSON deserialization for a single document. + /// + [Benchmark(Description = "Deserialize Single (JSON)")] + [BenchmarkCategory("Single")] + public Person? Deserialize_Json() { return JsonSerializer.Deserialize(_jsonData); } - /// - /// Benchmarks BSON serialization for a list of documents. - /// - [Benchmark(Description = "Serialize List 10k (BSON loop)")] - [BenchmarkCategory("Batch")] - public void Serialize_List_Bson() + /// + /// Benchmarks BSON serialization for a list of documents. + /// + [Benchmark(Description = "Serialize List 10k (BSON loop)")] + [BenchmarkCategory("Batch")] + public void Serialize_List_Bson() { foreach (var p in _people) { @@ -167,43 +168,37 @@ public class SerializationBenchmarks } } - /// - /// Benchmarks JSON serialization for a list of documents. - /// - [Benchmark(Description = "Serialize List 10k (JSON loop)")] - [BenchmarkCategory("Batch")] - public void Serialize_List_Json() + /// + /// Benchmarks JSON serialization for a list of documents. + /// + [Benchmark(Description = "Serialize List 10k (JSON loop)")] + [BenchmarkCategory("Batch")] + public void Serialize_List_Json() { - foreach (var p in _people) - { - JsonSerializer.SerializeToUtf8Bytes(p); - } + foreach (var p in _people) JsonSerializer.SerializeToUtf8Bytes(p); } - /// - /// Benchmarks BSON deserialization for a list of documents. - /// - [Benchmark(Description = "Deserialize List 10k (BSON loop)")] - [BenchmarkCategory("Batch")] - public void Deserialize_List_Bson() + /// + /// Benchmarks BSON deserialization for a list of documents. + /// + [Benchmark(Description = "Deserialize List 10k (BSON loop)")] + [BenchmarkCategory("Batch")] + public void Deserialize_List_Bson() { - foreach (var data in _bsonDataList) + foreach (byte[] data in _bsonDataList) { var reader = new BsonSpanReader(data, _keys); _mapper.Deserialize(reader); } } - /// - /// Benchmarks JSON deserialization for a list of documents. - /// - [Benchmark(Description = "Deserialize List 10k (JSON loop)")] - [BenchmarkCategory("Batch")] - public void Deserialize_List_Json() + /// + /// Benchmarks JSON deserialization for a list of documents. + /// + [Benchmark(Description = "Deserialize List 10k (JSON loop)")] + [BenchmarkCategory("Batch")] + public void Deserialize_List_Json() { - foreach (var data in _jsonDataList) - { - JsonSerializer.Deserialize(data); - } + foreach (byte[] data in _jsonDataList) JsonSerializer.Deserialize(data); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Storage/DatabaseSizeBenchmark.cs b/tests/CBDD.Tests.Benchmark/Storage/DatabaseSizeBenchmark.cs index dc44356..0d81be9 100644 --- a/tests/CBDD.Tests.Benchmark/Storage/DatabaseSizeBenchmark.cs +++ b/tests/CBDD.Tests.Benchmark/Storage/DatabaseSizeBenchmark.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using System.IO.Compression; using Microsoft.Extensions.Logging; using Serilog.Context; using ZB.MOM.WW.CBDD.Bson; @@ -10,47 +11,47 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark; internal static class DatabaseSizeBenchmark { + private const int BatchSize = 50_000; + private const int ProgressInterval = 1_000_000; private static readonly int[] TargetCounts = [10_000, 1_000_000, 10_000_000]; + private static readonly CompressionOptions CompressedBrotliFast = new() { EnableCompression = true, MinSizeBytes = 256, MinSavingsPercent = 0, Codec = CompressionCodec.Brotli, - Level = System.IO.Compression.CompressionLevel.Fastest + Level = CompressionLevel.Fastest }; private static readonly Scenario[] Scenarios = [ // Separate compression set (no compaction) new( - Set: "compression", - Name: "CompressionOnly-Uncompressed", - CompressionOptions: CompressionOptions.Default, - RunCompaction: false), + "compression", + "CompressionOnly-Uncompressed", + CompressionOptions.Default, + false), new( - Set: "compression", - Name: "CompressionOnly-Compressed-BrotliFast", - CompressionOptions: CompressedBrotliFast, - RunCompaction: false), + "compression", + "CompressionOnly-Compressed-BrotliFast", + CompressedBrotliFast, + false), // Separate compaction set (compaction enabled) new( - Set: "compaction", - Name: "Compaction-Uncompressed", - CompressionOptions: CompressionOptions.Default, - RunCompaction: true), + "compaction", + "Compaction-Uncompressed", + CompressionOptions.Default, + true), new( - Set: "compaction", - Name: "Compaction-Compressed-BrotliFast", - CompressionOptions: CompressedBrotliFast, - RunCompaction: true) + "compaction", + "Compaction-Compressed-BrotliFast", + CompressedBrotliFast, + true) ]; - private const int BatchSize = 50_000; - private const int ProgressInterval = 1_000_000; - /// - /// Tests run. + /// Tests run. /// /// Logger for benchmark progress and results. public static void Run(ILogger logger) @@ -62,109 +63,101 @@ internal static class DatabaseSizeBenchmark logger.LogInformation("Scenarios: {Scenarios}", string.Join(", ", Scenarios.Select(x => $"{x.Set}:{x.Name}"))); logger.LogInformation("Batch size: {BatchSize:N0}", BatchSize); - foreach (var targetCount in TargetCounts) + foreach (int targetCount in TargetCounts) + foreach (var scenario in Scenarios) { - foreach (var scenario in Scenarios) + string dbPath = Path.Combine(Path.GetTempPath(), + $"cbdd_size_{scenario.Name}_{targetCount}_{Guid.NewGuid():N}.db"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); + using var _ = LogContext.PushProperty("TargetCount", targetCount); + using var __ = LogContext.PushProperty("Scenario", scenario.Name); + using var ___ = LogContext.PushProperty("ScenarioSet", scenario.Set); + + logger.LogInformation( + "Starting {Set} scenario {Scenario} for target {TargetCount:N0} docs", + scenario.Set, + scenario.Name, + targetCount); + + var insertStopwatch = Stopwatch.StartNew(); + CompressionStats compressionStats = default; + CompactionStats compactionStats = new(); + long preCompactDbBytes; + long preCompactWalBytes; + long postCompactDbBytes; + long postCompactWalBytes; + + using (var storage = new StorageEngine(dbPath, PageFileConfig.Default, scenario.CompressionOptions)) + using (var transactionHolder = new BenchmarkTransactionHolder(storage)) { - var dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_size_{scenario.Name}_{targetCount}_{Guid.NewGuid():N}.db"); - var walPath = Path.ChangeExtension(dbPath, ".wal"); - using var _ = LogContext.PushProperty("TargetCount", targetCount); - using var __ = LogContext.PushProperty("Scenario", scenario.Name); - using var ___ = LogContext.PushProperty("ScenarioSet", scenario.Set); + var collection = new DocumentCollection( + storage, + transactionHolder, + new SizeBenchmarkDocumentMapper()); - logger.LogInformation( - "Starting {Set} scenario {Scenario} for target {TargetCount:N0} docs", - scenario.Set, - scenario.Name, - targetCount); - - var insertStopwatch = Stopwatch.StartNew(); - CompressionStats compressionStats = default; - CompactionStats compactionStats = new(); - long preCompactDbBytes; - long preCompactWalBytes; - long postCompactDbBytes; - long postCompactWalBytes; - - using (var storage = new StorageEngine(dbPath, PageFileConfig.Default, scenario.CompressionOptions)) - using (var transactionHolder = new BenchmarkTransactionHolder(storage)) + var inserted = 0; + while (inserted < targetCount) { - var collection = new DocumentCollection( - storage, - transactionHolder, - new SizeBenchmarkDocumentMapper()); + int currentBatchSize = Math.Min(BatchSize, targetCount - inserted); + var documents = new SizeBenchmarkDocument[currentBatchSize]; + int baseValue = inserted; - var inserted = 0; - while (inserted < targetCount) - { - var currentBatchSize = Math.Min(BatchSize, targetCount - inserted); - var documents = new SizeBenchmarkDocument[currentBatchSize]; - var baseValue = inserted; + for (var i = 0; i < currentBatchSize; i++) documents[i] = CreateDocument(baseValue + i); - for (var i = 0; i < currentBatchSize; i++) - { - documents[i] = CreateDocument(baseValue + i); - } + collection.InsertBulk(documents); + transactionHolder.CommitAndReset(); - collection.InsertBulk(documents); - transactionHolder.CommitAndReset(); - - inserted += currentBatchSize; - if (inserted == targetCount || inserted % ProgressInterval == 0) - { - logger.LogInformation("Inserted {Inserted:N0}/{TargetCount:N0}", inserted, targetCount); - } - } - - insertStopwatch.Stop(); - preCompactDbBytes = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0; - preCompactWalBytes = File.Exists(walPath) ? new FileInfo(walPath).Length : 0; - - if (scenario.RunCompaction) - { - compactionStats = storage.Compact(new CompactionOptions - { - EnableTailTruncation = true, - DefragmentSlottedPages = true, - NormalizeFreeList = true - }); - } - - postCompactDbBytes = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0; - postCompactWalBytes = File.Exists(walPath) ? new FileInfo(walPath).Length : 0; - compressionStats = storage.GetCompressionStats(); + inserted += currentBatchSize; + if (inserted == targetCount || inserted % ProgressInterval == 0) + logger.LogInformation("Inserted {Inserted:N0}/{TargetCount:N0}", inserted, targetCount); } - var result = new SizeResult( - scenario.Set, - scenario.Name, - scenario.RunCompaction, - targetCount, - insertStopwatch.Elapsed, - preCompactDbBytes, - preCompactWalBytes, - postCompactDbBytes, - postCompactWalBytes, - compactionStats, - compressionStats); - results.Add(result); + insertStopwatch.Stop(); + preCompactDbBytes = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0; + preCompactWalBytes = File.Exists(walPath) ? new FileInfo(walPath).Length : 0; - logger.LogInformation( - "Completed {Set}:{Scenario} {TargetCount:N0} docs in {Elapsed}. pre={PreTotal}, post={PostTotal}, shrink={Shrink}, compactApplied={CompactionApplied}, compactReclaim={CompactReclaim}, compRatio={CompRatio}", - scenario.Set, - scenario.Name, - targetCount, - insertStopwatch.Elapsed, - FormatBytes(result.PreCompactTotalBytes), - FormatBytes(result.PostCompactTotalBytes), - FormatBytes(result.ShrinkBytes), - scenario.RunCompaction, - FormatBytes(result.CompactionStats.ReclaimedFileBytes), - result.CompressionRatioText); + if (scenario.RunCompaction) + compactionStats = storage.Compact(new CompactionOptions + { + EnableTailTruncation = true, + DefragmentSlottedPages = true, + NormalizeFreeList = true + }); - TryDelete(dbPath); - TryDelete(walPath); + postCompactDbBytes = File.Exists(dbPath) ? new FileInfo(dbPath).Length : 0; + postCompactWalBytes = File.Exists(walPath) ? new FileInfo(walPath).Length : 0; + compressionStats = storage.GetCompressionStats(); } + + var result = new SizeResult( + scenario.Set, + scenario.Name, + scenario.RunCompaction, + targetCount, + insertStopwatch.Elapsed, + preCompactDbBytes, + preCompactWalBytes, + postCompactDbBytes, + postCompactWalBytes, + compactionStats, + compressionStats); + results.Add(result); + + logger.LogInformation( + "Completed {Set}:{Scenario} {TargetCount:N0} docs in {Elapsed}. pre={PreTotal}, post={PostTotal}, shrink={Shrink}, compactApplied={CompactionApplied}, compactReclaim={CompactReclaim}, compRatio={CompRatio}", + scenario.Set, + scenario.Name, + targetCount, + insertStopwatch.Elapsed, + FormatBytes(result.PreCompactTotalBytes), + FormatBytes(result.PostCompactTotalBytes), + FormatBytes(result.ShrinkBytes), + scenario.RunCompaction, + FormatBytes(result.CompactionStats.ReclaimedFileBytes), + result.CompressionRatioText); + + TryDelete(dbPath); + TryDelete(walPath); } logger.LogInformation("=== Size Benchmark Summary ==="); @@ -172,7 +165,6 @@ internal static class DatabaseSizeBenchmark .OrderBy(x => x.Set) .ThenBy(x => x.TargetCount) .ThenBy(x => x.Scenario)) - { logger.LogInformation( "{Set,-11} | {Scenario,-38} | {Count,12:N0} docs | insert={Elapsed,12} | pre={Pre,12} | post={Post,12} | shrink={Shrink,12} | compact={CompactBytes,12} | ratio={Ratio}", result.Set, @@ -184,7 +176,6 @@ internal static class DatabaseSizeBenchmark FormatBytes(result.ShrinkBytes), FormatBytes(result.CompactionStats.ReclaimedFileBytes), result.CompressionRatioText); - } WriteSummaryCsv(results, logger); } @@ -201,10 +192,7 @@ internal static class DatabaseSizeBenchmark private static void TryDelete(string path) { - if (File.Exists(path)) - { - File.Delete(path); - } + if (File.Exists(path)) File.Delete(path); } private static string FormatBytes(long bytes) @@ -224,9 +212,9 @@ internal static class DatabaseSizeBenchmark private static void WriteSummaryCsv(IEnumerable results, ILogger logger) { - var outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "results"); + string outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "results"); Directory.CreateDirectory(outputDirectory); - var outputPath = Path.Combine(outputDirectory, "DatabaseSizeBenchmark-results.csv"); + string outputPath = Path.Combine(outputDirectory, "DatabaseSizeBenchmark-results.csv"); var lines = new List { @@ -234,7 +222,6 @@ internal static class DatabaseSizeBenchmark }; foreach (var result in results.OrderBy(x => x.Set).ThenBy(x => x.TargetCount).ThenBy(x => x.Scenario)) - { lines.Add(string.Join(",", result.Set, result.Scenario, @@ -246,7 +233,6 @@ internal static class DatabaseSizeBenchmark result.ShrinkBytes.ToString(), result.CompactionStats.ReclaimedFileBytes.ToString(), result.CompressionRatioText)); - } File.WriteAllLines(outputPath, lines); logger.LogInformation("Database size summary CSV written to {OutputPath}", outputPath); @@ -268,20 +254,22 @@ internal static class DatabaseSizeBenchmark CompressionStats CompressionStats) { /// - /// Gets or sets the pre compact total bytes. + /// Gets or sets the pre compact total bytes. /// public long PreCompactTotalBytes => PreCompactDbBytes + PreCompactWalBytes; + /// - /// Gets or sets the post compact total bytes. + /// Gets or sets the post compact total bytes. /// public long PostCompactTotalBytes => PostCompactDbBytes + PostCompactWalBytes; + /// - /// Gets or sets the shrink bytes. + /// Gets or sets the shrink bytes. /// public long ShrinkBytes => PreCompactTotalBytes - PostCompactTotalBytes; /// - /// Gets or sets the compression ratio text. + /// Gets or sets the compression ratio text. /// public string CompressionRatioText => CompressionStats.BytesAfterCompression > 0 @@ -292,15 +280,17 @@ internal static class DatabaseSizeBenchmark private sealed class SizeBenchmarkDocument { /// - /// Gets or sets the id. + /// Gets or sets the id. /// public ObjectId Id { get; set; } + /// - /// Gets or sets the value. + /// Gets or sets the value. /// public int Value { get; set; } + /// - /// Gets or sets the name. + /// Gets or sets the name. /// public string Name { get; set; } = string.Empty; } @@ -311,15 +301,21 @@ internal static class DatabaseSizeBenchmark public override string CollectionName => "size_documents"; /// - public override ObjectId GetId(SizeBenchmarkDocument entity) => entity.Id; + public override ObjectId GetId(SizeBenchmarkDocument entity) + { + return entity.Id; + } /// - public override void SetId(SizeBenchmarkDocument entity, ObjectId id) => entity.Id = id; + public override void SetId(SizeBenchmarkDocument entity, ObjectId id) + { + entity.Id = id; + } /// public override int Serialize(SizeBenchmarkDocument entity, BsonSpanWriter writer) { - var sizePos = writer.BeginDocument(); + int sizePos = writer.BeginDocument(); writer.WriteObjectId("_id", entity.Id); writer.WriteInt32("value", entity.Value); writer.WriteString("name", entity.Name); @@ -336,12 +332,9 @@ internal static class DatabaseSizeBenchmark while (reader.Remaining > 0) { var bsonType = reader.ReadBsonType(); - if (bsonType == BsonType.EndOfDocument) - { - break; - } + if (bsonType == BsonType.EndOfDocument) break; - var name = reader.ReadElementHeader(); + string name = reader.ReadElementHeader(); switch (name) { case "_id": @@ -362,4 +355,4 @@ internal static class DatabaseSizeBenchmark return document; } } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Workloads/ManualBenchmark.cs b/tests/CBDD.Tests.Benchmark/Workloads/ManualBenchmark.cs index e4cdca6..9fae229 100755 --- a/tests/CBDD.Tests.Benchmark/Workloads/ManualBenchmark.cs +++ b/tests/CBDD.Tests.Benchmark/Workloads/ManualBenchmark.cs @@ -1,5 +1,4 @@ using System.Diagnostics; -using System.IO; using System.Text; using Microsoft.Extensions.Logging; using Serilog.Context; @@ -8,7 +7,7 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark; public class ManualBenchmark { - private static StringBuilder _log = new(); + private static readonly StringBuilder _log = new(); private static void Log(ILogger logger, string message = "") { @@ -16,11 +15,11 @@ public class ManualBenchmark _log.AppendLine(message); } - /// - /// Tests run. - /// - /// Logger for benchmark progress and results. - public static void Run(ILogger logger) + /// + /// Tests run. + /// + /// Logger for benchmark progress and results. + public static void Run(ILogger logger) { using var _ = LogContext.PushProperty("Benchmark", nameof(ManualBenchmark)); _log.Clear(); @@ -60,10 +59,7 @@ public class ManualBenchmark try { var sw = Stopwatch.StartNew(); - for (int i = 0; i < 1000; i++) - { - readBench.DocumentDb_FindById(); - } + for (var i = 0; i < 1000; i++) readBench.DocumentDb_FindById(); sw.Stop(); readByIdMs = sw.ElapsedMilliseconds; Log(logger, $" CBDD FindById x1000: {readByIdMs} ms ({(double)readByIdMs / 1000:F3} ms/op)"); @@ -101,14 +97,11 @@ public class ManualBenchmark Log(logger, $"FindById x1000: {readByIdMs} ms"); Log(logger, $"Single Insert: {singleInsertMs} ms"); - var artifactsDir = Path.Combine(AppContext.BaseDirectory, "BenchmarkDotNet.Artifacts", "results"); - if (!Directory.Exists(artifactsDir)) - { - Directory.CreateDirectory(artifactsDir); - } + string artifactsDir = Path.Combine(AppContext.BaseDirectory, "BenchmarkDotNet.Artifacts", "results"); + if (!Directory.Exists(artifactsDir)) Directory.CreateDirectory(artifactsDir); - var filePath = Path.Combine(artifactsDir, "manual_report.txt"); + string filePath = Path.Combine(artifactsDir, "manual_report.txt"); File.WriteAllText(filePath, _log.ToString()); logger.LogInformation("Report saved to: {FilePath}", filePath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Workloads/MixedWorkloadBenchmarks.cs b/tests/CBDD.Tests.Benchmark/Workloads/MixedWorkloadBenchmarks.cs index 81d46f3..3239266 100644 --- a/tests/CBDD.Tests.Benchmark/Workloads/MixedWorkloadBenchmarks.cs +++ b/tests/CBDD.Tests.Benchmark/Workloads/MixedWorkloadBenchmarks.cs @@ -1,6 +1,6 @@ +using System.Text; using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Compression; @@ -16,28 +16,29 @@ namespace ZB.MOM.WW.CBDD.Tests.Benchmark; [JsonExporterAttribute.Full] public class MixedWorkloadBenchmarks { + private readonly List _activeIds = []; + private DocumentCollection _collection = null!; + + private string _dbPath = string.Empty; + private int _nextValueSeed; + private StorageEngine _storage = null!; + private BenchmarkTransactionHolder _transactionHolder = null!; + private string _walPath = string.Empty; + /// - /// Gets or sets whether periodic online compaction is enabled. + /// Gets or sets whether periodic online compaction is enabled. /// [Params(false, true)] public bool PeriodicCompaction { get; set; } /// - /// Gets or sets the number of operations per benchmark iteration. + /// Gets or sets the number of operations per benchmark iteration. /// [Params(800)] public int Operations { get; set; } - private string _dbPath = string.Empty; - private string _walPath = string.Empty; - private StorageEngine _storage = null!; - private BenchmarkTransactionHolder _transactionHolder = null!; - private DocumentCollection _collection = null!; - private readonly List _activeIds = []; - private int _nextValueSeed; - /// - /// Prepares benchmark storage and seed data for each iteration. + /// Prepares benchmark storage and seed data for each iteration. /// [IterationSetup] public void Setup() @@ -71,7 +72,7 @@ public class MixedWorkloadBenchmarks } /// - /// Cleans up benchmark resources for each iteration. + /// Cleans up benchmark resources for each iteration. /// [IterationCleanup] public void Cleanup() @@ -84,7 +85,7 @@ public class MixedWorkloadBenchmarks } /// - /// Benchmarks a mixed insert/update/delete workload. + /// Benchmarks a mixed insert/update/delete workload. /// [Benchmark(Baseline = true)] [BenchmarkCategory("MixedWorkload")] @@ -94,7 +95,7 @@ public class MixedWorkloadBenchmarks for (var i = 1; i <= Operations; i++) { - var mode = i % 5; + int mode = i % 5; if (mode is 0 or 1) { var id = _collection.Insert(CreatePerson(_nextValueSeed++)); @@ -104,7 +105,7 @@ public class MixedWorkloadBenchmarks { if (_activeIds.Count > 0) { - var idx = random.Next(_activeIds.Count); + int idx = random.Next(_activeIds.Count); var id = _activeIds[idx]; var current = _collection.FindById(id); if (current != null) @@ -119,20 +120,16 @@ public class MixedWorkloadBenchmarks { if (_activeIds.Count > 100) { - var idx = random.Next(_activeIds.Count); + int idx = random.Next(_activeIds.Count); var id = _activeIds[idx]; _collection.Delete(id); _activeIds.RemoveAt(idx); } } - if (i % 50 == 0) - { - _transactionHolder.CommitAndReset(); - } + if (i % 50 == 0) _transactionHolder.CommitAndReset(); if (PeriodicCompaction && i % 200 == 0) - { _storage.RunOnlineCompactionPass(new CompactionOptions { OnlineMode = true, @@ -141,7 +138,6 @@ public class MixedWorkloadBenchmarks MaxOnlineDuration = TimeSpan.FromMilliseconds(120), EnableTailTruncation = true }); - } } _transactionHolder.CommitAndReset(); @@ -155,7 +151,7 @@ public class MixedWorkloadBenchmarks Id = ObjectId.NewObjectId(), FirstName = $"First_{seed}", LastName = $"Last_{seed}", - Age = 18 + (seed % 60), + Age = 18 + seed % 60, Bio = BuildPayload(seed), CreatedAt = DateTime.UnixEpoch.AddSeconds(seed), Balance = seed, @@ -170,7 +166,7 @@ public class MixedWorkloadBenchmarks private static string BuildPayload(int seed) { - var builder = new System.Text.StringBuilder(1800); + var builder = new StringBuilder(1800); for (var i = 0; i < 64; i++) { builder.Append("mixed-"); @@ -182,4 +178,4 @@ public class MixedWorkloadBenchmarks return builder.ToString(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/Workloads/PerformanceGateSmoke.cs b/tests/CBDD.Tests.Benchmark/Workloads/PerformanceGateSmoke.cs index 6f32a77..d2f90d0 100644 --- a/tests/CBDD.Tests.Benchmark/Workloads/PerformanceGateSmoke.cs +++ b/tests/CBDD.Tests.Benchmark/Workloads/PerformanceGateSmoke.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using ZB.MOM.WW.CBDD.Bson; @@ -14,21 +15,21 @@ internal static class PerformanceGateSmoke private const int CompressionDocumentCount = 1_500; /// - /// Runs the performance gate smoke probes and writes a report. + /// Runs the performance gate smoke probes and writes a report. /// /// The logger. public static void Run(ILogger logger) { var compaction = RunCompactionProbe(); - var compressionOff = RunCompressionGcProbe(enableCompression: false); - var compressionOn = RunCompressionGcProbe(enableCompression: true); + var compressionOff = RunCompressionGcProbe(false); + var compressionOn = RunCompressionGcProbe(true); var report = new PerformanceGateReport( DateTimeOffset.UtcNow, compaction, compressionOff, compressionOn); - var reportPath = WriteReport(report); + string reportPath = WriteReport(report); logger.LogInformation("Performance gate smoke report written to {ReportPath}", reportPath); @@ -52,8 +53,8 @@ internal static class PerformanceGateSmoke private static CompactionProbeResult RunCompactionProbe() { - var dbPath = NewDbPath("gate_compaction"); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string dbPath = NewDbPath("gate_compaction"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); try { @@ -62,18 +63,12 @@ internal static class PerformanceGateSmoke var collection = new DocumentCollection(storage, transactionHolder, new PersonMapper()); var ids = new List(CompactionDocumentCount); - for (var i = 0; i < CompactionDocumentCount; i++) - { - ids.Add(collection.Insert(CreatePerson(i, includeLargeBio: true))); - } + for (var i = 0; i < CompactionDocumentCount; i++) ids.Add(collection.Insert(CreatePerson(i, true))); transactionHolder.CommitAndReset(); storage.Checkpoint(); - for (var i = 0; i < ids.Count; i += 3) - { - collection.Delete(ids[i]); - } + for (var i = 0; i < ids.Count; i += 3) collection.Delete(ids[i]); for (var i = 0; i < ids.Count; i += 5) { @@ -117,8 +112,8 @@ internal static class PerformanceGateSmoke private static CompressionGcProbeResult RunCompressionGcProbe(bool enableCompression) { - var dbPath = NewDbPath(enableCompression ? "gate_gc_on" : "gate_gc_off"); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string dbPath = NewDbPath(enableCompression ? "gate_gc_on" : "gate_gc_off"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var compressionOptions = enableCompression ? new CompressionOptions { @@ -140,16 +135,13 @@ internal static class PerformanceGateSmoke GC.WaitForPendingFinalizers(); GC.Collect(); - var g0Before = GC.CollectionCount(0); - var g1Before = GC.CollectionCount(1); - var g2Before = GC.CollectionCount(2); - var allocBefore = GC.GetTotalAllocatedBytes(true); + int g0Before = GC.CollectionCount(0); + int g1Before = GC.CollectionCount(1); + int g2Before = GC.CollectionCount(2); + long allocBefore = GC.GetTotalAllocatedBytes(true); var ids = new ObjectId[CompressionDocumentCount]; - for (var i = 0; i < CompressionDocumentCount; i++) - { - ids[i] = collection.Insert(CreatePerson(i, includeLargeBio: true)); - } + for (var i = 0; i < CompressionDocumentCount; i++) ids[i] = collection.Insert(CreatePerson(i, true)); transactionHolder.CommitAndReset(); @@ -166,17 +158,17 @@ internal static class PerformanceGateSmoke transactionHolder.CommitAndReset(); - var readCount = collection.FindAll().Count(); + int readCount = collection.FindAll().Count(); transactionHolder.CommitAndReset(); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); - var g0After = GC.CollectionCount(0); - var g1After = GC.CollectionCount(1); - var g2After = GC.CollectionCount(2); - var allocAfter = GC.GetTotalAllocatedBytes(true); + int g0After = GC.CollectionCount(0); + int g1After = GC.CollectionCount(1); + int g2After = GC.CollectionCount(2); + long allocAfter = GC.GetTotalAllocatedBytes(true); return new CompressionGcProbeResult( enableCompression, @@ -198,11 +190,11 @@ internal static class PerformanceGateSmoke private static string WriteReport(PerformanceGateReport report) { - var outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "results"); + string outputDirectory = Path.Combine(Directory.GetCurrentDirectory(), "BenchmarkDotNet.Artifacts", "results"); Directory.CreateDirectory(outputDirectory); - var reportPath = Path.Combine(outputDirectory, "PerformanceGateSmoke-report.json"); - var json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }); + string reportPath = Path.Combine(outputDirectory, "PerformanceGateSmoke-report.json"); + string json = JsonSerializer.Serialize(report, new JsonSerializerOptions { WriteIndented = true }); File.WriteAllText(reportPath, json); return reportPath; } @@ -214,7 +206,7 @@ internal static class PerformanceGateSmoke Id = ObjectId.NewObjectId(), FirstName = $"First_{i}", LastName = $"Last_{i}", - Age = 20 + (i % 50), + Age = 20 + i % 50, Bio = includeLargeBio ? BuildBio(i) : $"bio-{i}", CreatedAt = DateTime.UnixEpoch.AddMinutes(i), Balance = 100 + i, @@ -239,7 +231,7 @@ internal static class PerformanceGateSmoke private static string BuildBio(int seed) { - var builder = new System.Text.StringBuilder(4500); + var builder = new StringBuilder(4500); for (var i = 0; i < 150; i++) { builder.Append("bio-"); @@ -253,14 +245,13 @@ internal static class PerformanceGateSmoke } private static string NewDbPath(string prefix) - => Path.Combine(Path.GetTempPath(), $"{prefix}_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"{prefix}_{Guid.NewGuid():N}.db"); + } private static void TryDelete(string path) { - if (File.Exists(path)) - { - File.Delete(path); - } + if (File.Exists(path)) File.Delete(path); } private sealed record PerformanceGateReport( @@ -284,4 +275,4 @@ internal static class PerformanceGateSmoke int Gen1Delta, int Gen2Delta, long AllocatedBytesDelta); -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests.Benchmark/ZB.MOM.WW.CBDD.Tests.Benchmark.csproj b/tests/CBDD.Tests.Benchmark/ZB.MOM.WW.CBDD.Tests.Benchmark.csproj index 568eb2c..2c77b08 100644 --- a/tests/CBDD.Tests.Benchmark/ZB.MOM.WW.CBDD.Tests.Benchmark.csproj +++ b/tests/CBDD.Tests.Benchmark/ZB.MOM.WW.CBDD.Tests.Benchmark.csproj @@ -1,25 +1,25 @@ - - Exe - net10.0 - ZB.MOM.WW.CBDD.Tests.Benchmark - ZB.MOM.WW.CBDD.Tests.Benchmark - enable - enable - false - + + Exe + net10.0 + ZB.MOM.WW.CBDD.Tests.Benchmark + ZB.MOM.WW.CBDD.Tests.Benchmark + enable + enable + false + - - - - - - - + + + + + + + - - - + + + diff --git a/tests/CBDD.Tests/Architecture/ArchitectureFitnessTests.cs b/tests/CBDD.Tests/Architecture/ArchitectureFitnessTests.cs index 47aa909..3016472 100644 --- a/tests/CBDD.Tests/Architecture/ArchitectureFitnessTests.cs +++ b/tests/CBDD.Tests/Architecture/ArchitectureFitnessTests.cs @@ -16,12 +16,12 @@ public class ArchitectureFitnessTests private const string FacadeProject = "src/CBDD/ZB.MOM.WW.CBDD.csproj"; /// - /// Executes Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection. + /// Executes Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection. /// [Fact] public void Solution_DependencyGraph_ShouldRemainAcyclic_AndFollowLayerDirection() { - var repoRoot = FindRepositoryRoot(); + string repoRoot = FindRepositoryRoot(); var projectGraph = LoadSolutionProjectGraph(repoRoot); // Explicit layer rules @@ -30,28 +30,27 @@ public class ArchitectureFitnessTests projectGraph[CoreProject].ShouldBe(new[] { BsonProject }); projectGraph[FacadeProject] .OrderBy(v => v, StringComparer.Ordinal) - .ShouldBe(new[] { BsonProject, CoreProject, SourceGeneratorsProject }.OrderBy(v => v, StringComparer.Ordinal)); + .ShouldBe(new[] { BsonProject, CoreProject, SourceGeneratorsProject }.OrderBy(v => v, + StringComparer.Ordinal)); // Source projects should not depend on tests. foreach (var kvp in projectGraph.Where(p => p.Key.StartsWith("src/", StringComparison.Ordinal))) - { kvp.Value.Any(dep => dep.StartsWith("tests/", StringComparison.Ordinal)) .ShouldBeFalse($"{kvp.Key} must not reference test projects."); - } HasCycle(projectGraph) .ShouldBeFalse("Project references must remain acyclic."); } /// - /// Executes HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface. + /// Executes HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface. /// [Fact] public void HighLevelCollectionApi_ShouldNotExpandRawBsonReaderWriterSurface() { var lowLevelTypes = new[] { typeof(BsonSpanReader), typeof(BsonSpanWriter) }; - var collectionOffenders = typeof(DocumentCollection<,>) + string[] collectionOffenders = typeof(DocumentCollection<,>) .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) .Where(m => lowLevelTypes.Any(t => MethodUsesType(m, t))) .Select(m => m.Name) @@ -61,7 +60,7 @@ public class ArchitectureFitnessTests collectionOffenders.ShouldBeEmpty(); - var dbContextOffenders = typeof(DocumentDbContext) + string[] dbContextOffenders = typeof(DocumentDbContext) .GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly) .Where(m => lowLevelTypes.Any(t => MethodUsesType(m, t))) .Select(m => m.Name) @@ -72,7 +71,7 @@ public class ArchitectureFitnessTests } /// - /// Executes CollectionAndIndexOrchestration_ShouldUseStoragePortInternally. + /// Executes CollectionAndIndexOrchestration_ShouldUseStoragePortInternally. /// [Fact] public void CollectionAndIndexOrchestration_ShouldUseStoragePortInternally() @@ -84,22 +83,23 @@ public class ArchitectureFitnessTests typeof(BTreeIndex), typeof(CollectionIndexManager<,>), typeof(CollectionSecondaryIndex<,>), - typeof(VectorSearchIndex), + typeof(VectorSearchIndex) }; - var fieldOffenders = targetTypes + string[] fieldOffenders = targetTypes .SelectMany(t => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public) .Where(f => f.FieldType == typeof(StorageEngine)) .Select(f => $"{t.Name}.{f.Name}")) .OrderBy(v => v) .ToArray(); - fieldOffenders.ShouldBeEmpty("Collection/index orchestration should hold IStorageEngine instead of concrete StorageEngine."); + fieldOffenders.ShouldBeEmpty( + "Collection/index orchestration should hold IStorageEngine instead of concrete StorageEngine."); } private static Dictionary> LoadSolutionProjectGraph(string repoRoot) { - var solutionPath = Path.Combine(repoRoot, "CBDD.slnx"); + string solutionPath = Path.Combine(repoRoot, "CBDD.slnx"); var solutionDoc = XDocument.Load(solutionPath); var projects = solutionDoc @@ -115,11 +115,11 @@ public class ArchitectureFitnessTests _ => new List(), StringComparer.Ordinal); - foreach (var project in projects) + foreach (string project in projects) { - var projectFile = Path.Combine(repoRoot, project); + string projectFile = Path.Combine(repoRoot, project); var projectDoc = XDocument.Load(projectFile); - var projectDir = Path.GetDirectoryName(projectFile)!; + string projectDir = Path.GetDirectoryName(projectFile)!; var refs = projectDoc .Descendants() @@ -127,7 +127,8 @@ public class ArchitectureFitnessTests .Select(e => e.Attribute("Include")?.Value) .Where(v => !string.IsNullOrWhiteSpace(v)) .Select(v => v!.Replace('\\', '/')) - .Select(v => NormalizePath(Path.GetRelativePath(repoRoot, Path.GetFullPath(Path.Combine(projectDir, v))))) + .Select(v => + NormalizePath(Path.GetRelativePath(repoRoot, Path.GetFullPath(Path.Combine(projectDir, v))))) .Where(projects.Contains) .Distinct(StringComparer.Ordinal) .OrderBy(v => v, StringComparer.Ordinal) @@ -143,30 +144,20 @@ public class ArchitectureFitnessTests { var state = graph.Keys.ToDictionary(k => k, _ => 0, StringComparer.Ordinal); - foreach (var node in graph.Keys) - { + foreach (string node in graph.Keys) if (state[node] == 0 && Visit(node)) - { return true; - } - } return false; bool Visit(string node) { state[node] = 1; // visiting - foreach (var dep in graph[node]) + foreach (string dep in graph[node]) { - if (state[dep] == 1) - { - return true; - } + if (state[dep] == 1) return true; - if (state[dep] == 0 && Visit(dep)) - { - return true; - } + if (state[dep] == 0 && Visit(dep)) return true; } state[node] = 2; // visited @@ -176,30 +167,19 @@ public class ArchitectureFitnessTests private static bool MethodUsesType(MethodInfo method, Type forbidden) { - if (TypeContains(method.ReturnType, forbidden)) - { - return true; - } + if (TypeContains(method.ReturnType, forbidden)) return true; return method.GetParameters().Any(p => TypeContains(p.ParameterType, forbidden)); } private static bool TypeContains(Type inspected, Type forbidden) { - if (inspected == forbidden) - { - return true; - } + if (inspected == forbidden) return true; - if (inspected.HasElementType && inspected.GetElementType() is { } elementType && TypeContains(elementType, forbidden)) - { - return true; - } + if (inspected.HasElementType && inspected.GetElementType() is { } elementType && + TypeContains(elementType, forbidden)) return true; - if (!inspected.IsGenericType) - { - return false; - } + if (!inspected.IsGenericType) return false; return inspected.GetGenericArguments().Any(t => TypeContains(t, forbidden)); } @@ -209,11 +189,8 @@ public class ArchitectureFitnessTests var current = new DirectoryInfo(AppContext.BaseDirectory); while (current != null) { - var solutionPath = Path.Combine(current.FullName, "CBDD.slnx"); - if (File.Exists(solutionPath)) - { - return current.FullName; - } + string solutionPath = Path.Combine(current.FullName, "CBDD.slnx"); + if (File.Exists(solutionPath)) return current.FullName; current = current.Parent; } @@ -222,5 +199,7 @@ public class ArchitectureFitnessTests } private static string NormalizePath(string path) - => path.Replace('\\', '/'); -} + { + return path.Replace('\\', '/'); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Bson/BsonDocumentAndBufferWriterTests.cs b/tests/CBDD.Tests/Bson/BsonDocumentAndBufferWriterTests.cs index 1ca6bd5..fc484ae 100644 --- a/tests/CBDD.Tests/Bson/BsonDocumentAndBufferWriterTests.cs +++ b/tests/CBDD.Tests/Bson/BsonDocumentAndBufferWriterTests.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.CBDD.Tests; public class BsonDocumentAndBufferWriterTests { /// - /// Verifies BSON document creation and typed retrieval roundtrip. + /// Verifies BSON document creation and typed retrieval roundtrip. /// [Fact] public void BsonDocument_Create_And_TryGet_RoundTrip() @@ -32,10 +32,10 @@ public class BsonDocumentAndBufferWriterTests var wrapped = new BsonDocument(doc.RawData.ToArray(), reverseMap); - wrapped.TryGetString("name", out var name).ShouldBeTrue(); + wrapped.TryGetString("name", out string? name).ShouldBeTrue(); name.ShouldBe("Alice"); - wrapped.TryGetInt32("age", out var age).ShouldBeTrue(); + wrapped.TryGetInt32("age", out int age).ShouldBeTrue(); age.ShouldBe(32); wrapped.TryGetObjectId("_id", out var id).ShouldBeTrue(); @@ -46,7 +46,7 @@ public class BsonDocumentAndBufferWriterTests } /// - /// Verifies typed getters return false for missing fields and type mismatches. + /// Verifies typed getters return false for missing fields and type mismatches. /// [Fact] public void BsonDocument_TryGet_Should_Return_False_For_Missing_Or_Wrong_Type() @@ -71,7 +71,7 @@ public class BsonDocumentAndBufferWriterTests } /// - /// Verifies the BSON document builder grows its internal buffer for large documents. + /// Verifies the BSON document builder grows its internal buffer for large documents. /// [Fact] public void BsonDocumentBuilder_Should_Grow_Buffer_When_Document_Is_Large() @@ -86,21 +86,18 @@ public class BsonDocumentAndBufferWriterTests } var builder = new BsonDocumentBuilder(keyMap); - for (int i = 1; i <= 180; i++) - { - builder.AddInt32($"k{i}", i); - } + for (var i = 1; i <= 180; i++) builder.AddInt32($"k{i}", i); var doc = builder.Build(); doc.Size.ShouldBeGreaterThan(1024); var wrapped = new BsonDocument(doc.RawData.ToArray(), reverseMap); - wrapped.TryGetInt32("k180", out var value).ShouldBeTrue(); + wrapped.TryGetInt32("k180", out int value).ShouldBeTrue(); value.ShouldBe(180); } /// - /// Verifies BSON buffer writer emits expected nested document and array layout. + /// Verifies BSON buffer writer emits expected nested document and array layout. /// [Fact] public void BsonBufferWriter_Should_Write_Nested_Document_And_Array() @@ -125,7 +122,7 @@ public class BsonDocumentAndBufferWriterTests writer.EndDocument(rootSizePos); int rootEnd = writer.Position; - var bytes = output.WrittenSpan.ToArray(); + byte[] bytes = output.WrittenSpan.ToArray(); PatchDocumentSize(bytes, childSizePos, childEnd); PatchDocumentSize(bytes, arraySizePos, arrayEnd); PatchDocumentSize(bytes, rootSizePos, rootEnd); @@ -164,7 +161,7 @@ public class BsonDocumentAndBufferWriterTests } /// - /// Verifies single-byte and C-string span reads operate correctly. + /// Verifies single-byte and C-string span reads operate correctly. /// [Fact] public void BsonSpanReader_ReadByte_And_ReadCStringSpan_Should_Work() @@ -172,10 +169,10 @@ public class BsonDocumentAndBufferWriterTests var singleByteReader = new BsonSpanReader(new byte[] { 0x2A }, new ConcurrentDictionary()); singleByteReader.ReadByte().ShouldBe((byte)0x2A); - var cstring = Encoding.UTF8.GetBytes("hello\0"); + byte[] cstring = Encoding.UTF8.GetBytes("hello\0"); var cstringReader = new BsonSpanReader(cstring, new ConcurrentDictionary()); var destination = new char[16]; - var written = cstringReader.ReadCString(destination); + int written = cstringReader.ReadCString(destination); new string(destination, 0, written).ShouldBe("hello"); } @@ -194,4 +191,4 @@ public class BsonDocumentAndBufferWriterTests { BinaryPrimitives.WriteInt32LittleEndian(output.AsSpan(sizePosition, 4), endPosition - sizePosition); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Bson/BsonSchemaTests.cs b/tests/CBDD.Tests/Bson/BsonSchemaTests.cs index d2563d7..6612b8c 100755 --- a/tests/CBDD.Tests/Bson/BsonSchemaTests.cs +++ b/tests/CBDD.Tests/Bson/BsonSchemaTests.cs @@ -1,39 +1,12 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; -using Xunit; -using System.Collections.Generic; -using System; -using System.Linq; namespace ZB.MOM.WW.CBDD.Tests; public class BsonSchemaTests { - public class SimpleEntity - { - /// - /// Gets or sets the identifier. - /// - public ObjectId Id { get; set; } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the age. - /// - public int Age { get; set; } - - /// - /// Gets or sets a value indicating whether the entity is active. - /// - public bool IsActive { get; set; } - } - /// - /// Verifies schema generation for a simple entity. + /// Verifies schema generation for a simple entity. /// [Fact] public void GenerateSchema_SimpleEntity() @@ -53,21 +26,8 @@ public class BsonSchemaTests ageField.Type.ShouldBe(BsonType.Int32); } - public class CollectionEntity - { - /// - /// Gets or sets tags. - /// - public List Tags { get; set; } = new(); - - /// - /// Gets or sets scores. - /// - public int[] Scores { get; set; } = Array.Empty(); - } - /// - /// Verifies schema generation for collection fields. + /// Verifies schema generation for collection fields. /// [Fact] public void GenerateSchema_Collections() @@ -83,16 +43,8 @@ public class BsonSchemaTests scores.ArrayItemType.ShouldBe(BsonType.Int32); } - public class NestedEntity - { - /// - /// Gets or sets the parent entity. - /// - public SimpleEntity Parent { get; set; } = new(); - } - /// - /// Verifies schema generation for nested document fields. + /// Verifies schema generation for nested document fields. /// [Fact] public void GenerateSchema_Nested() @@ -105,16 +57,8 @@ public class BsonSchemaTests parent.NestedSchema.Fields.ShouldContain(f => f.Name == "_id"); } - public class ComplexCollectionEntity - { - /// - /// Gets or sets items. - /// - public List Items { get; set; } = new(); - } - /// - /// Verifies schema generation for collections of complex types. + /// Verifies schema generation for collections of complex types. /// [Fact] public void GenerateSchema_ComplexCollection() @@ -133,4 +77,56 @@ public class BsonSchemaTests items.NestedSchema.ShouldNotBeNull(); items.NestedSchema.Fields.ShouldContain(f => f.Name == "_id"); } -} + + public class SimpleEntity + { + /// + /// Gets or sets the identifier. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the age. + /// + public int Age { get; set; } + + /// + /// Gets or sets a value indicating whether the entity is active. + /// + public bool IsActive { get; set; } + } + + public class CollectionEntity + { + /// + /// Gets or sets tags. + /// + public List Tags { get; set; } = new(); + + /// + /// Gets or sets scores. + /// + public int[] Scores { get; set; } = Array.Empty(); + } + + public class NestedEntity + { + /// + /// Gets or sets the parent entity. + /// + public SimpleEntity Parent { get; set; } = new(); + } + + public class ComplexCollectionEntity + { + /// + /// Gets or sets items. + /// + public List Items { get; set; } = new(); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Bson/BsonSpanReaderWriterTests.cs b/tests/CBDD.Tests/Bson/BsonSpanReaderWriterTests.cs index 8e19cb7..f4b76cd 100755 --- a/tests/CBDD.Tests/Bson/BsonSpanReaderWriterTests.cs +++ b/tests/CBDD.Tests/Bson/BsonSpanReaderWriterTests.cs @@ -1,6 +1,5 @@ -using ZB.MOM.WW.CBDD.Bson; -using Xunit; using System.Collections.Concurrent; +using ZB.MOM.WW.CBDD.Bson; namespace ZB.MOM.WW.CBDD.Tests; @@ -10,13 +9,17 @@ public class BsonSpanReaderWriterTests private readonly ConcurrentDictionary _keys = new(); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public BsonSpanReaderWriterTests() { ushort id = 1; - string[] initialKeys = ["name", "age", "active", "_id", "val", "dec", "timestamp", "int32", "int64", "double", "data", "child", "value", "0", "1"]; - foreach (var key in initialKeys) + string[] initialKeys = + [ + "name", "age", "active", "_id", "val", "dec", "timestamp", "int32", "int64", "double", "data", "child", + "value", "0", "1" + ]; + foreach (string key in initialKeys) { _keyMap[key] = id; _keys[id] = key; @@ -25,7 +28,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests write and read simple document. + /// Tests write and read simple document. /// [Fact] public void WriteAndRead_SimpleDocument() @@ -33,7 +36,7 @@ public class BsonSpanReaderWriterTests Span buffer = stackalloc byte[256]; var writer = new BsonSpanWriter(buffer, _keyMap); - var sizePos = writer.BeginDocument(); + int sizePos = writer.BeginDocument(); writer.WriteString("name", "John"); writer.WriteInt32("age", 30); writer.WriteBoolean("active", true); @@ -42,29 +45,29 @@ public class BsonSpanReaderWriterTests var documentBytes = buffer[..writer.Position]; var reader = new BsonSpanReader(documentBytes, _keys); - var size = reader.ReadDocumentSize(); + int size = reader.ReadDocumentSize(); size.ShouldBe(writer.Position); var type1 = reader.ReadBsonType(); - var name1 = reader.ReadElementHeader(); - var value1 = reader.ReadString(); + string name1 = reader.ReadElementHeader(); + string value1 = reader.ReadString(); type1.ShouldBe(BsonType.String); name1.ShouldBe("name"); value1.ShouldBe("John"); var type2 = reader.ReadBsonType(); - var name2 = reader.ReadElementHeader(); - var value2 = reader.ReadInt32(); + string name2 = reader.ReadElementHeader(); + int value2 = reader.ReadInt32(); type2.ShouldBe(BsonType.Int32); name2.ShouldBe("age"); value2.ShouldBe(30); var type3 = reader.ReadBsonType(); - var name3 = reader.ReadElementHeader(); - var value3 = reader.ReadBoolean(); + string name3 = reader.ReadElementHeader(); + bool value3 = reader.ReadBoolean(); type3.ShouldBe(BsonType.Boolean); name3.ShouldBe("active"); @@ -72,7 +75,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests write and read object id. + /// Tests write and read object id. /// [Fact] public void WriteAndRead_ObjectId() @@ -82,7 +85,7 @@ public class BsonSpanReaderWriterTests var oid = ObjectId.NewObjectId(); - var sizePos = writer.BeginDocument(); + int sizePos = writer.BeginDocument(); writer.WriteObjectId("_id", oid); writer.EndDocument(sizePos); @@ -91,7 +94,7 @@ public class BsonSpanReaderWriterTests reader.ReadDocumentSize(); var type = reader.ReadBsonType(); - var name = reader.ReadElementHeader(); + string name = reader.ReadElementHeader(); var readOid = reader.ReadObjectId(); type.ShouldBe(BsonType.ObjectId); @@ -100,7 +103,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests read write double. + /// Tests read write double. /// [Fact] public void ReadWrite_Double() @@ -112,8 +115,8 @@ public class BsonSpanReaderWriterTests var reader = new BsonSpanReader(buffer, _keys); var type = reader.ReadBsonType(); - var name = reader.ReadElementHeader(); - var val = reader.ReadDouble(); + string name = reader.ReadElementHeader(); + double val = reader.ReadDouble(); type.ShouldBe(BsonType.Double); name.ShouldBe("val"); @@ -121,7 +124,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests read write decimal128 round trip. + /// Tests read write decimal128 round trip. /// [Fact] public void ReadWrite_Decimal128_RoundTrip() @@ -129,13 +132,13 @@ public class BsonSpanReaderWriterTests var buffer = new byte[256]; var writer = new BsonSpanWriter(buffer, _keyMap); - decimal original = 123456.789m; + var original = 123456.789m; writer.WriteDecimal128("dec", original); var reader = new BsonSpanReader(buffer, _keys); var type = reader.ReadBsonType(); - var name = reader.ReadElementHeader(); - var val = reader.ReadDecimal128(); + string name = reader.ReadElementHeader(); + decimal val = reader.ReadDecimal128(); type.ShouldBe(BsonType.Decimal128); name.ShouldBe("dec"); @@ -143,7 +146,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests write and read date time. + /// Tests write and read date time. /// [Fact] public void WriteAndRead_DateTime() @@ -156,7 +159,7 @@ public class BsonSpanReaderWriterTests var expectedTime = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Millisecond, DateTimeKind.Utc); - var sizePos = writer.BeginDocument(); + int sizePos = writer.BeginDocument(); writer.WriteDateTime("timestamp", expectedTime); writer.EndDocument(sizePos); @@ -165,7 +168,7 @@ public class BsonSpanReaderWriterTests reader.ReadDocumentSize(); var type = reader.ReadBsonType(); - var name = reader.ReadElementHeader(); + string name = reader.ReadElementHeader(); var readTime = reader.ReadDateTime(); type.ShouldBe(BsonType.DateTime); @@ -174,7 +177,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests write and read numeric types. + /// Tests write and read numeric types. /// [Fact] public void WriteAndRead_NumericTypes() @@ -182,7 +185,7 @@ public class BsonSpanReaderWriterTests Span buffer = stackalloc byte[256]; var writer = new BsonSpanWriter(buffer, _keyMap); - var sizePos = writer.BeginDocument(); + int sizePos = writer.BeginDocument(); writer.WriteInt32("int32", int.MaxValue); writer.WriteInt64("int64", long.MaxValue); writer.WriteDouble("double", 3.14159); @@ -207,7 +210,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests write and read binary. + /// Tests write and read binary. /// [Fact] public void WriteAndRead_Binary() @@ -217,7 +220,7 @@ public class BsonSpanReaderWriterTests byte[] testData = [1, 2, 3, 4, 5]; - var sizePos = writer.BeginDocument(); + int sizePos = writer.BeginDocument(); writer.WriteBinary("data", testData); writer.EndDocument(sizePos); @@ -226,8 +229,8 @@ public class BsonSpanReaderWriterTests reader.ReadDocumentSize(); var type = reader.ReadBsonType(); - var name = reader.ReadElementHeader(); - var readData = reader.ReadBinary(out var subtype); + string name = reader.ReadElementHeader(); + var readData = reader.ReadBinary(out byte subtype); type.ShouldBe(BsonType.Binary); name.ShouldBe("data"); @@ -236,7 +239,7 @@ public class BsonSpanReaderWriterTests } /// - /// Tests write and read nested document. + /// Tests write and read nested document. /// [Fact] public void WriteAndRead_NestedDocument() @@ -244,10 +247,10 @@ public class BsonSpanReaderWriterTests Span buffer = stackalloc byte[512]; var writer = new BsonSpanWriter(buffer, _keyMap); - var rootSizePos = writer.BeginDocument(); + int rootSizePos = writer.BeginDocument(); writer.WriteString("name", "Parent"); - var childSizePos = writer.BeginDocument("child"); + int childSizePos = writer.BeginDocument("child"); writer.WriteString("name", "Child"); writer.WriteInt32("value", 42); writer.EndDocument(childSizePos); @@ -256,7 +259,7 @@ public class BsonSpanReaderWriterTests var documentBytes = buffer[..writer.Position]; var reader = new BsonSpanReader(documentBytes, _keys); - var rootSize = reader.ReadDocumentSize(); + int rootSize = reader.ReadDocumentSize(); rootSize.ShouldBe(writer.Position); @@ -276,4 +279,4 @@ public class BsonSpanReaderWriterTests reader.ReadElementHeader().ShouldBe("value"); reader.ReadInt32().ShouldBe(42); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Cdc/CdcScalabilityTests.cs b/tests/CBDD.Tests/Cdc/CdcScalabilityTests.cs index 7f907d2..eec526d 100755 --- a/tests/CBDD.Tests/Cdc/CdcScalabilityTests.cs +++ b/tests/CBDD.Tests/Cdc/CdcScalabilityTests.cs @@ -1,29 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using ZB.MOM.WW.CBDD.Core.CDC; using ZB.MOM.WW.CBDD.Shared; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class CdcScalabilityTests : IDisposable { - private readonly Shared.TestDbContext _db; + private readonly TestDbContext _db; private readonly string _dbPath; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public CdcScalabilityTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cdc_scaling_{Guid.NewGuid()}.db"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies CDC dispatch reaches all registered subscribers. + /// Disposes test resources and removes temporary files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + string wal = Path.ChangeExtension(_dbPath, ".wal"); + if (File.Exists(wal)) File.Delete(wal); + } + + /// + /// Verifies CDC dispatch reaches all registered subscribers. /// [Fact] public async Task Test_Cdc_1000_Subscribers_Receive_Events() @@ -34,13 +39,10 @@ public class CdcScalabilityTests : IDisposable var subscriptions = new List(); // 1. Create 1000 subscribers - for (int i = 0; i < SubscriberCount; i++) + for (var i = 0; i < SubscriberCount; i++) { int index = i; - var sub = _db.People.Watch().Subscribe(_ => - { - Interlocked.Increment(ref eventCounts[index]); - }); + var sub = _db.People.Watch().Subscribe(_ => { Interlocked.Increment(ref eventCounts[index]); }); subscriptions.Add(sub); } @@ -53,16 +55,13 @@ public class CdcScalabilityTests : IDisposable await Task.Delay(1000, ct); // 4. Verify all subscribers received both events - for (int i = 0; i < SubscriberCount; i++) - { - eventCounts[i].ShouldBe(2); - } + for (var i = 0; i < SubscriberCount; i++) eventCounts[i].ShouldBe(2); foreach (var sub in subscriptions) sub.Dispose(); } /// - /// Verifies a slow subscriber does not block other subscribers. + /// Verifies a slow subscriber does not block other subscribers. /// [Fact(Skip = "Performance test - run manually when needed")] public async Task Test_Cdc_Slow_Subscriber_Does_Not_Block_Others() @@ -80,10 +79,7 @@ public class CdcScalabilityTests : IDisposable }); // 2. Register a fast subscriber - using var fastSub = _db.People.Watch().Subscribe(_ => - { - Interlocked.Increment(ref fastEventCount); - }); + using var fastSub = _db.People.Watch().Subscribe(_ => { Interlocked.Increment(ref fastEventCount); }); // 3. Perform a write _db.People.Insert(new Person { Id = 1, Name = "John", Age = 30 }); @@ -107,15 +103,4 @@ public class CdcScalabilityTests : IDisposable await Task.Delay(2500, ct); // Wait for the second one in slow sub to be processed after the first Sleep slowEventCount.ShouldBe(2); } - - /// - /// Disposes test resources and removes temporary files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - var wal = Path.ChangeExtension(_dbPath, ".wal"); - if (File.Exists(wal)) File.Delete(wal); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Cdc/CdcTests.cs b/tests/CBDD.Tests/Cdc/CdcTests.cs index 355f9e2..7bc9157 100755 --- a/tests/CBDD.Tests/Cdc/CdcTests.cs +++ b/tests/CBDD.Tests/Cdc/CdcTests.cs @@ -1,15 +1,8 @@ -using System; using System.Collections.Concurrent; -using System.Collections.Generic; using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.CDC; using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; @@ -17,33 +10,43 @@ public class CdcTests : IDisposable { private static readonly TimeSpan DefaultEventTimeout = TimeSpan.FromSeconds(3); private static readonly TimeSpan PollInterval = TimeSpan.FromMilliseconds(10); + private readonly TestDbContext _db; private readonly string _dbPath = $"cdc_test_{Guid.NewGuid()}.db"; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public CdcTests() { - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies that an insert operation publishes a CDC event. + /// Disposes test resources and removes temporary files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + if (File.Exists(_dbPath + "-wal")) File.Delete(_dbPath + "-wal"); + } + + /// + /// Verifies that an insert operation publishes a CDC event. /// [Fact] public async Task Test_Cdc_Basic_Insert_Fires_Event() { var ct = TestContext.Current.CancellationToken; var events = new ConcurrentQueue>(); - using var subscription = _db.People.Watch(capturePayload: true).Subscribe(events.Enqueue); + using var subscription = _db.People.Watch(true).Subscribe(events.Enqueue); var person = new Person { Id = 1, Name = "John", Age = 30 }; _db.People.Insert(person); _db.SaveChanges(); - await WaitForEventCountAsync(events, expectedCount: 1, ct); + await WaitForEventCountAsync(events, 1, ct); var snapshot = events.ToArray(); snapshot.Length.ShouldBe(1); @@ -54,20 +57,20 @@ public class CdcTests : IDisposable } /// - /// Verifies payload is omitted when CDC capture payload is disabled. + /// Verifies payload is omitted when CDC capture payload is disabled. /// [Fact] public async Task Test_Cdc_No_Payload_When_Not_Requested() { var ct = TestContext.Current.CancellationToken; var events = new ConcurrentQueue>(); - using var subscription = _db.People.Watch(capturePayload: false).Subscribe(events.Enqueue); + using var subscription = _db.People.Watch(false).Subscribe(events.Enqueue); var person = new Person { Id = 1, Name = "John", Age = 30 }; _db.People.Insert(person); _db.SaveChanges(); - await WaitForEventCountAsync(events, expectedCount: 1, ct); + await WaitForEventCountAsync(events, 1, ct); var snapshot = events.ToArray(); snapshot.Length.ShouldBe(1); @@ -75,14 +78,14 @@ public class CdcTests : IDisposable } /// - /// Verifies CDC events are published only for committed changes. + /// Verifies CDC events are published only for committed changes. /// [Fact] public async Task Test_Cdc_Commit_Only() { var ct = TestContext.Current.CancellationToken; var events = new ConcurrentQueue>(); - using var subscription = _db.People.Watch(capturePayload: true).Subscribe(events.Enqueue); + using var subscription = _db.People.Watch(true).Subscribe(events.Enqueue); using (var txn = _db.BeginTransaction()) { @@ -101,21 +104,21 @@ public class CdcTests : IDisposable txn.Commit(); } - await WaitForEventCountAsync(events, expectedCount: 1, ct); + await WaitForEventCountAsync(events, 1, ct); var snapshot = events.ToArray(); snapshot.Length.ShouldBe(1); snapshot[0].DocumentId.ShouldBe(2); } /// - /// Verifies update and delete operations publish CDC events. + /// Verifies update and delete operations publish CDC events. /// [Fact] public async Task Test_Cdc_Update_And_Delete() { var ct = TestContext.Current.CancellationToken; var events = new ConcurrentQueue>(); - using var subscription = _db.People.Watch(capturePayload: true).Subscribe(events.Enqueue); + using var subscription = _db.People.Watch(true).Subscribe(events.Enqueue); var person = new Person { Id = 1, Name = "John", Age = 30 }; _db.People.Insert(person); @@ -128,7 +131,7 @@ public class CdcTests : IDisposable _db.People.Delete(1); _db.SaveChanges(); - await WaitForEventCountAsync(events, expectedCount: 3, ct); + await WaitForEventCountAsync(events, 3, ct); var snapshot = events.ToArray(); snapshot.Length.ShouldBe(3); @@ -140,16 +143,6 @@ public class CdcTests : IDisposable snapshot[2].DocumentId.ShouldBe(1); } - /// - /// Disposes test resources and removes temporary files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - if (File.Exists(_dbPath + "-wal")) File.Delete(_dbPath + "-wal"); - } - private static async Task WaitForEventCountAsync( ConcurrentQueue> events, int expectedCount, @@ -158,10 +151,7 @@ public class CdcTests : IDisposable var sw = Stopwatch.StartNew(); while (sw.Elapsed < DefaultEventTimeout) { - if (events.Count >= expectedCount) - { - return; - } + if (events.Count >= expectedCount) return; await Task.Delay(PollInterval, ct); } @@ -174,12 +164,12 @@ public class CdcTests : IDisposable public static class ObservableExtensions { /// - /// Subscribes to an observable sequence using an action callback. + /// Subscribes to an observable sequence using an action callback. /// /// The event type. /// The observable sequence. /// The callback for next events. - /// An subscription. + /// An subscription. public static IDisposable Subscribe(this IObservable observable, Action onNext) { return observable.Subscribe(new AnonymousObserver(onNext)); @@ -190,26 +180,36 @@ public static class ObservableExtensions private readonly Action _onNext; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The callback for next events. - public AnonymousObserver(Action onNext) => _onNext = onNext; + public AnonymousObserver(Action onNext) + { + _onNext = onNext; + } /// - /// Handles completion. + /// Handles completion. /// - public void OnCompleted() { } + public void OnCompleted() + { + } /// - /// Handles an observable error. + /// Handles an observable error. /// /// The observed error. - public void OnError(Exception error) { } + public void OnError(Exception error) + { + } /// - /// Handles the next value. + /// Handles the next value. /// /// The observed value. - public void OnNext(T value) => _onNext(value); + public void OnNext(T value) + { + _onNext(value); + } } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/AsyncTests.cs b/tests/CBDD.Tests/Collections/AsyncTests.cs index 857f256..fe09029 100755 --- a/tests/CBDD.Tests/Collections/AsyncTests.cs +++ b/tests/CBDD.Tests/Collections/AsyncTests.cs @@ -1,9 +1,4 @@ -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; @@ -12,7 +7,7 @@ public class AsyncTests : IDisposable private readonly string _dbPath; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public AsyncTests() { @@ -20,7 +15,7 @@ public class AsyncTests : IDisposable } /// - /// Executes Dispose. + /// Executes Dispose. /// public void Dispose() { @@ -29,14 +24,14 @@ public class AsyncTests : IDisposable } /// - /// Executes Async_Transaction_Commit_Should_Persist_Data. + /// Executes Async_Transaction_Commit_Should_Persist_Data. /// [Fact] public async Task Async_Transaction_Commit_Should_Persist_Data() { var ct = TestContext.Current.CancellationToken; - using (var db = new Shared.TestDbContext(_dbPath)) + using (var db = new TestDbContext(_dbPath)) { using (var txn = await db.BeginTransactionAsync(ct)) { @@ -47,7 +42,7 @@ public class AsyncTests : IDisposable } // Verify with new storage engine instance - using var db2 = new Shared.TestDbContext(_dbPath); + using var db2 = new TestDbContext(_dbPath); var doc1 = db2.AsyncDocs.FindById(1); doc1.ShouldNotBeNull(); doc1.Name.ShouldBe("Async1"); @@ -58,14 +53,14 @@ public class AsyncTests : IDisposable } /// - /// Executes Async_Transaction_Rollback_Should_Discard_Data. + /// Executes Async_Transaction_Rollback_Should_Discard_Data. /// [Fact] public async Task Async_Transaction_Rollback_Should_Discard_Data() { var ct = TestContext.Current.CancellationToken; - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); using (var txn = await db.BeginTransactionAsync(ct)) { db.AsyncDocs.Insert(new AsyncDoc { Id = 3, Name = "RollbackMe" }); @@ -76,12 +71,12 @@ public class AsyncTests : IDisposable } /// - /// Executes Bulk_Async_Insert_Should_Persist_Data. + /// Executes Bulk_Async_Insert_Should_Persist_Data. /// [Fact] public async Task Bulk_Async_Insert_Should_Persist_Data() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); var docs = Enumerable.Range(1, 100).Select(i => new AsyncDoc { Id = i + 5000, Name = $"Bulk{i}" }); var ids = await db.AsyncDocs.InsertBulkAsync(docs); @@ -94,23 +89,20 @@ public class AsyncTests : IDisposable } /// - /// Executes Bulk_Async_Update_Should_Persist_Changes. + /// Executes Bulk_Async_Update_Should_Persist_Changes. /// [Fact] public async Task Bulk_Async_Update_Should_Persist_Changes() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); // 1. Insert 100 docs var docs = Enumerable.Range(1, 100).Select(i => new AsyncDoc { Id = i + 6000, Name = $"Original{i}" }).ToList(); await db.AsyncDocs.InsertBulkAsync(docs); // 2. Update all docs - foreach (var doc in docs) - { - doc.Name = $"Updated{doc.Id - 6000}"; - } + foreach (var doc in docs) doc.Name = $"Updated{doc.Id - 6000}"; - var count = await db.AsyncDocs.UpdateBulkAsync(docs); + int count = await db.AsyncDocs.UpdateBulkAsync(docs); count.ShouldBe(100); @@ -121,23 +113,24 @@ public class AsyncTests : IDisposable } /// - /// Executes High_Concurrency_Async_Commits. + /// Executes High_Concurrency_Async_Commits. /// [Fact] public async Task High_Concurrency_Async_Commits() { var ct = TestContext.Current.CancellationToken; - using var db = new Shared.TestDbContext(Path.Combine(Path.GetTempPath(), $"cbdd_async_concurrency_{Guid.NewGuid()}.db")); - int threadCount = 2; - int docsPerThread = 50; + using var db = + new TestDbContext(Path.Combine(Path.GetTempPath(), $"cbdd_async_concurrency_{Guid.NewGuid()}.db")); + var threadCount = 2; + var docsPerThread = 50; var tasks = Enumerable.Range(0, threadCount).Select(async i => { // Test mix of implicit and explicit transactions - for (int j = 0; j < docsPerThread; j++) + for (var j = 0; j < docsPerThread; j++) { - int id = (i * docsPerThread) + j + 8000; + int id = i * docsPerThread + j + 8000; await db.AsyncDocs.InsertAsync(new AsyncDoc { Id = id, Name = $"Thread{i}_Doc{j}" }); } }); @@ -146,7 +139,7 @@ public class AsyncTests : IDisposable await db.SaveChangesAsync(ct); // Verify count - var count = db.AsyncDocs.Scan(_ => true).Count(); + int count = db.AsyncDocs.Scan(_ => true).Count(); count.ShouldBe(threadCount * docsPerThread); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/BulkOperationsTests.cs b/tests/CBDD.Tests/Collections/BulkOperationsTests.cs index d491b4e..e438786 100755 --- a/tests/CBDD.Tests/Collections/BulkOperationsTests.cs +++ b/tests/CBDD.Tests/Collections/BulkOperationsTests.cs @@ -1,33 +1,27 @@ using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -using Xunit; -using static ZB.MOM.WW.CBDD.Tests.SchemaTests; namespace ZB.MOM.WW.CBDD.Tests; public class BulkOperationsTests : IDisposable { + private readonly TestDbContext _dbContext; private readonly string _dbPath; private readonly string _walPath; - private readonly Shared.TestDbContext _dbContext; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public BulkOperationsTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"test_bulk_{Guid.NewGuid()}.db"); _walPath = Path.Combine(Path.GetTempPath(), $"test_bulk_{Guid.NewGuid()}.wal"); - _dbContext = new Shared.TestDbContext(_dbPath); + _dbContext = new TestDbContext(_dbPath); } /// - /// Executes Dispose. + /// Executes Dispose. /// public void Dispose() { @@ -35,17 +29,14 @@ public class BulkOperationsTests : IDisposable } /// - /// Executes UpdateBulk_UpdatesMultipleDocuments. + /// Executes UpdateBulk_UpdatesMultipleDocuments. /// [Fact] public void UpdateBulk_UpdatesMultipleDocuments() { // Arrange: Insert 100 users var users = new List(); - for (int i = 0; i < 100; i++) - { - users.Add(new User { Id = ObjectId.NewObjectId(), Name = $"User {i}", Age = 20 }); - } + for (var i = 0; i < 100; i++) users.Add(new User { Id = ObjectId.NewObjectId(), Name = $"User {i}", Age = 20 }); _dbContext.Users.InsertBulk(users); _dbContext.SaveChanges(); @@ -57,7 +48,7 @@ public class BulkOperationsTests : IDisposable } // Act - var updatedCount = _dbContext.Users.UpdateBulk(users); + int updatedCount = _dbContext.Users.UpdateBulk(users); _dbContext.SaveChanges(); // Assert @@ -74,41 +65,32 @@ public class BulkOperationsTests : IDisposable } /// - /// Executes DeleteBulk_RemovesMultipleDocuments. + /// Executes DeleteBulk_RemovesMultipleDocuments. /// [Fact] public void DeleteBulk_RemovesMultipleDocuments() { // Arrange: Insert 100 users var users = new List(); - for (int i = 0; i < 100; i++) - { - users.Add(new User { Id = ObjectId.NewObjectId(), Name = $"User {i}", Age = 20 }); - } + for (var i = 0; i < 100; i++) users.Add(new User { Id = ObjectId.NewObjectId(), Name = $"User {i}", Age = 20 }); _dbContext.Users.InsertBulk(users); _dbContext.SaveChanges(); var idsToDelete = users.Take(50).Select(u => u.Id).ToList(); // Act - var deletedCount = _dbContext.Users.DeleteBulk(idsToDelete); + int deletedCount = _dbContext.Users.DeleteBulk(idsToDelete); _dbContext.SaveChanges(); // Assert deletedCount.ShouldBe(50); // Verify deleted - foreach (var id in idsToDelete) - { - _dbContext.Users.FindById(id).ShouldBeNull(); - } + foreach (var id in idsToDelete) _dbContext.Users.FindById(id).ShouldBeNull(); // Verify remaining var remaining = users.Skip(50).ToList(); - foreach (var u in remaining) - { - _dbContext.Users.FindById(u.Id).ShouldNotBeNull(); - } + foreach (var u in remaining) _dbContext.Users.FindById(u.Id).ShouldNotBeNull(); // Verify count // Note: Count() is not fully implemented efficiently yet (iterates everything), but FindAll().Count() works @@ -116,7 +98,7 @@ public class BulkOperationsTests : IDisposable } /// - /// Executes DeleteBulk_WithTransaction_Rollworks. + /// Executes DeleteBulk_WithTransaction_Rollworks. /// [Fact] public void DeleteBulk_WithTransaction_Rollworks() @@ -137,4 +119,4 @@ public class BulkOperationsTests : IDisposable // Assert: Should still exist _dbContext.Users.FindById(user.Id).ShouldNotBeNull(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/DocumentCollectionDeleteTests.cs b/tests/CBDD.Tests/Collections/DocumentCollectionDeleteTests.cs index fc62c35..1f9c13c 100755 --- a/tests/CBDD.Tests/Collections/DocumentCollectionDeleteTests.cs +++ b/tests/CBDD.Tests/Collections/DocumentCollectionDeleteTests.cs @@ -1,32 +1,27 @@ using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class DocumentCollectionDeleteTests : IDisposable { + private readonly TestDbContext _dbContext; private readonly string _dbPath; private readonly string _walPath; - private readonly Shared.TestDbContext _dbContext; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DocumentCollectionDeleteTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"test_delete_{Guid.NewGuid()}.db"); _walPath = Path.Combine(Path.GetTempPath(), $"test_delete_{Guid.NewGuid()}.wal"); - _dbContext = new Shared.TestDbContext(_dbPath); + _dbContext = new TestDbContext(_dbPath); } /// - /// Releases test resources. + /// Releases test resources. /// public void Dispose() { @@ -34,7 +29,7 @@ public class DocumentCollectionDeleteTests : IDisposable } /// - /// Verifies delete removes both the document and its index entry. + /// Verifies delete removes both the document and its index entry. /// [Fact] public void Delete_RemovesDocumentAndIndexEntry() @@ -47,7 +42,7 @@ public class DocumentCollectionDeleteTests : IDisposable _dbContext.Users.FindById(user.Id).ShouldNotBeNull(); // Delete - var deleted = _dbContext.Users.Delete(user.Id); + bool deleted = _dbContext.Users.Delete(user.Id); _dbContext.SaveChanges(); // Assert @@ -62,19 +57,19 @@ public class DocumentCollectionDeleteTests : IDisposable } /// - /// Verifies delete returns false for a non-existent document. + /// Verifies delete returns false for a non-existent document. /// [Fact] public void Delete_NonExistent_ReturnsFalse() { var id = ObjectId.NewObjectId(); - var deleted = _dbContext.Users.Delete(id); + bool deleted = _dbContext.Users.Delete(id); _dbContext.SaveChanges(); deleted.ShouldBeFalse(); } /// - /// Verifies deletes inside a transaction commit successfully. + /// Verifies deletes inside a transaction commit successfully. /// [Fact] public void Delete_WithTransaction_CommitsSuccessfully() @@ -92,4 +87,4 @@ public class DocumentCollectionDeleteTests : IDisposable // Verify _dbContext.Users.FindById(user.Id).ShouldBeNull(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/DocumentCollectionIndexApiTests.cs b/tests/CBDD.Tests/Collections/DocumentCollectionIndexApiTests.cs index c5bdbbf..eab1a8f 100644 --- a/tests/CBDD.Tests/Collections/DocumentCollectionIndexApiTests.cs +++ b/tests/CBDD.Tests/Collections/DocumentCollectionIndexApiTests.cs @@ -5,20 +5,31 @@ namespace ZB.MOM.WW.CBDD.Tests; public class DocumentCollectionIndexApiTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DocumentCollectionIndexApiTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"collection_index_api_{Guid.NewGuid():N}.db"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies vector index creation and deletion behavior. + /// Disposes test resources and removes temporary files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + string wal = Path.ChangeExtension(_dbPath, ".wal"); + if (File.Exists(wal)) File.Delete(wal); + } + + /// + /// Verifies vector index creation and deletion behavior. /// [Fact] public void CreateVectorIndex_And_DropIndex_Should_Work() @@ -39,34 +50,23 @@ public class DocumentCollectionIndexApiTests : IDisposable } /// - /// Verifies ensure-index returns existing indexes when already present. + /// Verifies ensure-index returns existing indexes when already present. /// [Fact] public void EnsureIndex_Should_Return_Existing_Index_When_Already_Present() { - var first = _db.People.EnsureIndex(p => p.Age, name: "idx_people_age"); - var second = _db.People.EnsureIndex(p => p.Age, name: "idx_people_age"); + var first = _db.People.EnsureIndex(p => p.Age, "idx_people_age"); + var second = _db.People.EnsureIndex(p => p.Age, "idx_people_age"); ReferenceEquals(first, second).ShouldBeTrue(); } /// - /// Verifies dropping the primary index name is rejected. + /// Verifies dropping the primary index name is rejected. /// [Fact] public void DropIndex_Should_Reject_Primary_Index_Name() { Should.Throw(() => _db.People.DropIndex("_id")); } - - /// - /// Disposes test resources and removes temporary files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - var wal = Path.ChangeExtension(_dbPath, ".wal"); - if (File.Exists(wal)) File.Delete(wal); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/DocumentCollectionTests.cs b/tests/CBDD.Tests/Collections/DocumentCollectionTests.cs index 9d794ab..73b2fa2 100755 --- a/tests/CBDD.Tests/Collections/DocumentCollectionTests.cs +++ b/tests/CBDD.Tests/Collections/DocumentCollectionTests.cs @@ -1,31 +1,35 @@ using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; namespace ZB.MOM.WW.CBDD.Tests; public class DocumentCollectionTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; private readonly string _walPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DocumentCollectionTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"test_collection_{Guid.NewGuid()}.db"); _walPath = Path.Combine(Path.GetTempPath(), $"test_collection_{Guid.NewGuid()}.wal"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies insert and find-by-id operations. + /// Releases test resources. + /// + public void Dispose() + { + _db?.Dispose(); + } + + /// + /// Verifies insert and find-by-id operations. /// [Fact] public void Insert_And_FindById_Works() @@ -46,7 +50,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies find-by-id returns null when no document is found. + /// Verifies find-by-id returns null when no document is found. /// [Fact] public void FindById_Returns_Null_When_Not_Found() @@ -59,7 +63,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies find-all returns all entities. + /// Verifies find-all returns all entities. /// [Fact] public void FindAll_Returns_All_Entities() @@ -81,7 +85,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies update modifies an existing entity. + /// Verifies update modifies an existing entity. /// [Fact] public void Update_Modifies_Entity() @@ -93,7 +97,7 @@ public class DocumentCollectionTests : IDisposable // Act user.Age = 31; - var updated = _db.Users.Update(user); + bool updated = _db.Users.Update(user); _db.SaveChanges(); // Assert @@ -105,7 +109,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies update returns false when the entity does not exist. + /// Verifies update returns false when the entity does not exist. /// [Fact] public void Update_Returns_False_When_Not_Found() @@ -114,7 +118,7 @@ public class DocumentCollectionTests : IDisposable var user = new User { Id = ObjectId.NewObjectId(), Name = "Ghost", Age = 99 }; // Act - var updated = _db.Users.Update(user); + bool updated = _db.Users.Update(user); _db.SaveChanges(); // Assert @@ -122,7 +126,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies delete removes an entity. + /// Verifies delete removes an entity. /// [Fact] public void Delete_Removes_Entity() @@ -133,7 +137,7 @@ public class DocumentCollectionTests : IDisposable _db.SaveChanges(); // Act - var deleted = _db.Users.Delete(id); + bool deleted = _db.Users.Delete(id); _db.SaveChanges(); // Assert @@ -142,13 +146,13 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies delete returns false when the entity does not exist. + /// Verifies delete returns false when the entity does not exist. /// [Fact] public void Delete_Returns_False_When_Not_Found() { // Act - var deleted = _db.Users.Delete(ObjectId.NewObjectId()); + bool deleted = _db.Users.Delete(ObjectId.NewObjectId()); _db.SaveChanges(); // Assert @@ -156,7 +160,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies count returns the correct entity count. + /// Verifies count returns the correct entity count. /// [Fact] public void Count_Returns_Correct_Count() @@ -167,14 +171,14 @@ public class DocumentCollectionTests : IDisposable _db.SaveChanges(); // Act - var count = _db.Users.Count(); + int count = _db.Users.Count(); // Assert count.ShouldBe(2); } /// - /// Verifies predicate queries filter entities correctly. + /// Verifies predicate queries filter entities correctly. /// [Fact] public void Find_With_Predicate_Filters_Correctly() @@ -194,7 +198,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies bulk insert stores multiple entities. + /// Verifies bulk insert stores multiple entities. /// [Fact] public void InsertBulk_Inserts_Multiple_Entities() @@ -217,7 +221,7 @@ public class DocumentCollectionTests : IDisposable } /// - /// Verifies inserts preserve an explicitly assigned identifier. + /// Verifies inserts preserve an explicitly assigned identifier. /// [Fact] public void Insert_With_SpecifiedId_RetainsId() @@ -238,12 +242,4 @@ public class DocumentCollectionTests : IDisposable found.Id.ShouldBe(id); found.Name.ShouldBe("SpecifiedID"); } - - /// - /// Releases test resources. - /// - public void Dispose() - { - _db?.Dispose(); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/InsertBulkTests.cs b/tests/CBDD.Tests/Collections/InsertBulkTests.cs index 3304f0f..e880020 100755 --- a/tests/CBDD.Tests/Collections/InsertBulkTests.cs +++ b/tests/CBDD.Tests/Collections/InsertBulkTests.cs @@ -1,29 +1,24 @@ -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; +using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class InsertBulkTests : IDisposable { + private readonly TestDbContext _db; private readonly string _testFile; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public InsertBulkTests() { _testFile = Path.GetTempFileName(); - _db = new Shared.TestDbContext(_testFile); + _db = new TestDbContext(_testFile); } /// - /// Disposes test resources. + /// Disposes test resources. /// public void Dispose() { @@ -31,16 +26,13 @@ public class InsertBulkTests : IDisposable } /// - /// Verifies bulk inserts are immediately persisted and visible. + /// Verifies bulk inserts are immediately persisted and visible. /// [Fact] public void InsertBulk_PersistsData_ImmediatelyVisible() { var users = new List(); - for (int i = 0; i < 50; i++) - { - users.Add(new User { Id = ZB.MOM.WW.CBDD.Bson.ObjectId.NewObjectId(), Name = $"User {i}", Age = 20 }); - } + for (var i = 0; i < 50; i++) users.Add(new User { Id = ObjectId.NewObjectId(), Name = $"User {i}", Age = 20 }); _db.Users.InsertBulk(users); _db.SaveChanges(); @@ -51,21 +43,23 @@ public class InsertBulkTests : IDisposable } /// - /// Verifies bulk inserts spanning multiple pages persist correctly. + /// Verifies bulk inserts spanning multiple pages persist correctly. /// [Fact] public void InsertBulk_SpanningMultiplePages_PersistsCorrectly() { // 16KB page. User ~50 bytes. 400 users -> ~20KB -> 2 pages. var users = new List(); - for (int i = 0; i < 400; i++) - { - users.Add(new User { Id = ZB.MOM.WW.CBDD.Bson.ObjectId.NewObjectId(), Name = $"User {i} with some long padding text to ensure we fill space {new string('x', 50)}", Age = 20 }); - } + for (var i = 0; i < 400; i++) + users.Add(new User + { + Id = ObjectId.NewObjectId(), + Name = $"User {i} with some long padding text to ensure we fill space {new string('x', 50)}", Age = 20 + }); _db.Users.InsertBulk(users); _db.SaveChanges(); _db.Users.Count().ShouldBe(400); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Collections/SetMethodTests.cs b/tests/CBDD.Tests/Collections/SetMethodTests.cs index 030a82e..b3a96b6 100755 --- a/tests/CBDD.Tests/Collections/SetMethodTests.cs +++ b/tests/CBDD.Tests/Collections/SetMethodTests.cs @@ -1,25 +1,24 @@ using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Shared; namespace ZB.MOM.WW.CBDD.Tests; public class SetMethodTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public SetMethodTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_set_{Guid.NewGuid()}.db"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Disposes the resources used by this instance. + /// Disposes the resources used by this instance. /// public void Dispose() { @@ -28,7 +27,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set object id returns correct collection. + /// Tests set object id returns correct collection. /// [Fact] public void Set_ObjectId_ReturnsCorrectCollection() @@ -39,7 +38,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set shorthand returns correct collection. + /// Tests set shorthand returns correct collection. /// [Fact] public void Set_Shorthand_ReturnsCorrectCollection() @@ -50,7 +49,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set int returns correct collection. + /// Tests set int returns correct collection. /// [Fact] public void Set_Int_ReturnsCorrectCollection() @@ -61,7 +60,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set string returns correct collection. + /// Tests set string returns correct collection. /// [Fact] public void Set_String_ReturnsCorrectCollection() @@ -72,7 +71,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set guid returns correct collection. + /// Tests set guid returns correct collection. /// [Fact] public void Set_Guid_ReturnsCorrectCollection() @@ -83,7 +82,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set custom key returns correct collection. + /// Tests set custom key returns correct collection. /// [Fact] public void Set_CustomKey_ReturnsCorrectCollection() @@ -94,7 +93,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set all object id collections return correct instances. + /// Tests set all object id collections return correct instances. /// [Fact] public void Set_AllObjectIdCollections_ReturnCorrectInstances() @@ -110,7 +109,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set all int collections return correct instances. + /// Tests set all int collections return correct instances. /// [Fact] public void Set_AllIntCollections_ReturnCorrectInstances() @@ -123,7 +122,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set string key collections return correct instances. + /// Tests set string key collections return correct instances. /// [Fact] public void Set_StringKeyCollections_ReturnCorrectInstances() @@ -132,7 +131,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set unregistered entity throws invalid operation exception. + /// Tests set unregistered entity throws invalid operation exception. /// [Fact] public void Set_UnregisteredEntity_ThrowsInvalidOperationException() @@ -141,7 +140,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set wrong key type throws invalid operation exception. + /// Tests set wrong key type throws invalid operation exception. /// [Fact] public void Set_WrongKeyType_ThrowsInvalidOperationException() @@ -150,7 +149,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set can perform operations. + /// Tests set can perform operations. /// [Fact] public void Set_CanPerformOperations() @@ -167,7 +166,7 @@ public class SetMethodTests : IDisposable } /// - /// Tests set with int key can perform operations. + /// Tests set with int key can perform operations. /// [Fact] public void Set_WithIntKey_CanPerformOperations() @@ -186,20 +185,20 @@ public class SetMethodTests : IDisposable public class SetMethodInheritanceTests : IDisposable { + private readonly TestExtendedDbContext _db; private readonly string _dbPath; - private readonly Shared.TestExtendedDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public SetMethodInheritanceTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_set_inherit_{Guid.NewGuid()}.db"); - _db = new Shared.TestExtendedDbContext(_dbPath); + _db = new TestExtendedDbContext(_dbPath); } /// - /// Disposes the resources used by this instance. + /// Disposes the resources used by this instance. /// public void Dispose() { @@ -208,7 +207,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set own collection returns correct instance. + /// Tests set own collection returns correct instance. /// [Fact] public void Set_OwnCollection_ReturnsCorrectInstance() @@ -219,7 +218,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set parent collection returns correct instance. + /// Tests set parent collection returns correct instance. /// [Fact] public void Set_ParentCollection_ReturnsCorrectInstance() @@ -230,7 +229,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set parent shorthand returns correct instance. + /// Tests set parent shorthand returns correct instance. /// [Fact] public void Set_ParentShorthand_ReturnsCorrectInstance() @@ -241,7 +240,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set parent int collection returns correct instance. + /// Tests set parent int collection returns correct instance. /// [Fact] public void Set_ParentIntCollection_ReturnsCorrectInstance() @@ -251,7 +250,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set parent custom key returns correct instance. + /// Tests set parent custom key returns correct instance. /// [Fact] public void Set_ParentCustomKey_ReturnsCorrectInstance() @@ -262,7 +261,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set unregistered entity throws invalid operation exception. + /// Tests set unregistered entity throws invalid operation exception. /// [Fact] public void Set_UnregisteredEntity_ThrowsInvalidOperationException() @@ -271,7 +270,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set own collection can perform operations. + /// Tests set own collection can perform operations. /// [Fact] public void Set_OwnCollection_CanPerformOperations() @@ -287,7 +286,7 @@ public class SetMethodInheritanceTests : IDisposable } /// - /// Tests set parent collection can perform operations. + /// Tests set parent collection can perform operations. /// [Fact] public void Set_ParentCollection_CanPerformOperations() @@ -301,4 +300,4 @@ public class SetMethodInheritanceTests : IDisposable found.ShouldNotBeNull(); found.Name.ShouldBe("Bob"); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compaction/CompactionCrashRecoveryTests.cs b/tests/CBDD.Tests/Compaction/CompactionCrashRecoveryTests.cs index 0e553bc..aa78a7d 100644 --- a/tests/CBDD.Tests/Compaction/CompactionCrashRecoveryTests.cs +++ b/tests/CBDD.Tests/Compaction/CompactionCrashRecoveryTests.cs @@ -7,7 +7,7 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompactionCrashRecoveryTests { /// - /// Verifies compaction resumes from marker phases and preserves data. + /// Verifies compaction resumes from marker phases and preserves data. /// /// The crash marker phase to resume from. [Theory] @@ -16,8 +16,8 @@ public class CompactionCrashRecoveryTests [InlineData("Swapped")] public void ResumeCompaction_FromCrashMarkerPhases_ShouldFinalizeAndPreserveData(string phase) { - var dbPath = NewDbPath(); - var markerPath = MarkerPath(dbPath); + string dbPath = NewDbPath(); + string markerPath = MarkerPath(dbPath); try { @@ -54,13 +54,13 @@ public class CompactionCrashRecoveryTests } /// - /// Verifies corrupted compaction markers are recovered deterministically. + /// Verifies corrupted compaction markers are recovered deterministically. /// [Fact] public void ResumeCompaction_WithCorruptedMarker_ShouldRecoverDeterministically() { - var dbPath = NewDbPath(); - var markerPath = MarkerPath(dbPath); + string dbPath = NewDbPath(); + string markerPath = MarkerPath(dbPath); try { @@ -96,13 +96,11 @@ public class CompactionCrashRecoveryTests { var ids = new List(); for (var i = 0; i < 120; i++) - { ids.Add(db.Users.Insert(new User { Name = $"user-{i:D4}-payload-{new string('x', 120)}", Age = i % 20 })); - } db.SaveChanges(); return ids; @@ -110,25 +108,30 @@ public class CompactionCrashRecoveryTests private static void WriteMarker(string markerPath, string dbPath, string phase) { - var safeDbPath = dbPath.Replace("\\", "\\\\", StringComparison.Ordinal); + string safeDbPath = dbPath.Replace("\\", "\\\\", StringComparison.Ordinal); var now = DateTimeOffset.UtcNow.ToString("O"); var json = $$""" - {"version":1,"phase":"{{phase}}","databasePath":"{{safeDbPath}}","startedAtUtc":"{{now}}","lastUpdatedUtc":"{{now}}","onlineMode":false,"mode":"InPlace"} - """; + {"version":1,"phase":"{{phase}}","databasePath":"{{safeDbPath}}","startedAtUtc":"{{now}}","lastUpdatedUtc":"{{now}}","onlineMode":false,"mode":"InPlace"} + """; File.WriteAllText(markerPath, json); } - private static string MarkerPath(string dbPath) => $"{dbPath}.compact.state"; + private static string MarkerPath(string dbPath) + { + return $"{dbPath}.compact.state"; + } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compaction_crash_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compaction_crash_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); - var markerPath = MarkerPath(dbPath); + string walPath = Path.ChangeExtension(dbPath, ".wal"); + string markerPath = MarkerPath(dbPath); if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compaction/CompactionOfflineTests.cs b/tests/CBDD.Tests/Compaction/CompactionOfflineTests.cs index 4c8280f..af98fea 100644 --- a/tests/CBDD.Tests/Compaction/CompactionOfflineTests.cs +++ b/tests/CBDD.Tests/Compaction/CompactionOfflineTests.cs @@ -1,4 +1,5 @@ using System.IO.MemoryMappedFiles; +using System.Text; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Core.Storage; @@ -9,30 +10,23 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompactionOfflineTests { /// - /// Tests offline compact should preserve logical data equivalence. + /// Tests offline compact should preserve logical data equivalence. /// [Fact] public void OfflineCompact_ShouldPreserveLogicalDataEquivalence() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); var ids = new List(); - for (var i = 0; i < 160; i++) - { - ids.Add(db.Users.Insert(new User { Name = $"user-{i:D4}", Age = i % 31 })); - } + for (var i = 0; i < 160; i++) ids.Add(db.Users.Insert(new User { Name = $"user-{i:D4}", Age = i % 31 })); for (var i = 0; i < ids.Count; i += 9) - { if (db.Users.FindById(ids[i]) != null) - { db.Users.Delete(ids[i]).ShouldBeTrue(); - } - } var updateTargets = db.Users.FindAll(u => u.Age % 4 == 0) .Select(u => u.Id) @@ -40,10 +34,7 @@ public class CompactionOfflineTests foreach (var id in updateTargets) { var user = db.Users.FindById(id); - if (user == null) - { - continue; - } + if (user == null) continue; user.Name += "-updated"; db.Users.Update(user).ShouldBeTrue(); @@ -76,25 +67,23 @@ public class CompactionOfflineTests } /// - /// Tests offline compact should keep index results consistent. + /// Tests offline compact should keep index results consistent. /// [Fact] public void OfflineCompact_ShouldKeepIndexResultsConsistent() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); for (var i = 0; i < 300; i++) - { db.People.Insert(new Person { Name = $"person-{i:D4}", Age = i % 12 }); - } db.SaveChanges(); db.ForceCheckpoint(); @@ -104,7 +93,7 @@ public class CompactionOfflineTests .ToDictionary(g => g.Key, g => g.Select(x => x.Name).OrderBy(x => x).ToArray()); db.SaveChanges(); - var indexNamesBefore = db.People.GetIndexes().Select(x => x.Name).OrderBy(x => x).ToArray(); + string[] indexNamesBefore = db.People.GetIndexes().Select(x => x.Name).OrderBy(x => x).ToArray(); var stats = db.Compact(new CompactionOptions { @@ -114,12 +103,12 @@ public class CompactionOfflineTests }); stats.PrePageCount.ShouldBeGreaterThanOrEqualTo(stats.PostPageCount); - var indexNamesAfter = db.People.GetIndexes().Select(x => x.Name).OrderBy(x => x).ToArray(); + string[] indexNamesAfter = db.People.GetIndexes().Select(x => x.Name).OrderBy(x => x).ToArray(); indexNamesAfter.ShouldBe(indexNamesBefore); - foreach (var age in expectedByAge.Keys.OrderBy(x => x)) + foreach (int age in expectedByAge.Keys.OrderBy(x => x)) { - var actual = db.People.FindAll(p => p.Age == age) + string[] actual = db.People.FindAll(p => p.Age == age) .Select(x => x.Name) .OrderBy(x => x) .ToArray(); @@ -134,25 +123,23 @@ public class CompactionOfflineTests } /// - /// Tests offline compact should rebuild hash index metadata and preserve results. + /// Tests offline compact should rebuild hash index metadata and preserve results. /// [Fact] public void OfflineCompact_ShouldRebuildHashIndexMetadataAndPreserveResults() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); for (var i = 0; i < 300; i++) - { db.People.Insert(new Person { Name = $"hash-person-{i:D4}", Age = i % 12 }); - } db.SaveChanges(); db.ForceCheckpoint(); @@ -165,7 +152,8 @@ public class CompactionOfflineTests metadata.ShouldNotBeNull(); var targetIndex = metadata!.Indexes - .FirstOrDefault(index => index.PropertyPaths.Any(path => path.Equals("Age", StringComparison.OrdinalIgnoreCase))); + .FirstOrDefault(index => + index.PropertyPaths.Any(path => path.Equals("Age", StringComparison.OrdinalIgnoreCase))); targetIndex.ShouldNotBeNull(); targetIndex!.Type = IndexType.Hash; @@ -191,9 +179,9 @@ public class CompactionOfflineTests runtimeIndex.ShouldNotBeNull(); runtimeIndex!.Type.ShouldBe(IndexType.Hash); - foreach (var age in expectedByAge.Keys.OrderBy(x => x)) + foreach (int age in expectedByAge.Keys.OrderBy(x => x)) { - var actual = db.People.FindAll(p => p.Age == age) + string[] actual = db.People.FindAll(p => p.Age == age) .Select(x => x.Name) .OrderBy(x => x) .ToArray(); @@ -208,12 +196,12 @@ public class CompactionOfflineTests } /// - /// Tests offline compact when tail is reclaimable should reduce file size. + /// Tests offline compact when tail is reclaimable should reduce file size. /// [Fact] public void OfflineCompact_WhenTailIsReclaimable_ShouldReduceFileSize() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var ids = new List(); try @@ -233,24 +221,20 @@ public class CompactionOfflineTests db.SaveChanges(); db.ForceCheckpoint(); - for (var i = ids.Count - 1; i >= 60; i--) - { + for (int i = ids.Count - 1; i >= 60; i--) if (db.Users.FindById(ids[i]) != null) - { db.Users.Delete(ids[i]).ShouldBeTrue(); - } - } db.SaveChanges(); db.ForceCheckpoint(); - var preCompactSize = new FileInfo(dbPath).Length; + long preCompactSize = new FileInfo(dbPath).Length; var stats = db.Compact(new CompactionOptions { EnableTailTruncation = true, MinimumRetainedPages = 2 }); - var postCompactSize = new FileInfo(dbPath).Length; + long postCompactSize = new FileInfo(dbPath).Length; postCompactSize.ShouldBeLessThanOrEqualTo(preCompactSize); stats.ReclaimedFileBytes.ShouldBeGreaterThanOrEqualTo(0); @@ -262,20 +246,17 @@ public class CompactionOfflineTests } /// - /// Tests offline compact with invalid primary root metadata should fail validation. + /// Tests offline compact with invalid primary root metadata should fail validation. /// [Fact] public void OfflineCompact_WithInvalidPrimaryRootMetadata_ShouldFailValidation() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); - for (var i = 0; i < 32; i++) - { - db.Users.Insert(new User { Name = $"invalid-primary-{i:D3}", Age = i }); - } + for (var i = 0; i < 32; i++) db.Users.Insert(new User { Name = $"invalid-primary-{i:D3}", Age = i }); db.SaveChanges(); db.ForceCheckpoint(); @@ -295,20 +276,18 @@ public class CompactionOfflineTests } /// - /// Tests offline compact with invalid secondary root metadata should fail validation. + /// Tests offline compact with invalid secondary root metadata should fail validation. /// [Fact] public void OfflineCompact_WithInvalidSecondaryRootMetadata_ShouldFailValidation() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); for (var i = 0; i < 48; i++) - { db.People.Insert(new Person { Name = $"invalid-secondary-{i:D3}", Age = i % 10 }); - } db.SaveChanges(); db.ForceCheckpoint(); @@ -329,12 +308,12 @@ public class CompactionOfflineTests } /// - /// Tests offline compact should report live bytes relocation and throughput telemetry. + /// Tests offline compact should report live bytes relocation and throughput telemetry. /// [Fact] public void OfflineCompact_ShouldReportLiveBytesRelocationAndThroughputTelemetry() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { @@ -342,21 +321,15 @@ public class CompactionOfflineTests var ids = new List(); for (var i = 0; i < 160; i++) - { ids.Add(db.Users.Insert(new User { Name = BuildPayload(i, 9_000), Age = i })); - } for (var i = 0; i < ids.Count; i += 7) - { if (db.Users.FindById(ids[i]) != null) - { db.Users.Delete(ids[i]).ShouldBeTrue(); - } - } db.SaveChanges(); db.ForceCheckpoint(); @@ -383,12 +356,12 @@ public class CompactionOfflineTests } /// - /// Tests offline compact when primary index points to deleted slot should fail validation. + /// Tests offline compact when primary index points to deleted slot should fail validation. /// [Fact] public void OfflineCompact_WhenPrimaryIndexPointsToDeletedSlot_ShouldFailValidation() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { @@ -408,7 +381,7 @@ public class CompactionOfflineTests db.Storage.ReadPage(location.PageId, null, page); var header = SlottedPageHeader.ReadFrom(page); - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(page.AsSpan(slotOffset, SlotEntry.Size)); slot.Flags |= SlotFlags.Deleted; slot.WriteTo(page.AsSpan(slotOffset, SlotEntry.Size)); @@ -441,7 +414,7 @@ public class CompactionOfflineTests private static string BuildPayload(int seed, int approxLength) { - var builder = new System.Text.StringBuilder(approxLength + 256); + var builder = new StringBuilder(approxLength + 256); var i = 0; while (builder.Length < approxLength) { @@ -457,11 +430,13 @@ public class CompactionOfflineTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compaction_offline_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compaction_offline_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; var tempPath = $"{dbPath}.compact.tmp"; var backupPath = $"{dbPath}.compact.bak"; @@ -471,4 +446,4 @@ public class CompactionOfflineTests if (File.Exists(tempPath)) File.Delete(tempPath); if (File.Exists(backupPath)) File.Delete(backupPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compaction/CompactionOnlineConcurrencyTests.cs b/tests/CBDD.Tests/Compaction/CompactionOnlineConcurrencyTests.cs index 0266a5d..c16ea92 100644 --- a/tests/CBDD.Tests/Compaction/CompactionOnlineConcurrencyTests.cs +++ b/tests/CBDD.Tests/Compaction/CompactionOnlineConcurrencyTests.cs @@ -7,12 +7,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompactionOnlineConcurrencyTests { /// - /// Verifies online compaction completes without deadlock under concurrent workload. + /// Verifies online compaction completes without deadlock under concurrent workload. /// [Fact] public async Task OnlineCompaction_WithConcurrentishWorkload_ShouldCompleteWithoutDeadlock() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var activeIds = new List(); var sync = new object(); var completedOps = 0; @@ -48,10 +48,7 @@ public class CompactionOnlineConcurrencyTests ObjectId? candidate = null; lock (sync) { - if (activeIds.Count > 0) - { - candidate = activeIds[i % activeIds.Count]; - } + if (activeIds.Count > 0) candidate = activeIds[i % activeIds.Count]; } if (candidate.HasValue) @@ -76,10 +73,7 @@ public class CompactionOnlineConcurrencyTests } } - if (candidate.HasValue) - { - db.Users.Delete(candidate.Value); - } + if (candidate.HasValue) db.Users.Delete(candidate.Value); } db.SaveChanges(); @@ -115,10 +109,7 @@ public class CompactionOnlineConcurrencyTests } var actualIds = allUsers.Select(x => x.Id).ToHashSet(); - foreach (var id in snapshotIds) - { - actualIds.ShouldContain(id); - } + foreach (var id in snapshotIds) actualIds.ShouldContain(id); } finally { @@ -127,14 +118,16 @@ public class CompactionOnlineConcurrencyTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compaction_online_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compaction_online_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compaction/CompactionWalCoordinationTests.cs b/tests/CBDD.Tests/Compaction/CompactionWalCoordinationTests.cs index 0589760..e11785d 100644 --- a/tests/CBDD.Tests/Compaction/CompactionWalCoordinationTests.cs +++ b/tests/CBDD.Tests/Compaction/CompactionWalCoordinationTests.cs @@ -7,21 +7,18 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompactionWalCoordinationTests { /// - /// Verifies offline compaction checkpoints and leaves the WAL empty. + /// Verifies offline compaction checkpoints and leaves the WAL empty. /// [Fact] public void OfflineCompact_ShouldCheckpointAndLeaveWalEmpty() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var markerPath = $"{dbPath}.compact.state"; try { using var db = new TestDbContext(dbPath); - for (var i = 0; i < 80; i++) - { - db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i }); - } + for (var i = 0; i < 80; i++) db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i }); db.SaveChanges(); db.Storage.GetWalSize().ShouldBeGreaterThan(0); @@ -46,13 +43,13 @@ public class CompactionWalCoordinationTests } /// - /// Verifies compaction after WAL recovery preserves durable data. + /// Verifies compaction after WAL recovery preserves durable data. /// [Fact] public void Compact_AfterWalRecovery_ShouldKeepDataDurable() { - var dbPath = NewDbPath(); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string dbPath = NewDbPath(); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var expected = new List<(ObjectId Id, string Name)>(); try @@ -76,10 +73,7 @@ public class CompactionWalCoordinationTests { recovered.Users.Count().ShouldBe(expected.Count); - foreach (var item in expected) - { - recovered.Users.FindById(item.Id)!.Name.ShouldBe(item.Name); - } + foreach (var item in expected) recovered.Users.FindById(item.Id)!.Name.ShouldBe(item.Name); recovered.SaveChanges(); recovered.Compact(); @@ -89,10 +83,7 @@ public class CompactionWalCoordinationTests using (var verify = new TestDbContext(dbPath)) { verify.Users.Count().ShouldBe(expected.Count); - foreach (var item in expected) - { - verify.Users.FindById(item.Id)!.Name.ShouldBe(item.Name); - } + foreach (var item in expected) verify.Users.FindById(item.Id)!.Name.ShouldBe(item.Name); } } finally @@ -102,14 +93,16 @@ public class CompactionWalCoordinationTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compaction_wal_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compaction_wal_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compression/CompressionCompatibilityTests.cs b/tests/CBDD.Tests/Compression/CompressionCompatibilityTests.cs index cbfddfd..ac65606 100644 --- a/tests/CBDD.Tests/Compression/CompressionCompatibilityTests.cs +++ b/tests/CBDD.Tests/Compression/CompressionCompatibilityTests.cs @@ -1,5 +1,6 @@ using System.IO.Compression; using System.Security.Cryptography; +using System.Text; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Storage; @@ -10,12 +11,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompressionCompatibilityTests { /// - /// Verifies opening legacy uncompressed files with compression enabled does not mutate database bytes. + /// Verifies opening legacy uncompressed files with compression enabled does not mutate database bytes. /// [Fact] public void OpeningLegacyUncompressedFile_WithCompressionEnabled_ShouldNotMutateDbFile() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var idList = new List(); try @@ -28,8 +29,8 @@ public class CompressionCompatibilityTests db.ForceCheckpoint(); } - var beforeSize = new FileInfo(dbPath).Length; - var beforeHash = ComputeFileHash(dbPath); + long beforeSize = new FileInfo(dbPath).Length; + string beforeHash = ComputeFileHash(dbPath); var compressionOptions = new CompressionOptions { @@ -47,8 +48,8 @@ public class CompressionCompatibilityTests reopened.Users.Count().ShouldBe(2); } - var afterSize = new FileInfo(dbPath).Length; - var afterHash = ComputeFileHash(dbPath); + long afterSize = new FileInfo(dbPath).Length; + string afterHash = ComputeFileHash(dbPath); afterSize.ShouldBe(beforeSize); afterHash.ShouldBe(beforeHash); @@ -60,12 +61,12 @@ public class CompressionCompatibilityTests } /// - /// Verifies mixed compressed and uncompressed documents remain readable after partial migration. + /// Verifies mixed compressed and uncompressed documents remain readable after partial migration. /// [Fact] public void MixedFormatDocuments_ShouldRemainReadableAfterPartialMigration() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); ObjectId legacyId; ObjectId compressedId; @@ -125,7 +126,7 @@ public class CompressionCompatibilityTests for (ushort slotIndex = 0; slotIndex < header.SlotCount; slotIndex++) { - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) continue; @@ -149,7 +150,7 @@ public class CompressionCompatibilityTests private static string BuildPayload(int approxLength) { - var builder = new System.Text.StringBuilder(approxLength + 256); + var builder = new StringBuilder(approxLength + 256); var i = 0; while (builder.Length < approxLength) { @@ -163,14 +164,16 @@ public class CompressionCompatibilityTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compression_compat_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compression_compat_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compression/CompressionCorruptionTests.cs b/tests/CBDD.Tests/Compression/CompressionCorruptionTests.cs index f0fa78f..f119f17 100644 --- a/tests/CBDD.Tests/Compression/CompressionCorruptionTests.cs +++ b/tests/CBDD.Tests/Compression/CompressionCorruptionTests.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using System.IO.Compression; +using System.Text; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Storage; @@ -10,12 +11,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompressionCorruptionTests { /// - /// Verifies corrupted compressed payload checksum triggers invalid data errors. + /// Verifies corrupted compressed payload checksum triggers invalid data errors. /// [Fact] public void Read_WithBadChecksum_ShouldThrowInvalidData() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = CompressionEnabledOptions(); try @@ -23,7 +24,7 @@ public class CompressionCorruptionTests using var db = new TestDbContext(dbPath, options); var id = InsertCheckpointAndCorrupt(db, header => { - var currentChecksum = BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(12, 4)); + uint currentChecksum = BinaryPrimitives.ReadUInt32LittleEndian(header.Slice(12, 4)); BinaryPrimitives.WriteUInt32LittleEndian(header.Slice(12, 4), currentChecksum + 1); }); @@ -38,21 +39,19 @@ public class CompressionCorruptionTests } /// - /// Verifies invalid original length metadata triggers invalid data errors. + /// Verifies invalid original length metadata triggers invalid data errors. /// [Fact] public void Read_WithBadOriginalLength_ShouldThrowInvalidData() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = CompressionEnabledOptions(); try { using var db = new TestDbContext(dbPath, options); - var id = InsertCheckpointAndCorrupt(db, header => - { - BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4, 4), -1); - }); + var id = InsertCheckpointAndCorrupt(db, + header => { BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4, 4), -1); }); var ex = Should.Throw(() => db.Users.FindById(id)); ex.Message.ShouldContain("decompress"); @@ -64,21 +63,19 @@ public class CompressionCorruptionTests } /// - /// Verifies oversized declared decompressed length enforces safety guardrails. + /// Verifies oversized declared decompressed length enforces safety guardrails. /// [Fact] public void Read_WithOversizedDeclaredLength_ShouldEnforceGuardrail() { - var dbPath = NewDbPath(); - var options = CompressionEnabledOptions(maxDecompressedSizeBytes: 2048); + string dbPath = NewDbPath(); + var options = CompressionEnabledOptions(2048); try { using var db = new TestDbContext(dbPath, options); - var id = InsertCheckpointAndCorrupt(db, header => - { - BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4, 4), 2049); - }); + var id = InsertCheckpointAndCorrupt(db, + header => { BinaryPrimitives.WriteInt32LittleEndian(header.Slice(4, 4), 2049); }); var ex = Should.Throw(() => db.Users.FindById(id)); ex.Message.ShouldContain("invalid decompressed length"); @@ -91,12 +88,12 @@ public class CompressionCorruptionTests } /// - /// Verifies invalid codec identifiers in compressed headers trigger invalid data errors. + /// Verifies invalid codec identifiers in compressed headers trigger invalid data errors. /// [Fact] public void Read_WithInvalidCodecId_ShouldThrowInvalidData() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = CompressionEnabledOptions(); try @@ -128,7 +125,7 @@ public class CompressionCorruptionTests db.SaveChanges(); db.ForceCheckpoint(); - var (pageId, slot, _) = FindFirstCompressedSlot(db.Storage); + (uint pageId, var slot, _) = FindFirstCompressedSlot(db.Storage); ((slot.Flags & SlotFlags.HasOverflow) != 0).ShouldBeFalse(); var page = new byte[db.Storage.PageSize]; @@ -152,7 +149,7 @@ public class CompressionCorruptionTests for (ushort slotIndex = 0; slotIndex < header.SlotCount; slotIndex++) { - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) continue; @@ -178,11 +175,9 @@ public class CompressionCorruptionTests }; } - private delegate void HeaderMutator(Span header); - private static string BuildPayload(int approxLength) { - var builder = new System.Text.StringBuilder(approxLength + 256); + var builder = new StringBuilder(approxLength + 256); var i = 0; while (builder.Length < approxLength) { @@ -196,14 +191,18 @@ public class CompressionCorruptionTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compression_corruption_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compression_corruption_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} + + private delegate void HeaderMutator(Span header); +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compression/CompressionInsertReadTests.cs b/tests/CBDD.Tests/Compression/CompressionInsertReadTests.cs index 018b607..5321f9a 100644 --- a/tests/CBDD.Tests/Compression/CompressionInsertReadTests.cs +++ b/tests/CBDD.Tests/Compression/CompressionInsertReadTests.cs @@ -1,4 +1,5 @@ using System.IO.Compression; +using System.Text; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Storage; @@ -9,12 +10,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompressionInsertReadTests { /// - /// Tests insert with threshold should store mixed compressed and uncompressed slots. + /// Tests insert with threshold should store mixed compressed and uncompressed slots. /// [Fact] public void Insert_WithThreshold_ShouldStoreMixedCompressedAndUncompressedSlots() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = new CompressionOptions { EnableCompression = true, @@ -50,12 +51,12 @@ public class CompressionInsertReadTests } /// - /// Tests find by id should read mixed compressed and uncompressed documents. + /// Tests find by id should read mixed compressed and uncompressed documents. /// [Fact] public void FindById_ShouldReadMixedCompressedAndUncompressedDocuments() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = new CompressionOptions { EnableCompression = true, @@ -98,12 +99,12 @@ public class CompressionInsertReadTests } /// - /// Tests insert when codec throws should fallback to uncompressed storage. + /// Tests insert when codec throws should fallback to uncompressed storage. /// [Fact] public void Insert_WhenCodecThrows_ShouldFallbackToUncompressedStorage() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = new CompressionOptions { EnableCompression = true, @@ -152,7 +153,7 @@ public class CompressionInsertReadTests for (ushort slotIndex = 0; slotIndex < header.SlotCount; slotIndex++) { - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) continue; @@ -168,7 +169,7 @@ public class CompressionInsertReadTests private static string BuildPayload(int approxLength) { - var builder = new System.Text.StringBuilder(approxLength + 256); + var builder = new StringBuilder(approxLength + 256); var i = 0; while (builder.Length < approxLength) { @@ -182,11 +183,13 @@ public class CompressionInsertReadTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compression_insert_read_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compression_insert_read_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); @@ -196,25 +199,29 @@ public class CompressionInsertReadTests private sealed class FailingBrotliCodec : ICompressionCodec { /// - /// Gets or sets the codec. + /// Gets or sets the codec. /// public CompressionCodec Codec => CompressionCodec.Brotli; /// - /// Tests compress. + /// Tests compress. /// /// Payload bytes to compress. /// Compression level. public byte[] Compress(ReadOnlySpan input, CompressionLevel level) - => throw new InvalidOperationException("Forced codec failure for test coverage."); + { + throw new InvalidOperationException("Forced codec failure for test coverage."); + } /// - /// Tests decompress. + /// Tests decompress. /// /// Compressed payload bytes. /// Expected decompressed payload length. /// Maximum allowed decompressed size. public byte[] Decompress(ReadOnlySpan input, int expectedLength, int maxDecompressedSizeBytes) - => throw new InvalidOperationException("This codec should not be used for reads in this scenario."); + { + throw new InvalidOperationException("This codec should not be used for reads in this scenario."); + } } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Compression/CompressionOverflowTests.cs b/tests/CBDD.Tests/Compression/CompressionOverflowTests.cs index a44eabd..7d92a0b 100644 --- a/tests/CBDD.Tests/Compression/CompressionOverflowTests.cs +++ b/tests/CBDD.Tests/Compression/CompressionOverflowTests.cs @@ -1,5 +1,6 @@ using System.IO.Compression; using System.IO.MemoryMappedFiles; +using System.Text; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Shared; @@ -9,12 +10,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CompressionOverflowTests { /// - /// Tests insert compressed document spanning overflow pages should round trip. + /// Tests insert compressed document spanning overflow pages should round trip. /// [Fact] public void Insert_CompressedDocumentSpanningOverflowPages_ShouldRoundTrip() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = new CompressionOptions { EnableCompression = true, @@ -28,7 +29,7 @@ public class CompressionOverflowTests { using var db = new TestDbContext(dbPath, TinyPageConfig(), options); - var payload = BuildPayload(300_000); + string payload = BuildPayload(300_000); var id = db.Users.Insert(new User { Name = payload, Age = 40 }); db.SaveChanges(); @@ -47,12 +48,12 @@ public class CompressionOverflowTests } /// - /// Tests update should transition across compression thresholds. + /// Tests update should transition across compression thresholds. /// [Fact] public void Update_ShouldTransitionAcrossCompressionThresholds() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); var options = new CompressionOptions { EnableCompression = true, @@ -123,13 +124,13 @@ public class CompressionOverflowTests for (ushort slotIndex = 0; slotIndex < header.SlotCount; slotIndex++) { - var slotOffset = SlottedPageHeader.Size + (slotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + slotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(buffer.AsSpan(slotOffset, SlotEntry.Size)); if ((slot.Flags & SlotFlags.Deleted) != 0) continue; - var isCompressed = (slot.Flags & SlotFlags.Compressed) != 0; - var hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; + bool isCompressed = (slot.Flags & SlotFlags.Compressed) != 0; + bool hasOverflow = (slot.Flags & SlotFlags.HasOverflow) != 0; if (isCompressed) compressed++; if (isCompressed && hasOverflow) @@ -152,7 +153,7 @@ public class CompressionOverflowTests private static string BuildPayload(int approxLength) { - var builder = new System.Text.StringBuilder(approxLength + 256); + var builder = new StringBuilder(approxLength + 256); var i = 0; while (builder.Length < approxLength) { @@ -166,14 +167,16 @@ public class CompressionOverflowTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"compression_overflow_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"compression_overflow_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Context/AutoInitTests.cs b/tests/CBDD.Tests/Context/AutoInitTests.cs index 5d5a086..c6e0e05 100755 --- a/tests/CBDD.Tests/Context/AutoInitTests.cs +++ b/tests/CBDD.Tests/Context/AutoInitTests.cs @@ -1,43 +1,42 @@ using ZB.MOM.WW.CBDD.Shared; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class AutoInitTests : IDisposable { - public class AutoInitTests : System.IDisposable + private const string DbPath = "autoinit.db"; + + /// + /// Initializes a new instance of the class. + /// + public AutoInitTests() { - private const string DbPath = "autoinit.db"; - - /// - /// Initializes a new instance of the class. - /// - public AutoInitTests() - { - if (File.Exists(DbPath)) File.Delete(DbPath); - } - - /// - /// Releases test resources. - /// - public void Dispose() - { - if (File.Exists(DbPath)) File.Delete(DbPath); - } - - /// - /// Verifies generated collection initializers set up collections automatically. - /// - [Fact] - public void Collections_Are_Initialized_By_Generator() - { - using var db = new Shared.TestDbContext(DbPath); - - // Verify Collection is not null (initialized by generated method) - db.AutoInitEntities.ShouldNotBeNull(); - - // Verify we can use it - db.AutoInitEntities.Insert(new AutoInitEntity { Id = 1, Name = "Test" }); - var stored = db.AutoInitEntities.FindById(1); - stored.ShouldNotBeNull(); - stored.Name.ShouldBe("Test"); - } + if (File.Exists(DbPath)) File.Delete(DbPath); } -} + + /// + /// Releases test resources. + /// + public void Dispose() + { + if (File.Exists(DbPath)) File.Delete(DbPath); + } + + /// + /// Verifies generated collection initializers set up collections automatically. + /// + [Fact] + public void Collections_Are_Initialized_By_Generator() + { + using var db = new TestDbContext(DbPath); + + // Verify Collection is not null (initialized by generated method) + db.AutoInitEntities.ShouldNotBeNull(); + + // Verify we can use it + db.AutoInitEntities.Insert(new AutoInitEntity { Id = 1, Name = "Test" }); + var stored = db.AutoInitEntities.FindById(1); + stored.ShouldNotBeNull(); + stored.Name.ShouldBe("Test"); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Context/DbContextInheritanceTests.cs b/tests/CBDD.Tests/Context/DbContextInheritanceTests.cs index 8654bf8..36ed23b 100755 --- a/tests/CBDD.Tests/Context/DbContextInheritanceTests.cs +++ b/tests/CBDD.Tests/Context/DbContextInheritanceTests.cs @@ -1,27 +1,23 @@ -using System; -using System.IO; -using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class DbContextInheritanceTests : IDisposable { + private readonly TestExtendedDbContext _db; private readonly string _dbPath; - private readonly Shared.TestExtendedDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DbContextInheritanceTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_inheritance_{Guid.NewGuid()}.db"); - _db = new Shared.TestExtendedDbContext(_dbPath); + _db = new TestExtendedDbContext(_dbPath); } /// - /// Releases test resources. + /// Releases test resources. /// public void Dispose() { @@ -30,7 +26,7 @@ public class DbContextInheritanceTests : IDisposable } /// - /// Verifies parent collections are initialized in the extended context. + /// Verifies parent collections are initialized in the extended context. /// [Fact] public void ExtendedContext_Should_Initialize_Parent_Collections() @@ -45,7 +41,7 @@ public class DbContextInheritanceTests : IDisposable } /// - /// Verifies extended context collections are initialized. + /// Verifies extended context collections are initialized. /// [Fact] public void ExtendedContext_Should_Initialize_Own_Collections() @@ -55,7 +51,7 @@ public class DbContextInheritanceTests : IDisposable } /// - /// Verifies parent collections are usable from the extended context. + /// Verifies parent collections are usable from the extended context. /// [Fact] public void ExtendedContext_Can_Use_Parent_Collections() @@ -73,7 +69,7 @@ public class DbContextInheritanceTests : IDisposable } /// - /// Verifies extended collections are usable from the extended context. + /// Verifies extended collections are usable from the extended context. /// [Fact] public void ExtendedContext_Can_Use_Own_Collections() @@ -95,7 +91,7 @@ public class DbContextInheritanceTests : IDisposable } /// - /// Verifies parent and extended collections can be used together. + /// Verifies parent and extended collections can be used together. /// [Fact] public void ExtendedContext_Can_Use_Both_Parent_And_Own_Collections() @@ -125,4 +121,4 @@ public class DbContextInheritanceTests : IDisposable retrievedExtended.ShouldNotBeNull(); retrievedExtended.Description.ShouldBe("Related to John"); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Context/DbContextTests.cs b/tests/CBDD.Tests/Context/DbContextTests.cs index 68b7c5d..6da7373 100755 --- a/tests/CBDD.Tests/Context/DbContextTests.cs +++ b/tests/CBDD.Tests/Context/DbContextTests.cs @@ -1,10 +1,9 @@ -using ZB.MOM.WW.CBDD.Bson; +using System.IO.Compression; +using System.IO.MemoryMappedFiles; +using System.Security.Cryptography; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Shared; -using System.Security.Cryptography; -using System.IO.Compression; -using System.IO.MemoryMappedFiles; namespace ZB.MOM.WW.CBDD.Tests; @@ -13,7 +12,7 @@ public class DbContextTests : IDisposable private string _dbPath; /// - /// Initializes test file paths for database context tests. + /// Initializes test file paths for database context tests. /// public DbContextTests() { @@ -21,12 +20,27 @@ public class DbContextTests : IDisposable } /// - /// Verifies the basic database context lifecycle works. + /// Disposes test resources and cleans up generated files. + /// + public void Dispose() + { + try + { + CleanupDbFiles(_dbPath); + } + catch + { + // Ignore cleanup errors + } + } + + /// + /// Verifies the basic database context lifecycle works. /// [Fact] public void DbContext_BasicLifecycle_Works() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); var user = new User { Name = "Alice", Age = 30 }; var id = db.Users.Insert(user); @@ -38,12 +52,12 @@ public class DbContextTests : IDisposable } /// - /// Verifies multiple CRUD operations execute correctly in one context. + /// Verifies multiple CRUD operations execute correctly in one context. /// [Fact] public void DbContext_MultipleOperations_Work() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); // Insert var alice = new User { Name = "Alice", Age = 30 }; @@ -69,32 +83,33 @@ public class DbContextTests : IDisposable } /// - /// Verifies disposing and reopening context preserves persisted data. + /// Verifies disposing and reopening context preserves persisted data. /// [Fact] public void DbContext_Dispose_ReleasesResources() { - _dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_reopen.db"); + _dbPath = Path.Combine(Path.GetTempPath(), "test_dbcontext_reopen.db"); var totalUsers = 0; // First context - insert and dispose (auto-checkpoint) - using (var db = new Shared.TestDbContext(_dbPath)) + using (var db = new TestDbContext(_dbPath)) { db.Users.Insert(new User { Name = "Test", Age = 20 }); db.SaveChanges(); // Explicitly save changes to ensure data is in WAL - var beforeCheckpointTotalUsers = db.Users.FindAll().Count(); + int beforeCheckpointTotalUsers = db.Users.FindAll().Count(); db.ForceCheckpoint(); // Force checkpoint to ensure data is persisted to main file totalUsers = db.Users.FindAll().Count(); - var countedUsers = db.Users.Count(); + int countedUsers = db.Users.Count(); totalUsers.ShouldBe(beforeCheckpointTotalUsers); } // Dispose → Commit → ForceCheckpoint → Write to PageFile // Should be able to open again and see persisted data - using var db2 = new Shared.TestDbContext(_dbPath); + using var db2 = new TestDbContext(_dbPath); totalUsers.ShouldBe(1); db2.Users.FindAll().Count().ShouldBe(totalUsers); db2.Users.Count().ShouldBe(totalUsers); } + private static string ComputeFileHash(string path) { using var stream = File.OpenRead(path); @@ -103,29 +118,31 @@ public class DbContextTests : IDisposable } /// - /// Verifies database file size and content change after insert and checkpoint. + /// Verifies database file size and content change after insert and checkpoint. /// [Fact] public void DatabaseFile_SizeAndContent_ChangeAfterInsert() { - var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbfile_{Guid.NewGuid()}.db"); + string dbPath = Path.Combine(Path.GetTempPath(), $"test_dbfile_{Guid.NewGuid()}.db"); // 1. Crea e chiudi database vuoto - using (var db = new Shared.TestDbContext(dbPath)) + using (var db = new TestDbContext(dbPath)) { db.Users.Insert(new User { Name = "Pippo", Age = 42 }); } - var initialSize = new FileInfo(dbPath).Length; - var initialHash = ComputeFileHash(dbPath); + + long initialSize = new FileInfo(dbPath).Length; + string initialHash = ComputeFileHash(dbPath); // 2. Riapri, inserisci, chiudi - using (var db = new Shared.TestDbContext(dbPath)) + using (var db = new TestDbContext(dbPath)) { db.Users.Insert(new User { Name = "Test", Age = 42 }); db.ForceCheckpoint(); // Forza persistenza } - var afterInsertSize = new FileInfo(dbPath).Length; - var afterInsertHash = ComputeFileHash(dbPath); + + long afterInsertSize = new FileInfo(dbPath).Length; + string afterInsertHash = ComputeFileHash(dbPath); // 3. Verifica che dimensione e hash siano cambiati afterInsertSize.ShouldNotBe(initialSize); @@ -133,25 +150,25 @@ public class DbContextTests : IDisposable } /// - /// Verifies the WAL file path is auto-derived from database path. + /// Verifies the WAL file path is auto-derived from database path. /// [Fact] public void DbContext_AutoDerivesWalPath() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); db.Users.Insert(new User { Name = "Test", Age = 20 }); - var walPath = Path.ChangeExtension(_dbPath, ".wal"); + string walPath = Path.ChangeExtension(_dbPath, ".wal"); File.Exists(walPath).ShouldBeTrue(); } /// - /// Verifies custom page file and compression options support roundtrip data access. + /// Verifies custom page file and compression options support roundtrip data access. /// [Fact] public void DbContext_WithCustomPageFileAndCompressionOptions_ShouldSupportRoundTrip() { - var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compression_{Guid.NewGuid():N}.db"); + string dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compression_{Guid.NewGuid():N}.db"); var options = new CompressionOptions { EnableCompression = true, @@ -170,8 +187,8 @@ public class DbContextTests : IDisposable try { - using var db = new Shared.TestDbContext(dbPath, config, options); - var payload = string.Concat(Enumerable.Repeat("compressible-", 3000)); + using var db = new TestDbContext(dbPath, config, options); + string payload = string.Concat(Enumerable.Repeat("compressible-", 3000)); var id = db.Users.Insert(new User { Name = payload, Age = 77 }); db.SaveChanges(); @@ -187,19 +204,16 @@ public class DbContextTests : IDisposable } /// - /// Verifies compact API returns stats and preserves data consistency. + /// Verifies compact API returns stats and preserves data consistency. /// [Fact] public void DbContext_CompactApi_ShouldReturnStatsAndPreserveData() { - var dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compact_{Guid.NewGuid():N}.db"); + string dbPath = Path.Combine(Path.GetTempPath(), $"test_dbcontext_compact_{Guid.NewGuid():N}.db"); try { - using var db = new Shared.TestDbContext(dbPath); - for (var i = 0; i < 120; i++) - { - db.Users.Insert(new User { Name = $"compact-{i:D3}", Age = i % 20 }); - } + using var db = new TestDbContext(dbPath); + for (var i = 0; i < 120; i++) db.Users.Insert(new User { Name = $"compact-{i:D3}", Age = i % 20 }); db.SaveChanges(); db.Users.Count().ShouldBe(120); @@ -221,29 +235,14 @@ public class DbContextTests : IDisposable } } - /// - /// Disposes test resources and cleans up generated files. - /// - public void Dispose() - { - try - { - CleanupDbFiles(_dbPath); - } - catch - { - // Ignore cleanup errors - } - } - private static void CleanupDbFiles(string dbPath) { if (File.Exists(dbPath)) File.Delete(dbPath); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Context/SourceGeneratorFeaturesTests.cs b/tests/CBDD.Tests/Context/SourceGeneratorFeaturesTests.cs index 4ea9d29..160e2a2 100755 --- a/tests/CBDD.Tests/Context/SourceGeneratorFeaturesTests.cs +++ b/tests/CBDD.Tests/Context/SourceGeneratorFeaturesTests.cs @@ -1,36 +1,48 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; -using System.Linq; namespace ZB.MOM.WW.CBDD.Tests; /// -/// Tests for Source Generator enhancements: -/// 1. Property inheritance from base classes (including Id) -/// 2. Exclusion of computed getter-only properties -/// 3. Recognition of advanced collection types (HashSet, ISet, LinkedList, etc.) +/// Tests for Source Generator enhancements: +/// 1. Property inheritance from base classes (including Id) +/// 2. Exclusion of computed getter-only properties +/// 3. Recognition of advanced collection types (HashSet, ISet, LinkedList, etc.) /// public class SourceGeneratorFeaturesTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; private readonly string _walPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public SourceGeneratorFeaturesTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"test_sg_features_{Guid.NewGuid()}.db"); _walPath = Path.Combine(Path.GetTempPath(), $"test_sg_features_{Guid.NewGuid()}.wal"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); + } + + /// + /// Disposes the resources used by this instance. + /// + public void Dispose() + { + _db?.Dispose(); + + if (File.Exists(_dbPath)) + File.Delete(_dbPath); + if (File.Exists(_walPath)) + File.Delete(_walPath); } #region Inheritance Tests /// - /// Tests derived entity inherits id from base class. + /// Tests derived entity inherits id from base class. /// [Fact] public void DerivedEntity_InheritsId_FromBaseClass() @@ -57,7 +69,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests derived entity update works with inherited id. + /// Tests derived entity update works with inherited id. /// [Fact] public void DerivedEntity_Update_WorksWithInheritedId() @@ -90,7 +102,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests derived entity query works with inherited properties. + /// Tests derived entity query works with inherited properties. /// [Fact] public void DerivedEntity_Query_WorksWithInheritedProperties() @@ -120,7 +132,7 @@ public class SourceGeneratorFeaturesTests : IDisposable #region Computed Properties Tests /// - /// Tests computed properties are not serialized. + /// Tests computed properties are not serialized. /// [Fact] public void ComputedProperties_AreNotSerialized() @@ -151,7 +163,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests computed properties update does not break. + /// Tests computed properties update does not break. /// [Fact] public void ComputedProperties_UpdateDoesNotBreak() @@ -189,7 +201,7 @@ public class SourceGeneratorFeaturesTests : IDisposable #region Advanced Collections Tests /// - /// Tests hash set serializes and deserializes. + /// Tests hash set serializes and deserializes. /// [Fact] public void HashSet_SerializesAndDeserializes() @@ -219,7 +231,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests iset serializes and deserializes. + /// Tests iset serializes and deserializes. /// [Fact] public void ISet_SerializesAndDeserializes() @@ -250,7 +262,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests linked list serializes and deserializes. + /// Tests linked list serializes and deserializes. /// [Fact] public void LinkedList_SerializesAndDeserializes() @@ -281,7 +293,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests queue serializes and deserializes. + /// Tests queue serializes and deserializes. /// [Fact] public void Queue_SerializesAndDeserializes() @@ -311,7 +323,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests stack serializes and deserializes. + /// Tests stack serializes and deserializes. /// [Fact] public void Stack_SerializesAndDeserializes() @@ -341,7 +353,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests hash set with nested objects serializes and deserializes. + /// Tests hash set with nested objects serializes and deserializes. /// [Fact] public void HashSet_WithNestedObjects_SerializesAndDeserializes() @@ -351,8 +363,10 @@ public class SourceGeneratorFeaturesTests : IDisposable { Name = "Test Nested HashSet" }; - entity.Addresses.Add(new Address { Street = "123 Main St", City = new City { Name = "NYC", ZipCode = "10001" } }); - entity.Addresses.Add(new Address { Street = "456 Oak Ave", City = new City { Name = "LA", ZipCode = "90001" } }); + entity.Addresses.Add( + new Address { Street = "123 Main St", City = new City { Name = "NYC", ZipCode = "10001" } }); + entity.Addresses.Add(new Address + { Street = "456 Oak Ave", City = new City { Name = "LA", ZipCode = "90001" } }); // Act var id = _db.AdvancedCollectionEntities.Insert(entity); @@ -371,7 +385,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests iset with nested objects serializes and deserializes. + /// Tests iset with nested objects serializes and deserializes. /// [Fact] public void ISet_WithNestedObjects_SerializesAndDeserializes() @@ -403,7 +417,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests advanced collections all types in single entity. + /// Tests advanced collections all types in single entity. /// [Fact] public void AdvancedCollections_AllTypesInSingleEntity() @@ -454,7 +468,7 @@ public class SourceGeneratorFeaturesTests : IDisposable #region Private Setters Tests /// - /// Tests entity with private setters can be deserialized. + /// Tests entity with private setters can be deserialized. /// [Fact] public void EntityWithPrivateSetters_CanBeDeserialized() @@ -475,7 +489,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests entity with private setters update works. + /// Tests entity with private setters update works. /// [Fact] public void EntityWithPrivateSetters_Update_Works() @@ -501,7 +515,7 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests entity with private setters query works. + /// Tests entity with private setters query works. /// [Fact] public void EntityWithPrivateSetters_Query_Works() @@ -530,7 +544,7 @@ public class SourceGeneratorFeaturesTests : IDisposable #region Init-Only Setters Tests /// - /// Tests entity with init setters can be deserialized. + /// Tests entity with init setters can be deserialized. /// [Fact] public void EntityWithInitSetters_CanBeDeserialized() @@ -557,15 +571,18 @@ public class SourceGeneratorFeaturesTests : IDisposable } /// - /// Tests entity with init setters query works. + /// Tests entity with init setters query works. /// [Fact] public void EntityWithInitSetters_Query_Works() { // Arrange - var entity1 = new EntityWithInitSetters { Id = ObjectId.NewObjectId(), Name = "Alpha", Age = 20, CreatedAt = DateTime.UtcNow }; - var entity2 = new EntityWithInitSetters { Id = ObjectId.NewObjectId(), Name = "Beta", Age = 30, CreatedAt = DateTime.UtcNow }; - var entity3 = new EntityWithInitSetters { Id = ObjectId.NewObjectId(), Name = "Gamma", Age = 40, CreatedAt = DateTime.UtcNow }; + var entity1 = new EntityWithInitSetters + { Id = ObjectId.NewObjectId(), Name = "Alpha", Age = 20, CreatedAt = DateTime.UtcNow }; + var entity2 = new EntityWithInitSetters + { Id = ObjectId.NewObjectId(), Name = "Beta", Age = 30, CreatedAt = DateTime.UtcNow }; + var entity3 = new EntityWithInitSetters + { Id = ObjectId.NewObjectId(), Name = "Gamma", Age = 40, CreatedAt = DateTime.UtcNow }; _db.InitSetterEntities.Insert(entity1); _db.InitSetterEntities.Insert(entity2); @@ -582,17 +599,4 @@ public class SourceGeneratorFeaturesTests : IDisposable } #endregion - - /// - /// Disposes the resources used by this instance. - /// - public void Dispose() - { - _db?.Dispose(); - - if (File.Exists(_dbPath)) - File.Delete(_dbPath); - if (File.Exists(_walPath)) - File.Delete(_walPath); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Context/TestDbContext.cs b/tests/CBDD.Tests/Context/TestDbContext.cs index 5435eff..0886e03 100755 --- a/tests/CBDD.Tests/Context/TestDbContext.cs +++ b/tests/CBDD.Tests/Context/TestDbContext.cs @@ -2,191 +2,220 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Compression; -using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Core.Metadata; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; +using ZB.MOM.WW.CBDD.Core.Indexing; +using ZB.MOM.WW.CBDD.Core.Metadata; +using ZB.MOM.WW.CBDD.Core.Storage; +using ZB.MOM.WW.CBDD.Core.Transactions; namespace ZB.MOM.WW.CBDD.Shared; /// -/// Test context with manual collection initialization -/// (Source Generator will automate this in the future) +/// Test context with manual collection initialization +/// (Source Generator will automate this in the future) /// public partial class TestDbContext : DocumentDbContext { /// - /// Gets or sets the AnnotatedUsers. + /// Initializes a new instance of the class. + /// + /// The database path. + public TestDbContext(string databasePath) + : this(databasePath, PageFileConfig.Default, CompressionOptions.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database path. + /// The compression options. + public TestDbContext(string databasePath, CompressionOptions compressionOptions) + : this(databasePath, PageFileConfig.Default, compressionOptions) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database path. + /// The page file configuration. + public TestDbContext(string databasePath, PageFileConfig pageFileConfig) + : this(databasePath, pageFileConfig, CompressionOptions.Default) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The database path. + /// The page file configuration. + /// The compression options. + /// The maintenance options. + public TestDbContext( + string databasePath, + PageFileConfig pageFileConfig, + CompressionOptions? compressionOptions, + MaintenanceOptions? maintenanceOptions = null) + : base(databasePath, pageFileConfig, compressionOptions, maintenanceOptions) + { + } + + /// + /// Gets or sets the AnnotatedUsers. /// public DocumentCollection AnnotatedUsers { get; set; } = null!; + /// - /// Gets or sets the Orders. + /// Gets or sets the Orders. /// public DocumentCollection Orders { get; set; } = null!; + /// - /// Gets or sets the TestDocuments. + /// Gets or sets the TestDocuments. /// public DocumentCollection TestDocuments { get; set; } = null!; + /// - /// Gets or sets the OrderDocuments. + /// Gets or sets the OrderDocuments. /// public DocumentCollection OrderDocuments { get; set; } = null!; + /// - /// Gets or sets the ComplexDocuments. + /// Gets or sets the ComplexDocuments. /// public DocumentCollection ComplexDocuments { get; set; } = null!; + /// - /// Gets or sets the Users. + /// Gets or sets the Users. /// public DocumentCollection Users { get; set; } = null!; + /// - /// Gets or sets the ComplexUsers. + /// Gets or sets the ComplexUsers. /// public DocumentCollection ComplexUsers { get; set; } = null!; + /// - /// Gets or sets the AutoInitEntities. + /// Gets or sets the AutoInitEntities. /// public DocumentCollection AutoInitEntities { get; set; } = null!; + /// - /// Gets or sets the People. + /// Gets or sets the People. /// public DocumentCollection People { get; set; } = null!; + /// - /// Gets or sets the PeopleV2. + /// Gets or sets the PeopleV2. /// public DocumentCollection PeopleV2 { get; set; } = null!; + /// - /// Gets or sets the Products. + /// Gets or sets the Products. /// public DocumentCollection Products { get; set; } = null!; + /// - /// Gets or sets the IntEntities. + /// Gets or sets the IntEntities. /// public DocumentCollection IntEntities { get; set; } = null!; + /// - /// Gets or sets the StringEntities. + /// Gets or sets the StringEntities. /// public DocumentCollection StringEntities { get; set; } = null!; + /// - /// Gets or sets the GuidEntities. + /// Gets or sets the GuidEntities. /// public DocumentCollection GuidEntities { get; set; } = null!; + /// - /// Gets or sets the CustomKeyEntities. + /// Gets or sets the CustomKeyEntities. /// public DocumentCollection CustomKeyEntities { get; set; } = null!; + /// - /// Gets or sets the AsyncDocs. + /// Gets or sets the AsyncDocs. /// public DocumentCollection AsyncDocs { get; set; } = null!; + /// - /// Gets or sets the SchemaUsers. + /// Gets or sets the SchemaUsers. /// public DocumentCollection SchemaUsers { get; set; } = null!; + /// - /// Gets or sets the VectorItems. + /// Gets or sets the VectorItems. /// public DocumentCollection VectorItems { get; set; } = null!; + /// - /// Gets or sets the GeoItems. + /// Gets or sets the GeoItems. /// public DocumentCollection GeoItems { get; set; } = null!; // Source Generator Feature Tests /// - /// Gets or sets the DerivedEntities. + /// Gets or sets the DerivedEntities. /// public DocumentCollection DerivedEntities { get; set; } = null!; + /// - /// Gets or sets the ComputedPropertyEntities. + /// Gets or sets the ComputedPropertyEntities. /// public DocumentCollection ComputedPropertyEntities { get; set; } = null!; + /// - /// Gets or sets the AdvancedCollectionEntities. + /// Gets or sets the AdvancedCollectionEntities. /// public DocumentCollection AdvancedCollectionEntities { get; set; } = null!; + /// - /// Gets or sets the PrivateSetterEntities. + /// Gets or sets the PrivateSetterEntities. /// public DocumentCollection PrivateSetterEntities { get; set; } = null!; + /// - /// Gets or sets the InitSetterEntities. + /// Gets or sets the InitSetterEntities. /// public DocumentCollection InitSetterEntities { get; set; } = null!; // Circular Reference Tests /// - /// Gets or sets the Employees. + /// Gets or sets the Employees. /// public DocumentCollection Employees { get; set; } = null!; + /// - /// Gets or sets the CategoryRefs. + /// Gets or sets the CategoryRefs. /// public DocumentCollection CategoryRefs { get; set; } = null!; + /// - /// Gets or sets the ProductRefs. + /// Gets or sets the ProductRefs. /// public DocumentCollection ProductRefs { get; set; } = null!; // Nullable String Id Test (UuidEntity scenario with inheritance) /// - /// Gets or sets the MockCounters. + /// Gets or sets the MockCounters. /// public DocumentCollection MockCounters { get; set; } = null!; // Temporal Types Test (DateTimeOffset, TimeSpan, DateOnly, TimeOnly) /// - /// Gets or sets the TemporalEntities. + /// Gets or sets the TemporalEntities. /// public DocumentCollection TemporalEntities { get; set; } = null!; - /// - /// Initializes a new instance of the class. - /// - /// The database path. - public TestDbContext(string databasePath) - : this(databasePath, PageFileConfig.Default, CompressionOptions.Default) - { - } + /// + /// Gets or sets the Storage. + /// + public StorageEngine Storage => Engine; - /// - /// Initializes a new instance of the class. - /// - /// The database path. - /// The compression options. - public TestDbContext(string databasePath, CompressionOptions compressionOptions) - : this(databasePath, PageFileConfig.Default, compressionOptions) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The database path. - /// The page file configuration. - public TestDbContext(string databasePath, PageFileConfig pageFileConfig) - : this(databasePath, pageFileConfig, CompressionOptions.Default) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The database path. - /// The page file configuration. - /// The compression options. - /// The maintenance options. - public TestDbContext( - string databasePath, - PageFileConfig pageFileConfig, - CompressionOptions? compressionOptions, - MaintenanceOptions? maintenanceOptions = null) - : base(databasePath, pageFileConfig, compressionOptions, maintenanceOptions) - { - } - - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(); modelBuilder.Entity().ToCollection("users"); @@ -207,11 +236,11 @@ public partial class TestDbContext : DocumentDbContext modelBuilder.Entity() .ToCollection("vector_items") - .HasVectorIndex(x => x.Embedding, dimensions: 3, metric: VectorMetric.L2, name: "idx_vector"); + .HasVectorIndex(x => x.Embedding, 3, VectorMetric.L2, "idx_vector"); modelBuilder.Entity() .ToCollection("geo_items") - .HasSpatialIndex(x => x.Location, name: "idx_spatial"); + .HasSpatialIndex(x => x.Location, "idx_spatial"); modelBuilder.Entity() .HasKey(x => x.Id) @@ -236,25 +265,20 @@ public partial class TestDbContext : DocumentDbContext modelBuilder.Entity().ToCollection("temporal_entities").HasKey(e => e.Id); } - /// - /// Executes ForceCheckpoint. - /// - public void ForceCheckpoint() - { - Engine.Checkpoint(); - } - - /// - /// Executes ForceCheckpoint with the requested checkpoint mode. - /// - /// Checkpoint mode to execute. - public CheckpointResult ForceCheckpoint(CheckpointMode mode) - { - return Engine.Checkpoint(mode); - } - - /// - /// Gets or sets the Storage. - /// - public StorageEngine Storage => Engine; -} + /// + /// Executes ForceCheckpoint. + /// + public void ForceCheckpoint() + { + Engine.Checkpoint(); + } + + /// + /// Executes ForceCheckpoint with the requested checkpoint mode. + /// + /// Checkpoint mode to execute. + public CheckpointResult ForceCheckpoint(CheckpointMode mode) + { + return Engine.Checkpoint(mode); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Context/TestExtendedDbContext.cs b/tests/CBDD.Tests/Context/TestExtendedDbContext.cs index 75a4a48..5f9592f 100755 --- a/tests/CBDD.Tests/Context/TestExtendedDbContext.cs +++ b/tests/CBDD.Tests/Context/TestExtendedDbContext.cs @@ -4,27 +4,27 @@ using ZB.MOM.WW.CBDD.Core.Metadata; namespace ZB.MOM.WW.CBDD.Shared; /// -/// Extended test context that inherits from TestDbContext. -/// Used to verify that collection initialization works correctly with inheritance. +/// Extended test context that inherits from TestDbContext. +/// Used to verify that collection initialization works correctly with inheritance. /// public partial class TestExtendedDbContext : TestDbContext { /// - /// Gets or sets the extended entities. + /// Initializes a new instance of the class. /// - public DocumentCollection ExtendedEntities { get; set; } = null!; - - /// - /// Initializes a new instance of the class. - /// - /// Database file path. - public TestExtendedDbContext(string databasePath) : base(databasePath) + /// Database file path. + public TestExtendedDbContext(string databasePath) : base(databasePath) { InitializeCollections(); } - /// - protected override void OnModelCreating(ModelBuilder modelBuilder) + /// + /// Gets or sets the extended entities. + /// + public DocumentCollection ExtendedEntities { get; set; } = null!; + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -32,4 +32,4 @@ public partial class TestExtendedDbContext : TestDbContext .ToCollection("extended_entities") .HasKey(e => e.Id); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Fixtures/MockEntities.cs b/tests/CBDD.Tests/Fixtures/MockEntities.cs index a337fab..df40e6c 100755 --- a/tests/CBDD.Tests/Fixtures/MockEntities.cs +++ b/tests/CBDD.Tests/Fixtures/MockEntities.cs @@ -1,819 +1,904 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Metadata; -using System; -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.Metadata; -namespace ZB.MOM.WW.CBDD.Shared +namespace ZB.MOM.WW.CBDD.Shared; +// --- Basic Entities --- + +public class User { - // --- Basic Entities --- - - public class User - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the age. - /// - public int Age { get; set; } - } - - // --- Complex Entities (Nested) --- - - public class ComplexUser - { - /// - /// Gets or sets the id. - /// - [BsonId] - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - - // Direct nested object - /// - /// Gets or sets the main address. - /// - public Address MainAddress { get; set; } = new(); - - // Collection of nested objects - /// - /// Gets or sets the other addresses. - /// - public List
OtherAddresses { get; set; } = new(); - - // Primitive collection - /// - /// Gets or sets the tags. - /// - public List Tags { get; set; } = new(); - - /// - /// Gets or sets the secret. - /// - [BsonIgnore] - public string Secret { get; set; } = ""; - } - - public class Address - { - /// - /// Gets or sets the street. - /// - public string Street { get; set; } = ""; - /// - /// Gets or sets the city. - /// - public City City { get; set; } = new(); // Depth 2 - } - - public class City - { - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the zip code. - /// - public string ZipCode { get; set; } = ""; - } - - // --- Primary Key Test Entities --- - - public class IntEntity - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the name. - /// - public string? Name { get; set; } - } - - public class StringEntity - { - /// - /// Gets or sets the id. - /// - public required string Id { get; set; } - /// - /// Gets or sets the value. - /// - public string? Value { get; set; } - } - - public class GuidEntity - { - /// - /// Gets or sets the id. - /// - public Guid Id { get; set; } - /// - /// Gets or sets the name. - /// - public string? Name { get; set; } - } + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } /// - /// Entity with string key NOT named "Id" - tests custom key name support + /// Gets or sets the name. /// - public class CustomKeyEntity - { - /// - /// Gets or sets the code. - /// - [System.ComponentModel.DataAnnotations.Key] - public required string Code { get; set; } - /// - /// Gets or sets the description. - /// - public string? Description { get; set; } - } - - // --- Multi-collection / Auto-init entities --- - - public class AutoInitEntity - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - } - - public class Person - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the age. - /// - public int Age { get; set; } - } - - public class Product - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the title. - /// - public string Title { get; set; } = ""; - /// - /// Gets or sets the price. - /// - public decimal Price { get; set; } - } - - public class AsyncDoc - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - } - - public class SchemaUser - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the address. - /// - public Address Address { get; set; } = new(); - } - - public class VectorEntity - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the title. - /// - public string Title { get; set; } = ""; - /// - /// Gets or sets the embedding. - /// - public float[] Embedding { get; set; } = Array.Empty(); - } - - public class GeoEntity - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the location. - /// - public (double Latitude, double Longitude) Location { get; set; } - } - - public record OrderId(string Value) - { - /// - /// Initializes a new instance. - /// - public OrderId() : this(string.Empty) { } - } - - public class OrderIdConverter : ValueConverter - { - /// - public override string ConvertToProvider(OrderId model) => model?.Value ?? string.Empty; - /// - public override OrderId ConvertFromProvider(string provider) => new OrderId(provider); - } - - public class Order - { - /// - /// Gets or sets the id. - /// - public OrderId Id { get; set; } = null!; - /// - /// Gets or sets the customer name. - /// - public string CustomerName { get; set; } = ""; - } - - public class TestDocument - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the category. - /// - public string Category { get; set; } = string.Empty; - /// - /// Gets or sets the amount. - /// - public int Amount { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - } - - public class OrderDocument - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the item name. - /// - public string ItemName { get; set; } = string.Empty; - /// - /// Gets or sets the quantity. - /// - public int Quantity { get; set; } - } - - public class OrderItem - { - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the price. - /// - public int Price { get; set; } - } - - public class ComplexDocument - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the title. - /// - public string Title { get; set; } = string.Empty; - /// - /// Gets or sets the shipping address. - /// - public Address ShippingAddress { get; set; } = new(); - /// - /// Gets or sets the items. - /// - public List Items { get; set; } = new(); - } - - [Table("custom_users", Schema = "test")] - public class AnnotatedUser - { - /// - /// Gets or sets the id. - /// - [Key] - public ObjectId Id { get; set; } - - /// - /// Gets or sets the name. - /// - [Required] - [Column("display_name")] - [StringLength(50, MinimumLength = 3)] - public string Name { get; set; } = ""; - - /// - /// Gets or sets the age. - /// - [Range(0, 150)] - public int Age { get; set; } - - /// - /// Gets the computed info. - /// - [NotMapped] - public string ComputedInfo => $"{Name} ({Age})"; - - /// - /// Gets or sets the location. - /// - [Column(TypeName = "geopoint")] - public (double Lat, double Lon) Location { get; set; } - } - public class PersonV2 - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the age. - /// - public int Age { get; set; } - } + public string Name { get; set; } = ""; /// - /// Entity used to test DbContext inheritance + /// Gets or sets the age. /// - public class ExtendedEntity - { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the description. - /// - public string Description { get; set; } = string.Empty; - /// - /// Gets or sets the created at. - /// - public DateTime CreatedAt { get; set; } - } + public int Age { get; set; } +} - // ===== SOURCE GENERATOR FEATURE TESTS ===== +// --- Complex Entities (Nested) --- + +public class ComplexUser +{ + /// + /// Gets or sets the id. + /// + [BsonId] + public ObjectId Id { get; set; } /// - /// Base entity with Id property - test inheritance + /// Gets or sets the name. /// - public class BaseEntityWithId - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the created at. - /// - public DateTime CreatedAt { get; set; } - } + public string Name { get; set; } = ""; + + // Direct nested object + /// + /// Gets or sets the main address. + /// + public Address MainAddress { get; set; } = new(); + + // Collection of nested objects + /// + /// Gets or sets the other addresses. + /// + public List
OtherAddresses { get; set; } = new(); + + // Primitive collection + /// + /// Gets or sets the tags. + /// + public List Tags { get; set; } = new(); /// - /// Derived entity that inherits Id from base class + /// Gets or sets the secret. /// - public class DerivedEntity : BaseEntityWithId - { - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the description. - /// - public string Description { get; set; } = string.Empty; - } + [BsonIgnore] + public string Secret { get; set; } = ""; +} + +public class Address +{ + /// + /// Gets or sets the street. + /// + public string Street { get; set; } = ""; /// - /// Entity with computed getter-only properties (should be excluded from serialization) + /// Gets or sets the city. /// - public class EntityWithComputedProperties - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the first name. - /// - public string FirstName { get; set; } = string.Empty; - /// - /// Gets or sets the last name. - /// - public string LastName { get; set; } = string.Empty; - /// - /// Gets or sets the birth year. - /// - public int BirthYear { get; set; } + public City City { get; set; } = new(); // Depth 2 +} - // Computed properties - should NOT be serialized - /// - /// Gets the full name. - /// - public string FullName => $"{FirstName} {LastName}"; - /// - /// Gets the age. - /// - public int Age => DateTime.Now.Year - BirthYear; - /// - /// Gets the display info. - /// - public string DisplayInfo => $"{FullName} (Age: {Age})"; - } +public class City +{ + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = ""; /// - /// Entity with advanced collection types (HashSet, ISet, LinkedList, etc.) + /// Gets or sets the zip code. /// - public class EntityWithAdvancedCollections - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; + public string ZipCode { get; set; } = ""; +} - // Various collection types that should all be recognized - /// - /// Gets or sets the tags. - /// - public HashSet Tags { get; set; } = new(); - /// - /// Gets or sets the numbers. - /// - public ISet Numbers { get; set; } = new HashSet(); - /// - /// Gets or sets the history. - /// - public LinkedList History { get; set; } = new(); - /// - /// Gets or sets the pending items. - /// - public Queue PendingItems { get; set; } = new(); - /// - /// Gets or sets the undo stack. - /// - public Stack UndoStack { get; set; } = new(); - - // Nested objects in collections - /// - /// Gets or sets the addresses. - /// - public HashSet
Addresses { get; set; } = new(); - /// - /// Gets or sets the favorite cities. - /// - public ISet FavoriteCities { get; set; } = new HashSet(); - } +// --- Primary Key Test Entities --- + +public class IntEntity +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } /// - /// Entity with private setters (requires reflection-based deserialization) + /// Gets or sets the name. /// - public class EntityWithPrivateSetters - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; private set; } - /// - /// Gets or sets the name. - /// - public string Name { get; private set; } = string.Empty; - /// - /// Gets or sets the age. - /// - public int Age { get; private set; } - /// - /// Gets or sets the created at. - /// - public DateTime CreatedAt { get; private set; } + public string? Name { get; set; } +} - // Factory method for creation - /// - /// Executes the create operation. - /// - /// The name. - /// The age. - public static EntityWithPrivateSetters Create(string name, int age) - { - return new EntityWithPrivateSetters - { - Id = ObjectId.NewObjectId(), - Name = name, - Age = age, - CreatedAt = DateTime.UtcNow - }; - } - } +public class StringEntity +{ + /// + /// Gets or sets the id. + /// + public required string Id { get; set; } /// - /// Entity with init-only setters (can use object initializer) + /// Gets or sets the value. /// - public class EntityWithInitSetters - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; init; } - /// - /// Gets or sets the name. - /// - public required string Name { get; init; } - /// - /// Gets or sets the age. - /// - public int Age { get; init; } - /// - /// Gets or sets the created at. - /// - public DateTime CreatedAt { get; init; } - } + public string? Value { get; set; } +} - // ======================================== - // Circular Reference Test Entities - // ======================================== +public class GuidEntity +{ + /// + /// Gets or sets the id. + /// + public Guid Id { get; set; } /// - /// Employee with self-referencing via ObjectIds (organizational hierarchy) - /// Tests: self-reference using referencing (BEST PRACTICE) - /// Recommended: Avoids embedding which can lead to large/circular documents + /// Gets or sets the name. /// - public class Employee - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the department. - /// - public string Department { get; set; } = string.Empty; - /// - /// Gets or sets the manager id. - /// - public ObjectId? ManagerId { get; set; } // Reference to manager - /// - /// Gets or sets the direct report ids. - /// - public List? DirectReportIds { get; set; } // References to direct reports (best practice) - } + public string? Name { get; set; } +} + +/// +/// Entity with string key NOT named "Id" - tests custom key name support +/// +public class CustomKeyEntity +{ + /// + /// Gets or sets the code. + /// + [Key] + public required string Code { get; set; } /// - /// Category with referenced products (N-N using ObjectId references) - /// Tests: N-N relationships using referencing (BEST PRACTICE for document databases) - /// Recommended: Avoids large documents, better for queries and updates + /// Gets or sets the description. /// - public class CategoryRef - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the description. - /// - public string Description { get; set; } = string.Empty; - /// - /// Gets or sets the product ids. - /// - public List? ProductIds { get; set; } // Only IDs - no embedding - } + public string? Description { get; set; } +} + +// --- Multi-collection / Auto-init entities --- + +public class AutoInitEntity +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } /// - /// Product with referenced categories (N-N using ObjectId references) - /// Tests: N-N relationships using referencing (BEST PRACTICE for document databases) - /// Recommended: Avoids large documents, better for queries and updates + /// Gets or sets the name. /// - public class ProductRef - { - /// - /// Gets or sets the id. - /// - public ObjectId Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the price. - /// - public decimal Price { get; set; } - /// - /// Gets or sets the category ids. - /// - public List? CategoryIds { get; set; } // Only IDs - no embedding - } + public string Name { get; set; } = string.Empty; +} - // ======================================== - // Nullable String Key Test (UuidEntity scenario) - // ======================================== +public class Person +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } /// - /// Base entity class that simulates CleanCore's BaseEntity{TId, TEntity} - /// This is the root of the hierarchy that causes the generator bug + /// Gets or sets the name. /// - public abstract class MockBaseEntity - where TId : IEquatable - where TEntity : class - { - /// - /// Gets or sets the id. - /// - [System.ComponentModel.DataAnnotations.Key] - public virtual TId? Id { get; set; } - - /// - /// Initializes a new instance. - /// - protected MockBaseEntity() { } - - /// - /// Initializes a new instance. - /// - /// The id. - protected MockBaseEntity(TId? id) - { - Id = id; - } - } + public string Name { get; set; } = ""; /// - /// Simulates CleanCore's UuidEntity{TEntity} which inherits from BaseEntity{string, TEntity} - /// Tests the bug where generator incorrectly chooses ObjectIdMapperBase instead of StringMapperBase - /// when the Id property is inherited and nullable + /// Gets or sets the age. /// - public abstract class MockUuidEntity : MockBaseEntity - where TEntity : class - { - /// - /// Initializes a new instance. - /// - protected MockUuidEntity() : base() { } - - /// - /// Initializes a new instance. - /// - /// The id. - protected MockUuidEntity(string? id) : base(id) { } - } + public int Age { get; set; } +} + +public class Product +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } /// - /// Concrete entity that inherits from MockUuidEntity, simulating Counter from CleanCore - /// This is the actual entity that will be stored in the collection + /// Gets or sets the title. /// - public class MockCounter : MockUuidEntity - { - /// - /// Initializes a new instance. - /// - public MockCounter() : base() { } - - /// - /// Initializes a new instance. - /// - /// The id. - public MockCounter(string? id) : base(id) { } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - /// - /// Gets or sets the value. - /// - public int Value { get; set; } - } + public string Title { get; set; } = ""; /// - /// Entity for testing temporal types: DateTimeOffset, TimeSpan, DateOnly, TimeOnly + /// Gets or sets the price. /// - public class TemporalEntity + public decimal Price { get; set; } +} + +public class AsyncDoc +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = ""; +} + +public class SchemaUser +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = ""; + + /// + /// Gets or sets the address. + /// + public Address Address { get; set; } = new(); +} + +public class VectorEntity +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the title. + /// + public string Title { get; set; } = ""; + + /// + /// Gets or sets the embedding. + /// + public float[] Embedding { get; set; } = Array.Empty(); +} + +public class GeoEntity +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = ""; + + /// + /// Gets or sets the location. + /// + public (double Latitude, double Longitude) Location { get; set; } +} + +public record OrderId(string Value) +{ + /// + /// Initializes a new instance. + /// + public OrderId() : this(string.Empty) { - /// - /// Gets or sets the id. - /// - [Key] - public ObjectId Id { get; set; } - - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = string.Empty; - - // DateTime types - /// - /// Gets or sets the created at. - /// - public DateTime CreatedAt { get; set; } - /// - /// Gets or sets the updated at. - /// - public DateTimeOffset UpdatedAt { get; set; } - /// - /// Gets or sets the last accessed at. - /// - public DateTimeOffset? LastAccessedAt { get; set; } - - // TimeSpan - /// - /// Gets or sets the duration. - /// - public TimeSpan Duration { get; set; } - /// - /// Gets or sets the optional duration. - /// - public TimeSpan? OptionalDuration { get; set; } - - // DateOnly and TimeOnly (.NET 6+) - /// - /// Gets or sets the birth date. - /// - public DateOnly BirthDate { get; set; } - /// - /// Gets or sets the anniversary. - /// - public DateOnly? Anniversary { get; set; } - - /// - /// Gets or sets the opening time. - /// - public TimeOnly OpeningTime { get; set; } - /// - /// Gets or sets the closing time. - /// - public TimeOnly? ClosingTime { get; set; } } } + +public class OrderIdConverter : ValueConverter +{ + /// + public override string ConvertToProvider(OrderId model) + { + return model?.Value ?? string.Empty; + } + + /// + public override OrderId ConvertFromProvider(string provider) + { + return new OrderId(provider); + } +} + +public class Order +{ + /// + /// Gets or sets the id. + /// + public OrderId Id { get; set; } = null!; + + /// + /// Gets or sets the customer name. + /// + public string CustomerName { get; set; } = ""; +} + +public class TestDocument +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the category. + /// + public string Category { get; set; } = string.Empty; + + /// + /// Gets or sets the amount. + /// + public int Amount { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; +} + +public class OrderDocument +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the item name. + /// + public string ItemName { get; set; } = string.Empty; + + /// + /// Gets or sets the quantity. + /// + public int Quantity { get; set; } +} + +public class OrderItem +{ + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the price. + /// + public int Price { get; set; } +} + +public class ComplexDocument +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the title. + /// + public string Title { get; set; } = string.Empty; + + /// + /// Gets or sets the shipping address. + /// + public Address ShippingAddress { get; set; } = new(); + + /// + /// Gets or sets the items. + /// + public List Items { get; set; } = new(); +} + +[Table("custom_users", Schema = "test")] +public class AnnotatedUser +{ + /// + /// Gets or sets the id. + /// + [Key] + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + [Required] + [Column("display_name")] + [StringLength(50, MinimumLength = 3)] + public string Name { get; set; } = ""; + + /// + /// Gets or sets the age. + /// + [Range(0, 150)] + public int Age { get; set; } + + /// + /// Gets the computed info. + /// + [NotMapped] + public string ComputedInfo => $"{Name} ({Age})"; + + /// + /// Gets or sets the location. + /// + [Column(TypeName = "geopoint")] + public (double Lat, double Lon) Location { get; set; } +} + +public class PersonV2 +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the age. + /// + public int Age { get; set; } +} + +/// +/// Entity used to test DbContext inheritance +/// +public class ExtendedEntity +{ + /// + /// Gets or sets the id. + /// + public int Id { get; set; } + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the created at. + /// + public DateTime CreatedAt { get; set; } +} + +// ===== SOURCE GENERATOR FEATURE TESTS ===== + +/// +/// Base entity with Id property - test inheritance +/// +public class BaseEntityWithId +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the created at. + /// + public DateTime CreatedAt { get; set; } +} + +/// +/// Derived entity that inherits Id from base class +/// +public class DerivedEntity : BaseEntityWithId +{ + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = string.Empty; +} + +/// +/// Entity with computed getter-only properties (should be excluded from serialization) +/// +public class EntityWithComputedProperties +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the first name. + /// + public string FirstName { get; set; } = string.Empty; + + /// + /// Gets or sets the last name. + /// + public string LastName { get; set; } = string.Empty; + + /// + /// Gets or sets the birth year. + /// + public int BirthYear { get; set; } + + // Computed properties - should NOT be serialized + /// + /// Gets the full name. + /// + public string FullName => $"{FirstName} {LastName}"; + + /// + /// Gets the age. + /// + public int Age => DateTime.Now.Year - BirthYear; + + /// + /// Gets the display info. + /// + public string DisplayInfo => $"{FullName} (Age: {Age})"; +} + +/// +/// Entity with advanced collection types (HashSet, ISet, LinkedList, etc.) +/// +public class EntityWithAdvancedCollections +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + // Various collection types that should all be recognized + /// + /// Gets or sets the tags. + /// + public HashSet Tags { get; set; } = new(); + + /// + /// Gets or sets the numbers. + /// + public ISet Numbers { get; set; } = new HashSet(); + + /// + /// Gets or sets the history. + /// + public LinkedList History { get; set; } = new(); + + /// + /// Gets or sets the pending items. + /// + public Queue PendingItems { get; set; } = new(); + + /// + /// Gets or sets the undo stack. + /// + public Stack UndoStack { get; set; } = new(); + + // Nested objects in collections + /// + /// Gets or sets the addresses. + /// + public HashSet
Addresses { get; set; } = new(); + + /// + /// Gets or sets the favorite cities. + /// + public ISet FavoriteCities { get; set; } = new HashSet(); +} + +/// +/// Entity with private setters (requires reflection-based deserialization) +/// +public class EntityWithPrivateSetters +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; private set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; private set; } = string.Empty; + + /// + /// Gets or sets the age. + /// + public int Age { get; private set; } + + /// + /// Gets or sets the created at. + /// + public DateTime CreatedAt { get; private set; } + + // Factory method for creation + /// + /// Executes the create operation. + /// + /// The name. + /// The age. + public static EntityWithPrivateSetters Create(string name, int age) + { + return new EntityWithPrivateSetters + { + Id = ObjectId.NewObjectId(), + Name = name, + Age = age, + CreatedAt = DateTime.UtcNow + }; + } +} + +/// +/// Entity with init-only setters (can use object initializer) +/// +public class EntityWithInitSetters +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; init; } + + /// + /// Gets or sets the name. + /// + public required string Name { get; init; } + + /// + /// Gets or sets the age. + /// + public int Age { get; init; } + + /// + /// Gets or sets the created at. + /// + public DateTime CreatedAt { get; init; } +} + +// ======================================== +// Circular Reference Test Entities +// ======================================== + +/// +/// Employee with self-referencing via ObjectIds (organizational hierarchy) +/// Tests: self-reference using referencing (BEST PRACTICE) +/// Recommended: Avoids embedding which can lead to large/circular documents +/// +public class Employee +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the department. + /// + public string Department { get; set; } = string.Empty; + + /// + /// Gets or sets the manager id. + /// + public ObjectId? ManagerId { get; set; } // Reference to manager + + /// + /// Gets or sets the direct report ids. + /// + public List? DirectReportIds { get; set; } // References to direct reports (best practice) +} + +/// +/// Category with referenced products (N-N using ObjectId references) +/// Tests: N-N relationships using referencing (BEST PRACTICE for document databases) +/// Recommended: Avoids large documents, better for queries and updates +/// +public class CategoryRef +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the description. + /// + public string Description { get; set; } = string.Empty; + + /// + /// Gets or sets the product ids. + /// + public List? ProductIds { get; set; } // Only IDs - no embedding +} + +/// +/// Product with referenced categories (N-N using ObjectId references) +/// Tests: N-N relationships using referencing (BEST PRACTICE for document databases) +/// Recommended: Avoids large documents, better for queries and updates +/// +public class ProductRef +{ + /// + /// Gets or sets the id. + /// + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the price. + /// + public decimal Price { get; set; } + + /// + /// Gets or sets the category ids. + /// + public List? CategoryIds { get; set; } // Only IDs - no embedding +} + +// ======================================== +// Nullable String Key Test (UuidEntity scenario) +// ======================================== + +/// +/// Base entity class that simulates CleanCore's BaseEntity{TId, TEntity} +/// This is the root of the hierarchy that causes the generator bug +/// +public abstract class MockBaseEntity + where TId : IEquatable + where TEntity : class +{ + /// + /// Initializes a new instance. + /// + protected MockBaseEntity() + { + } + + /// + /// Initializes a new instance. + /// + /// The id. + protected MockBaseEntity(TId? id) + { + Id = id; + } + + /// + /// Gets or sets the id. + /// + [Key] + public virtual TId? Id { get; set; } +} + +/// +/// Simulates CleanCore's UuidEntity{TEntity} which inherits from BaseEntity{string, TEntity} +/// Tests the bug where generator incorrectly chooses ObjectIdMapperBase instead of StringMapperBase +/// when the Id property is inherited and nullable +/// +public abstract class MockUuidEntity : MockBaseEntity + where TEntity : class +{ + /// + /// Initializes a new instance. + /// + protected MockUuidEntity() + { + } + + /// + /// Initializes a new instance. + /// + /// The id. + protected MockUuidEntity(string? id) : base(id) + { + } +} + +/// +/// Concrete entity that inherits from MockUuidEntity, simulating Counter from CleanCore +/// This is the actual entity that will be stored in the collection +/// +public class MockCounter : MockUuidEntity +{ + /// + /// Initializes a new instance. + /// + public MockCounter() + { + } + + /// + /// Initializes a new instance. + /// + /// The id. + public MockCounter(string? id) : base(id) + { + } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + /// + /// Gets or sets the value. + /// + public int Value { get; set; } +} + +/// +/// Entity for testing temporal types: DateTimeOffset, TimeSpan, DateOnly, TimeOnly +/// +public class TemporalEntity +{ + /// + /// Gets or sets the id. + /// + [Key] + public ObjectId Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = string.Empty; + + // DateTime types + /// + /// Gets or sets the created at. + /// + public DateTime CreatedAt { get; set; } + + /// + /// Gets or sets the updated at. + /// + public DateTimeOffset UpdatedAt { get; set; } + + /// + /// Gets or sets the last accessed at. + /// + public DateTimeOffset? LastAccessedAt { get; set; } + + // TimeSpan + /// + /// Gets or sets the duration. + /// + public TimeSpan Duration { get; set; } + + /// + /// Gets or sets the optional duration. + /// + public TimeSpan? OptionalDuration { get; set; } + + // DateOnly and TimeOnly (.NET 6+) + /// + /// Gets or sets the birth date. + /// + public DateOnly BirthDate { get; set; } + + /// + /// Gets or sets the anniversary. + /// + public DateOnly? Anniversary { get; set; } + + /// + /// Gets or sets the opening time. + /// + public TimeOnly OpeningTime { get; set; } + + /// + /// Gets or sets the closing time. + /// + public TimeOnly? ClosingTime { get; set; } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/GlobalUsings.cs b/tests/CBDD.Tests/GlobalUsings.cs index e07ccbe..dc921b6 100644 --- a/tests/CBDD.Tests/GlobalUsings.cs +++ b/tests/CBDD.Tests/GlobalUsings.cs @@ -1 +1 @@ -global using Shouldly; +global using Shouldly; \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/BTreeDeleteUnderflowTests.cs b/tests/CBDD.Tests/Indexing/BTreeDeleteUnderflowTests.cs index c2e9be9..6a3e9ce 100644 --- a/tests/CBDD.Tests/Indexing/BTreeDeleteUnderflowTests.cs +++ b/tests/CBDD.Tests/Indexing/BTreeDeleteUnderflowTests.cs @@ -6,44 +6,37 @@ namespace ZB.MOM.WW.CBDD.Tests; public class BTreeDeleteUnderflowTests { /// - /// Executes Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges. + /// Executes Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges. /// [Fact] public void Delete_HeavyWorkload_Should_Remain_Queryable_After_Merges() { - var dbPath = Path.Combine(Path.GetTempPath(), $"btree_underflow_{Guid.NewGuid():N}.db"); + string dbPath = Path.Combine(Path.GetTempPath(), $"btree_underflow_{Guid.NewGuid():N}.db"); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); var index = new BTreeIndex(storage, IndexOptions.CreateBTree("k")); - var insertTxn = storage.BeginTransaction().TransactionId; - for (int i = 1; i <= 240; i++) - { + ulong insertTxn = storage.BeginTransaction().TransactionId; + for (var i = 1; i <= 240; i++) index.Insert(IndexKey.Create(i), new DocumentLocation((uint)(1000 + i), 0), insertTxn); - } storage.CommitTransaction(insertTxn); - var deleteTxn = storage.BeginTransaction().TransactionId; - for (int i = 1; i <= 190; i++) - { + ulong deleteTxn = storage.BeginTransaction().TransactionId; + for (var i = 1; i <= 190; i++) index.Delete(IndexKey.Create(i), new DocumentLocation((uint)(1000 + i), 0), deleteTxn).ShouldBeTrue(); - } storage.CommitTransaction(deleteTxn); - for (int i = 1; i <= 190; i++) - { - index.TryFind(IndexKey.Create(i), out _, 0).ShouldBeFalse(); - } + for (var i = 1; i <= 190; i++) index.TryFind(IndexKey.Create(i), out _, 0).ShouldBeFalse(); - for (int i = 191; i <= 240; i++) + for (var i = 191; i <= 240; i++) { index.TryFind(IndexKey.Create(i), out var location, 0).ShouldBeTrue(); location.PageId.ShouldBe((uint)(1000 + i)); } - var remaining = index.GreaterThan(IndexKey.Create(190), orEqual: false, 0).ToList(); + var remaining = index.GreaterThan(IndexKey.Create(190), false, 0).ToList(); remaining.Count.ShouldBe(50); remaining.First().Key.ShouldBe(IndexKey.Create(191)); remaining.Last().Key.ShouldBe(IndexKey.Create(240)); @@ -51,8 +44,8 @@ public class BTreeDeleteUnderflowTests finally { if (File.Exists(dbPath)) File.Delete(dbPath); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); } } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/CollectionIndexManagerAndDefinitionTests.cs b/tests/CBDD.Tests/Indexing/CollectionIndexManagerAndDefinitionTests.cs index 68007de..bae0035 100644 --- a/tests/CBDD.Tests/Indexing/CollectionIndexManagerAndDefinitionTests.cs +++ b/tests/CBDD.Tests/Indexing/CollectionIndexManagerAndDefinitionTests.cs @@ -8,20 +8,20 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CollectionIndexManagerAndDefinitionTests { /// - /// Tests find best index should prefer unique index. + /// Tests find best index should prefer unique index. /// [Fact] public void FindBestIndex_Should_Prefer_Unique_Index() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); var mapper = new ZB_MOM_WW_CBDD_Shared_PersonMapper(); using var manager = new CollectionIndexManager(storage, mapper, "people_idx_pref_unique"); - manager.CreateIndex(p => p.Age, name: "idx_age", unique: false); - manager.CreateIndex(p => p.Age, name: "idx_age_unique", unique: true); + manager.CreateIndex(p => p.Age, "idx_age"); + manager.CreateIndex(p => p.Age, "idx_age_unique", true); var best = manager.FindBestIndex("Age"); @@ -36,12 +36,12 @@ public class CollectionIndexManagerAndDefinitionTests } /// - /// Tests find best compound index should choose longest prefix. + /// Tests find best compound index should choose longest prefix. /// [Fact] public void FindBestCompoundIndex_Should_Choose_Longest_Prefix() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); @@ -76,12 +76,12 @@ public class CollectionIndexManagerAndDefinitionTests } /// - /// Tests drop index should remove metadata and be idempotent. + /// Tests drop index should remove metadata and be idempotent. /// [Fact] public void DropIndex_Should_Remove_Metadata_And_Be_Idempotent() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); const string collectionName = "people_idx_drop"; try @@ -91,7 +91,7 @@ public class CollectionIndexManagerAndDefinitionTests using (var manager = new CollectionIndexManager(storage, mapper, collectionName)) { - manager.CreateIndex(p => p.Age, name: "idx_age", unique: false); + manager.CreateIndex(p => p.Age, "idx_age"); manager.DropIndex("idx_age").ShouldBeTrue(); manager.DropIndex("idx_age").ShouldBeFalse(); manager.GetIndexInfo().ShouldBeEmpty(); @@ -107,7 +107,7 @@ public class CollectionIndexManagerAndDefinitionTests } /// - /// Tests collection index definition should respect query support rules. + /// Tests collection index definition should respect query support rules. /// [Fact] public void CollectionIndexDefinition_Should_Respect_Query_Support_Rules() @@ -129,7 +129,7 @@ public class CollectionIndexManagerAndDefinitionTests } /// - /// Tests collection index info to string should include diagnostics. + /// Tests collection index info to string should include diagnostics. /// [Fact] public void CollectionIndexInfo_ToString_Should_Include_Diagnostics() @@ -150,16 +150,18 @@ public class CollectionIndexManagerAndDefinitionTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"idx_mgr_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"idx_mgr_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { if (File.Exists(dbPath)) File.Delete(dbPath); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); - var altWalPath = dbPath + "-wal"; + string altWalPath = dbPath + "-wal"; if (File.Exists(altWalPath)) File.Delete(altWalPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/CursorTests.cs b/tests/CBDD.Tests/Indexing/CursorTests.cs index 9c01cff..9efcf2f 100755 --- a/tests/CBDD.Tests/Indexing/CursorTests.cs +++ b/tests/CBDD.Tests/Indexing/CursorTests.cs @@ -1,18 +1,16 @@ -using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Bson; -using Xunit; +using ZB.MOM.WW.CBDD.Core.Storage; namespace ZB.MOM.WW.CBDD.Tests; public class CursorTests : IDisposable { - private readonly string _testFile; - private readonly StorageEngine _storage; private readonly BTreeIndex _index; + private readonly StorageEngine _storage; + private readonly string _testFile; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public CursorTests() { @@ -25,9 +23,18 @@ public class CursorTests : IDisposable SeedData(); } + /// + /// Disposes the resources used by this instance. + /// + public void Dispose() + { + _storage.Dispose(); + if (File.Exists(_testFile)) File.Delete(_testFile); + } + private void SeedData() { - var txnId = _storage.BeginTransaction().TransactionId; + ulong txnId = _storage.BeginTransaction().TransactionId; // Insert 10, 20, 30 _index.Insert(IndexKey.Create(10), new DocumentLocation(1, 0), txnId); @@ -38,7 +45,7 @@ public class CursorTests : IDisposable } /// - /// Tests move to first should position at first. + /// Tests move to first should position at first. /// [Fact] public void MoveToFirst_ShouldPositionAtFirst() @@ -49,7 +56,7 @@ public class CursorTests : IDisposable } /// - /// Tests move to last should position at last. + /// Tests move to last should position at last. /// [Fact] public void MoveToLast_ShouldPositionAtLast() @@ -60,7 +67,7 @@ public class CursorTests : IDisposable } /// - /// Tests move next should traverse forward. + /// Tests move next should traverse forward. /// [Fact] public void MoveNext_ShouldTraverseForward() @@ -78,7 +85,7 @@ public class CursorTests : IDisposable } /// - /// Tests move prev should traverse backward. + /// Tests move prev should traverse backward. /// [Fact] public void MovePrev_ShouldTraverseBackward() @@ -96,7 +103,7 @@ public class CursorTests : IDisposable } /// - /// Tests seek should position exact or next. + /// Tests seek should position exact or next. /// [Fact] public void Seek_ShouldPositionExact_OrNext() @@ -116,13 +123,4 @@ public class CursorTests : IDisposable // Current should throw invalid Should.Throw(() => cursor.Current); } - - /// - /// Disposes the resources used by this instance. - /// - public void Dispose() - { - _storage.Dispose(); - if (File.Exists(_testFile)) File.Delete(_testFile); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/GeospatialStressTests.cs b/tests/CBDD.Tests/Indexing/GeospatialStressTests.cs index 1466a23..d195fb0 100644 --- a/tests/CBDD.Tests/Indexing/GeospatialStressTests.cs +++ b/tests/CBDD.Tests/Indexing/GeospatialStressTests.cs @@ -4,34 +4,43 @@ namespace ZB.MOM.WW.CBDD.Tests; public class GeospatialStressTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes database state for geospatial stress tests. + /// Initializes database state for geospatial stress tests. /// public GeospatialStressTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"geo_stress_{Guid.NewGuid():N}.db"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies spatial index handles node splits and query operations under load. + /// Disposes test resources and removes generated files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + string wal = Path.ChangeExtension(_dbPath, ".wal"); + if (File.Exists(wal)) File.Delete(wal); + } + + /// + /// Verifies spatial index handles node splits and query operations under load. /// [Fact] public void SpatialIndex_Should_Handle_Node_Splits_And_Queries() { const int count = 350; - for (int i = 0; i < count; i++) - { + for (var i = 0; i < count; i++) _db.GeoItems.Insert(new GeoEntity { Name = $"pt-{i}", - Location = (40.0 + (i * 0.001), -73.0 - (i * 0.001)) + Location = (40.0 + i * 0.001, -73.0 - i * 0.001) }); - } _db.SaveChanges(); @@ -45,15 +54,4 @@ public class GeospatialStressTests : IDisposable var near = _db.GeoItems.Near("idx_spatial", (40.10, -73.10), 30.0).ToList(); near.Count.ShouldBeGreaterThan(0); } - - /// - /// Disposes test resources and removes generated files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - var wal = Path.ChangeExtension(_dbPath, ".wal"); - if (File.Exists(wal)) File.Delete(wal); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/GeospatialTests.cs b/tests/CBDD.Tests/Indexing/GeospatialTests.cs index 317c026..d870b60 100755 --- a/tests/CBDD.Tests/Indexing/GeospatialTests.cs +++ b/tests/CBDD.Tests/Indexing/GeospatialTests.cs @@ -1,28 +1,33 @@ -using Xunit; using ZB.MOM.WW.CBDD.Core.Indexing; -using System.IO; -using System.Linq; -using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Shared; namespace ZB.MOM.WW.CBDD.Tests; public class GeospatialTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public GeospatialTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_geo_{Guid.NewGuid()}.db"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies spatial within queries return expected results. + /// Disposes test resources and removes temporary files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + } + + /// + /// Verifies spatial within queries return expected results. /// [Fact] public void Can_Insert_And_Search_Within() @@ -45,7 +50,7 @@ public class GeospatialTests : IDisposable } /// - /// Verifies near queries return expected proximity results. + /// Verifies near queries return expected proximity results. /// [Fact] public void Can_Search_Near_Proximity() @@ -69,7 +74,7 @@ public class GeospatialTests : IDisposable } /// - /// Verifies LINQ near integration returns expected results. + /// Verifies LINQ near integration returns expected results. /// [Fact] public void LINQ_Integration_Near_Works() @@ -79,8 +84,8 @@ public class GeospatialTests : IDisposable // LINQ query using .Near() extension var query = from p in _db.GeoItems.AsQueryable() - where p.Location.Near(milan, 10.0) - select p; + where p.Location.Near(milan, 10.0) + select p; var results = query.ToList(); @@ -89,7 +94,7 @@ public class GeospatialTests : IDisposable } /// - /// Verifies LINQ within integration returns expected results. + /// Verifies LINQ within integration returns expected results. /// [Fact] public void LINQ_Integration_Within_Works() @@ -102,19 +107,10 @@ public class GeospatialTests : IDisposable // LINQ query using .Within() extension var results = _db.GeoItems.AsQueryable() - .Where(p => p.Location.Within(min, max)) - .ToList(); + .Where(p => p.Location.Within(min, max)) + .ToList(); results.Count().ShouldBe(1); results[0].Name.ShouldBe("Milan Office"); } - - /// - /// Disposes test resources and removes temporary files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/HashIndexTests.cs b/tests/CBDD.Tests/Indexing/HashIndexTests.cs index eb88acc..488c0b4 100644 --- a/tests/CBDD.Tests/Indexing/HashIndexTests.cs +++ b/tests/CBDD.Tests/Indexing/HashIndexTests.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.CBDD.Tests; public class HashIndexTests { /// - /// Executes Insert_And_TryFind_Should_Return_Location. + /// Executes Insert_And_TryFind_Should_Return_Location. /// [Fact] public void Insert_And_TryFind_Should_Return_Location() @@ -23,7 +23,7 @@ public class HashIndexTests } /// - /// Executes Unique_HashIndex_Should_Throw_On_Duplicate_Key. + /// Executes Unique_HashIndex_Should_Throw_On_Duplicate_Key. /// [Fact] public void Unique_HashIndex_Should_Throw_On_Duplicate_Key() @@ -45,7 +45,7 @@ public class HashIndexTests } /// - /// Executes Remove_Should_Remove_Only_Matching_Entry. + /// Executes Remove_Should_Remove_Only_Matching_Entry. /// [Fact] public void Remove_Should_Remove_Only_Matching_Entry() @@ -71,7 +71,7 @@ public class HashIndexTests } /// - /// Executes FindAll_Should_Return_All_Matching_Entries. + /// Executes FindAll_Should_Return_All_Matching_Entries. /// [Fact] public void FindAll_Should_Return_All_Matching_Entries() @@ -88,4 +88,4 @@ public class HashIndexTests matches.Count.ShouldBe(2); matches.All(e => e.Key == key).ShouldBeTrue(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/IndexDirectionTests.cs b/tests/CBDD.Tests/Indexing/IndexDirectionTests.cs index 137f48f..e8771a8 100755 --- a/tests/CBDD.Tests/Indexing/IndexDirectionTests.cs +++ b/tests/CBDD.Tests/Indexing/IndexDirectionTests.cs @@ -1,31 +1,25 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Shared; -using System; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class IndexDirectionTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath = "index_direction_tests.db"; - private readonly Shared.TestDbContext _db; - /// - /// Initializes database state for index direction tests. + /// Initializes database state for index direction tests. /// public IndexDirectionTests() { if (File.Exists(_dbPath)) File.Delete(_dbPath); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); // _db.Database.EnsureCreated(); // Not needed/doesn't exist? StorageEngine handles creation. } /// - /// Disposes test resources and deletes temporary files. + /// Disposes test resources and deletes temporary files. /// public void Dispose() { @@ -34,7 +28,7 @@ public class IndexDirectionTests : IDisposable } /// - /// Verifies forward range scans return values in ascending order. + /// Verifies forward range scans return values in ascending order. /// [Fact] public void Range_Forward_ReturnsOrderedResults() @@ -42,20 +36,21 @@ public class IndexDirectionTests : IDisposable var collection = _db.People; var index = collection.EnsureIndex(p => p.Age, "idx_age"); - var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }).ToList(); + var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }) + .ToList(); collection.InsertBulk(people); _db.SaveChanges(); // Scan Forward - var results = index.Range(10, 20, IndexDirection.Forward).ToList(); + var results = index.Range(10, 20).ToList(); results.Count.ShouldBe(11); // 10 to 20 inclusive collection.FindByLocation(results.First())!.Age.ShouldBe(10); // First is 10 - collection.FindByLocation(results.Last())!.Age.ShouldBe(20); // Last is 20 + collection.FindByLocation(results.Last())!.Age.ShouldBe(20); // Last is 20 } /// - /// Verifies backward range scans return values in descending order. + /// Verifies backward range scans return values in descending order. /// [Fact] public void Range_Backward_ReturnsReverseOrderedResults() @@ -63,7 +58,8 @@ public class IndexDirectionTests : IDisposable var collection = _db.People; var index = collection.EnsureIndex(p => p.Age, "idx_age"); - var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }).ToList(); + var people = Enumerable.Range(1, 100).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }) + .ToList(); collection.InsertBulk(people); _db.SaveChanges(); @@ -72,11 +68,11 @@ public class IndexDirectionTests : IDisposable results.Count.ShouldBe(11); // 10 to 20 inclusive collection.FindByLocation(results.First())!.Age.ShouldBe(20); // First is 20 (Reverse) - collection.FindByLocation(results.Last())!.Age.ShouldBe(10); // Last is 10 + collection.FindByLocation(results.Last())!.Age.ShouldBe(10); // Last is 10 } /// - /// Verifies backward scans across split index pages return complete result sets. + /// Verifies backward scans across split index pages return complete result sets. /// [Fact] public void Range_Backward_WithMultiplePages_ReturnsReverseOrderedResults() @@ -88,7 +84,8 @@ public class IndexDirectionTests : IDisposable // Entry size approx 10 bytes key + 6 bytes loc + overhead // 1000 items * 20 bytes = 20KB > 4KB. var count = 1000; - var people = Enumerable.Range(1, count).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }).ToList(); + var people = Enumerable.Range(1, count).Select(i => new Person { Id = i, Name = $"Person {i}", Age = i }) + .ToList(); collection.InsertBulk(people); _db.SaveChanges(); @@ -105,4 +102,4 @@ public class IndexDirectionTests : IDisposable // collection.FindByLocation(results.First(), null)!.Age.ShouldBe(count); // Max Age (Fails: Max is likely 255) // collection.FindByLocation(results.Last(), null)!.Age.ShouldBe(1); // Min Age (Fails: Min is likely 256) } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/IndexOptimizationTests.cs b/tests/CBDD.Tests/Indexing/IndexOptimizationTests.cs index 8af673d..121931d 100755 --- a/tests/CBDD.Tests/Indexing/IndexOptimizationTests.cs +++ b/tests/CBDD.Tests/Indexing/IndexOptimizationTests.cs @@ -1,163 +1,161 @@ -using Xunit; -using ZB.MOM.WW.CBDD.Core.Query; -using ZB.MOM.WW.CBDD.Core.Indexing; using System.Linq.Expressions; -using System.Collections.Generic; -using System; +using ZB.MOM.WW.CBDD.Core.Indexing; +using ZB.MOM.WW.CBDD.Core.Query; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class IndexOptimizationTests { - public class IndexOptimizationTests + /// + /// Tests optimizer identifies equality. + /// + [Fact] + public void Optimizer_Identifies_Equality() { - public class TestEntity + var indexes = new List { - /// - /// Gets or sets the id. - /// - public int Id { get; set; } - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = ""; - /// - /// Gets or sets the age. - /// - public int Age { get; set; } - } + new() { Name = "idx_age", PropertyPaths = ["Age"] } + }; - /// - /// Tests optimizer identifies equality. - /// - [Fact] - public void Optimizer_Identifies_Equality() - { - var indexes = new List - { - new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] } - }; + Expression> predicate = x => x.Age == 30; + var model = new QueryModel { WhereClause = predicate }; - Expression> predicate = x => x.Age == 30; - var model = new QueryModel { WhereClause = predicate }; + var result = IndexOptimizer.TryOptimize(model, indexes); - var result = IndexOptimizer.TryOptimize(model, indexes); - - result.ShouldNotBeNull(); - result.IndexName.ShouldBe("idx_age"); - result.MinValue.ShouldBe(30); - result.MaxValue.ShouldBe(30); - result.IsRange.ShouldBeFalse(); - } - - /// - /// Tests optimizer identifies range greater than. - /// - [Fact] - public void Optimizer_Identifies_Range_GreaterThan() - { - var indexes = new List - { - new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] } - }; - - Expression> predicate = x => x.Age > 25; - var model = new QueryModel { WhereClause = predicate }; - - var result = IndexOptimizer.TryOptimize(model, indexes); - - result.ShouldNotBeNull(); - result.IndexName.ShouldBe("idx_age"); - result.MinValue.ShouldBe(25); - result.MaxValue.ShouldBeNull(); - result.IsRange.ShouldBeTrue(); - } - - /// - /// Tests optimizer identifies range less than. - /// - [Fact] - public void Optimizer_Identifies_Range_LessThan() - { - var indexes = new List - { - new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] } - }; - - Expression> predicate = x => x.Age < 50; - var model = new QueryModel { WhereClause = predicate }; - - var result = IndexOptimizer.TryOptimize(model, indexes); - - result.ShouldNotBeNull(); - result.IndexName.ShouldBe("idx_age"); - result.MinValue.ShouldBeNull(); - result.MaxValue.ShouldBe(50); - result.IsRange.ShouldBeTrue(); - } - - /// - /// Tests optimizer identifies range between simulated. - /// - [Fact] - public void Optimizer_Identifies_Range_Between_Simulated() - { - var indexes = new List - { - new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] } - }; - - Expression> predicate = x => x.Age > 20 && x.Age < 40; - var model = new QueryModel { WhereClause = predicate }; - - var result = IndexOptimizer.TryOptimize(model, indexes); - - result.ShouldNotBeNull(); - result.IndexName.ShouldBe("idx_age"); - result.MinValue.ShouldBe(20); - result.MaxValue.ShouldBe(40); - result.IsRange.ShouldBeTrue(); - } - - /// - /// Tests optimizer identifies starts with. - /// - [Fact] - public void Optimizer_Identifies_StartsWith() - { - var indexes = new List - { - new CollectionIndexInfo { Name = "idx_name", PropertyPaths = ["Name"], Type = IndexType.BTree } - }; - - Expression> predicate = x => x.Name.StartsWith("Ali"); - var model = new QueryModel { WhereClause = predicate }; - - var result = IndexOptimizer.TryOptimize(model, indexes); - - result.ShouldNotBeNull(); - result.IndexName.ShouldBe("idx_name"); - result.MinValue.ShouldBe("Ali"); - // "Ali" + next char -> "Alj" - result.MaxValue.ShouldBe("Alj"); - result.IsRange.ShouldBeTrue(); - } - - /// - /// Tests optimizer ignores non indexed fields. - /// - [Fact] - public void Optimizer_Ignores_NonIndexed_Fields() - { - var indexes = new List - { - new CollectionIndexInfo { Name = "idx_age", PropertyPaths = ["Age"] } - }; - - Expression> predicate = x => x.Name == "Alice"; // Name is not indexed - var model = new QueryModel { WhereClause = predicate }; - - var result = IndexOptimizer.TryOptimize(model, indexes); - - result.ShouldBeNull(); - } + result.ShouldNotBeNull(); + result.IndexName.ShouldBe("idx_age"); + result.MinValue.ShouldBe(30); + result.MaxValue.ShouldBe(30); + result.IsRange.ShouldBeFalse(); } -} + + /// + /// Tests optimizer identifies range greater than. + /// + [Fact] + public void Optimizer_Identifies_Range_GreaterThan() + { + var indexes = new List + { + new() { Name = "idx_age", PropertyPaths = ["Age"] } + }; + + Expression> predicate = x => x.Age > 25; + var model = new QueryModel { WhereClause = predicate }; + + var result = IndexOptimizer.TryOptimize(model, indexes); + + result.ShouldNotBeNull(); + result.IndexName.ShouldBe("idx_age"); + result.MinValue.ShouldBe(25); + result.MaxValue.ShouldBeNull(); + result.IsRange.ShouldBeTrue(); + } + + /// + /// Tests optimizer identifies range less than. + /// + [Fact] + public void Optimizer_Identifies_Range_LessThan() + { + var indexes = new List + { + new() { Name = "idx_age", PropertyPaths = ["Age"] } + }; + + Expression> predicate = x => x.Age < 50; + var model = new QueryModel { WhereClause = predicate }; + + var result = IndexOptimizer.TryOptimize(model, indexes); + + result.ShouldNotBeNull(); + result.IndexName.ShouldBe("idx_age"); + result.MinValue.ShouldBeNull(); + result.MaxValue.ShouldBe(50); + result.IsRange.ShouldBeTrue(); + } + + /// + /// Tests optimizer identifies range between simulated. + /// + [Fact] + public void Optimizer_Identifies_Range_Between_Simulated() + { + var indexes = new List + { + new() { Name = "idx_age", PropertyPaths = ["Age"] } + }; + + Expression> predicate = x => x.Age > 20 && x.Age < 40; + var model = new QueryModel { WhereClause = predicate }; + + var result = IndexOptimizer.TryOptimize(model, indexes); + + result.ShouldNotBeNull(); + result.IndexName.ShouldBe("idx_age"); + result.MinValue.ShouldBe(20); + result.MaxValue.ShouldBe(40); + result.IsRange.ShouldBeTrue(); + } + + /// + /// Tests optimizer identifies starts with. + /// + [Fact] + public void Optimizer_Identifies_StartsWith() + { + var indexes = new List + { + new() { Name = "idx_name", PropertyPaths = ["Name"], Type = IndexType.BTree } + }; + + Expression> predicate = x => x.Name.StartsWith("Ali"); + var model = new QueryModel { WhereClause = predicate }; + + var result = IndexOptimizer.TryOptimize(model, indexes); + + result.ShouldNotBeNull(); + result.IndexName.ShouldBe("idx_name"); + result.MinValue.ShouldBe("Ali"); + // "Ali" + next char -> "Alj" + result.MaxValue.ShouldBe("Alj"); + result.IsRange.ShouldBeTrue(); + } + + /// + /// Tests optimizer ignores non indexed fields. + /// + [Fact] + public void Optimizer_Ignores_NonIndexed_Fields() + { + var indexes = new List + { + new() { Name = "idx_age", PropertyPaths = ["Age"] } + }; + + Expression> predicate = x => x.Name == "Alice"; // Name is not indexed + var model = new QueryModel { WhereClause = predicate }; + + var result = IndexOptimizer.TryOptimize(model, indexes); + + result.ShouldBeNull(); + } + + public class TestEntity + { + /// + /// Gets or sets the id. + /// + public int Id { get; set; } + + /// + /// Gets or sets the name. + /// + public string Name { get; set; } = ""; + + /// + /// Gets or sets the age. + /// + public int Age { get; set; } + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/PrimaryKeyTests.cs b/tests/CBDD.Tests/Indexing/PrimaryKeyTests.cs index 6f0961b..bd0ab16 100755 --- a/tests/CBDD.Tests/Indexing/PrimaryKeyTests.cs +++ b/tests/CBDD.Tests/Indexing/PrimaryKeyTests.cs @@ -1,10 +1,4 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Metadata; using ZB.MOM.WW.CBDD.Shared; -using System; -using System.Buffers; namespace ZB.MOM.WW.CBDD.Tests; @@ -13,7 +7,7 @@ public class PrimaryKeyTests : IDisposable private readonly string _dbPath = "primary_key_tests.db"; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public PrimaryKeyTests() { @@ -21,7 +15,7 @@ public class PrimaryKeyTests : IDisposable } /// - /// Executes Dispose. + /// Executes Dispose. /// public void Dispose() { @@ -29,12 +23,12 @@ public class PrimaryKeyTests : IDisposable } /// - /// Executes Test_Int_PrimaryKey. + /// Executes Test_Int_PrimaryKey. /// [Fact] public void Test_Int_PrimaryKey() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); var entity = new IntEntity { Id = 1, Name = "Test 1" }; db.IntEntities.Insert(entity); @@ -56,12 +50,12 @@ public class PrimaryKeyTests : IDisposable } /// - /// Executes Test_String_PrimaryKey. + /// Executes Test_String_PrimaryKey. /// [Fact] public void Test_String_PrimaryKey() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); var entity = new StringEntity { Id = "key1", Value = "Value 1" }; db.StringEntities.Insert(entity); @@ -78,12 +72,12 @@ public class PrimaryKeyTests : IDisposable } /// - /// Executes Test_Guid_PrimaryKey. + /// Executes Test_Guid_PrimaryKey. /// [Fact] public void Test_Guid_PrimaryKey() { - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); var id = Guid.NewGuid(); var entity = new GuidEntity { Id = id, Name = "Guid Test" }; @@ -100,13 +94,13 @@ public class PrimaryKeyTests : IDisposable } /// - /// Executes Test_String_PrimaryKey_With_Custom_Name. + /// Executes Test_String_PrimaryKey_With_Custom_Name. /// [Fact] public void Test_String_PrimaryKey_With_Custom_Name() { // Test entity with string key NOT named "Id" (named "Code" instead) - using var db = new Shared.TestDbContext(_dbPath); + using var db = new TestDbContext(_dbPath); var entity = new CustomKeyEntity { Code = "ABC123", Description = "Test Description" }; db.CustomKeyEntities.Insert(entity); @@ -131,4 +125,4 @@ public class PrimaryKeyTests : IDisposable db.SaveChanges(); db.CustomKeyEntities.FindById("ABC123").ShouldBeNull(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/VectorMathTests.cs b/tests/CBDD.Tests/Indexing/VectorMathTests.cs index b50e8e6..3e724d2 100644 --- a/tests/CBDD.Tests/Indexing/VectorMathTests.cs +++ b/tests/CBDD.Tests/Indexing/VectorMathTests.cs @@ -5,7 +5,7 @@ namespace ZB.MOM.WW.CBDD.Tests; public class VectorMathTests { /// - /// Verifies distance calculations across all supported vector metrics. + /// Verifies distance calculations across all supported vector metrics. /// [Fact] public void Distance_Should_Cover_All_Metrics() @@ -13,19 +13,19 @@ public class VectorMathTests float[] v1 = [1f, 2f]; float[] v2 = [3f, 4f]; - var cosineDistance = VectorMath.Distance(v1, v2, VectorMetric.Cosine); - var l2Distance = VectorMath.Distance(v1, v2, VectorMetric.L2); - var dotDistance = VectorMath.Distance(v1, v2, VectorMetric.DotProduct); + float cosineDistance = VectorMath.Distance(v1, v2, VectorMetric.Cosine); + float l2Distance = VectorMath.Distance(v1, v2, VectorMetric.L2); + float dotDistance = VectorMath.Distance(v1, v2, VectorMetric.DotProduct); l2Distance.ShouldBe(8f); dotDistance.ShouldBe(-11f); - var expectedCosine = 1f - (11f / (MathF.Sqrt(5f) * 5f)); + float expectedCosine = 1f - 11f / (MathF.Sqrt(5f) * 5f); MathF.Abs(cosineDistance - expectedCosine).ShouldBeLessThan(0.0001f); } /// - /// Verifies cosine similarity returns zero when one vector has zero magnitude. + /// Verifies cosine similarity returns zero when one vector has zero magnitude. /// [Fact] public void CosineSimilarity_Should_Return_Zero_For_ZeroMagnitude_Vector() @@ -37,7 +37,7 @@ public class VectorMathTests } /// - /// Verifies dot product throws for mismatched vector lengths. + /// Verifies dot product throws for mismatched vector lengths. /// [Fact] public void DotProduct_Should_Throw_For_Length_Mismatch() @@ -49,7 +49,7 @@ public class VectorMathTests } /// - /// Verifies squared Euclidean distance throws for mismatched vector lengths. + /// Verifies squared Euclidean distance throws for mismatched vector lengths. /// [Fact] public void EuclideanDistanceSquared_Should_Throw_For_Length_Mismatch() @@ -59,4 +59,4 @@ public class VectorMathTests Should.Throw(() => VectorMath.EuclideanDistanceSquared(v1, v2)); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/VectorSearchTests.cs b/tests/CBDD.Tests/Indexing/VectorSearchTests.cs index 40058fd..d957c0f 100755 --- a/tests/CBDD.Tests/Indexing/VectorSearchTests.cs +++ b/tests/CBDD.Tests/Indexing/VectorSearchTests.cs @@ -1,23 +1,20 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Shared; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class VectorSearchTests { /// - /// Verifies basic vector-search query behavior. + /// Verifies basic vector-search query behavior. /// [Fact] public void Test_VectorSearch_Basic() { - string dbPath = "vector_test.db"; + var dbPath = "vector_test.db"; if (File.Exists(dbPath)) File.Delete(dbPath); - using (var db = new Shared.TestDbContext(dbPath)) + using (var db = new TestDbContext(dbPath)) { db.VectorItems.Insert(new VectorEntity { Title = "Near", Embedding = [1.0f, 1.0f, 1.0f] }); db.VectorItems.Insert(new VectorEntity { Title = "Far", Embedding = [10.0f, 10.0f, 10.0f] }); @@ -31,4 +28,4 @@ public class VectorSearchTests File.Delete(dbPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Indexing/WalIndexTests.cs b/tests/CBDD.Tests/Indexing/WalIndexTests.cs index dac360e..0260dcf 100755 --- a/tests/CBDD.Tests/Indexing/WalIndexTests.cs +++ b/tests/CBDD.Tests/Indexing/WalIndexTests.cs @@ -1,25 +1,19 @@ using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -using System.Buffers; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class WalIndexTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly string _walPath; - private readonly Shared.TestDbContext _db; private readonly ITestOutputHelper _output; + private readonly string _walPath; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Test output sink. public WalIndexTests(ITestOutputHelper output) @@ -29,11 +23,41 @@ public class WalIndexTests : IDisposable // WAL defaults to .wal next to db _walPath = Path.ChangeExtension(_dbPath, ".wal"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Verifies index writes are recorded in the WAL. + /// Releases test resources. + /// + public void Dispose() + { + try + { + _db?.Dispose(); // Safe to call multiple times + } + catch + { + } + + try + { + if (File.Exists(_dbPath)) File.Delete(_dbPath); + } + catch + { + } + + try + { + if (File.Exists(_walPath)) File.Delete(_walPath); + } + catch + { + } + } + + /// + /// Verifies index writes are recorded in the WAL. /// [Fact] public void IndexWritesAreLoggedToWal() @@ -71,8 +95,8 @@ public class WalIndexTests : IDisposable _output.WriteLine($"Found {writeRecords.Count} Write records for Txn {txn.TransactionId}"); // Analyze pages - int indexPageCount = 0; - int dataPageCount = 0; + var indexPageCount = 0; + var dataPageCount = 0; foreach (var record in writeRecords) { @@ -89,21 +113,18 @@ public class WalIndexTests : IDisposable private PageType ParsePageType(byte[]? pageData) { - if (pageData == null || pageData.Length < 32) return (PageType)0; + if (pageData == null || pageData.Length < 32) return 0; // PageType is at offset 4 (1 byte) return (PageType)pageData[4]; // Casting byte to PageType } /// - /// Verifies offline compaction leaves the WAL empty. + /// Verifies offline compaction leaves the WAL empty. /// [Fact] public void Compact_ShouldLeaveWalEmpty_AfterOfflineRun() { - for (var i = 0; i < 100; i++) - { - _db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 }); - } + for (var i = 0; i < 100; i++) _db.Users.Insert(new User { Name = $"wal-compact-{i:D3}", Age = i % 30 }); _db.SaveChanges(); _db.Storage.GetWalSize().ShouldBeGreaterThan(0); @@ -121,24 +142,22 @@ public class WalIndexTests : IDisposable } /// - /// Verifies WAL recovery followed by compaction preserves data. + /// Verifies WAL recovery followed by compaction preserves data. /// [Fact] public void Recover_WithCommittedWal_ThenCompact_ShouldPreserveData() { - var dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db"); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string dbPath = Path.Combine(Path.GetTempPath(), $"test_wal_recover_compact_{Guid.NewGuid():N}.db"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; var expectedIds = new List(); try { - using (var writer = new Shared.TestDbContext(dbPath)) + using (var writer = new TestDbContext(dbPath)) { for (var i = 0; i < 48; i++) - { expectedIds.Add(writer.Users.Insert(new User { Name = $"recover-{i:D3}", Age = i % 10 })); - } writer.SaveChanges(); writer.Storage.GetWalSize().ShouldBeGreaterThan(0); @@ -146,16 +165,13 @@ public class WalIndexTests : IDisposable new FileInfo(walPath).Length.ShouldBeGreaterThan(0); - using (var recovered = new Shared.TestDbContext(dbPath)) + using (var recovered = new TestDbContext(dbPath)) { recovered.Users.Count().ShouldBe(expectedIds.Count); recovered.Compact(); recovered.Storage.GetWalSize().ShouldBe(0); - foreach (var id in expectedIds) - { - recovered.Users.FindById(id).ShouldNotBeNull(); - } + foreach (var id in expectedIds) recovered.Users.FindById(id).ShouldNotBeNull(); } } finally @@ -165,19 +181,4 @@ public class WalIndexTests : IDisposable if (File.Exists(markerPath)) File.Delete(markerPath); } } - - /// - /// Releases test resources. - /// - public void Dispose() - { - try - { - _db?.Dispose(); // Safe to call multiple times - } - catch { } - - try { if (File.Exists(_dbPath)) File.Delete(_dbPath); } catch { } - try { if (File.Exists(_walPath)) File.Delete(_walPath); } catch { } - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Query/AdvancedQueryTests.cs b/tests/CBDD.Tests/Query/AdvancedQueryTests.cs index adbd920..50e5639 100755 --- a/tests/CBDD.Tests/Query/AdvancedQueryTests.cs +++ b/tests/CBDD.Tests/Query/AdvancedQueryTests.cs @@ -1,267 +1,259 @@ -using Xunit; -using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Bson; -using System.Linq; -using System.Collections.Generic; -using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Core.Storage; -using System; -using System.IO; using ZB.MOM.WW.CBDD.Shared; +using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class AdvancedQueryTests : IDisposable { - public class AdvancedQueryTests : IDisposable + private readonly TestDbContext _db; + private readonly string _dbPath; + + /// + /// Initializes test database state used by advanced query tests. + /// + public AdvancedQueryTests() { - private readonly string _dbPath; - private readonly Shared.TestDbContext _db; + _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_advanced_{Guid.NewGuid()}.db"); + _db = new TestDbContext(_dbPath); - /// - /// Initializes test database state used by advanced query tests. - /// - public AdvancedQueryTests() - { - _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_advanced_{Guid.NewGuid()}.db"); - _db = new Shared.TestDbContext(_dbPath); - - // Seed Data - _db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 10, Name = "Item1" }); - _db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 20, Name = "Item2" }); - _db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 30, Name = "Item3" }); - _db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 40, Name = "Item4" }); - _db.TestDocuments.Insert(new TestDocument { Category = "C", Amount = 50, Name = "Item5" }); - _db.SaveChanges(); - } - - /// - /// Disposes test resources and removes temporary files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - } - - /// - /// Verifies grouping by a simple key returns expected groups and counts. - /// - [Fact] - public void GroupBy_Simple_Key_Works() - { - var groups = _db.TestDocuments.AsQueryable() - .GroupBy(x => x.Category) - .ToList(); - - groups.Count.ShouldBe(3); - - var groupA = groups.First(g => g.Key == "A"); - groupA.Count().ShouldBe(2); - groupA.ShouldContain(x => x.Amount == 10); - groupA.ShouldContain(x => x.Amount == 20); - - var groupB = groups.First(g => g.Key == "B"); - groupB.Count().ShouldBe(2); - - var groupC = groups.First(g => g.Key == "C"); - groupC.Count().ShouldBe(1); - } - - /// - /// Verifies grouped projection with aggregation returns expected totals. - /// - [Fact] - public void GroupBy_With_Aggregation_Select() - { - var results = _db.TestDocuments.AsQueryable() - .GroupBy(x => x.Category) - .Select(g => new { Category = g.Key, Total = g.Sum(x => x.Amount) }) - .OrderBy(x => x.Category) - .ToList(); - - results.Count.ShouldBe(3); - results[0].Category.ShouldBe("A"); - results[0].Total.ShouldBe(30); // 10 + 20 - - results[1].Category.ShouldBe("B"); - results[1].Total.ShouldBe(70); // 30 + 40 - - results[2].Category.ShouldBe("C"); - results[2].Total.ShouldBe(50); // 50 - } - - /// - /// Verifies direct aggregate operators return expected values. - /// - [Fact] - public void Aggregations_Direct_Works() - { - var query = _db.TestDocuments.AsQueryable(); - - query.Count().ShouldBe(5); - query.Sum(x => x.Amount).ShouldBe(150); - query.Average(x => x.Amount).ShouldBe(30.0); - query.Min(x => x.Amount).ShouldBe(10); - query.Max(x => x.Amount).ShouldBe(50); - } - - /// - /// Verifies aggregate operators with predicates return expected values. - /// - [Fact] - public void Aggregations_With_Predicate_Works() - { - var query = _db.TestDocuments.AsQueryable().Where(x => x.Category == "A"); - - query.Count().ShouldBe(2); - query.Sum(x => x.Amount).ShouldBe(30); - } - - /// - /// Verifies in-memory join query execution returns expected rows. - /// - [Fact] - public void Join_Works_InMemory() - { - // Create a second collection for joining - _db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item1", Quantity = 5 }); - _db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item3", Quantity = 2 }); - _db.SaveChanges(); - - var query = _db.TestDocuments.AsQueryable() - .Join(_db.OrderDocuments.AsQueryable(), - doc => doc.Name, - order => order.ItemName, - (doc, order) => new { doc.Name, doc.Category, order.Quantity }) - .OrderBy(x => x.Name) - .ToList(); - - query.Count.ShouldBe(2); - - query[0].Name.ShouldBe("Item1"); - query[0].Category.ShouldBe("A"); - query[0].Quantity.ShouldBe(5); - - query[1].Name.ShouldBe("Item3"); - query[1].Category.ShouldBe("B"); - query[1].Quantity.ShouldBe(2); - } - - - /// - /// Verifies projection of nested object properties works. - /// - [Fact] - public void Select_Project_Nested_Object() - { - var doc = new ComplexDocument - { - Id = ObjectId.NewObjectId(), - Title = "Order1", - ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" }, - Items = new List - { - new OrderItem { Name = "Laptop", Price = 1000 }, - new OrderItem { Name = "Mouse", Price = 50 } - } - }; - _db.ComplexDocuments.Insert(doc); - _db.SaveChanges(); - - var query = _db.ComplexDocuments.AsQueryable() - .Select(x => x.ShippingAddress) - .ToList(); - - query.Count().ShouldBe(1); - query[0].City.Name.ShouldBe("New York"); - query[0].Street.ShouldBe("5th Ave"); - } - - /// - /// Verifies projection of nested scalar fields works. - /// - [Fact] - public void Select_Project_Nested_Field() - { - var doc = new ComplexDocument - { - Id = ObjectId.NewObjectId(), - Title = "Order1", - ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" } - }; - _db.ComplexDocuments.Insert(doc); - _db.SaveChanges(); - - var cities = _db.ComplexDocuments.AsQueryable() - .Select(x => x.ShippingAddress.City.Name) - .ToList(); - - cities.Count().ShouldBe(1); - cities[0].ShouldBe("New York"); - } - - /// - /// Verifies anonymous projection including nested values works. - /// - [Fact] - public void Select_Anonymous_Complex() - { - ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers.ZB_MOM_WW_CBDD_Shared_CityMapper cityMapper = new ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers.ZB_MOM_WW_CBDD_Shared_CityMapper(); - var doc = new ComplexDocument - { - Id = ObjectId.NewObjectId(), - Title = "Order1", - ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" } - }; - - - _db.ComplexDocuments.Insert(doc); - _db.SaveChanges(); - - var result = _db.ComplexDocuments.AsQueryable() - .Select(x => new { x.Title, x.ShippingAddress.City }) - .ToList(); - - result.Count().ShouldBe(1); - result[0].Title.ShouldBe("Order1"); - result[0].City.Name.ShouldBe("New York"); - } - - /// - /// Verifies projection and retrieval of nested arrays of objects works. - /// - [Fact] - public void Select_Project_Nested_Array_Of_Objects() - { - var doc = new ComplexDocument - { - Id = ObjectId.NewObjectId(), - Title = "Order with Items", - ShippingAddress = new Address { City = new City { Name = "Los Angeles" }, Street = "Hollywood Blvd" }, - Items = new List - { - new OrderItem { Name = "Laptop", Price = 1500 }, - new OrderItem { Name = "Mouse", Price = 25 }, - new OrderItem { Name = "Keyboard", Price = 75 } - } - }; - _db.ComplexDocuments.Insert(doc); - _db.SaveChanges(); - - // Retrieve the full document and verify Items array - var retrieved = _db.ComplexDocuments.FindAll().First(); - - retrieved.Title.ShouldBe("Order with Items"); - retrieved.ShippingAddress.City.Name.ShouldBe("Los Angeles"); - retrieved.ShippingAddress.Street.ShouldBe("Hollywood Blvd"); - - // Verify array of nested objects - retrieved.Items.Count.ShouldBe(3); - retrieved.Items[0].Name.ShouldBe("Laptop"); - retrieved.Items[0].Price.ShouldBe(1500); - retrieved.Items[1].Name.ShouldBe("Mouse"); - retrieved.Items[1].Price.ShouldBe(25); - retrieved.Items[2].Name.ShouldBe("Keyboard"); - retrieved.Items[2].Price.ShouldBe(75); - } + // Seed Data + _db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 10, Name = "Item1" }); + _db.TestDocuments.Insert(new TestDocument { Category = "A", Amount = 20, Name = "Item2" }); + _db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 30, Name = "Item3" }); + _db.TestDocuments.Insert(new TestDocument { Category = "B", Amount = 40, Name = "Item4" }); + _db.TestDocuments.Insert(new TestDocument { Category = "C", Amount = 50, Name = "Item5" }); + _db.SaveChanges(); } -} + + /// + /// Disposes test resources and removes temporary files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + } + + /// + /// Verifies grouping by a simple key returns expected groups and counts. + /// + [Fact] + public void GroupBy_Simple_Key_Works() + { + var groups = _db.TestDocuments.AsQueryable() + .GroupBy(x => x.Category) + .ToList(); + + groups.Count.ShouldBe(3); + + var groupA = groups.First(g => g.Key == "A"); + groupA.Count().ShouldBe(2); + groupA.ShouldContain(x => x.Amount == 10); + groupA.ShouldContain(x => x.Amount == 20); + + var groupB = groups.First(g => g.Key == "B"); + groupB.Count().ShouldBe(2); + + var groupC = groups.First(g => g.Key == "C"); + groupC.Count().ShouldBe(1); + } + + /// + /// Verifies grouped projection with aggregation returns expected totals. + /// + [Fact] + public void GroupBy_With_Aggregation_Select() + { + var results = _db.TestDocuments.AsQueryable() + .GroupBy(x => x.Category) + .Select(g => new { Category = g.Key, Total = g.Sum(x => x.Amount) }) + .OrderBy(x => x.Category) + .ToList(); + + results.Count.ShouldBe(3); + results[0].Category.ShouldBe("A"); + results[0].Total.ShouldBe(30); // 10 + 20 + + results[1].Category.ShouldBe("B"); + results[1].Total.ShouldBe(70); // 30 + 40 + + results[2].Category.ShouldBe("C"); + results[2].Total.ShouldBe(50); // 50 + } + + /// + /// Verifies direct aggregate operators return expected values. + /// + [Fact] + public void Aggregations_Direct_Works() + { + var query = _db.TestDocuments.AsQueryable(); + + query.Count().ShouldBe(5); + query.Sum(x => x.Amount).ShouldBe(150); + query.Average(x => x.Amount).ShouldBe(30.0); + query.Min(x => x.Amount).ShouldBe(10); + query.Max(x => x.Amount).ShouldBe(50); + } + + /// + /// Verifies aggregate operators with predicates return expected values. + /// + [Fact] + public void Aggregations_With_Predicate_Works() + { + var query = _db.TestDocuments.AsQueryable().Where(x => x.Category == "A"); + + query.Count().ShouldBe(2); + query.Sum(x => x.Amount).ShouldBe(30); + } + + /// + /// Verifies in-memory join query execution returns expected rows. + /// + [Fact] + public void Join_Works_InMemory() + { + // Create a second collection for joining + _db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item1", Quantity = 5 }); + _db.OrderDocuments.Insert(new OrderDocument { ItemName = "Item3", Quantity = 2 }); + _db.SaveChanges(); + + var query = _db.TestDocuments.AsQueryable() + .Join(_db.OrderDocuments.AsQueryable(), + doc => doc.Name, + order => order.ItemName, + (doc, order) => new { doc.Name, doc.Category, order.Quantity }) + .OrderBy(x => x.Name) + .ToList(); + + query.Count.ShouldBe(2); + + query[0].Name.ShouldBe("Item1"); + query[0].Category.ShouldBe("A"); + query[0].Quantity.ShouldBe(5); + + query[1].Name.ShouldBe("Item3"); + query[1].Category.ShouldBe("B"); + query[1].Quantity.ShouldBe(2); + } + + + /// + /// Verifies projection of nested object properties works. + /// + [Fact] + public void Select_Project_Nested_Object() + { + var doc = new ComplexDocument + { + Id = ObjectId.NewObjectId(), + Title = "Order1", + ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" }, + Items = new List + { + new() { Name = "Laptop", Price = 1000 }, + new() { Name = "Mouse", Price = 50 } + } + }; + _db.ComplexDocuments.Insert(doc); + _db.SaveChanges(); + + var query = _db.ComplexDocuments.AsQueryable() + .Select(x => x.ShippingAddress) + .ToList(); + + query.Count().ShouldBe(1); + query[0].City.Name.ShouldBe("New York"); + query[0].Street.ShouldBe("5th Ave"); + } + + /// + /// Verifies projection of nested scalar fields works. + /// + [Fact] + public void Select_Project_Nested_Field() + { + var doc = new ComplexDocument + { + Id = ObjectId.NewObjectId(), + Title = "Order1", + ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" } + }; + _db.ComplexDocuments.Insert(doc); + _db.SaveChanges(); + + var cities = _db.ComplexDocuments.AsQueryable() + .Select(x => x.ShippingAddress.City.Name) + .ToList(); + + cities.Count().ShouldBe(1); + cities[0].ShouldBe("New York"); + } + + /// + /// Verifies anonymous projection including nested values works. + /// + [Fact] + public void Select_Anonymous_Complex() + { + var cityMapper = new ZB_MOM_WW_CBDD_Shared_CityMapper(); + var doc = new ComplexDocument + { + Id = ObjectId.NewObjectId(), + Title = "Order1", + ShippingAddress = new Address { City = new City { Name = "New York" }, Street = "5th Ave" } + }; + + + _db.ComplexDocuments.Insert(doc); + _db.SaveChanges(); + + var result = _db.ComplexDocuments.AsQueryable() + .Select(x => new { x.Title, x.ShippingAddress.City }) + .ToList(); + + result.Count().ShouldBe(1); + result[0].Title.ShouldBe("Order1"); + result[0].City.Name.ShouldBe("New York"); + } + + /// + /// Verifies projection and retrieval of nested arrays of objects works. + /// + [Fact] + public void Select_Project_Nested_Array_Of_Objects() + { + var doc = new ComplexDocument + { + Id = ObjectId.NewObjectId(), + Title = "Order with Items", + ShippingAddress = new Address { City = new City { Name = "Los Angeles" }, Street = "Hollywood Blvd" }, + Items = new List + { + new() { Name = "Laptop", Price = 1500 }, + new() { Name = "Mouse", Price = 25 }, + new() { Name = "Keyboard", Price = 75 } + } + }; + _db.ComplexDocuments.Insert(doc); + _db.SaveChanges(); + + // Retrieve the full document and verify Items array + var retrieved = _db.ComplexDocuments.FindAll().First(); + + retrieved.Title.ShouldBe("Order with Items"); + retrieved.ShippingAddress.City.Name.ShouldBe("Los Angeles"); + retrieved.ShippingAddress.Street.ShouldBe("Hollywood Blvd"); + + // Verify array of nested objects + retrieved.Items.Count.ShouldBe(3); + retrieved.Items[0].Name.ShouldBe("Laptop"); + retrieved.Items[0].Price.ShouldBe(1500); + retrieved.Items[1].Name.ShouldBe("Mouse"); + retrieved.Items[1].Price.ShouldBe(25); + retrieved.Items[2].Name.ShouldBe("Keyboard"); + retrieved.Items[2].Price.ShouldBe(75); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Query/LinqTests.cs b/tests/CBDD.Tests/Query/LinqTests.cs index a8c7118..7c34fc9 100755 --- a/tests/CBDD.Tests/Query/LinqTests.cs +++ b/tests/CBDD.Tests/Query/LinqTests.cs @@ -1,166 +1,157 @@ -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Bson; -using Xunit; -using System; -using System.IO; -using System.Linq; -using System.Collections.Generic; using ZB.MOM.WW.CBDD.Shared; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class LinqTests : IDisposable { - public class LinqTests : IDisposable + private readonly TestDbContext _db; + private readonly string _testFile; + + /// + /// Initializes a new instance of the class. + /// + public LinqTests() { - private readonly string _testFile; - private readonly Shared.TestDbContext _db; + _testFile = Path.Combine(Path.GetTempPath(), $"linq_tests_{Guid.NewGuid()}.db"); + if (File.Exists(_testFile)) File.Delete(_testFile); + string wal = Path.ChangeExtension(_testFile, ".wal"); + if (File.Exists(wal)) File.Delete(wal); - /// - /// Initializes a new instance of the class. - /// - public LinqTests() - { - _testFile = Path.Combine(Path.GetTempPath(), $"linq_tests_{Guid.NewGuid()}.db"); - if (File.Exists(_testFile)) File.Delete(_testFile); - var wal = Path.ChangeExtension(_testFile, ".wal"); - if (File.Exists(wal)) File.Delete(wal); + _db = new TestDbContext(_testFile); - _db = new Shared.TestDbContext(_testFile); - - // Seed Data - _db.Users.Insert(new User { Name = "Alice", Age = 30 }); - _db.Users.Insert(new User { Name = "Bob", Age = 25 }); - _db.Users.Insert(new User { Name = "Charlie", Age = 35 }); - _db.Users.Insert(new User { Name = "Dave", Age = 20 }); - _db.Users.Insert(new User { Name = "Eve", Age = 40 }); - _db.SaveChanges(); - } - - /// - /// Disposes test resources and removes temporary files. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_testFile)) File.Delete(_testFile); - var wal = Path.ChangeExtension(_testFile, ".wal"); - if (File.Exists(wal)) File.Delete(wal); - } - - /// - /// Verifies where filters return matching documents. - /// - [Fact] - public void Where_FiltersDocuments() - { - var query = _db.Users.AsQueryable().Where(x => x.Age > 28); - var results = query.ToList(); - - results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40) - results.ShouldNotContain(d => d.Name == "Bob"); - } - - /// - /// Verifies order by returns sorted documents. - /// - [Fact] - public void OrderBy_SortsDocuments() - { - var results = _db.Users.AsQueryable().OrderBy(x => x.Age).ToList(); - - results.Count.ShouldBe(5); - results[0].Name.ShouldBe("Dave"); // 20 - results[1].Name.ShouldBe("Bob"); // 25 - results.Last().Name.ShouldBe("Eve"); // 40 - } - - /// - /// Verifies skip and take support pagination. - /// - [Fact] - public void SkipTake_Pagination() - { - var results = _db.Users.AsQueryable() - .OrderBy(x => x.Age) - .Skip(1) - .Take(2) - .ToList(); - - results.Count.ShouldBe(2); - results[0].Name.ShouldBe("Bob"); // 25 (Skipped Dave) - results[1].Name.ShouldBe("Alice"); // 30 - } - - /// - /// Verifies select supports projections. - /// - [Fact] - public void Select_Projections() - { - var names = _db.Users.AsQueryable() - .Where(x => x.Age < 30) - .OrderBy(x => x.Age) - .Select(x => x.Name) - .ToList(); - - names.Count.ShouldBe(2); - names[0].ShouldBe("Dave"); - names[1].ShouldBe("Bob"); - } - /// - /// Verifies indexed where queries use index-backed filtering. - /// - [Fact] - public void IndexedWhere_UsedIndex() - { - // Create index on Age - _db.Users.EnsureIndex(x => x.Age, "idx_age", false); - - var query = _db.Users.AsQueryable().Where(x => x.Age > 25); - var results = query.ToList(); - - results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40) - results.ShouldNotContain(d => d.Name == "Bob"); // Age 25 (filtered out by strict >) - results.ShouldNotContain(d => d.Name == "Dave"); // Age 20 - } - /// - /// Verifies starts-with predicates can use an index. - /// - [Fact] - public void StartsWith_UsedIndex() - { - // Create index on Name - _db.Users.EnsureIndex(x => x.Name!, "idx_name", false); - - // StartsWith "Cha" -> Should find "Charlie" - var query = _db.Users.AsQueryable().Where(x => x.Name!.StartsWith("Cha")); - var results = query.ToList(); - - results.Count().ShouldBe(1); - results[0].Name.ShouldBe("Charlie"); - } - - /// - /// Verifies range predicates can use an index. - /// - [Fact] - public void Between_UsedIndex() - { - // Create index on Age - _db.Users.EnsureIndex(x => x.Age, "idx_age_between", false); - - // Age >= 22 && Age <= 32 - // Alice(30), Bob(25) -> Should be found. - // Dave(20), Charlie(35), Eve(40) -> excluded. - - var query = _db.Users.AsQueryable().Where(x => x.Age >= 22 && x.Age <= 32); - var results = query.ToList(); - - results.Count.ShouldBe(2); - results.ShouldContain(x => x.Name == "Alice"); - results.ShouldContain(x => x.Name == "Bob"); - } + // Seed Data + _db.Users.Insert(new User { Name = "Alice", Age = 30 }); + _db.Users.Insert(new User { Name = "Bob", Age = 25 }); + _db.Users.Insert(new User { Name = "Charlie", Age = 35 }); + _db.Users.Insert(new User { Name = "Dave", Age = 20 }); + _db.Users.Insert(new User { Name = "Eve", Age = 40 }); + _db.SaveChanges(); } -} + + /// + /// Disposes test resources and removes temporary files. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_testFile)) File.Delete(_testFile); + string wal = Path.ChangeExtension(_testFile, ".wal"); + if (File.Exists(wal)) File.Delete(wal); + } + + /// + /// Verifies where filters return matching documents. + /// + [Fact] + public void Where_FiltersDocuments() + { + var query = _db.Users.AsQueryable().Where(x => x.Age > 28); + var results = query.ToList(); + + results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40) + results.ShouldNotContain(d => d.Name == "Bob"); + } + + /// + /// Verifies order by returns sorted documents. + /// + [Fact] + public void OrderBy_SortsDocuments() + { + var results = _db.Users.AsQueryable().OrderBy(x => x.Age).ToList(); + + results.Count.ShouldBe(5); + results[0].Name.ShouldBe("Dave"); // 20 + results[1].Name.ShouldBe("Bob"); // 25 + results.Last().Name.ShouldBe("Eve"); // 40 + } + + /// + /// Verifies skip and take support pagination. + /// + [Fact] + public void SkipTake_Pagination() + { + var results = _db.Users.AsQueryable() + .OrderBy(x => x.Age) + .Skip(1) + .Take(2) + .ToList(); + + results.Count.ShouldBe(2); + results[0].Name.ShouldBe("Bob"); // 25 (Skipped Dave) + results[1].Name.ShouldBe("Alice"); // 30 + } + + /// + /// Verifies select supports projections. + /// + [Fact] + public void Select_Projections() + { + var names = _db.Users.AsQueryable() + .Where(x => x.Age < 30) + .OrderBy(x => x.Age) + .Select(x => x.Name) + .ToList(); + + names.Count.ShouldBe(2); + names[0].ShouldBe("Dave"); + names[1].ShouldBe("Bob"); + } + + /// + /// Verifies indexed where queries use index-backed filtering. + /// + [Fact] + public void IndexedWhere_UsedIndex() + { + // Create index on Age + _db.Users.EnsureIndex(x => x.Age, "idx_age"); + + var query = _db.Users.AsQueryable().Where(x => x.Age > 25); + var results = query.ToList(); + + results.Count.ShouldBe(3); // Alice(30), Charlie(35), Eve(40) + results.ShouldNotContain(d => d.Name == "Bob"); // Age 25 (filtered out by strict >) + results.ShouldNotContain(d => d.Name == "Dave"); // Age 20 + } + + /// + /// Verifies starts-with predicates can use an index. + /// + [Fact] + public void StartsWith_UsedIndex() + { + // Create index on Name + _db.Users.EnsureIndex(x => x.Name!, "idx_name"); + + // StartsWith "Cha" -> Should find "Charlie" + var query = _db.Users.AsQueryable().Where(x => x.Name!.StartsWith("Cha")); + var results = query.ToList(); + + results.Count().ShouldBe(1); + results[0].Name.ShouldBe("Charlie"); + } + + /// + /// Verifies range predicates can use an index. + /// + [Fact] + public void Between_UsedIndex() + { + // Create index on Age + _db.Users.EnsureIndex(x => x.Age, "idx_age_between"); + + // Age >= 22 && Age <= 32 + // Alice(30), Bob(25) -> Should be found. + // Dave(20), Charlie(35), Eve(40) -> excluded. + + var query = _db.Users.AsQueryable().Where(x => x.Age >= 22 && x.Age <= 32); + var results = query.ToList(); + + results.Count.ShouldBe(2); + results.ShouldContain(x => x.Name == "Alice"); + results.ShouldContain(x => x.Name == "Bob"); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Query/QueryPrimitivesTests.cs b/tests/CBDD.Tests/Query/QueryPrimitivesTests.cs index d3bc5ae..fb05075 100755 --- a/tests/CBDD.Tests/Query/QueryPrimitivesTests.cs +++ b/tests/CBDD.Tests/Query/QueryPrimitivesTests.cs @@ -1,18 +1,16 @@ -using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Bson; -using Xunit; +using ZB.MOM.WW.CBDD.Core.Storage; namespace ZB.MOM.WW.CBDD.Tests; public class QueryPrimitivesTests : IDisposable { - private readonly string _testFile; - private readonly StorageEngine _storage; private readonly BTreeIndex _index; + private readonly StorageEngine _storage; + private readonly string _testFile; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public QueryPrimitivesTests() { @@ -26,12 +24,21 @@ public class QueryPrimitivesTests : IDisposable SeedData(); } + /// + /// Executes Dispose. + /// + public void Dispose() + { + _storage.Dispose(); + File.Delete(_testFile); + } + private void SeedData() { // Insert keys: 10, 20, 30, 40, 50 // And strings: "A", "AB", "ABC", "B", "C" - var txnId = _storage.BeginTransaction().TransactionId; + ulong txnId = _storage.BeginTransaction().TransactionId; Insert(10, txnId); Insert(20, txnId); @@ -59,7 +66,7 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes Equal_ShouldFindExactMatch. + /// Executes Equal_ShouldFindExactMatch. /// [Fact] public void Equal_ShouldFindExactMatch() @@ -72,7 +79,7 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes Equal_ShouldReturnEmpty_WhenNotFound. + /// Executes Equal_ShouldReturnEmpty_WhenNotFound. /// [Fact] public void Equal_ShouldReturnEmpty_WhenNotFound() @@ -84,13 +91,13 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes GreaterThan_ShouldReturnMatches. + /// Executes GreaterThan_ShouldReturnMatches. /// [Fact] public void GreaterThan_ShouldReturnMatches() { var key = IndexKey.Create(30); - var result = _index.GreaterThan(key, orEqual: false, 0).ToList(); + var result = _index.GreaterThan(key, false, 0).ToList(); (result.Count >= 2).ShouldBeTrue(); result[0].Key.ShouldBe(IndexKey.Create(40)); @@ -98,13 +105,13 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes GreaterThanOrEqual_ShouldReturnMatches. + /// Executes GreaterThanOrEqual_ShouldReturnMatches. /// [Fact] public void GreaterThanOrEqual_ShouldReturnMatches() { var key = IndexKey.Create(30); - var result = _index.GreaterThan(key, orEqual: true, 0).ToList(); + var result = _index.GreaterThan(key, true, 0).ToList(); (result.Count >= 3).ShouldBeTrue(); result[0].Key.ShouldBe(IndexKey.Create(30)); @@ -113,13 +120,13 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes LessThan_ShouldReturnMatches. + /// Executes LessThan_ShouldReturnMatches. /// [Fact] public void LessThan_ShouldReturnMatches() { var key = IndexKey.Create(30); - var result = _index.LessThan(key, orEqual: false, 0).ToList(); + var result = _index.LessThan(key, false, 0).ToList(); result.Count.ShouldBe(2); // 20, 10 (Order is backward?) // LessThan yields backward? @@ -129,14 +136,14 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes Between_ShouldReturnRange. + /// Executes Between_ShouldReturnRange. /// [Fact] public void Between_ShouldReturnRange() { var start = IndexKey.Create(20); var end = IndexKey.Create(40); - var result = _index.Between(start, end, startInclusive: true, endInclusive: true, 0).ToList(); + var result = _index.Between(start, end, true, true, 0).ToList(); result.Count.ShouldBe(3); // 20, 30, 40 result[0].Key.ShouldBe(IndexKey.Create(20)); @@ -145,7 +152,7 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes StartsWith_ShouldReturnPrefixMatches. + /// Executes StartsWith_ShouldReturnPrefixMatches. /// [Fact] public void StartsWith_ShouldReturnPrefixMatches() @@ -158,7 +165,7 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes Like_ShouldSupportWildcards. + /// Executes Like_ShouldSupportWildcards. /// [Fact] public void Like_ShouldSupportWildcards() @@ -176,7 +183,7 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes Like_Underscore_ShouldMatchSingleChar. + /// Executes Like_Underscore_ShouldMatchSingleChar. /// [Fact] public void Like_Underscore_ShouldMatchSingleChar() @@ -188,7 +195,7 @@ public class QueryPrimitivesTests : IDisposable } /// - /// Executes In_ShouldReturnSpecificKeys. + /// Executes In_ShouldReturnSpecificKeys. /// [Fact] public void In_ShouldReturnSpecificKeys() @@ -201,13 +208,4 @@ public class QueryPrimitivesTests : IDisposable result[1].Key.ShouldBe(IndexKey.Create(30)); result[2].Key.ShouldBe(IndexKey.Create(50)); } - - /// - /// Executes Dispose. - /// - public void Dispose() - { - _storage.Dispose(); - File.Delete(_testFile); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Query/ScanTests.cs b/tests/CBDD.Tests/Query/ScanTests.cs index 3c36114..8b531fa 100755 --- a/tests/CBDD.Tests/Query/ScanTests.cs +++ b/tests/CBDD.Tests/Query/ScanTests.cs @@ -1,128 +1,111 @@ -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Bson; -using Xunit; -using System; -using System.IO; -using System.Linq; -using System.Collections.Generic; using ZB.MOM.WW.CBDD.Shared; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class ScanTests : IDisposable { - public class ScanTests : IDisposable + private readonly TestDbContext _db; + private readonly string _testFile; + + /// + /// Initializes a new instance of the class. + /// + public ScanTests() { - private readonly string _testFile; - private readonly Shared.TestDbContext _db; + _testFile = Path.Combine(Path.GetTempPath(), $"scan_tests_{Guid.NewGuid()}.db"); + if (File.Exists(_testFile)) File.Delete(_testFile); + string wal = Path.ChangeExtension(_testFile, ".wal"); + if (File.Exists(wal)) File.Delete(wal); - /// - /// Initializes a new instance of the class. - /// - public ScanTests() + _db = new TestDbContext(_testFile); + } + + /// + /// Executes Dispose. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_testFile)) File.Delete(_testFile); + string wal = Path.ChangeExtension(_testFile, ".wal"); + if (File.Exists(wal)) File.Delete(wal); + } + + /// + /// Executes Scan_FindsMatchingDocuments. + /// + [Fact] + public void Scan_FindsMatchingDocuments() + { + // Arrange + _db.Users.Insert(new User { Name = "Alice", Age = 30 }); + _db.Users.Insert(new User { Name = "Bob", Age = 25 }); + _db.Users.Insert(new User { Name = "Charlie", Age = 35 }); + _db.SaveChanges(); + + // Act: Find users older than 28 + var results = _db.Users.Scan(reader => ParseAge(reader) > 28).ToList(); + + // Assert + results.Count.ShouldBe(2); + results.ShouldContain(d => d.Name == "Alice"); + results.ShouldContain(d => d.Name == "Charlie"); + } + + /// + /// Executes Repro_Insert_Loop_Hang. + /// + [Fact] + public void Repro_Insert_Loop_Hang() + { + // Reproduce hang reported by user at 501 documents + var count = 600; + for (var i = 0; i < count; i++) _db.Users.Insert(new User { Name = $"User_{i}", Age = i }); + _db.SaveChanges(); + } + + /// + /// Executes ParallelScan_FindsMatchingDocuments. + /// + [Fact] + public void ParallelScan_FindsMatchingDocuments() + { + // Arrange + var count = 1000; + for (var i = 0; i < count; i++) _db.Users.Insert(new User { Name = $"User_{i}", Age = i }); + _db.SaveChanges(); + + // Act: Find users with Age >= 500 + // Parallelism 2 to force partitioning + var results = _db.Users.ParallelScan(reader => ParseAge(reader) >= 500, 2).ToList(); + + // Assert + results.Count.ShouldBe(500); + } + + private int ParseAge(BsonSpanReader reader) + { + try { - _testFile = Path.Combine(Path.GetTempPath(), $"scan_tests_{Guid.NewGuid()}.db"); - if (File.Exists(_testFile)) File.Delete(_testFile); - var wal = Path.ChangeExtension(_testFile, ".wal"); - if (File.Exists(wal)) File.Delete(wal); - - _db = new Shared.TestDbContext(_testFile); - } - - /// - /// Executes Dispose. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_testFile)) File.Delete(_testFile); - var wal = Path.ChangeExtension(_testFile, ".wal"); - if (File.Exists(wal)) File.Delete(wal); - } - - /// - /// Executes Scan_FindsMatchingDocuments. - /// - [Fact] - public void Scan_FindsMatchingDocuments() - { - // Arrange - _db.Users.Insert(new User { Name = "Alice", Age = 30 }); - _db.Users.Insert(new User { Name = "Bob", Age = 25 }); - _db.Users.Insert(new User { Name = "Charlie", Age = 35 }); - _db.SaveChanges(); - - // Act: Find users older than 28 - var results = _db.Users.Scan(reader => ParseAge(reader) > 28).ToList(); - - // Assert - results.Count.ShouldBe(2); - results.ShouldContain(d => d.Name == "Alice"); - results.ShouldContain(d => d.Name == "Charlie"); - } - - /// - /// Executes Repro_Insert_Loop_Hang. - /// - [Fact] - public void Repro_Insert_Loop_Hang() - { - // Reproduce hang reported by user at 501 documents - int count = 600; - for (int i = 0; i < count; i++) + reader.ReadDocumentSize(); + while (reader.Remaining > 0) { - _db.Users.Insert(new User { Name = $"User_{i}", Age = i }); + var type = reader.ReadBsonType(); + if (type == 0) break; // End of doc + + string name = reader.ReadElementHeader(); + + if (name == "age") return reader.ReadInt32(); + + reader.SkipValue(type); } - _db.SaveChanges(); } - - /// - /// Executes ParallelScan_FindsMatchingDocuments. - /// - [Fact] - public void ParallelScan_FindsMatchingDocuments() + catch { - // Arrange - int count = 1000; - for (int i = 0; i < count; i++) - { - _db.Users.Insert(new User { Name = $"User_{i}", Age = i }); - } - _db.SaveChanges(); - - // Act: Find users with Age >= 500 - // Parallelism 2 to force partitioning - var results = _db.Users.ParallelScan(reader => ParseAge(reader) >= 500, degreeOfParallelism: 2).ToList(); - - // Assert - results.Count.ShouldBe(500); - } - - private int ParseAge(BsonSpanReader reader) - { - try - { - reader.ReadDocumentSize(); - while (reader.Remaining > 0) - { - var type = reader.ReadBsonType(); - if (type == 0) break; // End of doc - - var name = reader.ReadElementHeader(); - - if (name == "age") - { - return reader.ReadInt32(); - } - else - { - reader.SkipValue(type); - } - } - } - catch { return -1; } return -1; } + + return -1; } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/AttributeTests.cs b/tests/CBDD.Tests/Schema/AttributeTests.cs index 91945ee..a131991 100755 --- a/tests/CBDD.Tests/Schema/AttributeTests.cs +++ b/tests/CBDD.Tests/Schema/AttributeTests.cs @@ -1,159 +1,187 @@ +using System.Collections.Concurrent; using System.ComponentModel.DataAnnotations; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class AttributeTests { - public class AttributeTests + private readonly ConcurrentDictionary _keyMap = new(StringComparer.OrdinalIgnoreCase); + private readonly ConcurrentDictionary _keys = new(); + + /// + /// Initializes lookup maps used by attribute mapper tests. + /// + public AttributeTests() { - // Use full path for mapper until we are sure of the namespace - private ZB_MOM_WW_CBDD_Shared_AnnotatedUserMapper CreateMapper() => new(); - - private readonly System.Collections.Concurrent.ConcurrentDictionary _keyMap = new(StringComparer.OrdinalIgnoreCase); - private readonly System.Collections.Concurrent.ConcurrentDictionary _keys = new(); - - /// - /// Initializes lookup maps used by attribute mapper tests. - /// - public AttributeTests() + ushort id = 1; + string[] keys = ["_id", "display_name", "age", "location", "0", "1"]; + foreach (string key in keys) { - ushort id = 1; - string[] keys = ["_id", "display_name", "age", "location", "0", "1"]; - foreach (var key in keys) - { - _keyMap[key] = id; - _keys[id] = key; - id++; - } - } - - /// - /// Verifies table attribute mapping resolves the expected collection name. - /// - [Fact] - public void Test_Table_Attribute_Mapping() - { - // Verify that the generated mapper has the correct collection name - var mapper = CreateMapper(); - mapper.CollectionName.ShouldBe("test.custom_users"); - } - - /// - /// Verifies required attribute validation is enforced. - /// - [Fact] - public void Test_Required_Validation() - { - var mapper = CreateMapper(); - var user = new AnnotatedUser { Name = "" }; // Required name is empty - var writer = new BsonSpanWriter(new byte[1024], _keyMap); - - bool thrown = false; - try - { - mapper.Serialize(user, writer); - } - catch (ValidationException) - { - thrown = true; - } - thrown.ShouldBeTrue("Should throw ValidationException for empty Name."); - } - - /// - /// Verifies string length attribute validation is enforced. - /// - [Fact] - public void Test_StringLength_Validation() - { - var mapper = CreateMapper(); - var user = new AnnotatedUser { Name = "Jo" }; // Too short - var writer = new BsonSpanWriter(new byte[1024], _keyMap); - - bool thrown = false; - try { mapper.Serialize(user, writer); } catch (ValidationException) { thrown = true; } - thrown.ShouldBeTrue("Should throw ValidationException for Name too short."); - - user.Name = new string('A', 51); // Too long - thrown = false; - try { mapper.Serialize(user, writer); } catch (ValidationException) { thrown = true; } - thrown.ShouldBeTrue("Should throw ValidationException for Name too long."); - } - - /// - /// Verifies range attribute validation is enforced. - /// - [Fact] - public void Test_Range_Validation() - { - var mapper = CreateMapper(); - var user = new AnnotatedUser { Name = "John", Age = 200 }; // Out of range - var writer = new BsonSpanWriter(new byte[1024], _keyMap); - - bool thrown = false; - try { mapper.Serialize(user, writer); } catch (ValidationException) { thrown = true; } - thrown.ShouldBeTrue("Should throw ValidationException for Age out of range."); - } - - /// - /// Verifies column attribute maps to the expected BSON field name. - /// - [Fact] - public void Test_Column_Name_Mapping() - { - var mapper = CreateMapper(); - var user = new AnnotatedUser { Name = "John", Age = 30 }; - var buffer = new byte[1024]; - var writer = new BsonSpanWriter(buffer, _keyMap); - - mapper.Serialize(user, writer); - - var reader = new BsonSpanReader(buffer, _keys); - reader.ReadDocumentSize(); - - bool foundDisplayName = false; - while (reader.Remaining > 0) - { - var type = reader.ReadBsonType(); - if (type == BsonType.EndOfDocument) break; - - var name = reader.ReadElementHeader(); - if (name == "display_name") foundDisplayName = true; - reader.SkipValue(type); - } - - foundDisplayName.ShouldBeTrue("BSON field name should be 'display_name' from [Column] attribute."); - } - - /// - /// Verifies not-mapped attribute excludes properties from BSON serialization. - /// - [Fact] - public void Test_NotMapped_Attribute() - { - var mapper = CreateMapper(); - var user = new AnnotatedUser { Name = "John", Age = 30 }; - var buffer = new byte[1024]; - var writer = new BsonSpanWriter(buffer, _keyMap); - - mapper.Serialize(user, writer); - - var reader = new BsonSpanReader(buffer, _keys); - reader.ReadDocumentSize(); - - bool foundComputed = false; - while (reader.Remaining > 0) - { - var type = reader.ReadBsonType(); - if (type == BsonType.EndOfDocument) break; - - var name = reader.ReadElementHeader(); - if (name == "ComputedInfo") foundComputed = true; - reader.SkipValue(type); - } - - foundComputed.ShouldBeFalse("ComputedInfo should not be mapped to BSON."); + _keyMap[key] = id; + _keys[id] = key; + id++; } } -} + + // Use full path for mapper until we are sure of the namespace + private ZB_MOM_WW_CBDD_Shared_AnnotatedUserMapper CreateMapper() + { + return new ZB_MOM_WW_CBDD_Shared_AnnotatedUserMapper(); + } + + /// + /// Verifies table attribute mapping resolves the expected collection name. + /// + [Fact] + public void Test_Table_Attribute_Mapping() + { + // Verify that the generated mapper has the correct collection name + var mapper = CreateMapper(); + mapper.CollectionName.ShouldBe("test.custom_users"); + } + + /// + /// Verifies required attribute validation is enforced. + /// + [Fact] + public void Test_Required_Validation() + { + var mapper = CreateMapper(); + var user = new AnnotatedUser { Name = "" }; // Required name is empty + var writer = new BsonSpanWriter(new byte[1024], _keyMap); + + var thrown = false; + try + { + mapper.Serialize(user, writer); + } + catch (ValidationException) + { + thrown = true; + } + + thrown.ShouldBeTrue("Should throw ValidationException for empty Name."); + } + + /// + /// Verifies string length attribute validation is enforced. + /// + [Fact] + public void Test_StringLength_Validation() + { + var mapper = CreateMapper(); + var user = new AnnotatedUser { Name = "Jo" }; // Too short + var writer = new BsonSpanWriter(new byte[1024], _keyMap); + + var thrown = false; + try + { + mapper.Serialize(user, writer); + } + catch (ValidationException) + { + thrown = true; + } + + thrown.ShouldBeTrue("Should throw ValidationException for Name too short."); + + user.Name = new string('A', 51); // Too long + thrown = false; + try + { + mapper.Serialize(user, writer); + } + catch (ValidationException) + { + thrown = true; + } + + thrown.ShouldBeTrue("Should throw ValidationException for Name too long."); + } + + /// + /// Verifies range attribute validation is enforced. + /// + [Fact] + public void Test_Range_Validation() + { + var mapper = CreateMapper(); + var user = new AnnotatedUser { Name = "John", Age = 200 }; // Out of range + var writer = new BsonSpanWriter(new byte[1024], _keyMap); + + var thrown = false; + try + { + mapper.Serialize(user, writer); + } + catch (ValidationException) + { + thrown = true; + } + + thrown.ShouldBeTrue("Should throw ValidationException for Age out of range."); + } + + /// + /// Verifies column attribute maps to the expected BSON field name. + /// + [Fact] + public void Test_Column_Name_Mapping() + { + var mapper = CreateMapper(); + var user = new AnnotatedUser { Name = "John", Age = 30 }; + var buffer = new byte[1024]; + var writer = new BsonSpanWriter(buffer, _keyMap); + + mapper.Serialize(user, writer); + + var reader = new BsonSpanReader(buffer, _keys); + reader.ReadDocumentSize(); + + var foundDisplayName = false; + while (reader.Remaining > 0) + { + var type = reader.ReadBsonType(); + if (type == BsonType.EndOfDocument) break; + + string name = reader.ReadElementHeader(); + if (name == "display_name") foundDisplayName = true; + reader.SkipValue(type); + } + + foundDisplayName.ShouldBeTrue("BSON field name should be 'display_name' from [Column] attribute."); + } + + /// + /// Verifies not-mapped attribute excludes properties from BSON serialization. + /// + [Fact] + public void Test_NotMapped_Attribute() + { + var mapper = CreateMapper(); + var user = new AnnotatedUser { Name = "John", Age = 30 }; + var buffer = new byte[1024]; + var writer = new BsonSpanWriter(buffer, _keyMap); + + mapper.Serialize(user, writer); + + var reader = new BsonSpanReader(buffer, _keys); + reader.ReadDocumentSize(); + + var foundComputed = false; + while (reader.Remaining > 0) + { + var type = reader.ReadBsonType(); + if (type == BsonType.EndOfDocument) break; + + string name = reader.ReadElementHeader(); + if (name == "ComputedInfo") foundComputed = true; + reader.SkipValue(type); + } + + foundComputed.ShouldBeFalse("ComputedInfo should not be mapped to BSON."); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/CircularReferenceTests.cs b/tests/CBDD.Tests/Schema/CircularReferenceTests.cs index ad86b4a..6d66339 100755 --- a/tests/CBDD.Tests/Schema/CircularReferenceTests.cs +++ b/tests/CBDD.Tests/Schema/CircularReferenceTests.cs @@ -1,44 +1,38 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Tests; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; /// -/// Tests for circular references and N-N relationships -/// Validates that the source generator handles: -/// 1. Self-referencing entities using ObjectId references (Employee → ManagerId, DirectReportIds) -/// 2. N-N via referencing with ObjectIds (CategoryRef/ProductRef) - BEST PRACTICE -/// -/// Note: Bidirectional embedding (Category ↔ Product with full objects) is NOT supported -/// by the source generator and is an anti-pattern for document databases. -/// Use referencing (ObjectIds) instead for N-N relationships. +/// Tests for circular references and N-N relationships +/// Validates that the source generator handles: +/// 1. Self-referencing entities using ObjectId references (Employee → ManagerId, DirectReportIds) +/// 2. N-N via referencing with ObjectIds (CategoryRef/ProductRef) - BEST PRACTICE +/// Note: Bidirectional embedding (Category ↔ Product with full objects) is NOT supported +/// by the source generator and is an anti-pattern for document databases. +/// Use referencing (ObjectIds) instead for N-N relationships. /// public class CircularReferenceTests : IDisposable { + private readonly TestDbContext _context; private readonly string _dbPath; - private readonly Shared.TestDbContext _context; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public CircularReferenceTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_circular_test_{Guid.NewGuid()}"); - _context = new Shared.TestDbContext(_dbPath); + _context = new TestDbContext(_dbPath); } /// - /// Executes Dispose. + /// Executes Dispose. /// public void Dispose() { _context?.Dispose(); - if (Directory.Exists(_dbPath)) - { - Directory.Delete(_dbPath, true); - } + if (Directory.Exists(_dbPath)) Directory.Delete(_dbPath, true); } // ======================================== @@ -46,7 +40,7 @@ public class CircularReferenceTests : IDisposable // ======================================== /// - /// Executes SelfReference_InsertAndQuery_ShouldWork. + /// Executes SelfReference_InsertAndQuery_ShouldWork. /// [Fact] public void SelfReference_InsertAndQuery_ShouldWork() @@ -125,7 +119,7 @@ public class CircularReferenceTests : IDisposable } /// - /// Executes SelfReference_UpdateDirectReports_ShouldPersist. + /// Executes SelfReference_UpdateDirectReports_ShouldPersist. /// [Fact] public void SelfReference_UpdateDirectReports_ShouldPersist() @@ -177,7 +171,7 @@ public class CircularReferenceTests : IDisposable } /// - /// Executes SelfReference_QueryByManagerId_ShouldWork. + /// Executes SelfReference_QueryByManagerId_ShouldWork. /// [Fact] public void SelfReference_QueryByManagerId_ShouldWork() @@ -230,7 +224,7 @@ public class CircularReferenceTests : IDisposable // ======================================== /// - /// Executes NtoNReferencing_InsertAndQuery_ShouldWork. + /// Executes NtoNReferencing_InsertAndQuery_ShouldWork. /// [Fact] public void NtoNReferencing_InsertAndQuery_ShouldWork() @@ -298,7 +292,7 @@ public class CircularReferenceTests : IDisposable } /// - /// Executes NtoNReferencing_UpdateRelationships_ShouldPersist. + /// Executes NtoNReferencing_UpdateRelationships_ShouldPersist. /// [Fact] public void NtoNReferencing_UpdateRelationships_ShouldPersist() @@ -358,7 +352,7 @@ public class CircularReferenceTests : IDisposable } /// - /// Executes NtoNReferencing_DocumentSize_RemainSmall. + /// Executes NtoNReferencing_DocumentSize_RemainSmall. /// [Fact] public void NtoNReferencing_DocumentSize_RemainSmall() @@ -390,7 +384,7 @@ public class CircularReferenceTests : IDisposable } /// - /// Executes NtoNReferencing_QueryByProductId_ShouldWork. + /// Executes NtoNReferencing_QueryByProductId_ShouldWork. /// [Fact] public void NtoNReferencing_QueryByProductId_ShouldWork() @@ -428,4 +422,4 @@ public class CircularReferenceTests : IDisposable categoriesWithProduct.ShouldContain(c => c.Name == "Category 1"); categoriesWithProduct.ShouldContain(c => c.Name == "Category 2"); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/NullableStringIdTests.cs b/tests/CBDD.Tests/Schema/NullableStringIdTests.cs index 27e678a..a0b2f16 100755 --- a/tests/CBDD.Tests/Schema/NullableStringIdTests.cs +++ b/tests/CBDD.Tests/Schema/NullableStringIdTests.cs @@ -1,170 +1,169 @@ using ZB.MOM.WW.CBDD.Shared; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +/// +/// Tests for entities with nullable string Id (like UuidEntity scenario from CleanCore) +/// This reproduces the bug where the generator incorrectly chose ObjectIdMapperBase +/// instead of StringMapperBase for inherited nullable string Id properties +/// +public class NullableStringIdTests : IDisposable { + private const string DbPath = "nullable_string_id.db"; + /// - /// Tests for entities with nullable string Id (like UuidEntity scenario from CleanCore) - /// This reproduces the bug where the generator incorrectly chose ObjectIdMapperBase - /// instead of StringMapperBase for inherited nullable string Id properties + /// Initializes a new instance of the class. /// - public class NullableStringIdTests : System.IDisposable + public NullableStringIdTests() { - private const string DbPath = "nullable_string_id.db"; - - /// - /// Initializes a new instance of the class. - /// - public NullableStringIdTests() - { - if (File.Exists(DbPath)) File.Delete(DbPath); - } - - /// - /// Disposes test resources. - /// - public void Dispose() - { - if (File.Exists(DbPath)) File.Delete(DbPath); - } - - /// - /// Verifies the mock counter collection is initialized. - /// - [Fact] - public void MockCounter_Collection_IsInitialized() - { - using var db = new Shared.TestDbContext(DbPath); - - // Verify Collection is not null (initialized by generated method) - db.MockCounters.ShouldNotBeNull(); - } - - /// - /// Verifies insert and find-by-id operations work for string identifiers. - /// - [Fact] - public void MockCounter_Insert_And_FindById_Works() - { - using var db = new Shared.TestDbContext(DbPath); - - var counter = new MockCounter("test-id-123") - { - Name = "TestCounter", - Value = 42 - }; - - // Insert should work with string Id - db.MockCounters.Insert(counter); - - // FindById should retrieve the entity - var stored = db.MockCounters.FindById("test-id-123"); - stored.ShouldNotBeNull(); - stored.Id.ShouldBe("test-id-123"); - stored.Name.ShouldBe("TestCounter"); - stored.Value.ShouldBe(42); - } - - /// - /// Verifies update operations work for string identifiers. - /// - [Fact] - public void MockCounter_Update_Works() - { - using var db = new Shared.TestDbContext(DbPath); - - var counter = new MockCounter("update-test") - { - Name = "Original", - Value = 10 - }; - - db.MockCounters.Insert(counter); - - // Update the entity - counter.Name = "Updated"; - counter.Value = 20; - db.MockCounters.Update(counter); - - // Verify update - var updated = db.MockCounters.FindById("update-test"); - updated.ShouldNotBeNull(); - updated.Name.ShouldBe("Updated"); - updated.Value.ShouldBe(20); - } - - /// - /// Verifies delete operations work for string identifiers. - /// - [Fact] - public void MockCounter_Delete_Works() - { - using var db = new Shared.TestDbContext(DbPath); - - var counter = new MockCounter("delete-test") - { - Name = "ToDelete", - Value = 99 - }; - - db.MockCounters.Insert(counter); - db.MockCounters.FindById("delete-test").ShouldNotBeNull(); - - // Delete the entity - db.MockCounters.Delete("delete-test"); - - // Verify deletion - var deleted = db.MockCounters.FindById("delete-test"); - deleted.ShouldBeNull(); - } - - /// - /// Verifies query operations work for string identifiers. - /// - [Fact] - public void MockCounter_Query_Works() - { - using var db = new Shared.TestDbContext(DbPath); - - db.MockCounters.Insert(new MockCounter("q1") { Name = "First", Value = 100 }); - db.MockCounters.Insert(new MockCounter("q2") { Name = "Second", Value = 200 }); - db.MockCounters.Insert(new MockCounter("q3") { Name = "Third", Value = 150 }); - - // Query all - var all = db.MockCounters.AsQueryable().ToList(); - all.Count.ShouldBe(3); - - // Query with condition - var highValues = db.MockCounters.AsQueryable() - .Where(c => c.Value > 150) - .ToList(); - - highValues.Count().ShouldBe(1); - highValues[0].Name.ShouldBe("Second"); - } - - /// - /// Verifies inherited string identifiers are stored and retrieved correctly. - /// - [Fact] - public void MockCounter_InheritedId_IsStoredCorrectly() - { - using var db = new Shared.TestDbContext(DbPath); - - // Test that the inherited nullable string Id from MockBaseEntity works correctly - var counter = new MockCounter("inherited-id-test") - { - Name = "Inherited", - Value = 777 - }; - - db.MockCounters.Insert(counter); - - var stored = db.MockCounters.FindById("inherited-id-test"); - stored.ShouldNotBeNull(); - - // Verify the Id is correctly stored and retrieved through inheritance - stored.Id.ShouldBe("inherited-id-test"); - stored.Id.ShouldBeOfType(); - } + if (File.Exists(DbPath)) File.Delete(DbPath); } -} + + /// + /// Disposes test resources. + /// + public void Dispose() + { + if (File.Exists(DbPath)) File.Delete(DbPath); + } + + /// + /// Verifies the mock counter collection is initialized. + /// + [Fact] + public void MockCounter_Collection_IsInitialized() + { + using var db = new TestDbContext(DbPath); + + // Verify Collection is not null (initialized by generated method) + db.MockCounters.ShouldNotBeNull(); + } + + /// + /// Verifies insert and find-by-id operations work for string identifiers. + /// + [Fact] + public void MockCounter_Insert_And_FindById_Works() + { + using var db = new TestDbContext(DbPath); + + var counter = new MockCounter("test-id-123") + { + Name = "TestCounter", + Value = 42 + }; + + // Insert should work with string Id + db.MockCounters.Insert(counter); + + // FindById should retrieve the entity + var stored = db.MockCounters.FindById("test-id-123"); + stored.ShouldNotBeNull(); + stored.Id.ShouldBe("test-id-123"); + stored.Name.ShouldBe("TestCounter"); + stored.Value.ShouldBe(42); + } + + /// + /// Verifies update operations work for string identifiers. + /// + [Fact] + public void MockCounter_Update_Works() + { + using var db = new TestDbContext(DbPath); + + var counter = new MockCounter("update-test") + { + Name = "Original", + Value = 10 + }; + + db.MockCounters.Insert(counter); + + // Update the entity + counter.Name = "Updated"; + counter.Value = 20; + db.MockCounters.Update(counter); + + // Verify update + var updated = db.MockCounters.FindById("update-test"); + updated.ShouldNotBeNull(); + updated.Name.ShouldBe("Updated"); + updated.Value.ShouldBe(20); + } + + /// + /// Verifies delete operations work for string identifiers. + /// + [Fact] + public void MockCounter_Delete_Works() + { + using var db = new TestDbContext(DbPath); + + var counter = new MockCounter("delete-test") + { + Name = "ToDelete", + Value = 99 + }; + + db.MockCounters.Insert(counter); + db.MockCounters.FindById("delete-test").ShouldNotBeNull(); + + // Delete the entity + db.MockCounters.Delete("delete-test"); + + // Verify deletion + var deleted = db.MockCounters.FindById("delete-test"); + deleted.ShouldBeNull(); + } + + /// + /// Verifies query operations work for string identifiers. + /// + [Fact] + public void MockCounter_Query_Works() + { + using var db = new TestDbContext(DbPath); + + db.MockCounters.Insert(new MockCounter("q1") { Name = "First", Value = 100 }); + db.MockCounters.Insert(new MockCounter("q2") { Name = "Second", Value = 200 }); + db.MockCounters.Insert(new MockCounter("q3") { Name = "Third", Value = 150 }); + + // Query all + var all = db.MockCounters.AsQueryable().ToList(); + all.Count.ShouldBe(3); + + // Query with condition + var highValues = db.MockCounters.AsQueryable() + .Where(c => c.Value > 150) + .ToList(); + + highValues.Count().ShouldBe(1); + highValues[0].Name.ShouldBe("Second"); + } + + /// + /// Verifies inherited string identifiers are stored and retrieved correctly. + /// + [Fact] + public void MockCounter_InheritedId_IsStoredCorrectly() + { + using var db = new TestDbContext(DbPath); + + // Test that the inherited nullable string Id from MockBaseEntity works correctly + var counter = new MockCounter("inherited-id-test") + { + Name = "Inherited", + Value = 777 + }; + + db.MockCounters.Insert(counter); + + var stored = db.MockCounters.FindById("inherited-id-test"); + stored.ShouldNotBeNull(); + + // Verify the Id is correctly stored and retrieved through inheritance + stored.Id.ShouldBe("inherited-id-test"); + stored.Id.ShouldBeOfType(); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/SchemaPersistenceTests.cs b/tests/CBDD.Tests/Schema/SchemaPersistenceTests.cs index 8bdbac6..3431317 100755 --- a/tests/CBDD.Tests/Schema/SchemaPersistenceTests.cs +++ b/tests/CBDD.Tests/Schema/SchemaPersistenceTests.cs @@ -1,33 +1,29 @@ -using System; -using System.IO; -using System.Linq; -using Xunit; +using System.Collections.Concurrent; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Bson.Schema; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Indexing; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; +using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Shared; +using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; namespace ZB.MOM.WW.CBDD.Tests; public class SchemaPersistenceTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public SchemaPersistenceTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"schema_test_{Guid.NewGuid()}.db"); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Disposes test resources and removes temporary files. + /// Disposes test resources and removes temporary files. /// public void Dispose() { @@ -36,7 +32,7 @@ public class SchemaPersistenceTests : IDisposable } /// - /// Verifies BSON schema serialization and deserialization round-trips correctly. + /// Verifies BSON schema serialization and deserialization round-trips correctly. /// [Fact] public void BsonSchema_Serialization_RoundTrip() @@ -65,12 +61,16 @@ public class SchemaPersistenceTests : IDisposable }; var buffer = new byte[1024]; - var keyMap = new System.Collections.Concurrent.ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); - var keys = new System.Collections.Concurrent.ConcurrentDictionary(); + var keyMap = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + var keys = new ConcurrentDictionary(); // Manual registration for schema keys ushort id = 1; - foreach (var k in new[] { "person", "id", "name", "age", "address", "city", "fields", "title", "type", "isnullable", "nestedschema", "t", "v", "f", "n", "b", "s", "a", "_v", "0", "1", "2", "3", "4", "5" }) + foreach (string k in new[] + { + "person", "id", "name", "age", "address", "city", "fields", "title", "type", "isnullable", + "nestedschema", "t", "v", "f", "n", "b", "s", "a", "_v", "0", "1", "2", "3", "4", "5" + }) { keyMap[k] = id; keys[id] = k; @@ -91,7 +91,7 @@ public class SchemaPersistenceTests : IDisposable } /// - /// Verifies collection metadata is persisted and reloaded correctly. + /// Verifies collection metadata is persisted and reloaded correctly. /// [Fact] public void StorageEngine_Collections_Metadata_Persistence() @@ -102,7 +102,8 @@ public class SchemaPersistenceTests : IDisposable PrimaryRootPageId = 10, SchemaRootPageId = 20 }; - meta.Indexes.Add(new IndexMetadata { Name = "age", IsUnique = false, Type = IndexType.BTree, PropertyPaths = ["Age"] }); + meta.Indexes.Add(new IndexMetadata + { Name = "age", IsUnique = false, Type = IndexType.BTree, PropertyPaths = ["Age"] }); _db.Storage.SaveCollectionMetadata(meta); @@ -116,38 +117,48 @@ public class SchemaPersistenceTests : IDisposable } /// - /// Verifies schema versioning appends new schema versions correctly. + /// Verifies schema versioning appends new schema versions correctly. /// [Fact] public void StorageEngine_Schema_Versioning() { - var schema1 = new BsonSchema { Title = "V1", Fields = { new BsonField { Name = "f1", Type = BsonType.String } } }; - var schema2 = new BsonSchema { Title = "V2", Fields = { new BsonField { Name = "f1", Type = BsonType.String }, new BsonField { Name = "f2", Type = BsonType.Int32 } } }; + var schema1 = new BsonSchema + { Title = "V1", Fields = { new BsonField { Name = "f1", Type = BsonType.String } } }; + var schema2 = new BsonSchema + { + Title = "V2", + Fields = + { + new BsonField { Name = "f1", Type = BsonType.String }, + new BsonField { Name = "f2", Type = BsonType.Int32 } + } + }; - var rootId = _db.Storage.AppendSchema(0, schema1); + uint rootId = _db.Storage.AppendSchema(0, schema1); rootId.ShouldNotBe(0u); var schemas = _db.Storage.GetSchemas(rootId); schemas.Count().ShouldBe(1); schemas[0].Title.ShouldBe("V1"); - var updatedRoot = _db.Storage.AppendSchema(rootId, schema2); + uint updatedRoot = _db.Storage.AppendSchema(rootId, schema2); updatedRoot.ShouldBe(rootId); schemas = _db.Storage.GetSchemas(rootId); - schemas.Count.ShouldBe(2, $"Expected 2 schemas but found {schemas.Count}. Titles: {(schemas.Count > 0 ? string.Join(", ", schemas.Select(s => s.Title)) : "None")}"); + schemas.Count.ShouldBe(2, + $"Expected 2 schemas but found {schemas.Count}. Titles: {(schemas.Count > 0 ? string.Join(", ", schemas.Select(s => s.Title)) : "None")}"); schemas[0].Title.ShouldBe("V1"); schemas[1].Title.ShouldBe("V2"); } /// - /// Verifies collection startup integrates schema versioning behavior. + /// Verifies collection startup integrates schema versioning behavior. /// [Fact] public void DocumentCollection_Integrates_Schema_Versioning_On_Startup() { // Use a dedicated database for this test to avoid schema pollution from _db - var testDbPath = Path.Combine(Path.GetTempPath(), $"schema_versioning_test_{Guid.NewGuid()}.db"); + string testDbPath = Path.Combine(Path.GetTempPath(), $"schema_versioning_test_{Guid.NewGuid()}.db"); try { @@ -155,7 +166,7 @@ public class SchemaPersistenceTests : IDisposable var schema1 = mapper1.GetSchema(); // 1. First startup - create DB and initialize Person collection - using (var db1 = new Shared.TestDbContext(testDbPath)) + using (var db1 = new TestDbContext(testDbPath)) { // Access only People collection to avoid initializing others var coll = db1.People; @@ -171,7 +182,7 @@ public class SchemaPersistenceTests : IDisposable } // 2. Restart with SAME schema (should NOT append) - using (var db2 = new Shared.TestDbContext(testDbPath)) + using (var db2 = new TestDbContext(testDbPath)) { var coll = db2.People; var meta = db2.Storage.GetCollectionMetadata("people_collection"); @@ -186,7 +197,7 @@ public class SchemaPersistenceTests : IDisposable // Since we can't change the actual Person class at runtime, this test verifies // that the same schema doesn't get re-appended. // A real-world scenario would involve deploying a new mapper version. - using (var db3 = new Shared.TestDbContext(testDbPath)) + using (var db3 = new TestDbContext(testDbPath)) { var coll = db3.People; var meta = db3.Storage.GetCollectionMetadata("people_collection"); @@ -205,7 +216,7 @@ public class SchemaPersistenceTests : IDisposable } /// - /// Verifies persisted documents include the schema version field. + /// Verifies persisted documents include the schema version field. /// [Fact] public void Document_Contains_Schema_Version_Field() @@ -214,7 +225,7 @@ public class SchemaPersistenceTests : IDisposable using (var coll = _db.People) { var person = new Person { Name = "John" }; - var id = coll.Insert(person); + int id = coll.Insert(person); _db.SaveChanges(); coll.Count().ShouldBe(1); @@ -232,7 +243,7 @@ public class SchemaPersistenceTests : IDisposable // Read raw bytes from page var pageBuffer = new byte[_db.Storage.PageSize]; _db.Storage.ReadPage(location.PageId, 0, pageBuffer); - var slotOffset = SlottedPageHeader.Size + (location.SlotIndex * SlotEntry.Size); + int slotOffset = SlottedPageHeader.Size + location.SlotIndex * SlotEntry.Size; var slot = SlotEntry.ReadFrom(pageBuffer.AsSpan(slotOffset)); var docData = pageBuffer.AsSpan(slot.Offset, slot.Length); @@ -241,7 +252,7 @@ public class SchemaPersistenceTests : IDisposable // Look for _v (BsonType.Int32 + 2-byte ID) ushort vId = _db.Storage.GetKeyMap()["_v"]; - string vIdHex = vId.ToString("X4"); + var vIdHex = vId.ToString("X4"); // Reverse endian for hex string check (ushort is LE) string vIdHexLE = vIdHex.Substring(2, 2) + vIdHex.Substring(0, 2); string pattern = "10" + vIdHexLE; @@ -255,4 +266,4 @@ public class SchemaPersistenceTests : IDisposable valueHex.ShouldBe("01000000"); } } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/SchemaTests.cs b/tests/CBDD.Tests/Schema/SchemaTests.cs index d3c5a2b..fb28b4f 100755 --- a/tests/CBDD.Tests/Schema/SchemaTests.cs +++ b/tests/CBDD.Tests/Schema/SchemaTests.cs @@ -1,23 +1,23 @@ +using System.Collections.Concurrent; using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -using System.Text; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class SchemaTests { - private static readonly System.Collections.Concurrent.ConcurrentDictionary _testKeyMap = new(StringComparer.OrdinalIgnoreCase); + private static readonly ConcurrentDictionary _testKeyMap = new(StringComparer.OrdinalIgnoreCase); + static SchemaTests() { ushort id = 1; - foreach (var k in new[] { "_id", "name", "mainaddress", "otheraddresses", "tags", "secret", "street", "city" }) _testKeyMap[k] = id++; + foreach (string k in new[] + { "_id", "name", "mainaddress", "otheraddresses", "tags", "secret", "street", "city" }) + _testKeyMap[k] = id++; } /// - /// Executes UsedKeys_ShouldReturnAllKeys. + /// Executes UsedKeys_ShouldReturnAllKeys. /// [Fact] public void UsedKeys_ShouldReturnAllKeys() @@ -33,11 +33,10 @@ public class SchemaTests keys.ShouldContain("secret"); keys.ShouldContain("street"); keys.ShouldContain("city"); - } /// - /// Executes GetSchema_ShouldReturnBsonSchema. + /// Executes GetSchema_ShouldReturnBsonSchema. /// [Fact] public void GetSchema_ShouldReturnBsonSchema() @@ -60,4 +59,4 @@ public class SchemaTests // Address in MockEntities has City (Nested) addressField.NestedSchema.Fields.ShouldContain(f => f.Name == "city"); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/TemporalTypesTests.cs b/tests/CBDD.Tests/Schema/TemporalTypesTests.cs index 043b639..f6d6d67 100755 --- a/tests/CBDD.Tests/Schema/TemporalTypesTests.cs +++ b/tests/CBDD.Tests/Schema/TemporalTypesTests.cs @@ -1,253 +1,250 @@ -using System; using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Tests; -using Xunit; -namespace ZB.MOM.WW.CBDD.Tests +namespace ZB.MOM.WW.CBDD.Tests; + +public class TemporalTypesTests : IDisposable { - public class TemporalTypesTests : IDisposable + private readonly TestDbContext _db; + private readonly string _dbPath; + + /// + /// Initializes a new instance of the class. + /// + public TemporalTypesTests() { - private readonly Shared.TestDbContext _db; - private readonly string _dbPath; - - /// - /// Initializes a new instance of the class. - /// - public TemporalTypesTests() - { - _dbPath = $"temporal_test_{Guid.NewGuid()}.db"; - _db = new Shared.TestDbContext(_dbPath); - } - - /// - /// Releases test resources. - /// - public void Dispose() - { - _db?.Dispose(); - if (File.Exists(_dbPath)) - File.Delete(_dbPath); - } - - /// - /// Verifies temporal entity collection initialization. - /// - [Fact] - public void TemporalEntity_Collection_IsInitialized() - { - _db.TemporalEntities.ShouldNotBeNull(); - } - - /// - /// Verifies temporal fields round-trip through insert and lookup. - /// - [Fact] - public void TemporalEntity_Insert_And_FindById_Works() - { - // Arrange - var now = DateTime.UtcNow; - var offset = DateTimeOffset.UtcNow; - var duration = TimeSpan.FromHours(5.5); - var birthDate = new DateOnly(1990, 5, 15); - var openingTime = new TimeOnly(9, 30, 0); - - var entity = new TemporalEntity - { - Id = ObjectId.NewObjectId(), - Name = "Test Entity", - CreatedAt = now, - UpdatedAt = offset, - LastAccessedAt = offset.AddDays(1), - Duration = duration, - OptionalDuration = TimeSpan.FromMinutes(30), - BirthDate = birthDate, - Anniversary = new DateOnly(2020, 6, 10), - OpeningTime = openingTime, - ClosingTime = new TimeOnly(18, 0, 0) - }; - - // Act - _db.TemporalEntities.Insert(entity); - var retrieved = _db.TemporalEntities.FindById(entity.Id); - - // Assert - retrieved.ShouldNotBeNull(); - retrieved.Name.ShouldBe(entity.Name); - - // DateTime comparison (allowing some millisecond precision loss) - (retrieved.CreatedAt.Ticks / 10000).ShouldBe(entity.CreatedAt.Ticks / 10000); // millisecond precision - - // DateTimeOffset comparison - (retrieved.UpdatedAt.UtcDateTime.Ticks / 10000).ShouldBe(entity.UpdatedAt.UtcDateTime.Ticks / 10000); - retrieved.LastAccessedAt.ShouldNotBeNull(); - (retrieved.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000).ShouldBe(entity.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000); - - // TimeSpan comparison - retrieved.Duration.ShouldBe(entity.Duration); - retrieved.OptionalDuration.ShouldNotBeNull(); - retrieved.OptionalDuration!.Value.ShouldBe(entity.OptionalDuration!.Value); - - // DateOnly comparison - retrieved.BirthDate.ShouldBe(entity.BirthDate); - retrieved.Anniversary.ShouldNotBeNull(); - retrieved.Anniversary!.Value.ShouldBe(entity.Anniversary!.Value); - - // TimeOnly comparison - retrieved.OpeningTime.ShouldBe(entity.OpeningTime); - retrieved.ClosingTime.ShouldNotBeNull(); - retrieved.ClosingTime!.Value.ShouldBe(entity.ClosingTime!.Value); - } - - /// - /// Verifies insert behavior when optional temporal fields are null. - /// - [Fact] - public void TemporalEntity_Insert_WithNullOptionalFields_Works() - { - // Arrange - var entity = new TemporalEntity - { - Id = ObjectId.NewObjectId(), - Name = "Minimal Entity", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Duration = TimeSpan.FromHours(1), - BirthDate = new DateOnly(1985, 3, 20), - OpeningTime = new TimeOnly(8, 0, 0), - // Optional fields left null - LastAccessedAt = null, - OptionalDuration = null, - Anniversary = null, - ClosingTime = null - }; - - // Act - _db.TemporalEntities.Insert(entity); - var retrieved = _db.TemporalEntities.FindById(entity.Id); - - // Assert - retrieved.ShouldNotBeNull(); - retrieved.Name.ShouldBe(entity.Name); - retrieved.LastAccessedAt.ShouldBeNull(); - retrieved.OptionalDuration.ShouldBeNull(); - retrieved.Anniversary.ShouldBeNull(); - retrieved.ClosingTime.ShouldBeNull(); - } - - /// - /// Verifies temporal entity updates persist correctly. - /// - [Fact] - public void TemporalEntity_Update_Works() - { - // Arrange - var entity = new TemporalEntity - { - Id = ObjectId.NewObjectId(), - Name = "Original", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Duration = TimeSpan.FromHours(1), - BirthDate = new DateOnly(1990, 1, 1), - OpeningTime = new TimeOnly(9, 0, 0) - }; - - _db.TemporalEntities.Insert(entity); - - // Act - Update temporal fields - entity.Name = "Updated"; - entity.UpdatedAt = DateTimeOffset.UtcNow.AddDays(1); - entity.Duration = TimeSpan.FromHours(2); - entity.BirthDate = new DateOnly(1991, 2, 2); - entity.OpeningTime = new TimeOnly(10, 0, 0); - - _db.TemporalEntities.Update(entity); - var retrieved = _db.TemporalEntities.FindById(entity.Id); - - // Assert - retrieved.ShouldNotBeNull(); - retrieved.Name.ShouldBe("Updated"); - retrieved.Duration.ShouldBe(entity.Duration); - retrieved.BirthDate.ShouldBe(entity.BirthDate); - retrieved.OpeningTime.ShouldBe(entity.OpeningTime); - } - - /// - /// Verifies querying temporal entities by temporal fields. - /// - [Fact] - public void TemporalEntity_Query_Works() - { - // Arrange - var birthDate1 = new DateOnly(1990, 1, 1); - var birthDate2 = new DateOnly(1995, 6, 15); - - var entity1 = new TemporalEntity - { - Id = ObjectId.NewObjectId(), - Name = "Person 1", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Duration = TimeSpan.FromHours(1), - BirthDate = birthDate1, - OpeningTime = new TimeOnly(9, 0, 0) - }; - - var entity2 = new TemporalEntity - { - Id = ObjectId.NewObjectId(), - Name = "Person 2", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Duration = TimeSpan.FromHours(2), - BirthDate = birthDate2, - OpeningTime = new TimeOnly(10, 0, 0) - }; - - _db.TemporalEntities.Insert(entity1); - _db.TemporalEntities.Insert(entity2); - - // Act - var results = _db.TemporalEntities.AsQueryable() - .Where(e => e.BirthDate == birthDate1) - .ToList(); - - // Assert - results.Count().ShouldBe(1); - results[0].Name.ShouldBe("Person 1"); - } - - /// - /// Verifies edge-case TimeSpan values are persisted correctly. - /// - [Fact] - public void TimeSpan_EdgeCases_Work() - { - // Arrange - Test various TimeSpan values - var entity = new TemporalEntity - { - Id = ObjectId.NewObjectId(), - Name = "TimeSpan Test", - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTimeOffset.UtcNow, - Duration = TimeSpan.Zero, - OptionalDuration = TimeSpan.MaxValue, - BirthDate = DateOnly.MinValue, - OpeningTime = TimeOnly.MinValue - }; - - // Act - _db.TemporalEntities.Insert(entity); - var retrieved = _db.TemporalEntities.FindById(entity.Id); - - // Assert - retrieved.ShouldNotBeNull(); - retrieved.Duration.ShouldBe(TimeSpan.Zero); - retrieved.OptionalDuration.ShouldNotBeNull(); - retrieved.OptionalDuration!.Value.ShouldBe(TimeSpan.MaxValue); - retrieved.BirthDate.ShouldBe(DateOnly.MinValue); - retrieved.OpeningTime.ShouldBe(TimeOnly.MinValue); - } + _dbPath = $"temporal_test_{Guid.NewGuid()}.db"; + _db = new TestDbContext(_dbPath); } -} + + /// + /// Releases test resources. + /// + public void Dispose() + { + _db?.Dispose(); + if (File.Exists(_dbPath)) + File.Delete(_dbPath); + } + + /// + /// Verifies temporal entity collection initialization. + /// + [Fact] + public void TemporalEntity_Collection_IsInitialized() + { + _db.TemporalEntities.ShouldNotBeNull(); + } + + /// + /// Verifies temporal fields round-trip through insert and lookup. + /// + [Fact] + public void TemporalEntity_Insert_And_FindById_Works() + { + // Arrange + var now = DateTime.UtcNow; + var offset = DateTimeOffset.UtcNow; + var duration = TimeSpan.FromHours(5.5); + var birthDate = new DateOnly(1990, 5, 15); + var openingTime = new TimeOnly(9, 30, 0); + + var entity = new TemporalEntity + { + Id = ObjectId.NewObjectId(), + Name = "Test Entity", + CreatedAt = now, + UpdatedAt = offset, + LastAccessedAt = offset.AddDays(1), + Duration = duration, + OptionalDuration = TimeSpan.FromMinutes(30), + BirthDate = birthDate, + Anniversary = new DateOnly(2020, 6, 10), + OpeningTime = openingTime, + ClosingTime = new TimeOnly(18, 0, 0) + }; + + // Act + _db.TemporalEntities.Insert(entity); + var retrieved = _db.TemporalEntities.FindById(entity.Id); + + // Assert + retrieved.ShouldNotBeNull(); + retrieved.Name.ShouldBe(entity.Name); + + // DateTime comparison (allowing some millisecond precision loss) + (retrieved.CreatedAt.Ticks / 10000).ShouldBe(entity.CreatedAt.Ticks / 10000); // millisecond precision + + // DateTimeOffset comparison + (retrieved.UpdatedAt.UtcDateTime.Ticks / 10000).ShouldBe(entity.UpdatedAt.UtcDateTime.Ticks / 10000); + retrieved.LastAccessedAt.ShouldNotBeNull(); + (retrieved.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000).ShouldBe( + entity.LastAccessedAt!.Value.UtcDateTime.Ticks / 10000); + + // TimeSpan comparison + retrieved.Duration.ShouldBe(entity.Duration); + retrieved.OptionalDuration.ShouldNotBeNull(); + retrieved.OptionalDuration!.Value.ShouldBe(entity.OptionalDuration!.Value); + + // DateOnly comparison + retrieved.BirthDate.ShouldBe(entity.BirthDate); + retrieved.Anniversary.ShouldNotBeNull(); + retrieved.Anniversary!.Value.ShouldBe(entity.Anniversary!.Value); + + // TimeOnly comparison + retrieved.OpeningTime.ShouldBe(entity.OpeningTime); + retrieved.ClosingTime.ShouldNotBeNull(); + retrieved.ClosingTime!.Value.ShouldBe(entity.ClosingTime!.Value); + } + + /// + /// Verifies insert behavior when optional temporal fields are null. + /// + [Fact] + public void TemporalEntity_Insert_WithNullOptionalFields_Works() + { + // Arrange + var entity = new TemporalEntity + { + Id = ObjectId.NewObjectId(), + Name = "Minimal Entity", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Duration = TimeSpan.FromHours(1), + BirthDate = new DateOnly(1985, 3, 20), + OpeningTime = new TimeOnly(8, 0, 0), + // Optional fields left null + LastAccessedAt = null, + OptionalDuration = null, + Anniversary = null, + ClosingTime = null + }; + + // Act + _db.TemporalEntities.Insert(entity); + var retrieved = _db.TemporalEntities.FindById(entity.Id); + + // Assert + retrieved.ShouldNotBeNull(); + retrieved.Name.ShouldBe(entity.Name); + retrieved.LastAccessedAt.ShouldBeNull(); + retrieved.OptionalDuration.ShouldBeNull(); + retrieved.Anniversary.ShouldBeNull(); + retrieved.ClosingTime.ShouldBeNull(); + } + + /// + /// Verifies temporal entity updates persist correctly. + /// + [Fact] + public void TemporalEntity_Update_Works() + { + // Arrange + var entity = new TemporalEntity + { + Id = ObjectId.NewObjectId(), + Name = "Original", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Duration = TimeSpan.FromHours(1), + BirthDate = new DateOnly(1990, 1, 1), + OpeningTime = new TimeOnly(9, 0, 0) + }; + + _db.TemporalEntities.Insert(entity); + + // Act - Update temporal fields + entity.Name = "Updated"; + entity.UpdatedAt = DateTimeOffset.UtcNow.AddDays(1); + entity.Duration = TimeSpan.FromHours(2); + entity.BirthDate = new DateOnly(1991, 2, 2); + entity.OpeningTime = new TimeOnly(10, 0, 0); + + _db.TemporalEntities.Update(entity); + var retrieved = _db.TemporalEntities.FindById(entity.Id); + + // Assert + retrieved.ShouldNotBeNull(); + retrieved.Name.ShouldBe("Updated"); + retrieved.Duration.ShouldBe(entity.Duration); + retrieved.BirthDate.ShouldBe(entity.BirthDate); + retrieved.OpeningTime.ShouldBe(entity.OpeningTime); + } + + /// + /// Verifies querying temporal entities by temporal fields. + /// + [Fact] + public void TemporalEntity_Query_Works() + { + // Arrange + var birthDate1 = new DateOnly(1990, 1, 1); + var birthDate2 = new DateOnly(1995, 6, 15); + + var entity1 = new TemporalEntity + { + Id = ObjectId.NewObjectId(), + Name = "Person 1", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Duration = TimeSpan.FromHours(1), + BirthDate = birthDate1, + OpeningTime = new TimeOnly(9, 0, 0) + }; + + var entity2 = new TemporalEntity + { + Id = ObjectId.NewObjectId(), + Name = "Person 2", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Duration = TimeSpan.FromHours(2), + BirthDate = birthDate2, + OpeningTime = new TimeOnly(10, 0, 0) + }; + + _db.TemporalEntities.Insert(entity1); + _db.TemporalEntities.Insert(entity2); + + // Act + var results = _db.TemporalEntities.AsQueryable() + .Where(e => e.BirthDate == birthDate1) + .ToList(); + + // Assert + results.Count().ShouldBe(1); + results[0].Name.ShouldBe("Person 1"); + } + + /// + /// Verifies edge-case TimeSpan values are persisted correctly. + /// + [Fact] + public void TimeSpan_EdgeCases_Work() + { + // Arrange - Test various TimeSpan values + var entity = new TemporalEntity + { + Id = ObjectId.NewObjectId(), + Name = "TimeSpan Test", + CreatedAt = DateTime.UtcNow, + UpdatedAt = DateTimeOffset.UtcNow, + Duration = TimeSpan.Zero, + OptionalDuration = TimeSpan.MaxValue, + BirthDate = DateOnly.MinValue, + OpeningTime = TimeOnly.MinValue + }; + + // Act + _db.TemporalEntities.Insert(entity); + var retrieved = _db.TemporalEntities.FindById(entity.Id); + + // Assert + retrieved.ShouldNotBeNull(); + retrieved.Duration.ShouldBe(TimeSpan.Zero); + retrieved.OptionalDuration.ShouldNotBeNull(); + retrieved.OptionalDuration!.Value.ShouldBe(TimeSpan.MaxValue); + retrieved.BirthDate.ShouldBe(DateOnly.MinValue); + retrieved.OpeningTime.ShouldBe(TimeOnly.MinValue); + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Schema/VisibilityTests.cs b/tests/CBDD.Tests/Schema/VisibilityTests.cs index 57acb5d..b3de7c3 100755 --- a/tests/CBDD.Tests/Schema/VisibilityTests.cs +++ b/tests/CBDD.Tests/Schema/VisibilityTests.cs @@ -1,48 +1,11 @@ -using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; -using Xunit; -using System.Linq; namespace ZB.MOM.WW.CBDD.Tests; public class VisibilityTests { - public class VisibilityEntity - { - // Should be included - /// - /// Gets or sets the normal prop. - /// - public int NormalProp { get; set; } - - // Should be included (serialization usually writes it) - /// - /// Gets or sets the private set prop. - /// - public int PrivateSetProp { get; private set; } - - // Should be included - /// - /// Gets or sets the init prop. - /// - public int InitProp { get; init; } - - // Fields - typically included in BSON if public, but reflection need GetFields - public string PublicField = string.Empty; - - // Should NOT be included - private int _privateField; - - // Helper to set private - /// - /// Tests set private. - /// - /// Value assigned to the private field. - public void SetPrivate(int val) => _privateField = val; - } - /// - /// Tests generate schema visibility checks. + /// Tests generate schema visibility checks. /// [Fact] public void GenerateSchema_VisibilityChecks() @@ -60,4 +23,41 @@ public class VisibilityTests schema.Fields.ShouldNotContain(f => f.Name == "_privatefield"); } -} + + public class VisibilityEntity + { + // Should NOT be included + private int _privateField; + + // Fields - typically included in BSON if public, but reflection need GetFields + public string PublicField = string.Empty; + + // Should be included + /// + /// Gets or sets the normal prop. + /// + public int NormalProp { get; set; } + + // Should be included (serialization usually writes it) + /// + /// Gets or sets the private set prop. + /// + public int PrivateSetProp { get; private set; } + + // Should be included + /// + /// Gets or sets the init prop. + /// + public int InitProp { get; init; } + + // Helper to set private + /// + /// Tests set private. + /// + /// Value assigned to the private field. + public void SetPrivate(int val) + { + _privateField = val; + } + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/CheckpointModeTests.cs b/tests/CBDD.Tests/Storage/CheckpointModeTests.cs index 91c408e..4560213 100644 --- a/tests/CBDD.Tests/Storage/CheckpointModeTests.cs +++ b/tests/CBDD.Tests/Storage/CheckpointModeTests.cs @@ -1,5 +1,4 @@ using System.Reflection; -using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; @@ -9,12 +8,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class CheckpointModeTests { /// - /// Verifies default checkpoint mode truncates WAL. + /// Verifies default checkpoint mode truncates WAL. /// [Fact] public void Checkpoint_Default_ShouldUseTruncate() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); @@ -36,12 +35,12 @@ public class CheckpointModeTests } /// - /// Verifies passive mode skips when checkpoint lock is contended. + /// Verifies passive mode skips when checkpoint lock is contended. /// [Fact] public void Checkpoint_Passive_ShouldSkip_WhenLockIsContended() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); @@ -67,13 +66,13 @@ public class CheckpointModeTests } /// - /// Verifies full checkpoint applies data and appends a checkpoint marker without truncating WAL. + /// Verifies full checkpoint applies data and appends a checkpoint marker without truncating WAL. /// [Fact] public void Checkpoint_Full_ShouldAppendMarker_AndPreserveWal() { - var dbPath = NewDbPath(); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string dbPath = NewDbPath(); + string walPath = Path.ChangeExtension(dbPath, ".wal"); try { @@ -82,7 +81,7 @@ public class CheckpointModeTests db.Users.Insert(new User { Name = "checkpoint-full", Age = 50 }); db.SaveChanges(); - var walBefore = db.Storage.GetWalSize(); + long walBefore = db.Storage.GetWalSize(); walBefore.ShouldBeGreaterThan(0); var result = db.Checkpoint(CheckpointMode.Full); @@ -103,12 +102,12 @@ public class CheckpointModeTests } /// - /// Verifies restart checkpoint clears WAL and allows subsequent writes. + /// Verifies restart checkpoint clears WAL and allows subsequent writes. /// [Fact] public void Checkpoint_Restart_ShouldResetWal_AndAcceptNewWrites() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); @@ -134,12 +133,12 @@ public class CheckpointModeTests } /// - /// Verifies recovery remains deterministic after a full checkpoint boundary. + /// Verifies recovery remains deterministic after a full checkpoint boundary. /// [Fact] public void Recover_AfterFullCheckpoint_ShouldApplyLatestCommitDeterministically() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { uint pageId; @@ -182,12 +181,12 @@ public class CheckpointModeTests } /// - /// Verifies asynchronous mode-based checkpoints return expected result metadata. + /// Verifies asynchronous mode-based checkpoints return expected result metadata. /// [Fact] public async Task CheckpointAsync_Full_ShouldReturnResult() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath); @@ -213,16 +212,18 @@ public class CheckpointModeTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"checkpoint_mode_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"checkpoint_mode_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { if (File.Exists(dbPath)) File.Delete(dbPath); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/DictionaryPageTests.cs b/tests/CBDD.Tests/Storage/DictionaryPageTests.cs index 3e460cd..00f86e1 100755 --- a/tests/CBDD.Tests/Storage/DictionaryPageTests.cs +++ b/tests/CBDD.Tests/Storage/DictionaryPageTests.cs @@ -1,7 +1,5 @@ -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Storage; using System.Text; -using Xunit; +using ZB.MOM.WW.CBDD.Core.Storage; namespace ZB.MOM.WW.CBDD.Tests; @@ -10,7 +8,7 @@ public class DictionaryPageTests private const int PageSize = 16384; /// - /// Verifies dictionary page initialization sets expected defaults. + /// Verifies dictionary page initialization sets expected defaults. /// [Fact] public void Initialize_ShouldSetupEmptyPage() @@ -30,7 +28,7 @@ public class DictionaryPageTests } /// - /// Verifies insert adds entries and keeps them ordered. + /// Verifies insert adds entries and keeps them ordered. /// [Fact] public void Insert_ShouldAddEntryAndSort() @@ -65,7 +63,7 @@ public class DictionaryPageTests } /// - /// Verifies key lookup returns the expected value. + /// Verifies key lookup returns the expected value. /// [Fact] public void TryFind_ShouldReturnCorrectValue() @@ -86,7 +84,7 @@ public class DictionaryPageTests } /// - /// Verifies inserts fail when the page is full. + /// Verifies inserts fail when the page is full. /// [Fact] public void Overflow_ShouldReturnFalse_WhenFull() @@ -94,18 +92,16 @@ public class DictionaryPageTests var page = new byte[PageSize]; DictionaryPage.Initialize(page, 1); - string bigKey = new string('X', 250); + var bigKey = new string('X', 250); - int count = 0; + var count = 0; while (true) { // Use unique keys - var key = bigKey + count; + string key = bigKey + count; if (!DictionaryPage.Insert(page, key, (ushort)count)) - { // Should fail here break; - } count++; if (count > 1000) throw new ShouldAssertException("Should have filled the page much earlier"); } @@ -118,16 +114,16 @@ public class DictionaryPageTests } /// - /// Verifies global lookup finds keys across chained dictionary pages. + /// Verifies global lookup finds keys across chained dictionary pages. /// [Fact] public void Chaining_ShouldFindKeysInLinkedPages() { - var dbPath = Path.Combine(Path.GetTempPath(), $"test_dict_chain_{Guid.NewGuid()}.db"); + string dbPath = Path.Combine(Path.GetTempPath(), $"test_dict_chain_{Guid.NewGuid()}.db"); using var storage = new StorageEngine(dbPath, PageFileConfig.Default); // 1. Create First Page - var page1Id = storage.AllocatePage(); + uint page1Id = storage.AllocatePage(); var pageBuffer = new byte[storage.PageSize]; DictionaryPage.Initialize(pageBuffer, page1Id); @@ -136,7 +132,7 @@ public class DictionaryPageTests DictionaryPage.Insert(pageBuffer, "KeyA", 200); // 2. Create Second Page - var page2Id = storage.AllocatePage(); + uint page2Id = storage.AllocatePage(); var page2Buffer = new byte[storage.PageSize]; DictionaryPage.Initialize(page2Buffer, page2Id); @@ -174,18 +170,18 @@ public class DictionaryPageTests } /// - /// Verifies global enumeration returns keys across chained dictionary pages. + /// Verifies global enumeration returns keys across chained dictionary pages. /// [Fact] public void FindAllGlobal_ShouldRetrieveAllKeys() { - var dbPath = Path.Combine(Path.GetTempPath(), $"test_dict_findall_{Guid.NewGuid()}.db"); + string dbPath = Path.Combine(Path.GetTempPath(), $"test_dict_findall_{Guid.NewGuid()}.db"); using var storage = new StorageEngine(dbPath, PageFileConfig.Default); // 1. Create Chain of 3 Pages - var page1Id = storage.AllocatePage(); - var page2Id = storage.AllocatePage(); - var page3Id = storage.AllocatePage(); + uint page1Id = storage.AllocatePage(); + uint page2Id = storage.AllocatePage(); + uint page3Id = storage.AllocatePage(); var buf = new byte[storage.PageSize]; @@ -226,4 +222,4 @@ public class DictionaryPageTests if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(Path.ChangeExtension(dbPath, ".wal"))) File.Delete(Path.ChangeExtension(dbPath, ".wal")); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/DictionaryPersistenceTests.cs b/tests/CBDD.Tests/Storage/DictionaryPersistenceTests.cs index 6290517..8d68dc5 100755 --- a/tests/CBDD.Tests/Storage/DictionaryPersistenceTests.cs +++ b/tests/CBDD.Tests/Storage/DictionaryPersistenceTests.cs @@ -1,10 +1,8 @@ using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Bson.Schema; using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Storage; -using Xunit; -using System.Collections.Generic; -using System.Linq; -using ZB.MOM.WW.CBDD.Bson.Schema; +using System.Diagnostics.CodeAnalysis; namespace ZB.MOM.WW.CBDD.Tests; @@ -14,7 +12,7 @@ public class DictionaryPersistenceTests : IDisposable private readonly StorageEngine _storage; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DictionaryPersistenceTests() { @@ -23,55 +21,18 @@ public class DictionaryPersistenceTests : IDisposable } /// - /// Disposes test resources and removes temporary files. + /// Disposes test resources and removes temporary files. /// public void Dispose() { _storage.Dispose(); if (File.Exists(_dbPath)) File.Delete(_dbPath); - var walPath = Path.ChangeExtension(_dbPath, ".wal"); + string walPath = Path.ChangeExtension(_dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); } - private class MockMapper : DocumentMapperBase> - { - private readonly string _collectionName; - private readonly List _keys; - - /// - /// Initializes a new instance of the class. - /// - /// The collection name. - /// The mapper keys. - public MockMapper(string name, params string[] keys) - { - _collectionName = name; - _keys = keys.ToList(); - } - - /// - public override string CollectionName => _collectionName; - /// - public override IEnumerable UsedKeys => _keys; - - /// - public override BsonSchema GetSchema() => new BsonSchema { Title = _collectionName }; - - /// - public override ObjectId GetId(Dictionary entity) => throw new NotImplementedException(); - - /// - public override void SetId(Dictionary entity, ObjectId id) => throw new NotImplementedException(); - - /// - public override int Serialize(Dictionary entity, BsonSpanWriter writer) => throw new NotImplementedException(); - - /// - public override Dictionary Deserialize(BsonSpanReader reader) => throw new NotImplementedException(); - } - /// - /// Verifies mapper registration adds all unique dictionary keys. + /// Verifies mapper registration adds all unique dictionary keys. /// [Fact] public void RegisterMappers_Registers_All_Unique_Keys() @@ -99,7 +60,7 @@ public class DictionaryPersistenceTests : IDisposable } /// - /// Verifies dictionary keys persist across storage restarts. + /// Verifies dictionary keys persist across storage restarts. /// [Fact] public void Dictionary_Keys_Persist_Across_Restarts() @@ -107,7 +68,7 @@ public class DictionaryPersistenceTests : IDisposable var mapper = new MockMapper("Coll1", "PersistedKey"); _storage.RegisterMappers(new IDocumentMapper[] { mapper }); - var originalId = _storage.GetOrAddDictionaryEntry("PersistedKey"); + ushort originalId = _storage.GetOrAddDictionaryEntry("PersistedKey"); originalId.ShouldNotBe((ushort)0); _storage.Dispose(); @@ -115,10 +76,78 @@ public class DictionaryPersistenceTests : IDisposable // Re-open using var storage2 = new StorageEngine(_dbPath, PageFileConfig.Default); - var recoveredId = storage2.GetOrAddDictionaryEntry("PersistedKey"); + ushort recoveredId = storage2.GetOrAddDictionaryEntry("PersistedKey"); recoveredId.ShouldBe(originalId); } + /// + /// Verifies nested schema fields are registered as dictionary keys. + /// + [Fact] + public void RegisterMappers_Handles_Nested_Keys() + { + var mapper = new NestedMockMapper(); + _storage.RegisterMappers(new IDocumentMapper[] { mapper }); + + _storage.GetOrAddDictionaryEntry("Top").ShouldNotBe((ushort)0); + _storage.GetOrAddDictionaryEntry("Child").ShouldNotBe((ushort)0); + } + + [SuppressMessage("ReSharper", "All", Justification = "Test-only stub mapper; members are intentionally not used.")] + private class MockMapper : DocumentMapperBase> + { + private readonly string _collectionName; + private readonly List _keys; + + /// + /// Initializes a new instance of the class. + /// + /// The collection name. + /// The mapper keys. + public MockMapper(string name, params string[] keys) + { + _collectionName = name; + _keys = keys.ToList(); + } + + /// + public override string CollectionName => _collectionName; + + /// + public override IEnumerable UsedKeys => _keys; + + /// + public override BsonSchema GetSchema() + { + return new BsonSchema { Title = _collectionName }; + } + + /// + public override ObjectId GetId(Dictionary entity) + { + throw new NotImplementedException(); + } + + /// + public override void SetId(Dictionary entity, ObjectId id) + { + throw new NotImplementedException(); + } + + /// + public override int Serialize(Dictionary entity, BsonSpanWriter writer) + { + throw new NotImplementedException(); + } + + /// + public override Dictionary Deserialize(BsonSpanReader reader) + { + throw new NotImplementedException(); + } + } + + [SuppressMessage("ReSharper", "All", Justification = "Test-only stub mapper; members are intentionally not used.")] private class NestedMockMapper : DocumentMapperBase { /// @@ -141,28 +170,27 @@ public class DictionaryPersistenceTests : IDisposable } /// - public override ObjectId GetId(object entity) => throw new NotImplementedException(); + public override ObjectId GetId(object entity) + { + throw new NotImplementedException(); + } /// - public override void SetId(object entity, ObjectId id) => throw new NotImplementedException(); + public override void SetId(object entity, ObjectId id) + { + throw new NotImplementedException(); + } /// - public override int Serialize(object entity, BsonSpanWriter writer) => throw new NotImplementedException(); + public override int Serialize(object entity, BsonSpanWriter writer) + { + throw new NotImplementedException(); + } /// - public override object Deserialize(BsonSpanReader reader) => throw new NotImplementedException(); - } - - /// - /// Verifies nested schema fields are registered as dictionary keys. - /// - [Fact] - public void RegisterMappers_Handles_Nested_Keys() - { - var mapper = new NestedMockMapper(); - _storage.RegisterMappers(new IDocumentMapper[] { mapper }); - - _storage.GetOrAddDictionaryEntry("Top").ShouldNotBe((ushort)0); - _storage.GetOrAddDictionaryEntry("Child").ShouldNotBe((ushort)0); + public override object Deserialize(BsonSpanReader reader) + { + throw new NotImplementedException(); + } } } diff --git a/tests/CBDD.Tests/Storage/DocumentOverflowTests.cs b/tests/CBDD.Tests/Storage/DocumentOverflowTests.cs index fdd8e5d..5b27bb4 100755 --- a/tests/CBDD.Tests/Storage/DocumentOverflowTests.cs +++ b/tests/CBDD.Tests/Storage/DocumentOverflowTests.cs @@ -1,33 +1,29 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Compression; -using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; -using ZB.MOM.WW.CBDD.Shared; -using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; using System.IO.Compression; using System.IO.MemoryMappedFiles; -using Xunit; +using ZB.MOM.WW.CBDD.Bson; +using ZB.MOM.WW.CBDD.Core.Compression; +using ZB.MOM.WW.CBDD.Core.Storage; +using ZB.MOM.WW.CBDD.Shared; namespace ZB.MOM.WW.CBDD.Tests; public class DocumentOverflowTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public DocumentOverflowTests() { _dbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_{Guid.NewGuid()}.db"); // Use default PageSize (16KB) - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Releases test resources. + /// Releases test resources. /// public void Dispose() { @@ -36,7 +32,7 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies inserting a medium-sized document succeeds. + /// Verifies inserting a medium-sized document succeeds. /// [Fact] public void Insert_MediumDoc_64KB_ShouldSucceed() @@ -60,7 +56,7 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies inserting a large document succeeds. + /// Verifies inserting a large document succeeds. /// [Fact] public void Insert_LargeDoc_100KB_ShouldSucceed() @@ -83,7 +79,7 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies inserting a very large document succeeds. + /// Verifies inserting a very large document succeeds. /// [Fact] public void Insert_HugeDoc_3MB_ShouldSucceed() @@ -109,7 +105,7 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies updating from a small payload to a huge payload succeeds. + /// Verifies updating from a small payload to a huge payload succeeds. /// [Fact] public void Update_SmallToHuge_ShouldSucceed() @@ -123,7 +119,7 @@ public class DocumentOverflowTests : IDisposable var hugeString = new string('U', 3 * 1024 * 1024); user.Name = hugeString; - var updated = _db.Users.Update(user); + bool updated = _db.Users.Update(user); _db.SaveChanges(); updated.ShouldBeTrue(); @@ -133,17 +129,17 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies bulk inserts with mixed payload sizes succeed. + /// Verifies bulk inserts with mixed payload sizes succeed. /// [Fact] public void InsertBulk_MixedSizes_ShouldSucceed() { var users = new List { - new User { Id = ObjectId.NewObjectId(), Name = "Small 1", Age = 1 }, - new User { Id = ObjectId.NewObjectId(), Name = new string('M', 100 * 1024), Age = 2 }, // 100KB - new User { Id = ObjectId.NewObjectId(), Name = "Small 2", Age = 3 }, - new User { Id = ObjectId.NewObjectId(), Name = new string('H', 3 * 1024 * 1024), Age = 4 } // 3MB + new() { Id = ObjectId.NewObjectId(), Name = "Small 1", Age = 1 }, + new() { Id = ObjectId.NewObjectId(), Name = new string('M', 100 * 1024), Age = 2 }, // 100KB + new() { Id = ObjectId.NewObjectId(), Name = "Small 2", Age = 3 }, + new() { Id = ObjectId.NewObjectId(), Name = new string('H', 3 * 1024 * 1024), Age = 4 } // 3MB }; var ids = _db.Users.InsertBulk(users); @@ -158,12 +154,12 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies huge inserts succeed with compression enabled and small page configuration. + /// Verifies huge inserts succeed with compression enabled and small page configuration. /// [Fact] public void Insert_HugeDoc_WithCompressionEnabledAndSmallPages_ShouldSucceed() { - var localDbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_compression_{Guid.NewGuid():N}.db"); + string localDbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_compression_{Guid.NewGuid():N}.db"); var options = new CompressionOptions { EnableCompression = true, @@ -175,7 +171,7 @@ public class DocumentOverflowTests : IDisposable try { - using var db = new Shared.TestDbContext(localDbPath, TinyPageConfig(), options); + using var db = new TestDbContext(localDbPath, TinyPageConfig(), options); var huge = new string('Z', 2 * 1024 * 1024); var id = db.Users.Insert(new User { @@ -197,12 +193,13 @@ public class DocumentOverflowTests : IDisposable } /// - /// Verifies updates from huge to small payloads succeed with compression enabled. + /// Verifies updates from huge to small payloads succeed with compression enabled. /// [Fact] public void Update_HugeToSmall_WithCompressionEnabled_ShouldSucceed() { - var localDbPath = Path.Combine(Path.GetTempPath(), $"test_overflow_compression_update_{Guid.NewGuid():N}.db"); + string localDbPath = + Path.Combine(Path.GetTempPath(), $"test_overflow_compression_update_{Guid.NewGuid():N}.db"); var options = new CompressionOptions { EnableCompression = true, @@ -214,7 +211,7 @@ public class DocumentOverflowTests : IDisposable try { - using var db = new Shared.TestDbContext(localDbPath, TinyPageConfig(), options); + using var db = new TestDbContext(localDbPath, TinyPageConfig(), options); var user = new User { Id = ObjectId.NewObjectId(), @@ -251,10 +248,10 @@ public class DocumentOverflowTests : IDisposable private static void CleanupLocalFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; if (File.Exists(dbPath)) File.Delete(dbPath); if (File.Exists(walPath)) File.Delete(walPath); if (File.Exists(markerPath)) File.Delete(markerPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/MaintenanceDiagnosticsAndMigrationTests.cs b/tests/CBDD.Tests/Storage/MaintenanceDiagnosticsAndMigrationTests.cs index ee81549..faf77da 100644 --- a/tests/CBDD.Tests/Storage/MaintenanceDiagnosticsAndMigrationTests.cs +++ b/tests/CBDD.Tests/Storage/MaintenanceDiagnosticsAndMigrationTests.cs @@ -1,4 +1,6 @@ using System.IO.Compression; +using System.Text; +using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Compression; using ZB.MOM.WW.CBDD.Core.Storage; using ZB.MOM.WW.CBDD.Shared; @@ -8,12 +10,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class MaintenanceDiagnosticsAndMigrationTests { /// - /// Verifies diagnostics APIs return page usage, compression, and fragmentation data. + /// Verifies diagnostics APIs return page usage, compression, and fragmentation data. /// [Fact] public void DiagnosticsApis_ShouldReturnPageUsageCompressionAndFragmentationData() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { @@ -28,13 +30,11 @@ public class MaintenanceDiagnosticsAndMigrationTests using var db = new TestDbContext(dbPath, options); for (var i = 0; i < 40; i++) - { db.Users.Insert(new User { Name = BuildPayload(i, 9000), Age = i }); - } db.SaveChanges(); db.ForceCheckpoint(); @@ -47,7 +47,8 @@ public class MaintenanceDiagnosticsAndMigrationTests byCollection.Any(x => x.CollectionName.Equals("users", StringComparison.OrdinalIgnoreCase)).ShouldBeTrue(); var compressionByCollection = db.GetCompressionRatioByCollection(); - var usersCompression = compressionByCollection.First(x => x.CollectionName.Equals("users", StringComparison.OrdinalIgnoreCase)); + var usersCompression = compressionByCollection.First(x => + x.CollectionName.Equals("users", StringComparison.OrdinalIgnoreCase)); usersCompression.DocumentCount.ShouldBeGreaterThan(0); usersCompression.BytesBeforeCompression.ShouldBeGreaterThan(0); usersCompression.BytesAfterCompression.ShouldBeGreaterThan(0); @@ -65,26 +66,24 @@ public class MaintenanceDiagnosticsAndMigrationTests } /// - /// Verifies compression migration dry-run and apply modes return deterministic stats and preserve data. + /// Verifies compression migration dry-run and apply modes return deterministic stats and preserve data. /// [Fact] public void MigrateCompression_DryRunAndApply_ShouldReturnDeterministicStatsAndPreserveData() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var db = new TestDbContext(dbPath, CompressionOptions.Default); - var ids = new List(); + var ids = new List(); for (var i = 0; i < 60; i++) - { ids.Add(db.Users.Insert(new User { Name = BuildPayload(i, 12000), Age = i % 17 })); - } db.SaveChanges(); db.ForceCheckpoint(); @@ -132,7 +131,7 @@ public class MaintenanceDiagnosticsAndMigrationTests private static string BuildPayload(int seed, int approxLength) { - var builder = new System.Text.StringBuilder(approxLength + 128); + var builder = new StringBuilder(approxLength + 128); var i = 0; while (builder.Length < approxLength) { @@ -148,11 +147,13 @@ public class MaintenanceDiagnosticsAndMigrationTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"maint_diag_migrate_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"maint_diag_migrate_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); var markerPath = $"{dbPath}.compact.state"; var tempPath = $"{dbPath}.compact.tmp"; var backupPath = $"{dbPath}.compact.bak"; @@ -163,4 +164,4 @@ public class MaintenanceDiagnosticsAndMigrationTests if (File.Exists(tempPath)) File.Delete(tempPath); if (File.Exists(backupPath)) File.Delete(backupPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/MetadataPersistenceTests.cs b/tests/CBDD.Tests/Storage/MetadataPersistenceTests.cs index 53b3a28..1c7f812 100755 --- a/tests/CBDD.Tests/Storage/MetadataPersistenceTests.cs +++ b/tests/CBDD.Tests/Storage/MetadataPersistenceTests.cs @@ -1,11 +1,8 @@ using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core.Collections; using ZB.MOM.WW.CBDD.Core.Indexing; using ZB.MOM.WW.CBDD.Core.Storage; -using ZB.MOM.WW.CBDD.Core.Transactions; using ZB.MOM.WW.CBDD.Shared; using ZB.MOM.WW.CBDD.Shared.TestDbContext_TestDbContext_Mappers; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; @@ -15,7 +12,7 @@ public class MetadataPersistenceTests : IDisposable private readonly string _walPath; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public MetadataPersistenceTests() { @@ -24,7 +21,16 @@ public class MetadataPersistenceTests : IDisposable } /// - /// Tests index definitions are persisted and reloaded. + /// Disposes the resources used by this instance. + /// + public void Dispose() + { + if (File.Exists(_dbPath)) File.Delete(_dbPath); + if (File.Exists(_walPath)) File.Delete(_walPath); + } + + /// + /// Tests index definitions are persisted and reloaded. /// [Fact] public void IndexDefinitions_ArePersisted_AndReloaded() @@ -66,19 +72,19 @@ public class MetadataPersistenceTests : IDisposable } /// - /// Tests ensure index does not recreate if index exists. + /// Tests ensure index does not recreate if index exists. /// [Fact] public void EnsureIndex_DoesNotRecreate_IfIndexExists() { // 1. Create index - using (var context = new Shared.TestDbContext(_dbPath)) + using (var context = new TestDbContext(_dbPath)) { context.Users.EnsureIndex(u => u.Age); } // 2. Re-open and EnsureIndex again - should be fast/no-op - using (var context = new Shared.TestDbContext(_dbPath)) + using (var context = new TestDbContext(_dbPath)) { var mapper = new ZB_MOM_WW_CBDD_Shared_UserMapper(); @@ -99,13 +105,4 @@ public class MetadataPersistenceTests : IDisposable results.Count().ShouldBe(1); } } - - /// - /// Disposes the resources used by this instance. - /// - public void Dispose() - { - if (File.Exists(_dbPath)) File.Delete(_dbPath); - if (File.Exists(_walPath)) File.Delete(_walPath); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/RobustnessTests.cs b/tests/CBDD.Tests/Storage/RobustnessTests.cs index 50ea406..f3ab4f2 100755 --- a/tests/CBDD.Tests/Storage/RobustnessTests.cs +++ b/tests/CBDD.Tests/Storage/RobustnessTests.cs @@ -1,52 +1,12 @@ using ZB.MOM.WW.CBDD.Bson; using ZB.MOM.WW.CBDD.Core.Collections; -using Xunit; -using System.Collections.Generic; -using System; -using System.Linq; namespace ZB.MOM.WW.CBDD.Tests; public class RobustnessTests { - public struct Point - { - /// - /// Gets or sets the X. - /// - public int X { get; set; } - /// - /// Gets or sets the Y. - /// - public int Y { get; set; } - } - - public class RobustEntity - { - /// - /// Gets or sets the NullableInts. - /// - public List NullableInts { get; set; } = new(); - /// - /// Gets or sets the Map. - /// - public Dictionary Map { get; set; } = new(); - /// - /// Gets or sets the EnumerableStrings. - /// - public IEnumerable EnumerableStrings { get; set; } = Array.Empty(); - /// - /// Gets or sets the Location. - /// - public Point Location { get; set; } - /// - /// Gets or sets the NullableLocation. - /// - public Point? NullableLocation { get; set; } - } - /// - /// Executes GenerateSchema_RobustnessChecks. + /// Executes GenerateSchema_RobustnessChecks. /// [Fact] public void GenerateSchema_RobustnessChecks() @@ -83,4 +43,45 @@ public class RobustnessTests nullableLocation.IsNullable.ShouldBeTrue(); nullableLocation.NestedSchema.ShouldNotBeNull(); } -} + + public struct Point + { + /// + /// Gets or sets the X. + /// + public int X { get; set; } + + /// + /// Gets or sets the Y. + /// + public int Y { get; set; } + } + + public class RobustEntity + { + /// + /// Gets or sets the NullableInts. + /// + public List NullableInts { get; set; } = new(); + + /// + /// Gets or sets the Map. + /// + public Dictionary Map { get; set; } = new(); + + /// + /// Gets or sets the EnumerableStrings. + /// + public IEnumerable EnumerableStrings { get; set; } = Array.Empty(); + + /// + /// Gets or sets the Location. + /// + public Point Location { get; set; } + + /// + /// Gets or sets the NullableLocation. + /// + public Point? NullableLocation { get; set; } + } +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/StorageEngineDictionaryTests.cs b/tests/CBDD.Tests/Storage/StorageEngineDictionaryTests.cs index eba22a2..c7157e6 100755 --- a/tests/CBDD.Tests/Storage/StorageEngineDictionaryTests.cs +++ b/tests/CBDD.Tests/Storage/StorageEngineDictionaryTests.cs @@ -1,11 +1,13 @@ using ZB.MOM.WW.CBDD.Core.Storage; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class StorageEngineDictionaryTests { - private string GetTempDbPath() => Path.Combine(Path.GetTempPath(), $"test_storage_dict_{Guid.NewGuid()}.db"); + private string GetTempDbPath() + { + return Path.Combine(Path.GetTempPath(), $"test_storage_dict_{Guid.NewGuid()}.db"); + } private void Cleanup(string path) { @@ -14,34 +16,37 @@ public class StorageEngineDictionaryTests } /// - /// Verifies dictionary pages are initialized and return normalized keys. + /// Verifies dictionary pages are initialized and return normalized keys. /// [Fact] public void StorageEngine_ShouldInitializeDictionary() { - var path = GetTempDbPath(); + string path = GetTempDbPath(); try { using (var storage = new StorageEngine(path, PageFileConfig.Default)) { // Should generate ID > 100 - var id = storage.GetOrAddDictionaryEntry("TestKey"); + ushort id = storage.GetOrAddDictionaryEntry("TestKey"); (id > DictionaryPage.ReservedValuesEnd).ShouldBeTrue(); - var key = storage.GetDictionaryKey(id); + string? key = storage.GetDictionaryKey(id); key.ShouldBe("testkey"); } } - finally { Cleanup(path); } + finally + { + Cleanup(path); + } } /// - /// Verifies dictionary entries persist across reopen. + /// Verifies dictionary entries persist across reopen. /// [Fact] public void StorageEngine_ShouldPersistDictionary() { - var path = GetTempDbPath(); + string path = GetTempDbPath(); try { ushort id1, id2; @@ -54,8 +59,8 @@ public class StorageEngineDictionaryTests // Reopen using (var storage = new StorageEngine(path, PageFileConfig.Default)) { - var val1 = storage.GetOrAddDictionaryEntry("Key1"); - var val2 = storage.GetOrAddDictionaryEntry("Key2"); + ushort val1 = storage.GetOrAddDictionaryEntry("Key1"); + ushort val2 = storage.GetOrAddDictionaryEntry("Key2"); val1.ShouldBe(id1); val2.ShouldBe(id2); @@ -64,16 +69,19 @@ public class StorageEngineDictionaryTests storage.GetDictionaryKey(val2).ShouldBe("key2"); } } - finally { Cleanup(path); } + finally + { + Cleanup(path); + } } /// - /// Verifies dictionary handling scales to many keys and remains durable. + /// Verifies dictionary handling scales to many keys and remains durable. /// [Fact] public void StorageEngine_ShouldHandleManyKeys() { - var path = GetTempDbPath(); + string path = GetTempDbPath(); try { const int keyCount = 3000; @@ -81,10 +89,10 @@ public class StorageEngineDictionaryTests using (var storage = new StorageEngine(path, PageFileConfig.Default)) { - for (int i = 0; i < keyCount; i++) + for (var i = 0; i < keyCount; i++) { var key = $"Key_{i}"; - var id = storage.GetOrAddDictionaryEntry(key); + ushort id = storage.GetOrAddDictionaryEntry(key); expectedIds[key] = id; } } @@ -92,22 +100,25 @@ public class StorageEngineDictionaryTests // Reopen and Verify using (var storage = new StorageEngine(path, PageFileConfig.Default)) { - for (int i = 0; i < keyCount; i++) + for (var i = 0; i < keyCount; i++) { var key = $"Key_{i}"; - var id = storage.GetOrAddDictionaryEntry(key); // Should get existing + ushort id = storage.GetOrAddDictionaryEntry(key); // Should get existing id.ShouldBe(expectedIds[key]); - var loadedKey = storage.GetDictionaryKey(id); + string? loadedKey = storage.GetDictionaryKey(id); loadedKey.ShouldBe(key.ToLowerInvariant()); } // Add new one - var newId = storage.GetOrAddDictionaryEntry("NewKeyAfterReopen"); + ushort newId = storage.GetOrAddDictionaryEntry("NewKeyAfterReopen"); (newId > 0).ShouldBeTrue(); expectedIds.ContainsValue(newId).ShouldBeFalse(); } } - finally { Cleanup(path); } + finally + { + Cleanup(path); + } } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Storage/StorageEngineTransactionProtocolTests.cs b/tests/CBDD.Tests/Storage/StorageEngineTransactionProtocolTests.cs index 5a2ed15..9f5b46d 100644 --- a/tests/CBDD.Tests/Storage/StorageEngineTransactionProtocolTests.cs +++ b/tests/CBDD.Tests/Storage/StorageEngineTransactionProtocolTests.cs @@ -6,12 +6,12 @@ namespace ZB.MOM.WW.CBDD.Tests; public class StorageEngineTransactionProtocolTests { /// - /// Verifies preparing an unknown transaction returns false. + /// Verifies preparing an unknown transaction returns false. /// [Fact] public void PrepareTransaction_Should_ReturnFalse_For_Unknown_Transaction() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); @@ -24,12 +24,12 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies committing a detached transaction object throws. + /// Verifies committing a detached transaction object throws. /// [Fact] public void CommitTransaction_With_TransactionObject_Should_Throw_When_Not_Active() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); @@ -44,18 +44,18 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies committing a transaction object persists writes and clears active state. + /// Verifies committing a transaction object persists writes and clears active state. /// [Fact] public void CommitTransaction_With_TransactionObject_Should_Commit_Writes() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); - var pageId = storage.AllocatePage(); + uint pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[0] = 0xAB; @@ -75,12 +75,12 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies committing by identifier with no writes does not throw. + /// Verifies committing by identifier with no writes does not throw. /// [Fact] public void CommitTransaction_ById_With_NoWrites_Should_Not_Throw() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); @@ -93,18 +93,18 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies committed transaction cache moves into readable state and active count is cleared. + /// Verifies committed transaction cache moves into readable state and active count is cleared. /// [Fact] public void MarkTransactionCommitted_Should_Move_Cache_And_Clear_ActiveCount() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); - var pageId = storage.AllocatePage(); + uint pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[5] = 0x5A; storage.WritePage(pageId, txn.TransactionId, data); @@ -124,17 +124,17 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies rollback discards uncommitted page writes. + /// Verifies rollback discards uncommitted page writes. /// [Fact] public void RollbackTransaction_Should_Discard_Uncommitted_Write() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); - var pageId = storage.AllocatePage(); + uint pageId = storage.AllocatePage(); var baseline = new byte[storage.PageSize]; baseline[0] = 0x11; storage.WritePageImmediate(pageId, baseline); @@ -159,18 +159,18 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies marking a transaction committed transitions state correctly. + /// Verifies marking a transaction committed transitions state correctly. /// [Fact] public void Transaction_MarkCommitted_Should_Transition_State() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); - var pageId = storage.AllocatePage(); + uint pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[3] = 0x33; storage.WritePage(pageId, txn.TransactionId, data); @@ -191,18 +191,18 @@ public class StorageEngineTransactionProtocolTests } /// - /// Verifies preparing then committing writes WAL data and updates transaction state. + /// Verifies preparing then committing writes WAL data and updates transaction state. /// [Fact] public void Transaction_Prepare_Should_Write_Wal_And_Transition_State() { - var dbPath = NewDbPath(); + string dbPath = NewDbPath(); try { using var storage = new StorageEngine(dbPath, PageFileConfig.Default); using var txn = storage.BeginTransaction(); - var pageId = storage.AllocatePage(); + uint pageId = storage.AllocatePage(); var data = new byte[storage.PageSize]; data[11] = 0x7B; storage.WritePage(pageId, txn.TransactionId, data); @@ -220,16 +220,18 @@ public class StorageEngineTransactionProtocolTests } private static string NewDbPath() - => Path.Combine(Path.GetTempPath(), $"storage_txn_{Guid.NewGuid():N}.db"); + { + return Path.Combine(Path.GetTempPath(), $"storage_txn_{Guid.NewGuid():N}.db"); + } private static void CleanupFiles(string dbPath) { if (File.Exists(dbPath)) File.Delete(dbPath); - var walPath = Path.ChangeExtension(dbPath, ".wal"); + string walPath = Path.ChangeExtension(dbPath, ".wal"); if (File.Exists(walPath)) File.Delete(walPath); - var altWalPath = dbPath + "-wal"; + string altWalPath = dbPath + "-wal"; if (File.Exists(altWalPath)) File.Delete(altWalPath); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Types/ObjectIdTests.cs b/tests/CBDD.Tests/Types/ObjectIdTests.cs index 4a669ac..ee9db24 100755 --- a/tests/CBDD.Tests/Types/ObjectIdTests.cs +++ b/tests/CBDD.Tests/Types/ObjectIdTests.cs @@ -1,12 +1,11 @@ using ZB.MOM.WW.CBDD.Bson; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class ObjectIdTests { /// - /// Verifies new object identifiers are 12 bytes long. + /// Verifies new object identifiers are 12 bytes long. /// [Fact] public void NewObjectId_ShouldCreate12ByteId() @@ -20,7 +19,7 @@ public class ObjectIdTests } /// - /// Verifies object identifiers round-trip from their binary form. + /// Verifies object identifiers round-trip from their binary form. /// [Fact] public void ObjectId_ShouldRoundTrip() @@ -36,7 +35,7 @@ public class ObjectIdTests } /// - /// Verifies object identifier equality behavior. + /// Verifies object identifier equality behavior. /// [Fact] public void ObjectId_Equals_ShouldWork() @@ -50,7 +49,7 @@ public class ObjectIdTests } /// - /// Verifies object identifier timestamps are recent UTC values. + /// Verifies object identifier timestamps are recent UTC values. /// [Fact] public void ObjectId_Timestamp_ShouldBeRecentUtc() @@ -61,4 +60,4 @@ public class ObjectIdTests (timestamp <= DateTime.UtcNow).ShouldBeTrue(); (timestamp >= DateTime.UtcNow.AddSeconds(-5)).ShouldBeTrue(); } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/Types/ValueObjectIdTests.cs b/tests/CBDD.Tests/Types/ValueObjectIdTests.cs index dd0c81c..444c206 100755 --- a/tests/CBDD.Tests/Types/ValueObjectIdTests.cs +++ b/tests/CBDD.Tests/Types/ValueObjectIdTests.cs @@ -1,28 +1,32 @@ -using ZB.MOM.WW.CBDD.Bson; -using ZB.MOM.WW.CBDD.Core; -using ZB.MOM.WW.CBDD.Core.Collections; -using ZB.MOM.WW.CBDD.Core.Metadata; using ZB.MOM.WW.CBDD.Shared; -using Xunit; namespace ZB.MOM.WW.CBDD.Tests; public class ValueObjectIdTests : IDisposable { + private readonly TestDbContext _db; private readonly string _dbPath = "value_object_ids.db"; - private readonly Shared.TestDbContext _db; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// public ValueObjectIdTests() { if (File.Exists(_dbPath)) File.Delete(_dbPath); - _db = new Shared.TestDbContext(_dbPath); + _db = new TestDbContext(_dbPath); } /// - /// Executes Should_Support_ValueObject_Id_Conversion. + /// Executes Dispose. + /// + public void Dispose() + { + _db.Dispose(); + if (File.Exists(_dbPath)) File.Delete(_dbPath); + } + + /// + /// Executes Should_Support_ValueObject_Id_Conversion. /// [Fact] public void Should_Support_ValueObject_Id_Conversion() @@ -41,13 +45,4 @@ public class ValueObjectIdTests : IDisposable retrieved.Id.Value.ShouldBe("ORD-123"); retrieved.CustomerName.ShouldBe("John Doe"); } - - /// - /// Executes Dispose. - /// - public void Dispose() - { - _db.Dispose(); - if (File.Exists(_dbPath)) File.Delete(_dbPath); - } -} +} \ No newline at end of file diff --git a/tests/CBDD.Tests/ZB.MOM.WW.CBDD.Tests.csproj b/tests/CBDD.Tests/ZB.MOM.WW.CBDD.Tests.csproj index 1ba3280..8d07ef4 100755 --- a/tests/CBDD.Tests/ZB.MOM.WW.CBDD.Tests.csproj +++ b/tests/CBDD.Tests/ZB.MOM.WW.CBDD.Tests.csproj @@ -1,39 +1,39 @@ - - - - net10.0 - ZB.MOM.WW.CBDD.Tests - ZB.MOM.WW.CBDD.Tests - enable - enable - true - false - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - + + + + net10.0 + ZB.MOM.WW.CBDD.Tests + ZB.MOM.WW.CBDD.Tests + enable + enable + true + false + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + diff --git a/tests/CBDD.Tests/coverage.cobertura.xml b/tests/CBDD.Tests/coverage.cobertura.xml index 4249f1f..8d605e8 100644 --- a/tests/CBDD.Tests/coverage.cobertura.xml +++ b/tests/CBDD.Tests/coverage.cobertura.xml @@ -1,34902 +1,36111 @@ - - - /Users/dohertj2/Desktop/CBDD/src/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + /Users/dohertj2/Desktop/CBDD/src/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file