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