//----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. // //----------------------------------------------------------------------- namespace Microsoft.Isam.Esent.Interop { using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; /// /// Base class for objects that represent a column value to be set. /// public abstract partial class ColumnValue { /// /// Internal grbit. /// private RetrieveColumnGrbit grbit; /// /// Initializes a new instance of the ColumnValue class. /// protected ColumnValue() { this.ItagSequence = 1; } /// /// Gets or sets the columnid to be set or retrieved. /// public JET_COLUMNID Columnid { get; set; } /// /// Gets the last set or retrieved value of the column. The /// value is returned as a generic object. /// public abstract object ValueAsObject { get; } /// /// Gets or sets column update options. /// public SetColumnGrbit SetGrbit { get; set; } /// /// Gets or sets column retrieval options. /// public RetrieveColumnGrbit RetrieveGrbit { get { return this.grbit; } set { this.ValidateRetrieveGrbit(value); this.grbit = value; } } /// /// Gets or sets the column itag sequence. /// public int ItagSequence { get; set; } /// /// Gets the warning generated by retrieving or setting this column. /// public JET_wrn Error { get; internal set; } /// /// Gets the byte length of a column value, which is zero if column is null, otherwise /// it matches the Size for fixed-size columns and represent the actual value byte /// length for variable sized columns (i.e. binary and string). For strings the length /// is determined in assumption two bytes per character. /// public abstract int Length { get; } /// /// Gets the size of the value in the column. This returns 0 for /// variable sized columns (i.e. binary and string). /// protected abstract int Size { get; } /// /// Returns a that represents the current . /// /// /// A that represents the current . /// public abstract override string ToString(); /// /// Recursive RetrieveColumns method for data pinning. This should pin a buffer and /// call the inherited RetrieveColumns method. /// /// The session to use. /// /// The table to retrieve the columns from. /// /// /// Column values to retrieve. /// internal static void RetrieveColumns(JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues) { const int MaxColumnValues = 1024; if (columnValues.Length > MaxColumnValues) { throw new ArgumentOutOfRangeException("columnValues", columnValues.Length, "Too many column values"); } unsafe { byte[] buffer = null; NATIVE_RETRIEVECOLUMN* nativeRetrievecolumns = stackalloc NATIVE_RETRIEVECOLUMN[columnValues.Length]; try { buffer = Caches.ColumnCache.Allocate(); Debug.Assert(MaxColumnValues * 16 < buffer.Length, "Maximum size of fixed columns could exceed buffer size"); fixed (byte* pinnedBuffer = buffer) { byte* currentBuffer = pinnedBuffer; int numVariableLengthColumns = columnValues.Length; // First the fixed-size columns for (int i = 0; i < columnValues.Length; ++i) { if (0 != columnValues[i].Size) { columnValues[i].MakeNativeRetrieveColumn(ref nativeRetrievecolumns[i]); nativeRetrievecolumns[i].pvData = new IntPtr(currentBuffer); nativeRetrievecolumns[i].cbData = checked((uint)columnValues[i].Size); currentBuffer += nativeRetrievecolumns[i].cbData; Debug.Assert(currentBuffer <= pinnedBuffer + buffer.Length, "Moved past end of pinned buffer"); numVariableLengthColumns--; } } // Now the variable-length columns if (numVariableLengthColumns > 0) { int bufferUsed = checked((int)(currentBuffer - pinnedBuffer)); int bufferRemaining = checked(buffer.Length - bufferUsed); int bufferPerColumn = bufferRemaining / numVariableLengthColumns; Debug.Assert(bufferPerColumn > 0, "Not enough buffer left to retrieve variable length columns"); // Now the variable-size columns for (int i = 0; i < columnValues.Length; ++i) { if (0 == columnValues[i].Size) { columnValues[i].MakeNativeRetrieveColumn(ref nativeRetrievecolumns[i]); nativeRetrievecolumns[i].pvData = new IntPtr(currentBuffer); nativeRetrievecolumns[i].cbData = checked((uint)bufferPerColumn); currentBuffer += nativeRetrievecolumns[i].cbData; Debug.Assert(currentBuffer <= pinnedBuffer + buffer.Length, "Moved past end of pinned buffer"); } } } // Retrieve the columns Api.Check(Api.Impl.JetRetrieveColumns(sesid, tableid, nativeRetrievecolumns, columnValues.Length)); // Propagate the warnings and output. for (int i = 0; i < columnValues.Length; ++i) { columnValues[i].Error = (JET_wrn)nativeRetrievecolumns[i].err; columnValues[i].ItagSequence = (int)nativeRetrievecolumns[i].itagSequence; } // Now parse out the columns that were retrieved successfully for (int i = 0; i < columnValues.Length; ++i) { if (nativeRetrievecolumns[i].err != (int)JET_wrn.BufferTruncated) { byte* columnBuffer = (byte*)nativeRetrievecolumns[i].pvData; int startIndex = checked((int)(columnBuffer - pinnedBuffer)); columnValues[i].GetValueFromBytes( buffer, startIndex, checked((int)nativeRetrievecolumns[i].cbActual), nativeRetrievecolumns[i].err); } } } // Finally retrieve the buffers where the columns weren't large enough. RetrieveTruncatedBuffers(sesid, tableid, columnValues, nativeRetrievecolumns); } finally { if (buffer != null) { Caches.ColumnCache.Free(ref buffer); } } } } /// /// Recursive SetColumns method for data pinning. This should populate the buffer and /// call the inherited SetColumns method. /// /// The session to use. /// /// The table to set the columns in. An update should be prepared. /// /// /// Column values to set. /// /// /// Structures to put the pinned data in. /// /// Offset of this object in the array. /// An error code. internal abstract unsafe int SetColumns(JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues, NATIVE_SETCOLUMN* nativeColumns, int i); /// /// Recursive SetColumns function used to pin data. /// /// The session to use. /// /// The table to set the columns in. An update should be prepared. /// /// /// Column values to set. /// /// /// Structures to put the pinned data in. /// /// Offset of this object in the array. /// The buffer for this object. /// Size of the buffer for ths object. /// True if this object is non null. /// An error code. /// /// This is marked as internal because it uses the NATIVE_SETCOLUMN type /// which is also marked as internal. It should be treated as a protected /// method though. /// internal unsafe int SetColumns( JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues, NATIVE_SETCOLUMN* nativeColumns, int i, void* buffer, int bufferSize, bool hasValue) { Debug.Assert(this == columnValues[i], "SetColumns should be called on the current object"); this.MakeNativeSetColumn(ref nativeColumns[i]); if (hasValue) { nativeColumns[i].cbData = checked((uint)bufferSize); nativeColumns[i].pvData = new IntPtr(buffer); if (0 == bufferSize) { nativeColumns[i].grbit |= (uint)SetColumnGrbit.ZeroLength; } } int err = i == columnValues.Length - 1 ? Api.Impl.JetSetColumns(sesid, tableid, nativeColumns, columnValues.Length) : columnValues[i + 1].SetColumns(sesid, tableid, columnValues, nativeColumns, i + 1); this.Error = (JET_wrn)nativeColumns[i].err; return err; } /// /// Given data retrieved from ESENT, decode the data and set the value in the ColumnValue object. /// /// An array of bytes. /// The starting position within the bytes. /// The number of bytes to decode. /// The error returned from ESENT. protected abstract void GetValueFromBytes(byte[] value, int startIndex, int count, int err); /// /// Validation for the requested retrieve options for the column. /// /// The retrieve options to validate. protected virtual void ValidateRetrieveGrbit(RetrieveColumnGrbit grbit) { // 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(); } } /// /// Retrieve the value for columns whose buffers were truncated. /// /// The session to use. /// The table to use. /// The column values. /// /// The native retrieve columns that match the column values. /// private static unsafe void RetrieveTruncatedBuffers(JET_SESID sesid, JET_TABLEID tableid, ColumnValue[] columnValues, NATIVE_RETRIEVECOLUMN* nativeRetrievecolumns) { for (int i = 0; i < columnValues.Length; ++i) { if (nativeRetrievecolumns[i].err == (int)JET_wrn.BufferTruncated) { var buffer = new byte[nativeRetrievecolumns[i].cbActual]; int actualSize; int err; var retinfo = new JET_RETINFO { itagSequence = columnValues[i].ItagSequence }; // Pin the buffer and retrieve the data fixed (byte* pinnedBuffer = buffer) { err = Api.Impl.JetRetrieveColumn( sesid, tableid, columnValues[i].Columnid, new IntPtr(pinnedBuffer), buffer.Length, out actualSize, columnValues[i].RetrieveGrbit, retinfo); } if (JET_wrn.BufferTruncated == (JET_wrn)err) { string error = string.Format( CultureInfo.CurrentCulture, "Column size changed from {0} to {1}. The record was probably updated by another thread.", buffer.Length, actualSize); Trace.TraceError(error); throw new InvalidOperationException(error); } // Throw errors, but put warnings in the structure Api.Check(err); columnValues[i].Error = (JET_wrn)err; // For BytesColumnValue this will copy the data to a new array. // If this situation becomes common we should simply use the array. columnValues[i].GetValueFromBytes(buffer, 0, actualSize, err); } } } /// /// Create a native SetColumn from this object. /// /// The native setcolumn structure to fill in. private void MakeNativeSetColumn(ref NATIVE_SETCOLUMN setcolumn) { setcolumn.columnid = this.Columnid.Value; setcolumn.grbit = (uint)this.SetGrbit; setcolumn.itagSequence = checked((uint)this.ItagSequence); } /// /// Create a native RetrieveColumn from this object. /// /// /// The retrieve column structure to fill in. /// private void MakeNativeRetrieveColumn(ref NATIVE_RETRIEVECOLUMN retrievecolumn) { retrievecolumn.columnid = this.Columnid.Value; retrievecolumn.grbit = (uint)this.RetrieveGrbit; retrievecolumn.itagSequence = checked((uint)this.ItagSequence); } } }