//----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. // //----------------------------------------------------------------------- namespace Microsoft.Isam.Esent.Interop { using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; /// /// This class provides a streaming interface to a long-value column /// (i.e. a column of type or /// ). /// [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", Justification = "This should match the unmanaged API, which isn't capitalized.")] public class ColumnStream : Stream { /// /// The size of the biggest long-value column ESENT supports. /// private const int MaxLongValueSize = 0x7fffffff; /// /// Session to use. /// private readonly JET_SESID sesid; /// /// Cursor to use. /// private readonly JET_TABLEID tableid; /// /// Columnid to use. /// private readonly JET_COLUMNID columnid; /// /// Current LV offset. /// private int ibLongValue; /// /// Initializes a new instance of the ColumnStream class. /// /// The session to use. /// The cursor to use. /// The columnid of the column to set/retrieve data from. public ColumnStream(JET_SESID sesid, JET_TABLEID tableid, JET_COLUMNID columnid) { // In some cases we rely on Int32 arithmetic overflow checking to catch // errors, which assumes that a long-value can store Int32.MaxValue bytes. Debug.Assert(MaxLongValueSize == int.MaxValue, "Expected maximum long value size to be Int32.MaxValue"); this.sesid = sesid; this.tableid = tableid; this.columnid = columnid; this.Itag = 1; } /// /// Gets or sets the itag of the column. /// public int Itag { get; set; } /// /// Gets a value indicating whether the stream supports reading. /// public override bool CanRead { [DebuggerStepThrough] get { return true; } } /// /// Gets a value indicating whether the stream supports writing. /// public override bool CanWrite { [DebuggerStepThrough] get { return true; } } /// /// Gets a value indicating whether the stream supports seeking. /// public override bool CanSeek { [DebuggerStepThrough] get { return true; } } /// /// Gets or sets the current position in the stream. /// public override long Position { [DebuggerStepThrough] get { return this.ibLongValue; } set { if (value < 0 || value > MaxLongValueSize) { throw new ArgumentOutOfRangeException("value", value, "A long-value offset has to be between 0 and 0x7fffffff bytes"); } this.ibLongValue = checked((int)value); } } /// /// Gets the current length of the stream. /// public override long Length { get { int size; var retinfo = new JET_RETINFO { itagSequence = this.Itag, ibLongValue = 0 }; Api.JetRetrieveColumn(this.sesid, this.tableid, this.columnid, null, 0, out size, RetrieveGrbit, retinfo); return size; } } /// /// Gets the options that should be used with JetRetrieveColumn. /// private static RetrieveColumnGrbit RetrieveGrbit { [DebuggerStepThrough] get { // Always use the RetrieveCopy options. This makes the ColumnStream work // well when setting a column. If we don't always use RetrieveCopy then // things like seeking from the end of a column might not work properly. return RetrieveColumnGrbit.RetrieveCopy; } } /// /// Returns a that represents the current . /// /// /// A that represents the current . /// public override string ToString() { return string.Format(CultureInfo.InvariantCulture, "ColumnStream(0x{0:x}:{1})", this.columnid.Value, this.Itag); } /// /// Flush the stream. /// public override void Flush() { // nothing is required } /// /// Writes a sequence of bytes to the current stream and advances the current /// position within this stream by the number of bytes written. /// /// The buffer to write from. /// The offset in the buffer to write. /// The number of bytes to write. public override void Write(byte[] buffer, int offset, int count) { CheckBufferArguments(buffer, offset, count); int length = checked((int)this.Length); JET_SETINFO setinfo; int newIbLongValue = checked(this.ibLongValue + count); // If our current position is beyond the end of the LV extend // the LV to the write point if (this.ibLongValue > length) { setinfo = new JET_SETINFO { itagSequence = this.Itag }; Api.JetSetColumn(this.sesid, this.tableid, this.columnid, null, this.ibLongValue, SetColumnGrbit.SizeLV, setinfo); length = this.ibLongValue; } SetColumnGrbit grbit; if (this.ibLongValue == length) { grbit = SetColumnGrbit.AppendLV; } else if (newIbLongValue >= length) { grbit = SetColumnGrbit.OverwriteLV | SetColumnGrbit.SizeLV; } else { grbit = SetColumnGrbit.OverwriteLV; } setinfo = new JET_SETINFO { itagSequence = this.Itag, ibLongValue = this.ibLongValue }; Api.JetSetColumn(this.sesid, this.tableid, this.columnid, buffer, count, offset, grbit, setinfo); checked { this.ibLongValue += count; } } /// /// Reads a sequence of bytes from the current stream and advances the /// position within the stream by the number of bytes read. /// /// The buffer to read into. /// The offset in the buffer to read into. /// The number of bytes to read. /// The number of bytes read into the buffer. public override int Read(byte[] buffer, int offset, int count) { CheckBufferArguments(buffer, offset, count); if (this.ibLongValue >= this.Length) { return 0; } int length; var retinfo = new JET_RETINFO { itagSequence = this.Itag, ibLongValue = this.ibLongValue }; Api.JetRetrieveColumn(this.sesid, this.tableid, this.columnid, buffer, count, offset, out length, RetrieveGrbit, retinfo); int bytesRead = Math.Min(length, count); checked { this.ibLongValue += bytesRead; } return bytesRead; } /// /// Sets the length of the stream. /// /// The desired length, in bytes. public override void SetLength(long value) { if (value > MaxLongValueSize || value < 0) { throw new ArgumentOutOfRangeException("value", value, "A LongValueStream cannot be longer than 0x7FFFFFF or less than 0 bytes"); } if (value < this.Length && value > 0) { // BUG: Shrinking the column multiple times and then growing it can sometimes hit an unpleasant // ESENT defect which causes a hang. To make sure we never have that problem we read out the data, // and insert into a new long-value. This is not efficient. var data = new byte[value]; var retinfo = new JET_RETINFO { itagSequence = this.Itag, ibLongValue = 0 }; int actualDataSize; Api.JetRetrieveColumn( this.sesid, this.tableid, this.columnid, data, data.Length, out actualDataSize, RetrieveGrbit, retinfo); var setinfo = new JET_SETINFO { itagSequence = this.Itag }; Api.JetSetColumn(this.sesid, this.tableid, this.columnid, data, data.Length, SetColumnGrbit.None, setinfo); } else { var setinfo = new JET_SETINFO { itagSequence = this.Itag }; SetColumnGrbit grbit = (0 == value) ? SetColumnGrbit.ZeroLength : SetColumnGrbit.SizeLV; Api.JetSetColumn(this.sesid, this.tableid, this.columnid, null, checked((int)value), grbit, setinfo); } // Setting the length moves the offset back to the end of the data if (this.ibLongValue > value) { this.ibLongValue = checked((int)value); } } /// /// Sets the position in the current stream. /// /// Byte offset relative to the origin parameter. /// A SeekOrigin indicating the reference point for the new position. /// The new position in the current stream. public override long Seek(long offset, SeekOrigin origin) { long newOffset; switch (origin) { case SeekOrigin.Begin: newOffset = offset; break; case SeekOrigin.End: newOffset = checked(this.Length + offset); break; case SeekOrigin.Current: newOffset = checked(this.ibLongValue + offset); break; default: throw new ArgumentOutOfRangeException("origin", origin, "Unknown origin"); } if (newOffset < 0 || newOffset > MaxLongValueSize) { throw new ArgumentOutOfRangeException("offset", offset, "invalid offset/origin combination"); } this.ibLongValue = checked((int)newOffset); return this.ibLongValue; } /// /// Check the buffer arguments given to Read/Write . /// /// The buffer. /// The offset in the buffer to read/write to. /// The number of bytes to read/write. private static void CheckBufferArguments(ICollection buffer, int offset, int count) { if (null == buffer) { throw new ArgumentNullException("buffer"); } if (offset < 0) { throw new ArgumentOutOfRangeException("offset", offset, "cannot be negative"); } if (count < 0) { throw new ArgumentOutOfRangeException("count", count, "cannot be negative"); } if (checked(buffer.Count - offset) < count) { throw new ArgumentOutOfRangeException("count", count, "cannot be larger than the size of the buffer"); } } } }