// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data.Common;
using System.Data.ProviderBase;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace System.Data.OleDb
{
    public sealed class OleDbDataReader : DbDataReader
    {
        private readonly CommandBehavior _commandBehavior;

        // object model interaction
        private OleDbConnection? _connection;
        private OleDbCommand? _command;

        // DataReader owns the parameter bindings until CloseDataReader
        // this allows OleDbCommand.Dispose to not require OleDbDataReader.Dispose
        private Bindings? _parameterBindings;

        // OLEDB interfaces
        private UnsafeNativeMethods.IMultipleResults? _imultipleResults;
        private UnsafeNativeMethods.IRowset? _irowset;
        private UnsafeNativeMethods.IRow? _irow;

        private ChapterHandle _chapterHandle = ChapterHandle.DB_NULL_HCHAPTER;

        private readonly int _depth;
        private bool _isClosed, _isRead, _hasRows, _hasRowsReadCheck;

        private long _sequentialBytesRead;
        private int _sequentialOrdinal;

        private Bindings?[]? _bindings; // _metadata contains the ColumnBinding

        // do we need to jump to the next accessor
        private int _nextAccessorForRetrieval;

        // must increment the counter before retrieving value so that
        // if an exception is thrown, user can continue without erroring again
        private int _nextValueForRetrieval;

        // record affected for the current dataset
        private IntPtr _recordsAffected = ADP.RecordsUnaffected;
        private bool _useIColumnsRowset;
        private bool _sequentialAccess;
        private bool _singleRow;

        // cached information for Reading (rowhandles/status)
        private nint _rowHandleFetchCount; // (>1 fails against jet)
        private RowHandleBuffer? _rowHandleNativeBuffer;

        private nint _rowFetchedCount;
        private int _currentRow;

        private DataTable? _dbSchemaTable;

        private int _visibleFieldCount;
        private MetaData[]? _metadata;
        private FieldNameLookup? _fieldNameLookup;

        // ctor for an ICommandText, IMultipleResults, IRowset, IRow
        // ctor for an ADODB.Recordset, ADODB.Record or Hierarchial resultset
        internal OleDbDataReader(OleDbConnection? connection, OleDbCommand? command, int depth, CommandBehavior commandBehavior)
        {
            _connection = connection;
            _command = command;
            _commandBehavior = commandBehavior;

            if ((null != command) && (0 == _depth))
            {
                _parameterBindings = command.TakeBindingOwnerShip();
            }
            _depth = depth;
        }

        private void Initialize()
        {
            CommandBehavior behavior = _commandBehavior;
            _useIColumnsRowset = (0 != (CommandBehavior.KeyInfo & behavior));
            _sequentialAccess = (0 != (CommandBehavior.SequentialAccess & behavior));
            if (0 == _depth)
            {
                _singleRow = (0 != (CommandBehavior.SingleRow & behavior));
            }
        }

        internal void InitializeIMultipleResults(object? result)
        {
            Initialize();
            _imultipleResults = (UnsafeNativeMethods.IMultipleResults?)result; // maybe null if no results
        }
        internal void InitializeIRowset(object? result, ChapterHandle chapterHandle, IntPtr recordsAffected)
        {
            // if from ADODB, connection will be null
            if ((null == _connection) || (ChapterHandle.DB_NULL_HCHAPTER != chapterHandle))
            {
                _rowHandleFetchCount = 1;
            }

            Initialize();
            _recordsAffected = recordsAffected;
            _irowset = (UnsafeNativeMethods.IRowset?)result; // maybe null if no results
            _chapterHandle = chapterHandle;
        }

        internal void InitializeIRow(object? result, IntPtr recordsAffected)
        {
            Initialize();
            Debug.Assert(_singleRow, "SingleRow not already set");
            _singleRow = true;
            _recordsAffected = recordsAffected;
            _irow = (UnsafeNativeMethods.IRow?)result; // maybe null if no results
            _hasRows = (null != _irow);
        }

        internal OleDbCommand? Command
        {
            get
            {
                return _command;
            }
        }

        public override int Depth
        {
            get
            {
                if (IsClosed)
                {
                    throw ADP.DataReaderClosed("Depth");
                }
                return _depth;
            }
        }

        public override int FieldCount
        {
            get
            {
                if (IsClosed)
                {
                    throw ADP.DataReaderClosed("FieldCount");
                }
                MetaData[]? metadata = MetaData;
                return ((null != metadata) ? metadata.Length : 0);
            }
        }

        public override bool HasRows
        {
            get
            {
                if (IsClosed)
                {
                    throw ADP.DataReaderClosed("HasRows");
                }
                return _hasRows;
            }
        }

        public override bool IsClosed
        {
            get
            { // if we have a rowset or multiplieresults, we may have more to read
                Debug.Assert((_singleRow && !_isClosed && !_isRead && (null == _irow) && (null == _irowset)) ||
                             _isClosed == ((null == _irow) && (null == _irowset) && (null == _imultipleResults)
                                           && (null == _dbSchemaTable) && (null == _connection) && (null == _command)),
                                           "IsClosed mismatch");
                return _isClosed;
            }
        }

        private MetaData[]? MetaData
        {
            get { return _metadata; }
        }

        public override int RecordsAffected
        {
            get
            {
                return ADP.IntPtrToInt32(_recordsAffected);
            }
        }

        /*
        internal long RecordsAffectedLong {
            get {
                return (long)_recordsAffected;
            }
        }*/

        public override object this[int index]
        {
            get
            {
                return GetValue(index);
            }
        }

        public override object this[string name]
        {
            get
            {
                int ordinal = GetOrdinal(name);
                return GetValue(ordinal);
            }
        }

        // grouping the native OLE DB casts togther by required interfaces and optional interfaces
        // want these to be methods, not properties otherwise they appear in VS7 managed debugger which attempts to evaluate them

        // required interface, safe cast
        private UnsafeNativeMethods.IAccessor IAccessor()
        {
            return (UnsafeNativeMethods.IAccessor)IRowset();
        }

        // required interface, safe cast
        private UnsafeNativeMethods.IRowsetInfo IRowsetInfo()
        {
            return (UnsafeNativeMethods.IRowsetInfo)IRowset();
        }

        private UnsafeNativeMethods.IRowset IRowset()
        {
            UnsafeNativeMethods.IRowset? irowset = _irowset;
            if (null == irowset)
            {
                Debug.Assert(false, "object is disposed");
                throw new ObjectDisposedException(GetType().Name);
            }
            return irowset;
        }

        private UnsafeNativeMethods.IRow IRow()
        {
            UnsafeNativeMethods.IRow? irow = _irow;
            if (null == irow)
            {
                Debug.Assert(false, "object is disposed");
                throw new ObjectDisposedException(GetType().Name);
            }
            return irow;
        }

        public override DataTable? GetSchemaTable()
        {
            DataTable? schemaTable = _dbSchemaTable;
            if (null == schemaTable)
            {
                MetaData[]? metadata = MetaData;
                if ((null != metadata) && (0 < metadata.Length))
                {
                    if ((0 < metadata.Length) && _useIColumnsRowset && (null != _connection))
                    {
                        AppendSchemaInfo();
                    }
                    schemaTable = BuildSchemaTable(metadata);
                }
                else if (IsClosed)
                {
                    throw ADP.DataReaderClosed("GetSchemaTable");
                }
                //GetSchemaTable() is defined to return null after NextResult returns false
                //throw ADP.DataReaderNoData();
            }
            return schemaTable;
        }

        internal void BuildMetaInfo()
        {
            Debug.Assert(null == _metadata, "BuildMetaInfo: already built, by _metadata");

            if (null != _irowset)
            {
                if (_useIColumnsRowset)
                {
                    BuildSchemaTableRowset(_irowset);
                }
                else
                {
                    BuildSchemaTableInfo(_irowset, false, false);
                }
                if (null != _metadata && 0 < _metadata.Length)
                {
                    // @devnote: because we want to use the DBACCESSOR_OPTIMIZED bit,
                    // we are required to create the accessor before fetching any rows
                    CreateAccessors(true);
                    Debug.Assert(null != _bindings, "unexpected dbBindings");
                }
            }
            else if (null != _irow)
            {
                BuildSchemaTableInfo(_irow, false, false);
                if (null != _metadata && 0 < _metadata.Length)
                {
                    CreateBindingsFromMetaData(true);
                }
            }
            if (null == _metadata)
            {
                _hasRows = false;
                _visibleFieldCount = 0;
                _metadata = Array.Empty<MetaData>();
            }
        }

        private DataTable BuildSchemaTable(MetaData[] metadata)
        {
            Debug.Assert(null == _dbSchemaTable, "BuildSchemaTable: schema table already exists");
            Debug.Assert(null != metadata, "BuildSchemaTable: no _metadata");

            DataTable schemaTable = new DataTable("SchemaTable");
            schemaTable.Locale = CultureInfo.InvariantCulture;
            schemaTable.MinimumCapacity = metadata.Length;

            DataColumn name = new DataColumn("ColumnName", typeof(string));
            DataColumn ordinal = new DataColumn("ColumnOrdinal", typeof(int));
            DataColumn size = new DataColumn("ColumnSize", typeof(int));
            DataColumn precision = new DataColumn("NumericPrecision", typeof(short));
            DataColumn scale = new DataColumn("NumericScale", typeof(short));

            DataColumn dataType = new DataColumn(SchemaTableColumn.DataType, typeof(Type));
            DataColumn providerType = new DataColumn("ProviderType", typeof(int));

            DataColumn isLong = new DataColumn("IsLong", typeof(bool));
            DataColumn allowDBNull = new DataColumn("AllowDBNull", typeof(bool));
            DataColumn isReadOnly = new DataColumn("IsReadOnly", typeof(bool));
            DataColumn isRowVersion = new DataColumn("IsRowVersion", typeof(bool));

            DataColumn isUnique = new DataColumn("IsUnique", typeof(bool));
            DataColumn isKey = new DataColumn("IsKey", typeof(bool));
            DataColumn isAutoIncrement = new DataColumn("IsAutoIncrement", typeof(bool));
            DataColumn isHidden = new DataColumn("IsHidden", typeof(bool));

            DataColumn baseSchemaName = new DataColumn("BaseSchemaName", typeof(string));
            DataColumn baseCatalogName = new DataColumn("BaseCatalogName", typeof(string));
            DataColumn baseTableName = new DataColumn("BaseTableName", typeof(string));
            DataColumn baseColumnName = new DataColumn("BaseColumnName", typeof(string));

            ordinal.DefaultValue = 0;
            isLong.DefaultValue = false;

            DataColumnCollection columns = schemaTable.Columns;

            columns.Add(name);
            columns.Add(ordinal);
            columns.Add(size);
            columns.Add(precision);
            columns.Add(scale);

            columns.Add(dataType);
            columns.Add(providerType);

            columns.Add(isLong);
            columns.Add(allowDBNull);
            columns.Add(isReadOnly);
            columns.Add(isRowVersion);

            columns.Add(isUnique);
            columns.Add(isKey);
            columns.Add(isAutoIncrement);
            if (_visibleFieldCount < metadata.Length)
            {
                columns.Add(isHidden);
            }

            columns.Add(baseSchemaName);
            columns.Add(baseCatalogName);
            columns.Add(baseTableName);
            columns.Add(baseColumnName);

            for (int i = 0; i < metadata.Length; ++i)
            {
                MetaData info = metadata[i];

                DataRow newRow = schemaTable.NewRow();
                newRow[name] = info.columnName;
                newRow[ordinal] = i;
                // @devnote: size is count of characters for WSTR or STR, bytes otherwise
                // @devnote: see OLEDB spec under IColumnsInfo::GetColumnInfo
                newRow[size] = ((info.type.enumOleDbType != OleDbType.BSTR) ? info.size : -1);
                newRow[precision] = info.precision;
                newRow[scale] = info.scale;

                newRow[dataType] = info.type.dataType;
                newRow[providerType] = info.type.enumOleDbType;
                newRow[isLong] = OleDbDataReader.IsLong(info.flags);
                if (info.isKeyColumn)
                {
                    newRow[allowDBNull] = OleDbDataReader.AllowDBNull(info.flags);
                }
                else
                {
                    newRow[allowDBNull] = OleDbDataReader.AllowDBNullMaybeNull(info.flags);
                }
                newRow[isReadOnly] = OleDbDataReader.IsReadOnly(info.flags);
                newRow[isRowVersion] = OleDbDataReader.IsRowVersion(info.flags);

                newRow[isUnique] = info.isUnique;
                newRow[isKey] = info.isKeyColumn;
                newRow[isAutoIncrement] = info.isAutoIncrement;
                if (_visibleFieldCount < metadata.Length)
                {
                    newRow[isHidden] = info.isHidden;
                }

                if (null != info.baseSchemaName)
                {
                    newRow[baseSchemaName] = info.baseSchemaName;
                }
                if (null != info.baseCatalogName)
                {
                    newRow[baseCatalogName] = info.baseCatalogName;
                }
                if (null != info.baseTableName)
                {
                    newRow[baseTableName] = info.baseTableName;
                }
                if (null != info.baseColumnName)
                {
                    newRow[baseColumnName] = info.baseColumnName;
                }

                schemaTable.Rows.Add(newRow);
                newRow.AcceptChanges();
            }

            // mark all columns as readonly
            int count = columns.Count;
            for (int i = 0; i < count; i++)
            {
                columns[i].ReadOnly = true;
            }

            _dbSchemaTable = schemaTable;
            return schemaTable;
        }

        private void BuildSchemaTableInfo(object handle, bool filterITypeInfo, bool filterChapters)
        {
            Debug.Assert(null == _dbSchemaTable, "non-null SchemaTable");
            Debug.Assert(null == _metadata, "non-null metadata");
            Debug.Assert(null != handle, "unexpected null rowset");
            UnsafeNativeMethods.IColumnsInfo? icolumnsInfo = (handle as UnsafeNativeMethods.IColumnsInfo);
            if (null == icolumnsInfo)
            {
                _dbSchemaTable = null;
#if DEBUG
                if (handle is UnsafeNativeMethods.IRow)
                {
                    Debug.Assert(false, "bad IRow - IColumnsInfo not available");
                }
                else
                {
                    Debug.Assert(handle is UnsafeNativeMethods.IRowset, "bad IRowset - IColumnsInfo not available");
                }
#endif
                return;
            }

            OleDbHResult hr;
            nint columnCount = 0; // column count
            IntPtr columnInfos = IntPtr.Zero; // ptr to byvalue tagDBCOLUMNINFO[]

            using (DualCoTaskMem safehandle = new DualCoTaskMem(icolumnsInfo, out columnCount, out columnInfos, out hr))
            {
                if (hr < 0)
                {
                    ProcessResults(hr);
                }
                if (0 < checked((int)columnCount))
                {
                    BuildSchemaTableInfoTable((int)columnCount, columnInfos, filterITypeInfo, filterChapters);
                }
            }
        }

        // create DataColumns
        // add DataColumns to DataTable
        // add schema information to DataTable
        // generate unique column names
        private void BuildSchemaTableInfoTable(int columnCount, IntPtr columnInfos, bool filterITypeInfo, bool filterChapters)
        {
            Debug.Assert(0 < columnCount, "BuildSchemaTableInfoTable - no column");

            int rowCount = 0;
            MetaData[] metainfo = new MetaData[columnCount];

            // for every column, build an equivalent to tagDBCOLUMNINFO
            tagDBCOLUMNINFO dbColumnInfo = new tagDBCOLUMNINFO();
            for (int i = 0, offset = 0; i < columnCount; ++i, offset += ODB.SizeOf_tagDBCOLUMNINFO)
            {
                Marshal.PtrToStructure(ADP.IntPtrOffset(columnInfos, offset), dbColumnInfo);
                if (0 >= dbColumnInfo.iOrdinal)
                {
                    continue;
                }
                if (OleDbDataReader.DoColumnDropFilter(dbColumnInfo.dwFlags))
                {
                    continue;
                }

                if (null == dbColumnInfo.pwszName)
                {
                    dbColumnInfo.pwszName = "";
                }
                if (filterITypeInfo && (ODB.DBCOLUMN_TYPEINFO == dbColumnInfo.pwszName))
                {
                    continue;
                }
                if (filterChapters && (NativeDBType.HCHAPTER == dbColumnInfo.wType))
                {
                    continue;  // filter chapters in IRowset from IDBSchemaRowset for DumpToTable
                }

                bool islong = OleDbDataReader.IsLong(dbColumnInfo.dwFlags);
                bool isfixed = OleDbDataReader.IsFixed(dbColumnInfo.dwFlags);
                NativeDBType dbType = NativeDBType.FromDBType(dbColumnInfo.wType, islong, isfixed);

                MetaData info = new MetaData();
                info.columnName = dbColumnInfo.pwszName;
                info.type = dbType;
                info.ordinal = dbColumnInfo.iOrdinal;
                nint maxsize = dbColumnInfo.ulColumnSize;
                info.size = (((maxsize < 0) || (int.MaxValue < maxsize)) ? int.MaxValue : (int)maxsize);
                info.flags = dbColumnInfo.dwFlags;
                info.precision = dbColumnInfo.bPrecision;
                info.scale = dbColumnInfo.bScale;

                info.kind = dbColumnInfo.columnid.eKind;
                switch (dbColumnInfo.columnid.eKind)
                {
                    case ODB.DBKIND_GUID_NAME:
                    case ODB.DBKIND_GUID_PROPID:
                    case ODB.DBKIND_GUID:
                        info.guid = dbColumnInfo.columnid.uGuid;
                        break;
                    default:
                        Debug.Assert(ODB.DBKIND_PGUID_NAME != dbColumnInfo.columnid.eKind, "OLE DB providers never return pGuid-style bindings.");
                        Debug.Assert(ODB.DBKIND_PGUID_PROPID != dbColumnInfo.columnid.eKind, "OLE DB providers never return pGuid-style bindings.");
                        info.guid = Guid.Empty;
                        break;
                }
                switch (dbColumnInfo.columnid.eKind)
                {
                    case ODB.DBKIND_GUID_PROPID:
                    case ODB.DBKIND_PROPID:
                        info.propid = dbColumnInfo.columnid.ulPropid;
                        break;
                    case ODB.DBKIND_GUID_NAME:
                    case ODB.DBKIND_NAME:
                        if (IntPtr.Zero != dbColumnInfo.columnid.ulPropid)
                        {
                            info.idname = Marshal.PtrToStringUni(dbColumnInfo.columnid.ulPropid);
                        }
                        else
                        {
                            info.idname = null;
                        }
                        break;
                    default:
                        info.propid = IntPtr.Zero;
                        break;
                }
                metainfo[rowCount] = info;

#if DEBUG
                if (AdapterSwitches.DataSchema.TraceVerbose)
                {
                    Debug.WriteLine($"OleDbDataReader[{info.ordinal}, {dbColumnInfo.pwszName}]={dbType.enumOleDbType},{dbType.dataSourceType}, {dbType.wType}");
                }
#endif
                rowCount++;
            }
            if (rowCount < columnCount)
            { // shorten names array appropriately
                MetaData[] tmpinfo = new MetaData[rowCount];
                for (int i = 0; i < rowCount; ++i)
                {
                    tmpinfo[i] = metainfo[i];
                }
                metainfo = tmpinfo;
            }
            _visibleFieldCount = rowCount;
            _metadata = metainfo;
        }

        private void BuildSchemaTableRowset(object handle)
        {
            Debug.Assert(null == _dbSchemaTable, "BuildSchemaTableRowset - non-null SchemaTable");
            Debug.Assert(null != handle, "BuildSchemaTableRowset(object) - unexpected null handle");
            UnsafeNativeMethods.IColumnsRowset? icolumnsRowset = (handle as UnsafeNativeMethods.IColumnsRowset);

            if (null != icolumnsRowset)
            {
                UnsafeNativeMethods.IRowset? rowset = null;
                IntPtr cOptColumns;
                OleDbHResult hr;

                using (DualCoTaskMem prgOptColumns = new DualCoTaskMem(icolumnsRowset, out cOptColumns, out hr))
                {
                    Debug.Assert((0 == hr) || prgOptColumns.IsInvalid, "GetAvailableCOlumns: unexpected return");
                    hr = icolumnsRowset.GetColumnsRowset(IntPtr.Zero, cOptColumns, prgOptColumns, ref ODB.IID_IRowset, 0, IntPtr.Zero, out rowset);
                }

                Debug.Assert((0 <= hr) || (null == rowset), "if GetColumnsRowset failed, rowset should be null");
                if (hr < 0)
                {
                    ProcessResults(hr);
                }
                DumpToSchemaTable(rowset);

                // release the rowset to avoid race condition between the GC and the user code causing
                // "Connection is busy with results for another command" exception
                if (null != rowset)
                {
                    Marshal.ReleaseComObject(rowset);
                }
            }
            else
            {
                _useIColumnsRowset = false;
                BuildSchemaTableInfo(handle, false, false);
            }
        }

        public override void Close()
        {
            OleDbConnection? con = _connection;
            OleDbCommand? cmd = _command;
            Bindings? bindings = _parameterBindings;
            _connection = null!;
            _command = null!;
            _parameterBindings = null;

            _isClosed = true;

            DisposeOpenResults();
            _hasRows = false;

            if ((null != cmd) && cmd.canceling)
            {
                DisposeNativeMultipleResults();

                if (null != bindings)
                {
                    bindings.CloseFromConnection();
                    bindings = null;
                }
            }
            else
            {
                UnsafeNativeMethods.IMultipleResults? multipleResults = _imultipleResults;
                _imultipleResults = null;

                if (null != multipleResults)
                {
                    // if we don't have a cmd, same as a cancel (don't call NextResults) which is ADODB behavior

                    try
                    {
                        // tricky code path is an exception is thrown
                        // causing connection to do a ResetState and connection.Close
                        // resulting in OleDbCommand.CloseFromConnection
                        if ((null != cmd) && !cmd.canceling)
                        {
                            IntPtr affected = IntPtr.Zero;
                            OleDbException? nextResultsFailure = NextResults(multipleResults, null, cmd, out affected);
                            _recordsAffected = AddRecordsAffected(_recordsAffected, affected);
                            if (null != nextResultsFailure)
                            {
                                throw nextResultsFailure;
                            }
                        }
                    }
                    finally
                    {
                        if (null != multipleResults)
                        {
                            Marshal.ReleaseComObject(multipleResults);
                        }
                    }
                }
            }

            if ((null != cmd) && (0 == _depth))
            {
                // return bindings back to the cmd after closure of root DataReader
                cmd.CloseFromDataReader(bindings);
            }

            if (null != con)
            {
                con.RemoveWeakReference(this);

                // if the DataReader is Finalized it will not close the connection
                if (IsCommandBehavior(CommandBehavior.CloseConnection))
                {
                    con.Close();
                }
            }

            // release unmanaged objects
            RowHandleBuffer? rowHandleNativeBuffer = _rowHandleNativeBuffer;
            _rowHandleNativeBuffer = null;
            rowHandleNativeBuffer?.Dispose();
        }

        internal void CloseReaderFromConnection(bool canceling)
        {
            // being called from the connection, we will have a command. that command
            // may be Disposed, but it doesn't matter since another command can't execute
            // until all DataReader are closed
            if (null != _command)
            { // UNDONE: understand why this could be null, it shouldn't be but was
                _command.canceling = canceling;
            }

            // called from the connection which will remove this from its ReferenceCollection
            // we want the NextResult behavior, but no errors
            _connection = null!;

            Close();
        }

        private void DisposeManagedRowset()
        {
            //not cleared after last rowset
            //_hasRows = false;

            _isRead = false;
            _hasRowsReadCheck = false;

            _nextAccessorForRetrieval = 0;
            _nextValueForRetrieval = 0;

            Bindings?[]? bindings = _bindings;
            _bindings = null;

            if (null != bindings)
            {
                for (int i = 0; i < bindings.Length; ++i)
                {
                    if (bindings[i] is Bindings binding)
                    {
                        binding.Dispose();
                    }
                }
            }

            _currentRow = 0;
            _rowFetchedCount = 0;

            _dbSchemaTable = null;
            _visibleFieldCount = 0;
            _metadata = null;
            _fieldNameLookup = null;
        }

        private void DisposeNativeMultipleResults()
        {
            UnsafeNativeMethods.IMultipleResults? imultipleResults = _imultipleResults;
            _imultipleResults = null;

            if (null != imultipleResults)
            {
                Marshal.ReleaseComObject(imultipleResults);
            }
        }

        private void DisposeNativeRowset()
        {
            UnsafeNativeMethods.IRowset? irowset = _irowset;
            _irowset = null;

            ChapterHandle chapter = _chapterHandle;
            _chapterHandle = ChapterHandle.DB_NULL_HCHAPTER;

            if (ChapterHandle.DB_NULL_HCHAPTER != chapter)
            {
                chapter.Dispose();
            }

            if (null != irowset)
            {
                Marshal.ReleaseComObject(irowset);
            }
        }

        private void DisposeNativeRow()
        {
            UnsafeNativeMethods.IRow? irow = _irow;
            _irow = null;

            if (null != irow)
            {
                Marshal.ReleaseComObject(irow);
            }
        }

        private void DisposeOpenResults()
        {
            DisposeManagedRowset();

            DisposeNativeRow();
            DisposeNativeRowset();
        }

        public override bool GetBoolean(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueBoolean();
        }

        public override byte GetByte(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueByte();
        }

        private ColumnBinding DoSequentialCheck(int ordinal, long dataIndex, string method)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);

            if (dataIndex > int.MaxValue)
            {
                throw ADP.InvalidSourceBufferIndex(0, dataIndex, "dataIndex");
            }
            if (_sequentialOrdinal != ordinal)
            {
                _sequentialOrdinal = ordinal;
                _sequentialBytesRead = 0;
            }
            else if (_sequentialAccess && (_sequentialBytesRead < dataIndex))
            {
                throw ADP.NonSeqByteAccess(dataIndex, _sequentialBytesRead, method);
            }
            // getting the value doesn't really belong, but it's common to both
            // callers GetBytes and GetChars so we might as well have the code here
            return binding;
        }

        public override long GetBytes(int ordinal, long dataIndex, byte[]? buffer, int bufferIndex, int length)
        {
            ColumnBinding binding = DoSequentialCheck(ordinal, dataIndex, ADP.GetBytes);
            byte[] value = binding.ValueByteArray();

            if (null == buffer)
            {
                return value.Length;
            }
            int srcIndex = (int)dataIndex;
            int byteCount = Math.Min(value.Length - srcIndex, length);
            if (srcIndex < 0)
            {
                throw ADP.InvalidSourceBufferIndex(value.Length, srcIndex, "dataIndex");
            }
            else if ((bufferIndex < 0) || (bufferIndex >= buffer.Length))
            {
                throw ADP.InvalidDestinationBufferIndex(buffer.Length, bufferIndex, "bufferIndex");
            }
            if (0 < byteCount)
            {
                // @usernote: user may encounter ArgumentException from Buffer.BlockCopy
                Buffer.BlockCopy(value, srcIndex, buffer, bufferIndex, byteCount);
                _sequentialBytesRead = srcIndex + byteCount;
            }
            else if (length < 0)
            {
                throw ADP.InvalidDataLength(length);
            }
            else
            {
                byteCount = 0;
            }
            return byteCount;
        }

        public override long GetChars(int ordinal, long dataIndex, char[]? buffer, int bufferIndex, int length)
        {
            ColumnBinding binding = DoSequentialCheck(ordinal, dataIndex, ADP.GetChars);
            string value = binding.ValueString();

            if (null == buffer)
            {
                return value.Length;
            }

            int srcIndex = (int)dataIndex;
            int charCount = Math.Min(value.Length - srcIndex, length);
            if (srcIndex < 0)
            {
                throw ADP.InvalidSourceBufferIndex(value.Length, srcIndex, "dataIndex");
            }
            else if ((bufferIndex < 0) || (bufferIndex >= buffer.Length))
            {
                throw ADP.InvalidDestinationBufferIndex(buffer.Length, bufferIndex, "bufferIndex");
            }
            if (0 < charCount)
            {
                // @usernote: user may encounter ArgumentException from String.CopyTo
                value.CopyTo(srcIndex, buffer, bufferIndex, charCount);
                _sequentialBytesRead = srcIndex + charCount;
            }
            else if (length < 0)
            {
                throw ADP.InvalidDataLength(length);
            }
            else
            {
                charCount = 0;
            }
            return charCount;
        }

        [EditorBrowsable(EditorBrowsableState.Never)]
        public override char GetChar(int ordinal)
        {
            throw ADP.NotSupported();
        }

        [EditorBrowsable(EditorBrowsableState.Advanced)]
        public new OleDbDataReader GetData(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueChapter();
        }

        protected override DbDataReader GetDbDataReader(int ordinal)
        {
            return GetData(ordinal);
        }

        internal OleDbDataReader ResetChapter(int bindingIndex, int index, RowBinding rowbinding, int valueOffset)
        {
            return GetDataForReader(_metadata![bindingIndex + index].ordinal, rowbinding, valueOffset);
        }

        private OleDbDataReader GetDataForReader(IntPtr ordinal, RowBinding rowbinding, int valueOffset)
        {
            UnsafeNativeMethods.IRowsetInfo rowsetInfo = IRowsetInfo();
            UnsafeNativeMethods.IRowset? result;
            OleDbHResult hr;
            hr = rowsetInfo.GetReferencedRowset((IntPtr)ordinal, ref ODB.IID_IRowset, out result);

            ProcessResults(hr);
            // Per docs result can be null only when hr is DB_E_NOTAREFERENCECOLUMN which in most of the cases will cause the exception in ProcessResult

            OleDbDataReader? reader = null;

            if (result != null)
            {
                // only when the first datareader is closed will the connection close
                ChapterHandle chapterHandle = ChapterHandle.CreateChapterHandle(result, rowbinding, valueOffset);
                reader = new OleDbDataReader(_connection, _command, 1 + Depth, _commandBehavior & ~CommandBehavior.CloseConnection);
                reader.InitializeIRowset(result, chapterHandle, ADP.RecordsUnaffected);
                reader.BuildMetaInfo();
                reader.HasRowsRead();

                // connection tracks all readers to prevent cmd from executing
                // until all readers (including nested) are closed
                _connection?.AddWeakReference(reader, OleDbReferenceCollection.DataReaderTag);
            }

            return reader!;
        }

        public override string GetDataTypeName(int index)
        {
            if (null != _metadata)
            {
                return _metadata[index].type.dataSourceType;
            }
            throw ADP.DataReaderNoData();
        }

        public override DateTime GetDateTime(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueDateTime();
        }

        public override decimal GetDecimal(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueDecimal();
        }

        public override double GetDouble(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueDouble();
        }

        public override IEnumerator GetEnumerator()
        {
            return new DbEnumerator((IDataReader)this, IsCommandBehavior(CommandBehavior.CloseConnection));
        }

        public override Type GetFieldType(int index)
        {
            if (null != _metadata)
            {
                Type? fieldType = _metadata[index].type.dataType;
                Debug.Assert(fieldType != null);
                return fieldType;
            }
            throw ADP.DataReaderNoData();
        }

        public override float GetFloat(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueSingle();
        }

        public override Guid GetGuid(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueGuid();
        }

        public override short GetInt16(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueInt16();
        }

        public override int GetInt32(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueInt32();
        }

        public override long GetInt64(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueInt64();
        }

        public override string GetName(int index)
        {
            if (null != _metadata)
            {
                Debug.Assert(null != _metadata[index].columnName);
                return _metadata[index].columnName;
            }
            throw ADP.DataReaderNoData();
        }

        public override int GetOrdinal(string name)
        {
            if (null == _fieldNameLookup)
            {
                if (null == _metadata)
                {
                    throw ADP.DataReaderNoData();
                }
                _fieldNameLookup = new FieldNameLookup(this, -1);
            }
            return _fieldNameLookup.GetOrdinal(name);
        }

        public override string GetString(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.ValueString();
        }

        public TimeSpan GetTimeSpan(int ordinal)
        {
            return (TimeSpan)GetValue(ordinal);
        }

        private MetaData DoValueCheck(int ordinal)
        {
            if (!_isRead)
            {
                // Read hasn't been called yet or no more data
                throw ADP.DataReaderNoData();
            }
            else if (_sequentialAccess && (ordinal < _nextValueForRetrieval))
            {
                throw ADP.NonSequentialColumnAccess(ordinal, _nextValueForRetrieval);
            }
            // @usernote: user may encounter the IndexOutOfRangeException
            MetaData info = _metadata![ordinal];
            return info;
        }

        private ColumnBinding GetColumnBinding(int ordinal)
        {
            MetaData info = DoValueCheck(ordinal);
            return GetValueBinding(info);
        }

        private ColumnBinding GetValueBinding(MetaData info)
        {
            ColumnBinding binding = info.columnBinding;
            Debug.Assert(null != binding, "null binding");

            // do we need to jump to the next accessor
            for (int i = _nextAccessorForRetrieval; i <= binding.IndexForAccessor; ++i)
            {
                Debug.Assert(_nextAccessorForRetrieval <= binding.IndexForAccessor, "backwards index for accessor");
                Debug.Assert(_nextAccessorForRetrieval == i, "failed to increment");

                if (_sequentialAccess)
                {
                    if (_nextValueForRetrieval != binding.Index)
                    { // release old value
                        _metadata![_nextValueForRetrieval].columnBinding.ResetValue();
                    }
                    _nextAccessorForRetrieval = binding.IndexForAccessor;
                }

                if (null != _irowset)
                {
                    GetRowDataFromHandle(); // will increment _nextAccessorForRetrieval
                }
                else if (null != _irow)
                {
                    GetRowValue(); // will increment _nextAccessorForRetrieval
                }
                else
                {
                    throw ADP.DataReaderNoData();
                }
            }

            // to enforce sequential access
            _nextValueForRetrieval = binding.Index;
            return binding;
        }

        public override object GetValue(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            object value = binding.Value();
            return value;
        }

        public override int GetValues(object[] values)
        {
            if (null == values)
            {
                throw ADP.ArgumentNull("values");
            }
            DoValueCheck(0);
            int count = Math.Min(values.Length, _visibleFieldCount);
            for (int i = 0; (i < _metadata!.Length) && (i < count); ++i)
            {
                ColumnBinding binding = GetValueBinding(_metadata[i]);
                values[i] = binding.Value();
            }
            return count;
        }

        private bool IsCommandBehavior(CommandBehavior condition)
        {
            return (condition == (condition & _commandBehavior));
        }

        public override bool IsDBNull(int ordinal)
        {
            ColumnBinding binding = GetColumnBinding(ordinal);
            return binding.IsValueNull();
        }

        private void ProcessResults(OleDbHResult hr)
        {
            Exception? e;
            if (null != _command)
            {
                e = OleDbConnection.ProcessResults(hr, _connection);
            }
            else
            {
                e = OleDbConnection.ProcessResults(hr, _connection);
            }
            if (null != e)
            { throw e; }
        }

        private static nint AddRecordsAffected(nint recordsAffected, nint affected)
        {
            if (0 <= affected)
            {
                if (0 <= recordsAffected)
                {
                    return recordsAffected + affected;
                }
                return affected;
            }
            return recordsAffected;
        }

        public override int VisibleFieldCount
        {
            get
            {
                if (IsClosed)
                {
                    throw ADP.DataReaderClosed("VisibleFieldCount");
                }
                return _visibleFieldCount;
            }
        }

        internal void HasRowsRead()
        {
            Debug.Assert(!_hasRowsReadCheck, "_hasRowsReadCheck not reset");
            bool flag = Read();
            _hasRows = flag;
            _hasRowsReadCheck = true;
            _isRead = false;
        }

        internal static OleDbException? NextResults(UnsafeNativeMethods.IMultipleResults? imultipleResults, OleDbConnection? connection, OleDbCommand command, out IntPtr recordsAffected)
        {
            recordsAffected = ADP.RecordsUnaffected;
            List<OleDbException>? exceptions = null;
            if (null != imultipleResults)
            {
                nint affected;
                OleDbHResult hr;

                // MSOLAP provider doesn't move onto the next result when calling GetResult with IID_NULL, but does return S_OK with 0 affected records.
                // we want to break out of that infinite loop for ExecuteNonQuery and the multiple result Close scenarios
                for (int loop = 0; ; ++loop)
                {
                    if ((null != command) && command.canceling)
                    {
                        break;
                    }
                    hr = imultipleResults.GetResult(IntPtr.Zero, ODB.DBRESULTFLAG_DEFAULT, ref ODB.IID_NULL, out affected, out _);

                    // If a provider doesn't support IID_NULL and returns E_NOINTERFACE we want to break out
                    // of the loop without throwing an exception.  Our behavior will match ADODB in that scenario
                    // where Recordset.Close just releases the interfaces without processing remaining results
                    if ((OleDbHResult.DB_S_NORESULT == hr) || (OleDbHResult.E_NOINTERFACE == hr))
                    {
                        break;
                    }
                    if (null != connection)
                    {
                        Exception? e = OleDbConnection.ProcessResults(hr, connection);
                        if (null != e)
                        {
                            OleDbException? excep = (e as OleDbException);
                            if (null != excep)
                            {
                                if (null == exceptions)
                                {
                                    exceptions = new List<OleDbException>();
                                }
                                exceptions.Add(excep);
                            }
                            else
                            {
                                Debug.Assert(OleDbHResult.DB_E_OBJECTOPEN == hr, "unexpected");
                                throw e; // we don't expect to be here, but it could happen
                            }
                        }
                    }
                    else if (hr < 0)
                    {
                        SafeNativeMethods.Wrapper.ClearErrorInfo();
                        break;
                    }
                    recordsAffected = AddRecordsAffected(recordsAffected, affected);

                    if (0 != (int)affected)
                    {
                        loop = 0;
                    }
                    else if (2000 <= loop)
                    { // (reason for more than 1000 iterations)
                        NextResultsInfinite();
                        break;
                    }
                }
            }
            if (null != exceptions)
            {
                return OleDbException.CombineExceptions(exceptions);
            }
            return null;
        }

        private static void NextResultsInfinite()
        {
            // edtriou's suggestion is that we debug assert so that users will learn of MSOLAP's misbehavior and not call ExecuteNonQuery
            Debug.Assert(false, "<oledb.OleDbDataReader.NextResultsInfinite|INFO> System.Data.OleDb.OleDbDataReader: 2000 IMultipleResult.GetResult(NULL, DBRESULTFLAG_DEFAULT, IID_NULL, NULL, NULL) iterations with 0 records affected. Stopping suspect infinite loop. To work-around try using ExecuteReader() and iterating through results with NextResult().\n");
        }

        public override bool NextResult()
        {
            bool retflag = false;
            if (IsClosed)
            {
                throw ADP.DataReaderClosed("NextResult");
            }
            _fieldNameLookup = null;

            OleDbCommand? command = _command;
            UnsafeNativeMethods.IMultipleResults? imultipleResults = _imultipleResults;
            if (null != imultipleResults)
            {
                DisposeOpenResults();
                _hasRows = false;

                while (true)
                {
                    Debug.Assert(null == _irow, "NextResult: row loop check");
                    Debug.Assert(null == _irowset, "NextResult: rowset loop check");

                    object? result;
                    OleDbHResult hr;
                    IntPtr affected;

                    if ((null != command) && command.canceling)
                    {
                        Close();
                        break;
                    }
                    hr = imultipleResults.GetResult(IntPtr.Zero, ODB.DBRESULTFLAG_DEFAULT, ref ODB.IID_IRowset, out affected, out result);

                    if ((0 <= hr) && (null != result))
                    {
                        _irowset = (UnsafeNativeMethods.IRowset)result;
                    }
                    _recordsAffected = AddRecordsAffected(_recordsAffected, affected);

                    if (OleDbHResult.DB_S_NORESULT == hr)
                    {
                        DisposeNativeMultipleResults();
                        break;
                    }
                    // @devnote: infomessage events may be fired from here
                    ProcessResults(hr);

                    if (null != _irowset)
                    {
                        BuildMetaInfo();
                        HasRowsRead();
                        retflag = true;
                        break;
                    }
                }
            }
            else
            {
                DisposeOpenResults();
                _hasRows = false;
            }
            return retflag;
        }

        public override bool Read()
        {
            bool retflag = false;
            OleDbCommand? command = _command;
            if ((null != command) && command.canceling)
            {
                DisposeOpenResults();
            }
            else if (null != _irowset)
            {
                if (_hasRowsReadCheck)
                {
                    _isRead = retflag = _hasRows;
                    _hasRowsReadCheck = false;
                }
                else if (_singleRow && _isRead)
                {
                    DisposeOpenResults();
                }
                else
                {
                    retflag = ReadRowset();
                }
            }
            else if (null != _irow)
            {
                retflag = ReadRow();
            }
            else if (IsClosed)
            {
                throw ADP.DataReaderClosed("Read");
            }
            return retflag;
        }

        private bool ReadRow()
        {
            if (_isRead)
            {
                _isRead = false; // for DoValueCheck

                DisposeNativeRow();

                _sequentialOrdinal = -1; // sequentialBytesRead will reset when used
            }
            else
            {
                _isRead = true;
                return (0 < _metadata!.Length);
            }
            return false;
        }

        private bool ReadRowset()
        {
            Debug.Assert(null != _irowset, "ReadRow: null IRowset");
            Debug.Assert(0 <= _metadata!.Length, "incorrect state for fieldCount");

            // releases bindings as necessary
            // bumps current row, else resets it back to initial state
            ReleaseCurrentRow();

            _sequentialOrdinal = -1; // sequentialBytesRead will reset when used

            // making the check if (null != irowset) unnecessary
            // if necessary, get next group of row handles
            if (0 == _rowFetchedCount)
            { // starts at (-1 <= 0)
                Debug.Assert(0 == _currentRow, "incorrect state for _currentRow");
                Debug.Assert(0 <= _metadata.Length, "incorrect state for fieldCount");
                Debug.Assert(0 == _nextAccessorForRetrieval, "incorrect state for nextAccessorForRetrieval");
                Debug.Assert(0 == _nextValueForRetrieval, "incorrect state for nextValueForRetrieval");

                // @devnote: releasing row handles occurs next time user calls read, skip, or close
                GetRowHandles(/*skipCount*/);
            }
            return ((_currentRow <= (int)_rowFetchedCount) && _isRead);
        }

        private void ReleaseCurrentRow()
        {
            Debug.Assert(null != _irowset, "ReleaseCurrentRow: no rowset");

            if (0 < (int)_rowFetchedCount)
            {
                // release the data in the current row
                Bindings?[]? bindings = _bindings;
                Debug.Assert(null != bindings, "ReleaseCurrentRow: null dbBindings");
                for (int i = 0; (i < bindings.Length) && (i < _nextAccessorForRetrieval); ++i)
                {
                    bindings[i]!.CleanupBindings();
                }
                _nextAccessorForRetrieval = 0;
                _nextValueForRetrieval = 0;

                _currentRow++;
                if (_currentRow == (int)_rowFetchedCount)
                {
                    ReleaseRowHandles();
                }
            }
        }

        private void CreateAccessors(bool allowMultipleAccessor)
        {
            Debug.Assert(null == _bindings, "CreateAccessor: dbBindings already exists");
            Debug.Assert(null != _irowset, "CreateAccessor: no IRowset available");
            Debug.Assert(null != _metadata && 0 < _metadata.Length, "no columns");

            Bindings[] dbBindings = CreateBindingsFromMetaData(allowMultipleAccessor);

            UnsafeNativeMethods.IAccessor iaccessor = IAccessor();
            for (int i = 0; i < dbBindings.Length; ++i)
            {
                OleDbHResult hr = dbBindings[i].CreateAccessor(iaccessor, ODB.DBACCESSOR_ROWDATA);
                if (hr < 0)
                {
                    ProcessResults(hr);
                }
            }

            if (0 == _rowHandleFetchCount)
            {
                _rowHandleFetchCount = 1;

                object? maxRows = GetPropertyValue(ODB.DBPROP_MAXROWS);
                if (maxRows is int intValue)
                {
                    _rowHandleFetchCount = intValue;
                    if ((0 == _rowHandleFetchCount) || (20 <= _rowHandleFetchCount))
                    {
                        _rowHandleFetchCount = 20;
                    }
                }
                else if (maxRows is long longValue)
                {
                    _rowHandleFetchCount = (nint)longValue;
                    if ((0 == _rowHandleFetchCount) || (20 <= _rowHandleFetchCount))
                    {
                        _rowHandleFetchCount = 20;
                    }
                }
            }
            if (null == _rowHandleNativeBuffer)
            {
                _rowHandleNativeBuffer = new RowHandleBuffer(_rowHandleFetchCount);
            }
        }

        private Bindings[] CreateBindingsFromMetaData(bool allowMultipleAccessor)
        {
            int bindingCount = 0;
            int currentBindingIndex = 0;

            MetaData[] metadata = _metadata!;

            int[] indexToBinding = new int[metadata.Length];
            int[] indexWithinBinding = new int[metadata.Length];

            // walk through the schemaRows to determine the number of binding groups
            if (allowMultipleAccessor)
            {
                if (null != _irowset)
                {
                    for (int i = 0; i < indexToBinding.Length; ++i)
                    {
                        indexToBinding[i] = bindingCount;
                        indexWithinBinding[i] = currentBindingIndex;
#if false
                        // @denote: single/multiple Accessors
                        if ((bindingCount < 2) && IsLong(metadata[i].flags)) {
                            bindingCount++;
                            currentBindingIndex = 0;
                        }
                        else {
                            currentBindingIndex++;
                        }
#elif false
                        // @devnote: one accessor per column option
                        bindingCount++;
                        currentBindingIndex = 0;
#else
                        // @devnote: one accessor only for IRowset
                        currentBindingIndex++;
#endif
                    }
                    if (0 < currentBindingIndex)
                    { // when blob is not the last column
                        bindingCount++;
                    }
                }
                else if (null != _irow)
                {
                    for (int i = 0; i < indexToBinding.Length; ++i)
                    {
                        indexToBinding[i] = i;
                        indexWithinBinding[i] = 0;
                    }
                    bindingCount = metadata.Length;
                }
            }
            else
            {
                for (int i = 0; i < indexToBinding.Length; ++i)
                {
                    indexToBinding[i] = 0;
                    indexWithinBinding[i] = i;
                }
                bindingCount = 1;
            }

            Bindings bindings;
            Bindings[] dbbindings = new Bindings[bindingCount];
            bindingCount = 0;

            // for every column, build tagDBBinding info
            for (int index = 0; index < metadata.Length; ++index)
            {
                Debug.Assert(indexToBinding[index] < dbbindings.Length, "bad indexToAccessor");
                bindings = dbbindings[indexToBinding[index]];
                if (null == bindings)
                {
                    bindingCount = 0;
                    for (int i = index; (i < metadata.Length) && (bindingCount == indexWithinBinding[i]); ++i)
                    {
                        bindingCount++;
                    }
                    dbbindings[indexToBinding[index]] = bindings = new Bindings((OleDbDataReader)this, (null != _irowset), bindingCount);

                    // runningTotal is buffered to start values on 16-byte boundary
                    // the first columnCount * 8 bytes are for the length and status fields
                    //bindings.DataBufferSize = (bindingCount + (bindingCount % 2)) * sizeof_int64;
                }
                MetaData info = metadata[index];

                int maxLen = info.type.fixlen;
                short getType = info.type.wType;

                Debug.Assert(NativeDBType.STR != getType, "Should have bound as WSTR");

                if (-1 != info.size)
                {
                    if (info.type.islong)
                    {
                        maxLen = IntPtr.Size;
                        getType = (short)((ushort)getType | (ushort)NativeDBType.BYREF);
                    }
                    else if (-1 == maxLen)
                    {
                        // @devnote: not using provider owned memory for PDC, no one really supports it anyway.
                        /*if (((null != connection) && connection.PropertyGetProviderOwnedMemory())
                            || ((null != command) && command.Connection.PropertyGetProviderOwnedMemory())) {
                            bindings.MemOwner = DBMemOwner.ProviderOwned;

                            bindings.MaxLen = IntPtr.Size;
                            bindings.DbType = (short) (getType | DbType.BYREF);
                        }
                        else*/

                        if (ODB.LargeDataSize < info.size)
                        {
                            maxLen = IntPtr.Size;
                            getType = (short)((ushort)getType | (ushort)NativeDBType.BYREF);
                        }
                        else if ((NativeDBType.WSTR == getType) && (-1 != info.size))
                        {
                            maxLen = info.size * 2 + 2;
                        }
                        else
                        {
                            maxLen = info.size;
                        }
                    }
                }
                else if (maxLen < 0)
                {
                    // if variable length and no defined size we require this to be byref at this time
                    /*if (((null != connection) && connection.PropertyGetProviderOwnedMemory())
                        || ((null != command) && command.Connection.PropertyGetProviderOwnedMemory())) {
                        bindings.MemOwner = DBMemOwner.ProviderOwned;
                    }*/
                    maxLen = IntPtr.Size;
                    getType = (short)((ushort)getType | (ushort)NativeDBType.BYREF);
                }

                currentBindingIndex = indexWithinBinding[index];
                bindings.CurrentIndex = currentBindingIndex;

                bindings.Ordinal = info.ordinal;
                bindings.Part = info.type.dbPart;
                bindings.Precision = (byte)info.precision;
                bindings.Scale = (byte)info.scale;
                bindings.DbType = (short)getType;
                bindings.MaxLen = maxLen; // also increments databuffer size (uses DbType)

                //bindings.ValueOffset  = // set via MaxLen
                //bindings.LengthOffset = // set via MaxLen
                //bindings.StatusOffset = // set via MaxLen
                //bindings.TypeInfoPtr  = 0;
                //bindings.ObjectPtr    = 0;
                //bindings.BindExtPtr   = 0;
                //bindings.MemOwner     = /*DBMEMOWNER_CLIENTOWNED*/0;
                //bindings.ParamIO      = ODB.DBPARAMIO_NOTPARAM;
                //bindings.Flags        = 0;
            }

            int count = 0, indexStart = 0;
            for (int i = 0; i < dbbindings.Length; ++i)
            {
                indexStart = dbbindings[i].AllocateForAccessor(this, indexStart, i);

                ColumnBinding[] columnBindings = dbbindings[i].ColumnBindings();
                for (int k = 0; k < columnBindings.Length; ++k)
                {
                    Debug.Assert(count == columnBindings[k].Index, "column binding mismatch");
                    metadata[count].columnBinding = columnBindings[k];
                    metadata[count].bindings = dbbindings[i];
                    count++;
                }
            }

            _bindings = dbbindings;
            return dbbindings;
        }

        private void GetRowHandles(/*int skipCount*/)
        {
            Debug.Assert(0 < (int)_rowHandleFetchCount, "GetRowHandles: bad _rowHandleFetchCount");
            Debug.Assert(!_isRead, "GetRowHandles: _isRead");

            OleDbHResult hr = 0;

            RowHandleBuffer rowHandleBuffer = _rowHandleNativeBuffer!;
            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                rowHandleBuffer.DangerousAddRef(ref mustRelease);

                IntPtr rowHandlesPtr = rowHandleBuffer.DangerousGetHandle();
                UnsafeNativeMethods.IRowset irowset = IRowset();
                try
                {
                    hr = irowset.GetNextRows(_chapterHandle.HChapter, /*skipCount*/IntPtr.Zero, _rowHandleFetchCount, out _rowFetchedCount, ref rowHandlesPtr);
                    Debug.Assert(rowHandleBuffer.DangerousGetHandle() == rowHandlesPtr, "rowhandlebuffer changed");
                }
                catch (System.InvalidCastException e)
                {
                    throw ODB.ThreadApartmentState(e);
                }
            }
            finally
            {
                if (mustRelease)
                {
                    rowHandleBuffer.DangerousRelease();
                }
            }

            //if (/*DB_S_ROWLIMITEXCEEDED*/0x00040EC0 == hr) {
            //    _rowHandleFetchCount = 1;
            //    _isRead = true;
            //} else
            if (hr < 0)
            {
                // filter out the BadStartPosition due to the skipCount which
                // maybe greater than the number of rows in the return rowset
                //const int /*OLEDB_Error.*/DB_E_BADSTARTPOSITION = unchecked((int)0x80040E1E);
                //if (DB_E_BADSTARTPOSITION != hr)
                ProcessResults(hr);
            }
            _isRead = ((OleDbHResult.DB_S_ENDOFROWSET != hr) || (0 < (int)_rowFetchedCount));
            _rowFetchedCount = Math.Max((int)_rowFetchedCount, 0);
        }

        private void GetRowDataFromHandle()
        {
            Debug.Assert(null != _bindings, "GetRowDataFromHandle: null dbBindings");
            Debug.Assert(null != _rowHandleNativeBuffer, "GetRowDataFromHandle: null dbBindings");

            OleDbHResult hr = 0;
            UnsafeNativeMethods.IRowset irowset = IRowset();

            IntPtr rowHandle = _rowHandleNativeBuffer.GetRowHandle(_currentRow);

            RowBinding rowBinding = _bindings[_nextAccessorForRetrieval]!.RowBinding()!;
            IntPtr accessorHandle = rowBinding.DangerousGetAccessorHandle();

            bool mustRelease = false;
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                rowBinding.DangerousAddRef(ref mustRelease);
                rowBinding.StartDataBlock();

                IntPtr dataPtr = rowBinding.DangerousGetDataPtr();
                hr = irowset.GetData(rowHandle, accessorHandle, dataPtr);
            }
            finally
            {
                if (mustRelease)
                {
                    rowBinding.DangerousRelease();
                }
            }

            _nextAccessorForRetrieval++;
            if (hr < 0)
            {
                ProcessResults(hr);
            }
        }

        private void ReleaseRowHandles()
        {
            Debug.Assert(0 < (int)_rowFetchedCount, "invalid _rowFetchedCount");

            OleDbHResult hr;
            UnsafeNativeMethods.IRowset irowset = IRowset();
            hr = irowset.ReleaseRows(_rowFetchedCount, _rowHandleNativeBuffer!, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);

            if (hr < 0)
            {
                //ProcessFailure(hr);
                //ProcessFailure(hr);
                SafeNativeMethods.Wrapper.ClearErrorInfo();
            }
            _rowFetchedCount = 0;
            _currentRow = 0;
            _isRead = false;
        }

        private object? GetPropertyValue(int propertyId)
        {
            if (null != _irowset)
            {
                return GetPropertyOnRowset(OleDbPropertySetGuid.Rowset, propertyId);
            }
            else if (null != _command)
            {
                return _command.GetPropertyValue(OleDbPropertySetGuid.Rowset, propertyId);
            }
            return OleDbPropertyStatus.NotSupported;
        }

        private object? GetPropertyOnRowset(Guid propertySet, int propertyID)
        {
            OleDbHResult hr;
            ItagDBPROP[] dbprops;
            UnsafeNativeMethods.IRowsetInfo irowsetinfo = IRowsetInfo();

            using (PropertyIDSet propidset = new PropertyIDSet(propertySet, propertyID))
            {
                using (DBPropSet propset = new DBPropSet(irowsetinfo, propidset, out hr))
                {
                    if (hr < 0)
                    {
                        // OLEDB Data Reader masks provider specific errors by raising "Internal Data Provider error 30."
                        // DBPropSet c-tor will register the exception and it will be raised at GetPropertySet call in case of failure
                        SafeNativeMethods.Wrapper.ClearErrorInfo();
                    }
                    dbprops = propset.GetPropertySet(0, out propertySet);
                }
            }
            if (OleDbPropertyStatus.Ok == dbprops[0].dwStatus)
            {
                return dbprops[0].vValue;
            }
            return dbprops[0].dwStatus;
        }

        private void GetRowValue()
        {
            Debug.Assert(null != _irow, "GetRowValue: null IRow");
            Debug.Assert(null != _metadata, "GetRowValue: null MetaData");

            Bindings bindings = _bindings![_nextAccessorForRetrieval]!;
            ColumnBinding[] columnBindings = bindings.ColumnBindings();
            RowBinding rowBinding = bindings.RowBinding()!;
            Debug.Assert(_nextValueForRetrieval <= columnBindings[0].Index, "backwards retrieval");

            bool mustReleaseBinding = false;
            bool[] mustRelease = new bool[columnBindings.Length];
            StringMemHandle?[] sptr = new StringMemHandle[columnBindings.Length];
            RuntimeHelpers.PrepareConstrainedRegions();
            try
            {
                for (int i = 0; i < columnBindings.Length; ++i)
                {
                    bindings.CurrentIndex = i;

                    sptr[i] = null;
                    MetaData info = _metadata[columnBindings[i].Index];
                    if ((ODB.DBKIND_GUID_NAME == info.kind) || (ODB.DBKIND_NAME == info.kind))
                    {
                        sptr[i] = new StringMemHandle(info.idname);
                        columnBindings[i]._sptr = sptr[i];
                    }

                    sptr[i]!.DangerousAddRef(ref mustRelease[i]);

                    IntPtr ulPropid = ((null != sptr[i]) ? sptr[i]!.DangerousGetHandle() : info.propid);
                    bindings.GuidKindName(info.guid, info.kind, ulPropid);
                }

                OleDbHResult hr;
                tagDBCOLUMNACCESS[] access = bindings.DBColumnAccess;

                rowBinding.DangerousAddRef(ref mustReleaseBinding);
                rowBinding.StartDataBlock();

                UnsafeNativeMethods.IRow irow = IRow();
                hr = irow.GetColumns((IntPtr)access.Length, access);
            }
            finally
            {
                if (mustReleaseBinding)
                {
                    rowBinding.DangerousRelease();
                }
                for (int i = 0; i < mustRelease.Length; i++)
                {
                    if (mustRelease[i])
                    {
                        sptr[i]!.DangerousRelease();
                    }
                }
            }
            _nextAccessorForRetrieval++;
        }

        private static int IndexOf(Hashtable hash, string name)
        {
            // via case sensitive search, first match with lowest ordinal matches
            object? index = hash[name];
            if (null != index)
            {
                return (int)index; // match via case-insensitive or by chance lowercase
            }

            // via case insensitive search, first match with lowest ordinal matches
            string tmp = name.ToLowerInvariant();
            index = hash[tmp]; // match via lowercase
            return ((null != index) ? (int)index : -1);
        }

        private void AppendSchemaInfo()
        {
            Debug.Assert(null != _connection, "null connection");
            Debug.Assert(null != _metadata, "no _metadata");

            if (_metadata.Length <= 0)
            {
                return;
            }

            int keyCount = 0;
            for (int i = 0; i < _metadata.Length; ++i)
            {
                if (_metadata[i].isKeyColumn && !_metadata[i].isHidden)
                {
                    keyCount++;
                }
            }
            if (0 != keyCount) /*|| _connection.IsServer_msdaora || _connection.IsServer_Microsoft_SQL)*/
            {
                return;
            }

            string schemaName, catalogName; // enforce single table
            string? baseSchemaName = null, baseCatalogName = null, baseTableName = null;
            for (int i = 0; i < _metadata.Length; ++i)
            {
                MetaData info = _metadata[i];
                if ((null != info.baseTableName) && (0 < info.baseTableName.Length))
                {
                    catalogName = info.baseCatalogName ?? "";
                    schemaName = info.baseSchemaName ?? "";
                    if (null == baseTableName)
                    {
                        baseSchemaName = schemaName;
                        baseCatalogName = catalogName;
                        baseTableName = info.baseTableName;
                    }
                    else if ((0 != ADP.SrcCompare(baseTableName, info.baseTableName))
                            || (0 != ADP.SrcCompare(baseCatalogName, catalogName))
                            || (0 != ADP.SrcCompare(baseSchemaName, schemaName)))
                    {
#if DEBUG
                        if (AdapterSwitches.DataSchema.TraceVerbose)
                        {
                            Debug.WriteLine("Multiple BaseTableName detected:"
                                + " <" + baseCatalogName + "." + baseCatalogName + "." + baseTableName + ">"
                                + " <" + info.baseCatalogName + "." + info.baseCatalogName + "." + info.baseTableName + ">");
                        }
#endif
                        baseTableName = null;
                        break;
                    }
                }
            }
            if (null == baseTableName)
            {
                return;
            }
            baseCatalogName = ADP.IsEmpty(baseCatalogName) ? null : baseCatalogName;
            baseSchemaName = ADP.IsEmpty(baseSchemaName) ? null : baseSchemaName;

            if (null != _connection)
            {
                if (ODB.DBPROPVAL_IC_SENSITIVE == _connection.QuotedIdentifierCase())
                {
                    string? p = null, s = null;
                    _connection.GetLiteralQuotes(ADP.GetSchemaTable, out s, out p);
                    if (null == s)
                    {
                        s = "";
                    }
                    if (null == p)
                    {
                        p = "";
                    }
                    baseTableName = s + baseTableName + p;
                }
            }

            Hashtable baseColumnNames = new Hashtable(_metadata.Length * 2);

            for (int i = _metadata.Length - 1; 0 <= i; --i)
            {
                string? basecolumname = _metadata[i].baseColumnName;
                if (!ADP.IsEmpty(basecolumname))
                {
                    baseColumnNames[basecolumname] = i;
                }
            }
            for (int i = 0; i < _metadata.Length; ++i)
            {
                string? basecolumname = _metadata[i].baseColumnName;
                if (!ADP.IsEmpty(basecolumname))
                {
                    basecolumname = basecolumname.ToLowerInvariant();
                    if (!baseColumnNames.Contains(basecolumname))
                    {
                        baseColumnNames[basecolumname] = i;
                    }
                }
            }

            // look for primary keys in the table
            if (_connection!.SupportSchemaRowset(OleDbSchemaGuid.Primary_Keys))
            {
                object?[] restrictions = new object?[] { baseCatalogName, baseSchemaName, baseTableName };
                keyCount = AppendSchemaPrimaryKey(baseColumnNames, restrictions);
            }
            if (0 != keyCount)
            {
                return;
            }

            // look for a single unique constraint that can be upgraded
            if (_connection.SupportSchemaRowset(OleDbSchemaGuid.Indexes))
            {
                object?[] restrictions = new object?[] { baseCatalogName, baseSchemaName, null, null, baseTableName };
                AppendSchemaUniqueIndexAsKey(baseColumnNames, restrictions);
            }
        }

        private int AppendSchemaPrimaryKey(Hashtable baseColumnNames, object?[] restrictions)
        {
            int keyCount = 0;
            bool partialPrimaryKey = false;
            DataTable? table = null;
            try
            {
                table = _connection!.GetSchemaRowset(OleDbSchemaGuid.Primary_Keys, restrictions);
            }
            catch (Exception e)
            {
                // UNDONE - should not be catching all exceptions!!!
                if (!ADP.IsCatchableExceptionType(e))
                {
                    throw;
                }

                ADP.TraceExceptionWithoutRethrow(e);
            }
            if (null != table)
            {
                DataColumnCollection dataColumns = table.Columns;
                int nameColumnIndex = dataColumns.IndexOf(ODB.COLUMN_NAME);

                if (-1 != nameColumnIndex)
                {
                    DataColumn nameColumn = dataColumns[nameColumnIndex];
                    foreach (DataRow dataRow in table.Rows)
                    {
                        string name = (string)dataRow[nameColumn, DataRowVersion.Default];

                        int metaindex = IndexOf(baseColumnNames, name);
                        if (0 <= metaindex)
                        {
                            MetaData info = _metadata![metaindex];
                            info.isKeyColumn = true;
                            info.flags &= ~ODB.DBCOLUMNFLAGS_ISNULLABLE;
                            keyCount++;
                        }
                        else
                        {
#if DEBUG
                            if (AdapterSwitches.DataSchema.TraceVerbose)
                            {
                                Debug.WriteLine($"PartialKeyColumn detected: <{name}> metaindex={metaindex}");
                            }
#endif
                            partialPrimaryKey = true;
                            break;
                        }
                    }
                }
            }
            if (partialPrimaryKey)
            { // partial primary key detected
                for (int i = 0; i < _metadata!.Length; ++i)
                {
                    _metadata[i].isKeyColumn = false;
                }
                return -1;
            }
            return keyCount;
        }

        private void AppendSchemaUniqueIndexAsKey(Hashtable baseColumnNames, object?[] restrictions)
        {
            bool partialPrimaryKey = false;
            DataTable? table = null;
            try
            {
                table = _connection!.GetSchemaRowset(OleDbSchemaGuid.Indexes, restrictions);
            }
            catch (Exception e)
            {
                // UNDONE - should not be catching all exceptions!!!
                if (!ADP.IsCatchableExceptionType(e))
                {
                    throw;
                }

                ADP.TraceExceptionWithoutRethrow(e);
            }
            if (null != table)
            {
                DataColumnCollection dataColumns = table.Columns;

                int indxIndex = dataColumns.IndexOf(ODB.INDEX_NAME);
                int pkeyIndex = dataColumns.IndexOf(ODB.PRIMARY_KEY);
                int uniqIndex = dataColumns.IndexOf(ODB.UNIQUE);
                int nameIndex = dataColumns.IndexOf(ODB.COLUMN_NAME);
                int nullIndex = dataColumns.IndexOf(ODB.NULLS);

                if ((-1 != indxIndex) && (-1 != pkeyIndex) && (-1 != uniqIndex) && (-1 != nameIndex))
                {
                    DataColumn indxColumn = dataColumns[indxIndex];
                    DataColumn pkeyColumn = dataColumns[pkeyIndex];
                    DataColumn uniqCOlumn = dataColumns[uniqIndex];
                    DataColumn nameColumn = dataColumns[nameIndex];
                    DataColumn? nulls = ((-1 != nullIndex) ? dataColumns[nullIndex] : null);

                    bool[] keys = new bool[_metadata!.Length];
                    bool[]? uniq = new bool[_metadata.Length];
                    string? uniqueIndexName = null;

                    // match pkey name BaseColumnName
                    foreach (DataRow dataRow in table.Rows)
                    {
                        bool isPKey = (!dataRow.IsNull(pkeyColumn, DataRowVersion.Default) && (bool)dataRow[pkeyColumn, DataRowVersion.Default]);
                        bool isUniq = (!dataRow.IsNull(uniqCOlumn, DataRowVersion.Default) && (bool)dataRow[uniqCOlumn, DataRowVersion.Default]);

                        if (isPKey || isUniq)
                        {
                            string name = (string)dataRow[nameColumn, DataRowVersion.Default];

                            int metaindex = IndexOf(baseColumnNames, name);
                            if (0 <= metaindex)
                            {
                                if (isPKey)
                                {
                                    keys[metaindex] = true;
                                }
                                if (isUniq && (null != uniq))
                                {
                                    uniq[metaindex] = true;

                                    string indexname = (string)dataRow[indxColumn, DataRowVersion.Default];
                                    if (null == uniqueIndexName)
                                    {
                                        uniqueIndexName = indexname;
                                    }
                                    else if (indexname != uniqueIndexName)
                                    {
#if DEBUG
                                        if (AdapterSwitches.DataSchema.TraceVerbose)
                                        {
                                            Debug.WriteLine($"MultipleUniqueIndexes detected: <{uniqueIndexName}> <{indexname}>");
                                        }
#endif
                                        uniq = null;
                                    }
                                }
                            }
                            else if (isPKey)
                            {
#if DEBUG
                                if (AdapterSwitches.DataSchema.TraceVerbose)
                                {
                                    Debug.WriteLine($"PartialKeyColumn detected: {name}");
                                }
#endif
                                partialPrimaryKey = true;
                                break;
                            }
                            else if (null != uniqueIndexName)
                            {
                                string indexname = (string)dataRow[indxColumn, DataRowVersion.Default];

                                if (indexname != uniqueIndexName)
                                {
#if DEBUG
                                    if (AdapterSwitches.DataSchema.TraceVerbose)
                                    {
                                        Debug.WriteLine($"PartialUniqueIndexes detected: <{uniqueIndexName}> <{indexname}>");
                                    }
#endif
                                    uniq = null;
                                }
                            }
                        }
                    }
                    if (partialPrimaryKey)
                    {
                        for (int i = 0; i < _metadata.Length; ++i)
                        {
                            _metadata[i].isKeyColumn = false;
                        }
                        return;
                    }
                    else if (null != uniq)
                    {
#if DEBUG
                        if (AdapterSwitches.DataSchema.TraceVerbose)
                        {
                            Debug.WriteLine($"upgrade single unique index to be a key: <{uniqueIndexName}>");
                        }
#endif
                        // upgrade single unique index to be a key
                        for (int i = 0; i < _metadata.Length; ++i)
                        {
                            _metadata[i].isKeyColumn = uniq[i];
                        }
                    }
                }
            }
        }

        private MetaData? FindMetaData(string name)
        {
            int index = _fieldNameLookup!.IndexOfName(name);
            return ((-1 != index) ? _metadata![index] : null);
        }

        internal void DumpToSchemaTable(UnsafeNativeMethods.IRowset? rowset)
        {
            List<MetaData> metainfo = new List<MetaData>();

            object? hiddenColumns = null;
            using (OleDbDataReader dataReader = new OleDbDataReader(_connection, _command, int.MinValue, 0))
            {
                dataReader.InitializeIRowset(rowset, ChapterHandle.DB_NULL_HCHAPTER, IntPtr.Zero);
                dataReader.BuildSchemaTableInfo(rowset!, true, false);

                hiddenColumns = GetPropertyValue(ODB.DBPROP_HIDDENCOLUMNS);
                if (0 == dataReader.FieldCount)
                {
                    return;
                }

                Debug.Assert(null == dataReader._fieldNameLookup, "lookup already exists");
                FieldNameLookup lookup = new FieldNameLookup(dataReader, -1);
                dataReader._fieldNameLookup = lookup;

                // This column, together with the DBCOLUMN_GUID and DBCOLUMN_PROPID
                // columns, forms the ID of the column. One or more (but not all) of these columns
                // will be NULL, depending on which elements of the DBID structure the provider uses.
                MetaData columnidname = dataReader.FindMetaData(ODB.DBCOLUMN_IDNAME)!;
                MetaData columnguid = dataReader.FindMetaData(ODB.DBCOLUMN_GUID)!;
                MetaData columnpropid = dataReader.FindMetaData(ODB.DBCOLUMN_PROPID)!;

                MetaData columnname = dataReader.FindMetaData(ODB.DBCOLUMN_NAME)!;
                MetaData columnordinal = dataReader.FindMetaData(ODB.DBCOLUMN_NUMBER)!;
                MetaData dbtype = dataReader.FindMetaData(ODB.DBCOLUMN_TYPE)!;
                MetaData columnsize = dataReader.FindMetaData(ODB.DBCOLUMN_COLUMNSIZE)!;
                MetaData numericprecision = dataReader.FindMetaData(ODB.DBCOLUMN_PRECISION)!;
                MetaData numericscale = dataReader.FindMetaData(ODB.DBCOLUMN_SCALE)!;
                MetaData columnflags = dataReader.FindMetaData(ODB.DBCOLUMN_FLAGS)!;
                MetaData baseschemaname = dataReader.FindMetaData(ODB.DBCOLUMN_BASESCHEMANAME)!;
                MetaData basecatalogname = dataReader.FindMetaData(ODB.DBCOLUMN_BASECATALOGNAME)!;
                MetaData basetablename = dataReader.FindMetaData(ODB.DBCOLUMN_BASETABLENAME)!;
                MetaData basecolumnname = dataReader.FindMetaData(ODB.DBCOLUMN_BASECOLUMNNAME)!;
                MetaData isautoincrement = dataReader.FindMetaData(ODB.DBCOLUMN_ISAUTOINCREMENT)!;
                MetaData isunique = dataReader.FindMetaData(ODB.DBCOLUMN_ISUNIQUE)!;
                MetaData iskeycolumn = dataReader.FindMetaData(ODB.DBCOLUMN_KEYCOLUMN)!;

                // @devnote: because we want to use the DBACCESSOR_OPTIMIZED bit,
                // we are required to create the accessor before fetching any rows
                dataReader.CreateAccessors(false);

                ColumnBinding binding;
                while (dataReader.ReadRowset())
                {
                    dataReader.GetRowDataFromHandle();

                    MetaData info = new MetaData();

                    binding = columnidname.columnBinding;
                    if (!binding.IsValueNull())
                    {
                        info.idname = (string)binding.Value();
                        info.kind = ODB.DBKIND_NAME;
                    }

                    binding = columnguid.columnBinding;
                    if (!binding.IsValueNull())
                    {
                        info.guid = binding.Value_GUID();
                        info.kind = ((ODB.DBKIND_NAME == info.kind) ? ODB.DBKIND_GUID_NAME : ODB.DBKIND_GUID);
                    }

                    binding = columnpropid.columnBinding;
                    if (!binding.IsValueNull())
                    {
                        info.propid = new IntPtr(binding.Value_UI4());
                        info.kind = ((ODB.DBKIND_GUID == info.kind) ? ODB.DBKIND_GUID_PROPID : ODB.DBKIND_PROPID);
                    }

                    binding = columnname.columnBinding;
                    if (!binding.IsValueNull())
                    {
                        info.columnName = (string)binding.Value();
                    }
                    else
                    {
                        info.columnName = "";
                    }

                    if (4 == IntPtr.Size)
                    {
                        info.ordinal = (nint)columnordinal.columnBinding.Value_UI4();
                        info.size = unchecked((int)columnsize.columnBinding.Value_UI4());
                    }
                    else
                    {
                        info.ordinal = (nint)columnordinal.columnBinding.Value_UI8();
                        info.size = ADP.IntPtrToInt32((nint)columnsize.columnBinding.Value_UI8());
                    }
                    short wType = unchecked((short)dbtype.columnBinding.Value_UI2());

                    binding = numericprecision.columnBinding;
                    if (!binding.IsValueNull())
                    {
                        info.precision = (byte)binding.Value_UI2();
                    }

                    binding = numericscale.columnBinding;
                    if (!binding.IsValueNull())
                    {
                        info.scale = (byte)binding.Value_I2();
                    }

                    info.flags = unchecked((int)columnflags.columnBinding.Value_UI4());

                    bool islong = OleDbDataReader.IsLong(info.flags);
                    bool isfixed = OleDbDataReader.IsFixed(info.flags);
                    NativeDBType dbType = NativeDBType.FromDBType(wType, islong, isfixed);

                    info.type = dbType;

                    if (null != isautoincrement)
                    {
                        binding = isautoincrement.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.isAutoIncrement = binding.Value_BOOL();
                        }
                    }
                    if (null != isunique)
                    {
                        binding = isunique.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.isUnique = binding.Value_BOOL();
                        }
                    }
                    if (null != iskeycolumn)
                    {
                        binding = iskeycolumn.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.isKeyColumn = binding.Value_BOOL();
                        }
                    }
                    if (null != baseschemaname)
                    {
                        binding = baseschemaname.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.baseSchemaName = binding.ValueString();
                        }
                    }
                    if (null != basecatalogname)
                    {
                        binding = basecatalogname.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.baseCatalogName = binding.ValueString();
                        }
                    }
                    if (null != basetablename)
                    {
                        binding = basetablename.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.baseTableName = binding.ValueString();
                        }
                    }
                    if (null != basecolumnname)
                    {
                        binding = basecolumnname.columnBinding;
                        if (!binding.IsValueNull())
                        {
                            info.baseColumnName = binding.ValueString();
                        }
                    }
                    metainfo.Add(info);
                }
            }

            int visibleCount = metainfo.Count;
            if (hiddenColumns is int)
            {
                visibleCount -= (int)hiddenColumns;
            }

            //  if one key column is invalidated, they all need to be invalidated. The SET is the key,
            //  and subsets likely are not accurate keys. Note the assumption that the two loops below
            //  will traverse the entire set of columns.
            bool disallowKeyColumns = false;

            for (int index = metainfo.Count - 1; visibleCount <= index; --index)
            {
                MetaData info = metainfo[index];

                info.isHidden = true;

                if (disallowKeyColumns)
                {
                    info.isKeyColumn = false;
                }
                else if (info.guid.Equals(ODB.DBCOL_SPECIALCOL))
                {
                    info.isKeyColumn = false;

                    // This is the first key column to be invalidated, scan back through the
                    //  columns we already processed to make sure none of those are marked as keys.
                    disallowKeyColumns = true;
                    for (int index2 = metainfo.Count - 1; index < index2; --index2)
                    {
                        metainfo[index2].isKeyColumn = false;
                    }
                }
            }

            for (int index = visibleCount - 1; 0 <= index; --index)
            {
                MetaData info = metainfo[index];

                if (disallowKeyColumns)
                {
                    info.isKeyColumn = false;
                }

                if (info.guid.Equals(ODB.DBCOL_SPECIALCOL))
                {
#if DEBUG
                    if (AdapterSwitches.DataSchema.TraceVerbose)
                    {
                        Debug.WriteLine($"Filtered Column: DBCOLUMN_GUID=DBCOL_SPECIALCOL DBCOLUMN_NAME={info.columnName} DBCOLUMN_KEYCOLUMN={info.isKeyColumn}");
                    }
#endif
                    info.isHidden = true;
                    visibleCount--;
                }
                else if (0 >= info.ordinal)
                {
#if DEBUG
                    if (AdapterSwitches.DataSchema.TraceVerbose)
                    {
                        Debug.WriteLine($"Filtered Column: DBCOLUMN_NUMBER={info.ordinal} DBCOLUMN_NAME={info.columnName}");
                    }
#endif
                    info.isHidden = true;
                    visibleCount--;
                }
                else if (OleDbDataReader.DoColumnDropFilter(info.flags))
                {
#if DEBUG
                    if (AdapterSwitches.DataSchema.TraceVerbose)
                    {
                        Debug.WriteLine($"Filtered Column: DBCOLUMN_FLAGS={info.flags:X8} DBCOLUMN_NAME={info.columnName}");
                    }
#endif
                    info.isHidden = true;
                    visibleCount--;
                }
            }

            // CONSIDER: perf tracking to see if we need to sort or not
            metainfo.Sort();
            _visibleFieldCount = visibleCount;
            _metadata = metainfo.ToArray();
        }

        internal static void GenerateSchemaTable(OleDbDataReader dataReader, object handle, CommandBehavior behavior)
        {
            if (0 != (CommandBehavior.KeyInfo & behavior))
            {
                dataReader.BuildSchemaTableRowset(handle); // tries IColumnsRowset first then IColumnsInfo
                dataReader.AppendSchemaInfo();
            }
            else
            {
                dataReader.BuildSchemaTableInfo(handle, false, false); // only tries IColumnsInfo
            }
            MetaData[]? metadata = dataReader.MetaData;
            if ((null != metadata) && (0 < metadata.Length))
            {
                dataReader.BuildSchemaTable(metadata);
            }
        }

        private static bool DoColumnDropFilter(int flags)
        {
            return (0 != (ODB.DBCOLUMNFLAGS_ISBOOKMARK & flags));
        }
        private static bool IsLong(int flags)
        {
            return (0 != (ODB.DBCOLUMNFLAGS_ISLONG & flags));
        }
        private static bool IsFixed(int flags)
        {
            return (0 != (ODB.DBCOLUMNFLAGS_ISFIXEDLENGTH & flags));
        }
        private static bool IsRowVersion(int flags)
        {
            return (0 != (ODB.DBCOLUMNFLAGS_ISROWID_DBCOLUMNFLAGS_ISROWVER & flags));
        }
        private static bool AllowDBNull(int flags)
        {
            return (0 != (ODB.DBCOLUMNFLAGS_ISNULLABLE & flags));
        }
        private static bool AllowDBNullMaybeNull(int flags)
        {
            return (0 != (ODB.DBCOLUMNFLAGS_ISNULLABLE_DBCOLUMNFLAGS_MAYBENULL & flags));
        }
        private static bool IsReadOnly(int flags)
        {
            return (0 == (ODB.DBCOLUMNFLAGS_WRITE_DBCOLUMNFLAGS_WRITEUNKNOWN & flags));
        }
    }

    internal sealed class MetaData : IComparable
    {
        internal Bindings? bindings;
        internal ColumnBinding columnBinding = null!; // Late-initialized

        internal string columnName = null!; // Late-initialized

        internal Guid guid;
        internal int kind;
        internal IntPtr propid;
        internal string? idname;

        internal NativeDBType type = null!; // Late-initialized

        internal nint ordinal;
        internal int size;

        internal int flags;

        internal byte precision;
        internal byte scale;

        internal bool isAutoIncrement;
        internal bool isUnique;
        internal bool isKeyColumn;
        internal bool isHidden;

        internal string? baseSchemaName;
        internal string? baseCatalogName;
        internal string? baseTableName;
        internal string? baseColumnName;

        int IComparable.CompareTo(object? obj)
        {
            if (obj is MetaData m && isHidden == m.isHidden)
            {
                if (ODB.IsRunningOnX86)
                {
                    return (int)ordinal - (int)m.ordinal;
                }
                else
                {
                    long v = (long)ordinal - (long)m.ordinal;
                    return ((0 < v) ? 1 : ((v < 0) ? -1 : 0));
                }
            }
            return (isHidden) ? 1 : -1; // ensure that all hidden columns come after non-hidden columns
        }

        internal MetaData()
        {
        }
    }
}
