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);
}
///