diff --git a/EsentCollections/ColumnConverter.cs b/EsentCollections/ColumnConverter.cs index cad0221..693d318 100644 --- a/EsentCollections/ColumnConverter.cs +++ b/EsentCollections/ColumnConverter.cs @@ -26,7 +26,7 @@ namespace Microsoft.Isam.Esent.Collections.Generic /// database. /// /// The type of the column. - internal class ColumnConverter + internal class ColumnConverter : IColumnConverter { /// /// A mapping of types to RetrieveColumn function names. @@ -97,12 +97,12 @@ internal class ColumnConverter /// /// The SetColumn delegate for this object. /// - private readonly SetColumnDelegate columnSetter; + private readonly SetColumnDelegate columnSetter; /// /// The RetrieveColumn delegate for this object. /// - private readonly RetrieveColumnDelegate columnRetriever; + private readonly RetrieveColumnDelegate columnRetriever; /// /// The column type for this object. @@ -141,25 +141,7 @@ public ColumnConverter() RuntimeHelpers.PrepareDelegate(this.columnSetter); RuntimeHelpers.PrepareDelegate(this.columnRetriever); } - - /// - /// Represents a SetColumn operation. - /// - /// The session to use. - /// The cursor to set the value in. An update should be prepared. - /// The column to set. - /// The value to set. - public delegate void SetColumnDelegate(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid, TColumn value); - - /// - /// Represents a RetrieveColumn operation. - /// - /// The session to use. - /// The cursor to retrieve the value from. - /// The column to retrieve. - /// The retrieved value. - public delegate TColumn RetrieveColumnDelegate(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid); - + /// /// Gets the type of database column the value should be stored in. /// @@ -175,7 +157,7 @@ public JET_coltyp Coltyp /// Gets a delegate that can be used to set the Key column with an object of /// type . /// - public SetColumnDelegate ColumnSetter + public SetColumnDelegate ColumnSetter { get { @@ -187,7 +169,7 @@ public SetColumnDelegate ColumnSetter /// Gets a delegate that can be used to retrieve the Key column, returning /// type . /// - public RetrieveColumnDelegate ColumnRetriever + public RetrieveColumnDelegate ColumnRetriever { get { @@ -787,7 +769,7 @@ private static bool SetColumnIfNull(JET_SESID sesid, JET_TABLEID tableid, JET /// Get the retrieve column delegate for the type. /// /// The retrieve column delegate for the type. - private static RetrieveColumnDelegate CreateRetrieveColumnDelegate() + private static RetrieveColumnDelegate CreateRetrieveColumnDelegate() { // Look for a method called "RetrieveColumnAs{Type}", which will return a // nullable version of the type (except for strings, which are are ready @@ -813,7 +795,7 @@ private static RetrieveColumnDelegate CreateRetrieveColumnDelegate() null); // Return the string/nullable type. - return (RetrieveColumnDelegate)Delegate.CreateDelegate(typeof(RetrieveColumnDelegate), retrieveColumnMethod); + return (RetrieveColumnDelegate)Delegate.CreateDelegate(typeof(RetrieveColumnDelegate), retrieveColumnMethod); } else { @@ -829,7 +811,7 @@ private static RetrieveColumnDelegate CreateRetrieveColumnDelegate() retrieveColumnArguments, null); - return (RetrieveColumnDelegate)Delegate.CreateDelegate(typeof(RetrieveColumnDelegate), retrieveNonNullableColumnMethod); + return (RetrieveColumnDelegate)Delegate.CreateDelegate(typeof(RetrieveColumnDelegate), retrieveNonNullableColumnMethod); } } @@ -837,7 +819,7 @@ private static RetrieveColumnDelegate CreateRetrieveColumnDelegate() /// Create the set column delegate. /// /// The set column delegate. - private static SetColumnDelegate CreateSetColumnDelegate() + private static SetColumnDelegate CreateSetColumnDelegate() { // Look for a method called "SetColumn", which takes a TColumn. // First look for a private method in this class that takes the @@ -855,7 +837,7 @@ private static SetColumnDelegate CreateSetColumnDelegate() null, setColumnArguments, null); - return (SetColumnDelegate)Delegate.CreateDelegate(typeof(SetColumnDelegate), setColumnMethod); + return (SetColumnDelegate)Delegate.CreateDelegate(typeof(SetColumnDelegate), setColumnMethod); } } } diff --git a/EsentCollections/IColumnConverter.cs b/EsentCollections/IColumnConverter.cs new file mode 100644 index 0000000..3de8966 --- /dev/null +++ b/EsentCollections/IColumnConverter.cs @@ -0,0 +1,52 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// +// +// Contains methods to set and get data from the ESENT database. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace Microsoft.Isam.Esent.Collections.Generic +{ + using Microsoft.Isam.Esent.Interop; + + /// + /// Represents a SetColumn operation. + /// + /// The session to use. + /// The cursor to set the value in. An update should be prepared. + /// The column to set. + /// The value to set. + public delegate void SetColumnDelegate(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid, TColumn value); + + /// + /// Represents a RetrieveColumn operation. + /// + /// The session to use. + /// The cursor to retrieve the value from. + /// The column to retrieve. + /// The retrieved value. + public delegate TColumn RetrieveColumnDelegate(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid); + + /// + /// Contains methods to set and get data from the ESENT database. + /// + public interface IColumnConverter + { + /// + /// Gets a delegate that can be used to set the Key column with an object of + /// + SetColumnDelegate ColumnSetter { get; } + + /// + /// Gets a delegate that can be used to retrieve the Key column, returning + /// + RetrieveColumnDelegate ColumnRetriever { get; } + + /// + /// Gets the type of database column the value should be stored in. + /// + JET_coltyp Coltyp { get; } + } +} \ No newline at end of file diff --git a/EsentCollections/PersistentDictionary.cs b/EsentCollections/PersistentDictionary.cs index ce0803e..ad8eeca 100644 --- a/EsentCollections/PersistentDictionary.cs +++ b/EsentCollections/PersistentDictionary.cs @@ -115,7 +115,7 @@ public sealed partial class PersistentDictionary : IDictionary /// The directory in which to create the database. /// - public PersistentDictionary(string directory) : this(directory, null, null) + public PersistentDictionary(string directory) : this(directory, null, null, null) { if (null == directory) { @@ -127,7 +127,7 @@ public PersistentDictionary(string directory) : this(directory, null, null) /// Initializes a new instance of the PersistentDictionary class. /// /// The custom config to use for creating the PersistentDictionary. - public PersistentDictionary(IConfigSet customConfig) : this(null, customConfig, null) + public PersistentDictionary(IConfigSet customConfig) : this(null, customConfig, null, null) { if (null == customConfig) { @@ -140,13 +140,32 @@ public PersistentDictionary(IConfigSet customConfig) : this(null, customConfig, /// /// The directory in which to create the database. /// The custom config to use for creating the PersistentDictionary. - public PersistentDictionary(string directory, IConfigSet customConfig) : this(directory, customConfig, null) + public PersistentDictionary(string directory, IConfigSet customConfig) : this(directory, customConfig, null, null) { if (directory == null && customConfig == null) { throw new ArgumentException("Must specify a valid directory or customConfig"); } } + + /// + /// Initializes a new instance of the PersistentDictionary class. + /// + /// The directory in which to create the database. + /// The custom config to use for creating the PersistentDictionary. + /// The custom converter for database value column. + public PersistentDictionary(string directory, IConfigSet customConfig, IColumnConverter valueColumnConverter) : this(directory, customConfig, null, valueColumnConverter) + { + if (directory == null && customConfig == null) + { + throw new ArgumentException("Must specify a valid directory or customConfig"); + } + + if (valueColumnConverter == null) + { + throw new ArgumentNullException("valueColumnConverter"); + } + } /// /// Initializes a new instance of the PersistentDictionary class. @@ -157,7 +176,7 @@ public PersistentDictionary(string directory, IConfigSet customConfig) : this(di /// /// The directory in which to create the database. /// - public PersistentDictionary(IEnumerable> dictionary, string directory) : this(directory, null, dictionary) + public PersistentDictionary(IEnumerable> dictionary, string directory) : this(directory, null, dictionary, null) { if (null == directory) { @@ -176,7 +195,7 @@ public PersistentDictionary(IEnumerable> dictionary, /// /// The IDictionary whose contents are copied to the new dictionary. /// The custom config to use for creating the PersistentDictionary. - public PersistentDictionary(IEnumerable> dictionary, IConfigSet customConfig) : this(null, customConfig, dictionary) + public PersistentDictionary(IEnumerable> dictionary, IConfigSet customConfig) : this(null, customConfig, dictionary, null) { if (null == customConfig) { @@ -200,7 +219,7 @@ public PersistentDictionary( IEnumerable> dictionary, string directory, IConfigSet customConfig) - : this(directory, customConfig, dictionary) + : this(directory, customConfig, dictionary, null) { if (directory == null && customConfig == null) { @@ -220,11 +239,13 @@ public PersistentDictionary( /// The directory to create the database in. /// The custom config to use for creating the PersistentDictionary. /// The IDictionary whose contents are copied to the new dictionary. + /// The custom converter for database value column. /// The constructor can either intialize PersistentDictionary from a directory string, or a full custom config set. But not both. private PersistentDictionary( string directory, IConfigSet customConfig, - IEnumerable> dictionary) + IEnumerable> dictionary, + IColumnConverter valueColumnConverter) { Contract.Requires(directory != null || customConfig != null); // At least 1 of the two arguments should be set if (directory == null && customConfig == null) @@ -232,7 +253,10 @@ private PersistentDictionary( return; // The calling constructor will throw an error } - this.converters = new PersistentDictionaryConverters(); + this.converters = valueColumnConverter == null ? + new PersistentDictionaryConverters() : + new PersistentDictionaryConverters(valueColumnConverter); + this.schema = new PersistentDictionaryConfig(); var defaultConfig = PersistentDictionaryDefaultConfig.GetDefaultDatabaseConfig(); var databaseConfig = new DatabaseConfig(); diff --git a/EsentCollections/PersistentDictionaryConverters.cs b/EsentCollections/PersistentDictionaryConverters.cs index 60f0513..eecbdb0 100644 --- a/EsentCollections/PersistentDictionaryConverters.cs +++ b/EsentCollections/PersistentDictionaryConverters.cs @@ -9,6 +9,8 @@ // // -------------------------------------------------------------------------------------------------------------------- +using System; + namespace Microsoft.Isam.Esent.Collections.Generic { using Microsoft.Isam.Esent.Interop; @@ -29,7 +31,20 @@ internal class PersistentDictionaryConverters /// /// Column converter for the value column. /// - private readonly ColumnConverter valueColumnConverter = new ColumnConverter(); + private readonly IColumnConverter valueColumnConverter; + + public PersistentDictionaryConverters() : this(new ColumnConverter()) + { + } + + public PersistentDictionaryConverters(IColumnConverter valueColumnConverter) + { + if (valueColumnConverter == null) + { + throw new ArgumentNullException("valueColumnConverter"); + } + this.valueColumnConverter = valueColumnConverter; + } /// /// Gets a delegate that can be used to call JetMakeKey with an object of @@ -47,7 +62,7 @@ public KeyColumnConverter.MakeKeyDelegate MakeKey /// Gets a delegate that can be used to set the Key column with an object of /// type . /// - public ColumnConverter.SetColumnDelegate SetKeyColumn + public SetColumnDelegate SetKeyColumn { get { @@ -59,7 +74,7 @@ public ColumnConverter.SetColumnDelegate SetKeyColumn /// Gets a delegate that can be used to set the Value column with an object of /// type . /// - public ColumnConverter.SetColumnDelegate SetValueColumn + public SetColumnDelegate SetValueColumn { get { @@ -71,7 +86,7 @@ public ColumnConverter.SetColumnDelegate SetValueColumn /// Gets a delegate that can be used to retrieve the Key column, returning /// an object of type . /// - public ColumnConverter.RetrieveColumnDelegate RetrieveKeyColumn + public RetrieveColumnDelegate RetrieveKeyColumn { get { @@ -83,7 +98,7 @@ public ColumnConverter.RetrieveColumnDelegate RetrieveKeyColumn /// Gets a delegate that can be used to retrieve the Value column, returning /// an object of type . /// - public ColumnConverter.RetrieveColumnDelegate RetrieveValueColumn + public RetrieveColumnDelegate RetrieveValueColumn { get { diff --git a/EsentCollectionsTests/CustomColumnConverterTests.cs b/EsentCollectionsTests/CustomColumnConverterTests.cs new file mode 100644 index 0000000..7cdd398 --- /dev/null +++ b/EsentCollectionsTests/CustomColumnConverterTests.cs @@ -0,0 +1,491 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// +// +// Tests for custom CustomColumnConverter. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace EsentCollectionsTests +{ + using System; + using System.Globalization; + using Microsoft.Database.Isam.Config; + using Microsoft.Isam.Esent.Collections.Generic; + using Microsoft.Isam.Esent.Interop; + using Microsoft.VisualStudio.TestTools.UnitTesting; + + /// TODO do we need implement IEquatable? + /// + /// An item for storing in PersistentDictionary for these tests. + /// + internal class PooledPersistentBlob + { + public static readonly TestArrayPool ArrayPool = new TestArrayPool(); + + /// + /// Byte array containing the Blob. + /// + private readonly byte[] blobData; + + /// + /// Length of the Blob. + /// + private readonly int length; + + /// Hash code to detect illegal changes to the Blob. + private readonly int blobHashCode; + + public PooledPersistentBlob(byte[] blobData, int length) + { + if (blobData != null && blobData.Length < length) + { + throw new ArgumentException(string.Format("length cannot be more, than the array length: blobData.length={0}, length={1}", blobData.Length, length)); + } + + this.blobData = blobData; + this.length = length; + + if (blobData == null) + { + return; + } + this.blobHashCode = blobData.GetHashCode(); + } + + /// + /// Returns the byte array representing the blob. + /// + /// The byte[] array. + public byte[] GetBytes() + { + return this.blobData; + } + + /// + /// Return the length of the blob. + /// + /// The length of the blob. + public int GetLength() + { + return this.length; + } + + /// + /// Get a copy of bytes of the blob. + /// + /// Cope of bytes of the blob. + public byte[] ToArray() + { + if (this.blobData == null || this.blobData.Length == 0 || this.length == 0) + { + return new byte[] { }; + } + + byte[] result = new byte[this.length]; + Array.Copy(this.blobData, result, this.length); + return result; + } + + /// + /// Returns the hash code for this instance. + /// + /// A Int32 that contains the hash code for this instance. + public override int GetHashCode() + { + return this.blobData.GetHashCode(); + } + + /// + /// Checks that this instance hasn't changed illegally, throws an exception if it did. + /// + public void CheckImmutability() + { + int currentHashCode = this.blobData != null ? this.blobData.GetHashCode() : 0; + if (currentHashCode != this.blobHashCode) + { + throw new InvalidOperationException("A PooledPersistentBlob was changed in memory without being changed in the associated PersistentDictionary."); + } + } + } + + /// + /// ColumnConverter for PooledPersistentBlob + /// + internal class PersistentBlobCustomColumnConverter : IColumnConverter + { + internal static int setColumnExecutionsCounter = 0; + internal static int retrieveColumnExecutionsCounter = 0; + internal const int initialAllocaitonSize = 93; + + public SetColumnDelegate ColumnSetter + { + get { return SetColumn; } + } + + public RetrieveColumnDelegate ColumnRetriever{ + get { return RetrieveColumn; } + } + + public JET_coltyp Coltyp + { + get { return JET_coltyp.LongBinary; } + } + + private static void SetColumn(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid, PooledPersistentBlob value) + { + value.CheckImmutability(); + Api.SetColumn(sesid, tableid, columnid, value.GetBytes(), value.GetLength(), SetColumnGrbit.None); + setColumnExecutionsCounter++; + } + + private static PooledPersistentBlob RetrieveColumn(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid) + { + retrieveColumnExecutionsCounter++; + + byte[] data = PooledPersistentBlob.ArrayPool.Rent(initialAllocaitonSize); + int dataSize; + + JET_wrn wrn; + + try + { + wrn = Api.RetrieveColumn(sesid, tableid, columnid, data, data.Length, out dataSize, RetrieveColumnGrbit.None, null); + } + catch (Exception ex) + { + PooledPersistentBlob.ArrayPool.Return(data); + throw; + } + + if (JET_wrn.ColumnNull == wrn) + { + // null column + PooledPersistentBlob.ArrayPool.Return(data); + return new PooledPersistentBlob(null, 0); + } + + if (JET_wrn.Success == wrn) + { + return new PooledPersistentBlob(data, dataSize); + } + + // there is more data to retrieve, so we need another buffer + PooledPersistentBlob.ArrayPool.Return(data); + + data = PooledPersistentBlob.ArrayPool.Rent(dataSize); + int newDataSize; + + try + { + wrn = Api.RetrieveColumn(sesid, tableid, columnid, data, data.Length, out newDataSize, RetrieveColumnGrbit.None, null); + } + catch (Exception ex) + { + PooledPersistentBlob.ArrayPool.Return(data); + throw; + } + + if (JET_wrn.BufferTruncated == wrn) + { + PooledPersistentBlob.ArrayPool.Return(data); + string error = string.Format( + CultureInfo.CurrentCulture, + "Column size changed from {0} to {1}. The record was probably updated by another thread.", + data.Length, + newDataSize); + throw new InvalidOperationException(error); + } + + return new PooledPersistentBlob(data, newDataSize); + } + } + + /// + /// Test a class that implements IColumnConverter. + /// + [TestClass] + public class CustomColumnConverterTests + { + /// + /// Path to put the dictionary in. + /// + private const string DictionaryPath = "CustomColumnConverter"; + + /// + /// The dictionary we are testing. + /// + private PersistentDictionary dictionary; + + /// + /// Test initialization. + /// + [TestInitialize] + public void Setup() + { + this.dictionary = new PersistentDictionary(DictionaryPath, new DatabaseConfig() + { + DisplayName = "CustomColumnConverterTestsDb" + }, new PersistentBlobCustomColumnConverter()); + } + + /// + /// Cleanup after the test. + /// + [TestCleanup] + public void Teardown() + { + PooledPersistentBlob.ArrayPool.arrayAllocatedCounter = 0; + PooledPersistentBlob.ArrayPool.arrayRentedCounter = 0; + PooledPersistentBlob.ArrayPool.arrayReturnedCounter = 0; + PooledPersistentBlob.ArrayPool.ClearPool(); + + PersistentBlobCustomColumnConverter.setColumnExecutionsCounter = 0; + PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter = 0; + + this.dictionary.Dispose(); + Cleanup.DeleteDirectoryWithRetry(DictionaryPath); + } + + /// + /// Can add/read an array with custom lenght. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadArrayWithLenghtTest() + { + byte[] expectedArray = new byte[]{1, 2, 3}; + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(new byte[]{1, 2, 3, 4, 5}, 3); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Can add, and twice read an array with custom lenght without additional allocation. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadArrayWithLenghtTwiceTest() + { + byte[] expectedArray = new byte[]{1, 2, 3}; + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(new byte[]{1, 2, 3, 4, 5}, 3); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + + // return array to pool + PooledPersistentBlob.ArrayPool.Return(actualBlob.GetBytes()); + CollectionAssert.AreNotEqual(expectedArray, actualBlob.ToArray()); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + + // read the second time + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(2, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(2, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Can add/read an array with full lenght. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadArrayWithFullLenghtTest() + { + byte[] expectedArray = new byte[]{1, 2, 3, 4, 5}; + byte[] storedArray = new byte[]{1, 2, 3, 4, 5}; + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(storedArray, storedArray.Length); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Can add/read a big array in LOH (>85kb) with custom lenght. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadArrayInLargeHeapWithLenghtTest() + { + const int lenght = 100_000; + byte[] expectedArray = new byte[lenght]; + byte[] storedArray = new byte[2 * lenght]; + + for (int i = 0; i < lenght * 2; i++) + { + if (i < lenght) + { + expectedArray[i] = (byte) (i % 256); + } + storedArray[i] = (byte) (i % 256); + } + + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(storedArray, lenght); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(2, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(2, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Can add/read a big array in LOH (>85kb) with full lenght. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadArrayInLargeHeapWithFullLenghtTest() + { + const int lenght = 100_000; + byte[] expectedArray = new byte[lenght]; + byte[] storedArray = new byte[lenght]; + + for (int i = 0; i < lenght; i++) + { + expectedArray[i] = (byte) (i % 256); + storedArray[i] = (byte) (i % 256); + } + + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(storedArray, storedArray.Length); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(2, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(2, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Can add/read an empty array. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadEmptyArrayWithLenghtTest() + { + byte[] expectedArray = new byte[]{}; + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(new byte[]{}, 0); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(0, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Can add/read an null array. + /// + [TestMethod] + [Priority(2)] + public void AddAndReadNullArrayWithLenghtTest() + { + byte[] expectedArray = new byte[]{}; + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(null, 0); + this.dictionary.Flush(); + + PooledPersistentBlob actualBlob; + Assert.IsTrue(this.dictionary.TryGetValue(key, out actualBlob)); + + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.setColumnExecutionsCounter, "setColumn should be executed once"); + Assert.AreEqual(1, PersistentBlobCustomColumnConverter.retrieveColumnExecutionsCounter, "retrieveColumn should be executed once"); + CollectionAssert.AreEqual(expectedArray, actualBlob.ToArray()); + + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayRentedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayAllocatedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.arrayReturnedCounter); + Assert.AreEqual(1, PooledPersistentBlob.ArrayPool.GetNumberOfCurrentArraysInPool()); + } + + /// + /// Cannot add an element, when data is null, but lenght is more than 0. + /// + [TestMethod] + [Priority(2)] + [ExpectedException(typeof(ArgumentException))] + public void AddWhenDataIsNullAndLenghtIsNot0Test() + { + string key = "key"; + this.dictionary[key] = new PooledPersistentBlob(null, 1); + this.dictionary.Flush(); + } + + /// + /// Cannot add an element, when data lenght is less than dataSize. + /// + [TestMethod] + [Priority(2)] + [ExpectedException(typeof(ArgumentException))] + public void AddWhenDataLenghtIsLessThanDataSizeTest() + { + string key = "key"; + byte[] data = new byte[]{1, 2, 3, 4, 5}; + this.dictionary[key] = new PooledPersistentBlob(data, data.Length + 1); + this.dictionary.Flush(); + } + } +} \ No newline at end of file diff --git a/EsentCollectionsTests/TestArrayPool.cs b/EsentCollectionsTests/TestArrayPool.cs new file mode 100644 index 0000000..2a05140 --- /dev/null +++ b/EsentCollectionsTests/TestArrayPool.cs @@ -0,0 +1,92 @@ +// -------------------------------------------------------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. +// +// +// Implementation of a simple ArrayPool for tests, where it is needed to have such class. +// +// -------------------------------------------------------------------------------------------------------------------- + +namespace EsentCollectionsTests +{ + using System; + using System.Collections.Generic; + using System.Linq; + + internal class TestArrayPool + { + private readonly Dictionary> buffers = new Dictionary>(); + internal int arrayAllocatedCounter = 0; + internal int arrayRentedCounter = 0; + internal int arrayReturnedCounter = 0; + + /// + /// Rent an array from the pool + /// + internal byte[] Rent(int minimumLength) + { + if (minimumLength == 0) + { + return new byte[] { }; + } + + arrayRentedCounter++; + int power = CalculateBucketIndex(minimumLength); + int arraySize = (int) Math.Pow(2, power); + + if (buffers.ContainsKey(power)) + { + List bytesList = buffers[power]; + if (bytesList.Count > 0) + { + byte[] result = bytesList[bytesList.Count - 1]; + bytesList.RemoveAt(bytesList.Count - 1); + return result; + } + } + else + { + buffers[power] = new List(); + } + + arrayAllocatedCounter++; + return new byte[arraySize]; + } + + /// + /// Clear an array and return it to the pool + /// + internal void Return(byte[] arr) + { + if (arr.Length == 0) + { + return; + } + + int arrLength = arr.Length; + Array.Clear(arr, 0, arrLength); + int power = CalculateBucketIndex(arrLength); + buffers[power].Add(arr); + + arrayReturnedCounter++; + } + + /// + /// Clear the pool + /// + internal void ClearPool() + { + buffers.Clear(); + } + + internal int GetNumberOfCurrentArraysInPool() + { + return buffers.Sum(bucket => bucket.Value.Count); + } + + private static int CalculateBucketIndex(int arrLength) + { + return (int) Math.Ceiling(Math.Log(arrLength, 2.0)); + } + } +} \ No newline at end of file diff --git a/EsentInterop/RetrieveColumnHelpers.cs b/EsentInterop/RetrieveColumnHelpers.cs index 45ab8b0..89173b0 100644 --- a/EsentInterop/RetrieveColumnHelpers.cs +++ b/EsentInterop/RetrieveColumnHelpers.cs @@ -285,6 +285,42 @@ public static byte[] RetrieveColumn( return data; } + /// + /// Retrieves a single column value from the current record. The record is that + /// record associated with the index entry at the current position of the cursor. + /// Alternatively, this function can retrieve a column from a record being created + /// in the cursor copy buffer. This function can also retrieve column data from an + /// index entry that references the current record. In addition to retrieving the + /// actual column value, JetRetrieveColumn can also be used to retrieve the size + /// of a column, before retrieving the column data itself so that application + /// buffers can be sized appropriately. + /// + /// The session to use. + /// The cursor to retrieve the column from. + /// The columnid to retrieve. + /// The data buffer to be retrieved into. + /// The size of the data buffer. + /// Returns the actual size of the data buffer. + /// Retrieve column options. + /// + /// If pretinfo is give as NULL then the function behaves as though an itagSequence + /// of 1 and an ibLongValue of 0 (zero) were given. This causes column retrieval to + /// retrieve the first value of a multi-valued column, and to retrieve long data at + /// offset 0 (zero). + /// + /// The data retrieved from the column. Null if the column is null. + public static JET_wrn RetrieveColumn( + JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid, byte[] data, int dataSize, out int actualDataSize, RetrieveColumnGrbit grbit, JET_RETINFO retinfo) + { + // We cannot support this request when there is no way to indicate that a column reference is returned. + if ((grbit & (RetrieveColumnGrbit)0x00020000) != 0) // UnpublishedGrbits.RetrieveAsRefIfNotInRecord + { + throw new EsentInvalidGrbitException(); + } + + return JetRetrieveColumn(sesid, tableid, columnid, data, dataSize, out actualDataSize, grbit, retinfo); + } + /// /// Retrieves a single column value from the current record. The record is that /// record associated with the index entry at the current position of the cursor. diff --git a/EsentInterop/SetColumnHelpers.cs b/EsentInterop/SetColumnHelpers.cs index bd3d731..72e6996 100644 --- a/EsentInterop/SetColumnHelpers.cs +++ b/EsentInterop/SetColumnHelpers.cs @@ -149,14 +149,39 @@ public static void SetColumn(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID /// The data to set. /// SetColumn options. public static void SetColumn(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid, byte[] data, SetColumnGrbit grbit) + { + int dataLength = (null == data) ? 0 : data.Length; + SetColumn(sesid, tableid, columnid, data, dataLength, grbit); + } + + /// + /// Modifies a single column value in a modified record to be inserted or to + /// update the current record. + /// + /// The session to use. + /// The cursor to update. An update should be prepared. + /// The columnid to set. + /// The data to set. + /// The size of data to set. + /// SetColumn options. + public static void SetColumn(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid, byte[] data, int dataSize, SetColumnGrbit grbit) { if ((null != data) && (0 == data.Length)) { grbit |= SetColumnGrbit.ZeroLength; } - int dataLength = (null == data) ? 0 : data.Length; - JetSetColumn(sesid, tableid, columnid, data, dataLength, grbit, null); + if (data == null && dataSize > 0) + { + throw new ArgumentException(string.Format("data is null, but dataSize is: {0}", dataSize)); + } + + if (data != null && data.Length < dataSize) + { + throw new ArgumentException(string.Format("data.Length is less, than dataSize: data.Length={0}, dataSize={1}", data.Length, dataSize)); + } + + JetSetColumn(sesid, tableid, columnid, data, dataSize, grbit, null); } ///