// 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.Data.Common;
using System.Data.ProviderBase;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;              // StringBuilder

namespace System.Data.Odbc
{
    public sealed class OdbcDataReader : DbDataReader
    {
        private OdbcCommand? _command;

        private int _recordAffected = -1;
        private FieldNameLookup? _fieldNameLookup;

        private DbCache? _dataCache;
        private enum HasRowsStatus
        {
            DontKnow = 0,
            HasRows = 1,
            HasNoRows = 2,
        }
        private HasRowsStatus _hasRows = HasRowsStatus.DontKnow;
        private bool _isClosed;
        private bool _isRead;
        private bool _isValidResult;
        private bool _noMoreResults;
        private bool _noMoreRows;
        private bool _skipReadOnce;
        private int _hiddenColumns;                 // number of hidden columns
        private readonly CommandBehavior _commandBehavior;

        // track current row and column, will be set on the first Fetch call
        private int _row = -1;
        private int _column = -1;

        // used to track position in field for successive reads in case of Sequential Access
        private long _sequentialBytesRead;

        private static int s_objectTypeCount; // Bid counter
        internal readonly int ObjectID = System.Threading.Interlocked.Increment(ref s_objectTypeCount);

        // the statement handle here is just a copy of the statement handle owned by the command
        // the DataReader must not free the statement handle. this is done by the command
        //

        private MetaData[]? _metadata;
        private DataTable? _schemaTable; // MDAC 68336
        private readonly string _cmdText;    // get a copy in case the command text on the command is changed ...
        private CMDWrapper? _cmdWrapper;

        internal OdbcDataReader(OdbcCommand command, CMDWrapper cmdWrapper, CommandBehavior commandbehavior)
        {
            Debug.Assert(command != null, "Command null on OdbcDataReader ctor");
            _command = command;
            _commandBehavior = commandbehavior;
            _cmdText = command.CommandText;    // get a copy in case the command text on the command is changed ...
            _cmdWrapper = cmdWrapper;
        }

        private CNativeBuffer Buffer
        {
            get
            {
                CNativeBuffer? value = _cmdWrapper!._dataReaderBuf;
                if (null == value)
                {
                    Debug.Fail("object is disposed");
                    throw new ObjectDisposedException(GetType().Name);
                }
                return value;
            }
        }

        private OdbcConnection? Connection
        {
            get
            {
                if (null != _cmdWrapper)
                {
                    return _cmdWrapper.Connection;
                }
                else
                {
                    return null;
                }
            }
        }

        internal OdbcCommand? Command
        {
            get
            {
                return _command;
            }
            set
            {
                _command = value;
            }
        }

        private OdbcStatementHandle StatementHandle
        {
            get
            {
                return _cmdWrapper!.StatementHandle!;
            }
        }

        private OdbcStatementHandle KeyInfoStatementHandle
        {
            get { return _cmdWrapper!.KeyInfoStatement!; }
        }

        internal bool IsBehavior(CommandBehavior behavior)
        {
            return IsCommandBehavior(behavior);
        }

        internal bool IsCancelingCommand
        {
            get
            {
                if (_command != null)
                {
                    return _command.Canceling;
                }
                return false;
            }
        }

        internal bool IsNonCancelingCommand
        {
            get
            {
                if (_command != null)
                {
                    return !_command.Canceling;
                }
                return false;
            }
        }

        public override int Depth
        {
            get
            {
                if (IsClosed)
                { // MDAC 63669
                    throw ADP.DataReaderClosed("Depth");
                }
                return 0;
            }
        }

        public override int FieldCount
        {
            get
            {
                if (IsClosed)
                { // MDAC 63669
                    throw ADP.DataReaderClosed("FieldCount");
                }
                if (_noMoreResults)
                {   // MDAC 93325
                    return 0;
                }
                if (null == _dataCache)
                {
                    ODBC32.SQLRETURN retcode = this.FieldCountNoThrow(out _);
                    if (retcode != ODBC32.SQLRETURN.SUCCESS)
                    {
                        Connection!.HandleError(StatementHandle, retcode);
                    }
                }
                return ((null != _dataCache) ? _dataCache._count : 0);
            }
        }

        // HasRows
        //
        // Use to detect whether there are one ore more rows in the result without going through Read
        // May be called at any time
        // Basically it calls Read and sets a flag so that the actual Read call will be skipped once
        //
        public override bool HasRows
        {
            get
            {
                if (IsClosed)
                {
                    throw ADP.DataReaderClosed("HasRows");
                }
                if (_hasRows == HasRowsStatus.DontKnow)
                {
                    Read();                     //
                    _skipReadOnce = true;       // need to skip Read once because we just did it
                }
                return (_hasRows == HasRowsStatus.HasRows);
            }
        }

        internal ODBC32.SQLRETURN FieldCountNoThrow(out short cColsAffected)
        {
            if (IsCancelingCommand)
            {
                cColsAffected = 0;
                return ODBC32.SQLRETURN.ERROR;
            }

            ODBC32.SQLRETURN retcode = StatementHandle.NumberOfResultColumns(out cColsAffected);
            if (retcode == ODBC32.SQLRETURN.SUCCESS)
            {
                _hiddenColumns = 0;
                if (IsCommandBehavior(CommandBehavior.KeyInfo))
                {
                    // we need to search for the first hidden column
                    //
                    if (!Connection!.ProviderInfo.NoSqlSoptSSNoBrowseTable && !Connection.ProviderInfo.NoSqlSoptSSHiddenColumns)
                    {
                        for (int i = 0; i < cColsAffected; i++)
                        {
                            SQLLEN isHidden = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_HIDDEN, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE);
                            if (isHidden.ToInt64() == 1)
                            {
                                _hiddenColumns = (int)cColsAffected - i;
                                cColsAffected = (short)i;
                                break;
                            }
                        }
                    }
                }
                _dataCache = new DbCache(this, cColsAffected);
            }
            else
            {
                cColsAffected = 0;
            }
            return retcode;
        }

        public override bool IsClosed
        {
            get
            {
                return _isClosed;
            }
        }

        private SQLLEN GetRowCount()
        {
            if (!IsClosed)
            {
                SQLLEN cRowsAffected;
                ODBC32.SQLRETURN retcode = StatementHandle.RowCount(out cRowsAffected);
                if (ODBC32.SQLRETURN.SUCCESS == retcode || ODBC32.SQLRETURN.SUCCESS_WITH_INFO == retcode)
                {
                    return cRowsAffected;
                }
            }
            return -1;
        }

        internal int CalculateRecordsAffected(int cRowsAffected)
        {
            if (0 <= cRowsAffected)
            {
                if (-1 == _recordAffected)
                {
                    _recordAffected = cRowsAffected;
                }
                else
                {
                    _recordAffected += cRowsAffected;
                }
            }
            return _recordAffected;
        }


        public override int RecordsAffected
        {
            get
            {
                return _recordAffected;
            }
        }

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

        public override object this[string value]
        {
            get
            {
                return GetValue(GetOrdinal(value));
            }
        }

        public override void Close()
        {
            Close(false);
        }

        private void Close(bool disposing)
        {
            Exception? error = null;

            CMDWrapper? wrapper = _cmdWrapper;
            if (null != wrapper && wrapper.StatementHandle != null)
            {
                // disposing
                // true to release both managed and unmanaged resources; false to release only unmanaged resources.
                //
                if (IsNonCancelingCommand)
                {
                    //Read any remaining results off the wire
                    // some batch statements may not be executed until SQLMoreResults is called.
                    // We want the user to be able to do ExecuteNonQuery or ExecuteReader
                    // and close without having iterate to get params or batch.
                    //
                    NextResult(disposing, !disposing); // false,true or true,false
                    if (null != _command)
                    {
                        if (_command.HasParameters)
                        {
                            // Output Parameters are not guareenteed to be returned until all the data
                            // from any restssets are read, so we do this after the above NextResult call(s)
                            _command.Parameters.GetOutputValues(_cmdWrapper!);
                        }
                        wrapper.FreeStatementHandle(ODBC32.STMT.CLOSE);
                        _command.CloseFromDataReader();
                    }
                }
                wrapper.FreeKeyInfoStatementHandle(ODBC32.STMT.CLOSE);
            }

            // if the command is still around we call CloseFromDataReader,
            // otherwise we need to dismiss the statement handle ourselves
            //
            if (null != _command)
            {
                _command.CloseFromDataReader();

                if (IsCommandBehavior(CommandBehavior.CloseConnection))
                {
                    Debug.Assert(null != Connection, "null cmd connection");
                    _command.Parameters.RebindCollection = true;
                    Connection.Close();
                }
            }
            else
            {
                wrapper?.Dispose();
            }

            _command = null;
            _isClosed = true;
            _dataCache = null;
            _metadata = null;
            _schemaTable = null;
            _isRead = false;
            _hasRows = HasRowsStatus.DontKnow;
            _isValidResult = false;
            _noMoreResults = true;
            _noMoreRows = true;
            _fieldNameLookup = null;

            SetCurrentRowColumnInfo(-1, 0);

            if ((null != error) && !disposing)
            {
                throw error;
            }
            _cmdWrapper = null;
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                Close(true);
            }
            // not delegating to base class because we know it only calls Close
            //base.Dispose(disposing)
        }

        public override string GetDataTypeName(int i)
        {
            if (null != _dataCache)
            {
                DbSchemaInfo info = _dataCache.GetSchema(i);
                return info._typename ??= GetColAttributeStr(i, ODBC32.SQL_DESC.TYPE_NAME, ODBC32.SQL_COLUMN.TYPE_NAME, ODBC32.HANDLER.THROW)!;
            }
            throw ADP.DataReaderNoData();
        }

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

        public override Type GetFieldType(int i)
        {
            if (null != _dataCache)
            {
                DbSchemaInfo info = _dataCache.GetSchema(i);
                return info._type ??= GetSqlType(i)._type;
            }
            throw ADP.DataReaderNoData();
        }

        public override string GetName(int i)
        {
            if (null != _dataCache)
            {
                DbSchemaInfo info = _dataCache.GetSchema(i);
                return info._name ??= GetColAttributeStr(i, ODBC32.SQL_DESC.NAME, ODBC32.SQL_COLUMN.NAME, ODBC32.HANDLER.THROW) ?? "";
            }
            throw ADP.DataReaderNoData();
        }

        public override int GetOrdinal(string value)
        {
            if (null == _fieldNameLookup)
            {
                if (null == _dataCache)
                {
                    throw ADP.DataReaderNoData();
                }
                _fieldNameLookup = new FieldNameLookup(this, -1);
            }
            return _fieldNameLookup.GetOrdinal(value); // MDAC 71470
        }

        private int IndexOf(string value)
        {
            if (null == _fieldNameLookup)
            {
                if (null == _dataCache)
                {
                    throw ADP.DataReaderNoData();
                }
                _fieldNameLookup = new FieldNameLookup(this, -1);
            }
            return _fieldNameLookup.IndexOf(value);
        }

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

        internal object GetValue(int i, TypeMap typemap)
        {
            switch (typemap._sql_type)
            {
                case ODBC32.SQL_TYPE.CHAR:
                case ODBC32.SQL_TYPE.VARCHAR:
                case ODBC32.SQL_TYPE.LONGVARCHAR:
                case ODBC32.SQL_TYPE.WCHAR:
                case ODBC32.SQL_TYPE.WVARCHAR:
                case ODBC32.SQL_TYPE.WLONGVARCHAR:
                    return internalGetString(i);

                case ODBC32.SQL_TYPE.DECIMAL:
                case ODBC32.SQL_TYPE.NUMERIC:
                    return internalGetDecimal(i);

                case ODBC32.SQL_TYPE.SMALLINT:
                    return internalGetInt16(i);

                case ODBC32.SQL_TYPE.INTEGER:
                    return internalGetInt32(i);

                case ODBC32.SQL_TYPE.REAL:
                    return internalGetFloat(i);

                case ODBC32.SQL_TYPE.FLOAT:
                case ODBC32.SQL_TYPE.DOUBLE:
                    return internalGetDouble(i);

                case ODBC32.SQL_TYPE.BIT:
                    return internalGetBoolean(i);

                case ODBC32.SQL_TYPE.TINYINT:
                    return internalGetByte(i);

                case ODBC32.SQL_TYPE.BIGINT:
                    return internalGetInt64(i);

                case ODBC32.SQL_TYPE.BINARY:
                case ODBC32.SQL_TYPE.VARBINARY:
                case ODBC32.SQL_TYPE.LONGVARBINARY:
                    return internalGetBytes(i);

                case ODBC32.SQL_TYPE.TYPE_DATE:
                    return internalGetDate(i);

                case ODBC32.SQL_TYPE.TYPE_TIME:
                    return internalGetTime(i);

                //                  case ODBC32.SQL_TYPE.TIMESTAMP:
                case ODBC32.SQL_TYPE.TYPE_TIMESTAMP:
                    return internalGetDateTime(i);

                case ODBC32.SQL_TYPE.GUID:
                    return internalGetGuid(i);

                case ODBC32.SQL_TYPE.SS_VARIANT:
                    //Note: SQL Variant is not an ODBC defined type.
                    //Instead of just binding it as a byte[], which is not very useful,
                    //we will actually code this specific for SQL Server.

                    //To obtain the sub-type, we need to first load the context (obtaining the length
                    //will work), and then query for a speicial SQLServer specific attribute.
                    if (_isRead)
                    {
                        if (_dataCache!.AccessIndex(i) == null)
                        {
                            bool isNotDbNull = QueryFieldInfo(i, ODBC32.SQL_C.BINARY, out _);
                            // if the value is DBNull, QueryFieldInfo will cache it
                            if (isNotDbNull)
                            {
                                //Delegate (for the sub type)
                                ODBC32.SQL_TYPE subtype = (ODBC32.SQL_TYPE)(int)GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.VARIANT_SQL_TYPE, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.THROW);
                                return GetValue(i, TypeMap.FromSqlType(subtype));
                            }
                        }
                        return _dataCache[i]!;
                    }
                    throw ADP.DataReaderNoData();



                default:
                    //Unknown types are bound strictly as binary
                    return internalGetBytes(i);
            }
        }

        public override object GetValue(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    _dataCache[i] = GetValue(i, GetSqlType(i));
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override int GetValues(object[] values)
        {
            if (_isRead)
            {
                int nValues = Math.Min(values.Length, FieldCount);
                for (int i = 0; i < nValues; ++i)
                {
                    values[i] = GetValue(i);
                }
                return nValues;
            }
            throw ADP.DataReaderNoData();
        }

        private TypeMap GetSqlType(int i)
        {
            //Note: Types are always returned (advertised) from ODBC as SQL_TYPEs, and
            //are always bound by the user as SQL_C types.
            TypeMap typeMap;
            DbSchemaInfo info = _dataCache!.GetSchema(i);
            if (!info._dbtype.HasValue)
            {
                info._dbtype = unchecked((ODBC32.SQL_TYPE)(int)GetColAttribute(i, ODBC32.SQL_DESC.CONCISE_TYPE, ODBC32.SQL_COLUMN.TYPE, ODBC32.HANDLER.THROW));
                typeMap = TypeMap.FromSqlType(info._dbtype.Value);
                if (typeMap._signType)
                {
                    bool sign = (GetColAttribute(i, ODBC32.SQL_DESC.UNSIGNED, ODBC32.SQL_COLUMN.UNSIGNED, ODBC32.HANDLER.THROW).ToInt64() != 0);
                    typeMap = TypeMap.UpgradeSignedType(typeMap, sign);
                    info._dbtype = typeMap._sql_type;
                }
            }
            else
            {
                typeMap = TypeMap.FromSqlType(info._dbtype.Value);
            }
            Connection!.SetSupportedType(info._dbtype.Value);
            return typeMap;
        }

        public override bool IsDBNull(int i)
        {
            //  Note: ODBC SQLGetData doesn't allow retrieving the column value twice.
            //  The rationale is that for ForwardOnly access (the default and LCD of drivers)
            //  we cannot obtain the data more than once, and even GetData(0) (to determine is-null)
            //  still obtains data for fixed length types.

            //  So simple code like:
            //      if (!rReader.IsDBNull(i))
            //          rReader.GetInt32(i)
            //
            //  Would fail, unless we cache on the IsDBNull call, and return the cached
            //  item for GetInt32.  This actually improves perf anyway, (even if the driver could
            //  support it), since we are not making a separate interop call...

            // Bug SQLBUVSTS01:110664 - available cases:
            // 1. random access - always cache the value (as before the fix), to minimize regression risk
            // 2. sequential access, fixed-size value: continue caching the value as before, again to minimize regression risk
            // 3. sequential access, variable-length value: this scenario did not work properly before the fix. Fix
            //                                              it now by calling GetData(length = 0).
            // 4. sequential access, cache value exists: just check the cache for DbNull (no validations done, again to minimize regressions)

            if (!IsCommandBehavior(CommandBehavior.SequentialAccess))
                return Convert.IsDBNull(GetValue(i)); // case 1, cache the value

            // in 'ideal' Sequential access support, we do not want cache the value in order to check if it is DbNull or not.
            // But, to minimize regressions, we will continue caching the fixed-size values (case 2), even with SequentialAccess
            // only in case of SequentialAccess with variable length data types (case 3), we will use GetData with zero length.

            object? cachedObj = _dataCache![i];
            if (cachedObj != null)
            {
                // case 4 - if cached object was created before, use it
                return Convert.IsDBNull(cachedObj);
            }

            // no cache, check for the type (cases 2 and 3)
            TypeMap typeMap = GetSqlType(i);
            if (typeMap._bufferSize > 0)
            {
                // case 2 - fixed-size types have _bufferSize set to positive value
                // call GetValue(i) as before the fix of SQLBUVSTS01:110664
                // note, when SQLGetData is called for a fixed length type, the buffer size is always ignored and
                // the data will always be read off the wire
                return Convert.IsDBNull(GetValue(i));
            }
            else
            {
                // case 3 - the data has variable-length type, read zero-length data to query for null
                // QueryFieldInfo will return false only if the object cached as DbNull
                // QueryFieldInfo will put DbNull in cache only if the SQLGetData returns SQL_NULL_DATA, otherwise it does not change it
                return !QueryFieldInfo(i, typeMap._sql_c, out _);
            }
        }

        public override byte GetByte(int i)
        {
            return (byte)internalGetByte(i);
        }

        private object internalGetByte(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.UTINYINT))
                    {
                        _dataCache[i] = Buffer.ReadByte(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override char GetChar(int i)
        {
            return (char)internalGetChar(i);
        }
        private object internalGetChar(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.WCHAR))
                    {
                        _dataCache[i] = Buffer.ReadChar(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override short GetInt16(int i)
        {
            return (short)internalGetInt16(i);
        }
        private object internalGetInt16(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.SSHORT))
                    {
                        _dataCache[i] = Buffer.ReadInt16(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override int GetInt32(int i)
        {
            return (int)internalGetInt32(i);
        }
        private object internalGetInt32(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.SLONG))
                    {
                        _dataCache[i] = Buffer.ReadInt32(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override long GetInt64(int i)
        {
            return (long)internalGetInt64(i);
        }
        // ---------------------------------------------------------------------------------------------- //
        // internal internalGetInt64
        // -------------------------
        // Get Value of type SQL_BIGINT
        // Since the driver refused to accept the type SQL_BIGINT we read that
        // as SQL_C_WCHAR and convert it back to the Int64 data type
        //
        private object internalGetInt64(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.WCHAR))
                    {
                        string value = (string)Buffer.MarshalToManaged(0, ODBC32.SQL_C.WCHAR, ODBC32.SQL_NTS);
                        _dataCache[i] = long.Parse(value, CultureInfo.InvariantCulture);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override bool GetBoolean(int i)
        {
            return (bool)internalGetBoolean(i);
        }
        private object internalGetBoolean(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.BIT))
                    {
                        _dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.BIT, -1);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override float GetFloat(int i)
        {
            return (float)internalGetFloat(i);
        }
        private object internalGetFloat(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.REAL))
                    {
                        _dataCache[i] = Buffer.ReadSingle(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public DateTime GetDate(int i)
        {
            return (DateTime)internalGetDate(i);
        }

        private object internalGetDate(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.TYPE_DATE))
                    {
                        _dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.TYPE_DATE, -1);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override DateTime GetDateTime(int i)
        {
            return (DateTime)internalGetDateTime(i);
        }

        private object internalGetDateTime(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.TYPE_TIMESTAMP))
                    {
                        _dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.TYPE_TIMESTAMP, -1);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override decimal GetDecimal(int i)
        {
            return (decimal)internalGetDecimal(i);
        }

        // ---------------------------------------------------------------------------------------------- //
        // internal GetDecimal
        // -------------------
        // Get Value of type SQL_DECIMAL or SQL_NUMERIC
        // Due to provider incompatibilities with SQL_DECIMAL or SQL_NUMERIC types we always read the value
        // as SQL_C_WCHAR and convert it back to the Decimal data type
        //
        private object internalGetDecimal(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.WCHAR))
                    {
                        string? s = null;
                        try
                        {
                            s = (string)Buffer.MarshalToManaged(0, ODBC32.SQL_C.WCHAR, ODBC32.SQL_NTS);
                            _dataCache[i] = decimal.Parse(s, System.Globalization.CultureInfo.InvariantCulture);
                        }
                        catch (OverflowException)
                        {
                            _dataCache[i] = s;
                            throw;
                        }
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override double GetDouble(int i)
        {
            return (double)internalGetDouble(i);
        }
        private object internalGetDouble(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.DOUBLE))
                    {
                        _dataCache[i] = Buffer.ReadDouble(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override Guid GetGuid(int i)
        {
            return (Guid)internalGetGuid(i);
        }

        private object internalGetGuid(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.GUID))
                    {
                        _dataCache[i] = Buffer.ReadGuid(0);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public override string GetString(int i)
        {
            return (string)internalGetString(i);
        }

        private object internalGetString(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    // Obtain _ALL_ the characters
                    // Note: We can bind directly as WCHAR in ODBC and the DM will convert to and
                    // from ANSI if not supported by the driver.
                    //

                    // Note: The driver always returns the raw length of the data, minus the
                    // terminator.  This means that our buffer length always includes the terminator
                    // charactor, so when determining which characters to count, and if more data
                    // exists, it should not take the terminator into effect.
                    //
                    CNativeBuffer buffer = Buffer;
                    // that does not make sense unless we expect four byte terminators
                    int cbMaxData = buffer.Length - 4;

                    // The first time GetData returns the true length (so we have to min it).
                    // We also pass in the true length to the marshal function since there could be
                    // embedded nulls
                    //
                    int lengthOrIndicator;
                    if (GetData(i, ODBC32.SQL_C.WCHAR, buffer.Length - 2, out lengthOrIndicator))
                    {
                        // RFC 50002644: we do not expect negative values from GetData call except SQL_NO_TOTAL(== -4)
                        // note that in general you should not trust third-party providers so such asserts should be
                        // followed by exception. I did not add it now to avoid breaking change
                        Debug.Assert(lengthOrIndicator >= 0 || lengthOrIndicator == ODBC32.SQL_NO_TOTAL, "unexpected lengthOrIndicator value");

                        if (lengthOrIndicator <= cbMaxData && (ODBC32.SQL_NO_TOTAL != lengthOrIndicator))
                        {
                            // all data read? good! Directly marshal to a string and we're done
                            //
                            string strdata = buffer.PtrToStringUni(0, Math.Min(lengthOrIndicator, cbMaxData) / 2);
                            _dataCache[i] = strdata;
                            return strdata;
                        }

                        // We need to chunk the data
                        // Char[] buffer for the junks
                        // StringBuilder for the actual string
                        //
                        char[] rgChars = new char[cbMaxData / 2];

                        // RFC 50002644: negative value cannot be used for capacity.
                        // in case of SQL_NO_TOTAL, set the capacity to cbMaxData, StringBuilder will automatically reallocate
                        // its internal buffer when appending more data
                        int cbBuilderInitialCapacity = (lengthOrIndicator == ODBC32.SQL_NO_TOTAL) ? cbMaxData : lengthOrIndicator;
                        StringBuilder builder = new StringBuilder(cbBuilderInitialCapacity / 2);

                        bool gotData;
                        int cchJunk;
                        int cbActual = cbMaxData;
                        int cbMissing = (ODBC32.SQL_NO_TOTAL == lengthOrIndicator) ? -1 : lengthOrIndicator - cbActual;

                        do
                        {
                            cchJunk = cbActual / 2;
                            buffer.ReadChars(0, rgChars, 0, cchJunk);
                            builder.Append(rgChars, 0, cchJunk);

                            if (0 == cbMissing)
                            {
                                break;  // done
                            }

                            gotData = GetData(i, ODBC32.SQL_C.WCHAR, buffer.Length - 2, out lengthOrIndicator);
                            // RFC 50002644: we do not expect negative values from GetData call except SQL_NO_TOTAL(== -4)
                            // note that in general you should not trust third-party providers so such asserts should be
                            // followed by exception. I did not add it now to avoid breaking change
                            Debug.Assert(lengthOrIndicator >= 0 || lengthOrIndicator == ODBC32.SQL_NO_TOTAL, "unexpected lengthOrIndicator value");

                            if (ODBC32.SQL_NO_TOTAL != lengthOrIndicator)
                            {
                                cbActual = Math.Min(lengthOrIndicator, cbMaxData);
                                if (0 < cbMissing)
                                {
                                    cbMissing -= cbActual;
                                }
                                else
                                {
                                    // it is a last call to SqlGetData that started with SQL_NO_TOTAL
                                    // the last call to SqlGetData must always return the length of the
                                    // data, not zero or SqlNoTotal (see Odbc Programmers Reference)
                                    Debug.Assert(cbMissing == -1 && lengthOrIndicator <= cbMaxData);
                                    cbMissing = 0;
                                }
                            }
                        }
                        while (gotData);

                        _dataCache[i] = builder.ToString();
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        public TimeSpan GetTime(int i)
        {
            return (TimeSpan)internalGetTime(i);
        }

        private object internalGetTime(int i)
        {
            if (_isRead)
            {
                if (_dataCache!.AccessIndex(i) == null)
                {
                    if (GetData(i, ODBC32.SQL_C.TYPE_TIME))
                    {
                        _dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.TYPE_TIME, -1);
                    }
                }
                return _dataCache[i]!;
            }
            throw ADP.DataReaderNoData();
        }

        private void SetCurrentRowColumnInfo(int row, int column)
        {
            if (_row != row || _column != column)
            {
                _row = row;
                _column = column;

                // reset the blob reader when moved to new column
                _sequentialBytesRead = 0;
            }
        }

        public override long GetBytes(int i, long dataIndex, byte[]? buffer, int bufferIndex, int length)
        {
            return GetBytesOrChars(i, dataIndex, buffer, false /* bytes buffer */, bufferIndex, length);
        }
        public override long GetChars(int i, long dataIndex, char[]? buffer, int bufferIndex, int length)
        {
            return GetBytesOrChars(i, dataIndex, buffer, true /* chars buffer */, bufferIndex, length);
        }

        // unify the implementation of GetChars and GetBytes to prevent code duplicate
        private long GetBytesOrChars(int i, long dataIndex, Array? buffer, bool isCharsBuffer, int bufferIndex, int length)
        {
            if (IsClosed)
            {
                throw ADP.DataReaderNoData();
            }
            if (!_isRead)
            {
                throw ADP.DataReaderNoData();
            }
            if (dataIndex < 0)
            {
                // test only for negative value here, Int32.MaxValue will be validated only in case of random access
                throw ADP.ArgumentOutOfRange(nameof(dataIndex));
            }
            if (bufferIndex < 0)
            {
                throw ADP.ArgumentOutOfRange(nameof(bufferIndex));
            }
            if (length < 0)
            {
                throw ADP.ArgumentOutOfRange(nameof(length));
            }

            string originalMethodName = isCharsBuffer ? "GetChars" : "GetBytes";

            // row/column info will be reset only if changed
            SetCurrentRowColumnInfo(_row, i);

            // Possible cases:
            // 1. random access, user asks for the value first time: bring it and cache the value
            // 2. random access, user already queried the value: use the cache
            // 3. sequential access, cache exists: user already read this value using different method (it becomes cached)
            //                       use the cache - preserve the original behavior to minimize regression risk
            // 4. sequential access, no cache: (fixed now) user reads the bytes/chars in sequential order (no cache)

            object? cachedObj;                 // The cached object (if there is one)

            // Get cached object, ensure the correct type using explicit cast, to preserve same behavior as before
            if (isCharsBuffer)
                cachedObj = (string?)_dataCache![i];
            else
                cachedObj = (byte[]?)_dataCache![i];

            bool isRandomAccess = !IsCommandBehavior(CommandBehavior.SequentialAccess);

            if (isRandomAccess || (cachedObj != null))
            {
                // random access (cases 1 or 2) and sequential access with cache (case 3)
                // preserve the original behavior as before the fix

                if (int.MaxValue < dataIndex)
                {
                    // indices greater than allocable size are not supported in random access
                    // (negative value is already tested in the beginning of ths function)
                    throw ADP.ArgumentOutOfRange(nameof(dataIndex));
                }

                if (cachedObj == null)
                {
                    // case 1, get the value and cache it
                    // internalGetString/internalGetBytes will get the entire value and cache it,
                    // since we are not in SequentialAccess (isRandomAccess is true), it is OK

                    if (isCharsBuffer)
                    {
                        cachedObj = (string)internalGetString(i);
                        Debug.Assert((cachedObj != null), "internalGetString should always return non-null or raise exception");
                    }
                    else
                    {
                        cachedObj = (byte[])internalGetBytes(i);
                        Debug.Assert((cachedObj != null), "internalGetBytes should always return non-null or raise exception");
                    }

                    // continue to case 2
                }

                // after this point the value is cached (case 2 or 3)
                // if it is DbNull, cast exception will be raised (same as before the 110664 fix)
                int cachedObjectLength = isCharsBuffer ? ((string)cachedObj).Length : ((byte[])cachedObj).Length;

                // the user can ask for the length of the field by passing in a null pointer for the buffer
                if (buffer == null)
                {
                    // return the length if that's all what user needs
                    return cachedObjectLength;
                }

                // user asks for bytes

                if (length == 0)
                {
                    return 0;   // Nothing to do ...
                }

                if (dataIndex >= cachedObjectLength)
                {
                    // no more bytes to read
                    // see also MDAC bug 73298
                    return 0;
                }

                int lengthFromDataIndex = cachedObjectLength - (int)dataIndex;
                int lengthOfCopy = Math.Min(lengthFromDataIndex, length);

                // silently reduce the length to avoid regression from EVERETT
                lengthOfCopy = Math.Min(lengthOfCopy, buffer.Length - bufferIndex);
                if (lengthOfCopy <= 0) return 0;                    // MDAC Bug 73298

                if (isCharsBuffer)
                    ((string)cachedObj).CopyTo((int)dataIndex, (char[])buffer, bufferIndex, lengthOfCopy);
                else
                    Array.Copy((byte[])cachedObj, (int)dataIndex, (byte[])buffer, bufferIndex, lengthOfCopy);

                return lengthOfCopy;
            }
            else
            {
                // sequential access, case 4

                // SQLBU:532243 -- For SequentialAccess we need to read a chunk of
                // data and not cache it.
                // Note: If the object was previous cached (see case 3 above), the function will go thru 'if' path, to minimize
                // regressions

                // the user can ask for the length of the field by passing in a null pointer for the buffer
                if (buffer == null)
                {
                    // Get len. of remaining data from provider
                    ODBC32.SQL_C sqlctype;
                    int cbLengthOrIndicator;
                    bool isDbNull;

                    sqlctype = isCharsBuffer ? ODBC32.SQL_C.WCHAR : ODBC32.SQL_C.BINARY;
                    isDbNull = !QueryFieldInfo(i, sqlctype, out cbLengthOrIndicator);

                    if (isDbNull)
                    {
                        // SQLBU 266054:

                        // GetChars:
                        //   in Orcas RTM: GetChars has always raised InvalidCastException.
                        //   in Orcas SP1: GetChars returned 0 if DbNull is not cached yet and InvalidCastException if it is in cache (from case 3).
                        //   Managed Providers team has decided to fix the GetChars behavior and raise InvalidCastException, as it was in RTM
                        //   Reason: returing 0 is wrong behavior since it conflicts with return value in case of empty data

                        // GetBytes:
                        //   In Orcas RTM: GetBytes(null buffer) returned -1 for null value if DbNull is not cached yet.
                        //   But, after calling IsDBNull, GetBytes(null) raised InvalidCastException.
                        //   In Orcas SP1: GetBytes always raises InvalidCastException for null value.
                        //   Managed Providers team has decided to keep the behavior of RTM for this case to fix the RTM's breaking change.
                        //   Reason: while -1 is wrong behavior, people might be already relying on it, so we should not be changing it.
                        //   Note: this will happen only on the first call to GetBytes(with null buffer).
                        //   If IsDbNull has already been called before or for second call to query for size,
                        //   DBNull is cached and GetBytes raises InvalidCastException in case 3 (see the cases above in this method).

                        if (isCharsBuffer)
                        {
                            throw ADP.InvalidCast();
                        }
                        else
                        {
                            return -1;
                        }
                    }
                    else
                    {
                        // the value is not null

                        // SQLBU 266054:
                        // If cbLengthOrIndicator is SQL_NO_TOTAL (-4), this call returns -4 or -2, depending on the type (GetChars=>-2, GetBytes=>-4).
                        // This is the Orcas RTM and SP1 behavior, changing this would be a breaking change.
                        // SQL_NO_TOTAL means that the driver does not know what is the remained length of the data, so we cannot really guess the value here.
                        // Reason: while returning different negative values depending on the type seems inconsistent,
                        // this is what we did in Orcas RTM and SP1 and user code might rely on this behavior => changing it would be a breaking change.
                        if (isCharsBuffer)
                        {
                            return cbLengthOrIndicator / 2; // return length in wide characters or -2 if driver returns SQL_NO_TOTAL
                        }
                        else
                        {
                            return cbLengthOrIndicator; // return length in bytes or -4 if driver returns SQL_NO_TOTAL
                        }
                    }
                }
                else
                {
                    // buffer != null, read the data

                    // check if user tries to read data that was already received
                    // if yes, this violates 'sequential access'
                    if ((isCharsBuffer && dataIndex < _sequentialBytesRead / 2) ||
                        (!isCharsBuffer && dataIndex < _sequentialBytesRead))
                    {
                        // backward reading is not allowed in sequential access
                        throw ADP.NonSeqByteAccess(
                            dataIndex,
                            _sequentialBytesRead,
                            originalMethodName
                            );
                    }

                    // note that it is actually not possible to read with an offset (dataIndex)
                    // therefore, adjust the data index relative to number of bytes already read
                    if (isCharsBuffer)
                        dataIndex -= _sequentialBytesRead / 2;
                    else
                        dataIndex -= _sequentialBytesRead;

                    if (dataIndex > 0)
                    {
                        // user asked to skip bytes - it is OK, even in case of sequential access
                        // forward the stream by dataIndex bytes/chars
                        int charsOrBytesRead = readBytesOrCharsSequentialAccess(i, null, isCharsBuffer, 0, dataIndex);
                        if (charsOrBytesRead < dataIndex)
                        {
                            // the stream ended before we forwarded to the requested index, stop now
                            return 0;
                        }
                    }

                    // ODBC driver now points to the correct position, start filling the user buffer from now

                    // Make sure we don't overflow the user provided buffer
                    // Note: SqlDataReader will raise exception if there is no enough room for length requested.
                    // In case of ODBC, I decided to keep this consistent with random access after consulting with PM.
                    length = Math.Min(length, buffer.Length - bufferIndex);
                    if (length <= 0)
                    {
                        // SQLBU 266054:
                        // if the data is null, the ideal behavior here is to raise InvalidCastException. But,
                        // * GetBytes returned 0 in Orcas RTM and SP1, continue to do so to avoid breaking change from Orcas RTM and SP1.
                        // * GetChars raised exception in RTM, and returned 0 in SP1: we decided to revert back to the RTM's behavior and raise InvalidCast
                        if (isCharsBuffer)
                        {
                            // for GetChars, ensure data is not null
                            // 2 bytes for '\0' termination, no data is actually read from the driver
                            bool isDbNull = !QueryFieldInfo(i, ODBC32.SQL_C.WCHAR, out _);
                            if (isDbNull)
                            {
                                throw ADP.InvalidCast();
                            }
                        }
                        // else - GetBytes - return now
                        return 0;
                    }

                    // fill the user's buffer
                    return readBytesOrCharsSequentialAccess(i, buffer, isCharsBuffer, bufferIndex, length);
                }
            }
        }

        // fill the user's buffer (char[] or byte[], depending on isCharsBuffer)
        // if buffer is null, just skip the bytesOrCharsLength bytes or chars
        private int readBytesOrCharsSequentialAccess(int i, Array? buffer, bool isCharsBuffer, int bufferIndex, long bytesOrCharsLength)
        {
            Debug.Assert(bufferIndex >= 0, "Negative buffer index");
            Debug.Assert(bytesOrCharsLength >= 0, "Negative number of bytes or chars to read");

            // validated by the caller
            Debug.Assert(buffer == null || bytesOrCharsLength <= (buffer.Length - bufferIndex), "Not enough space in user's buffer");

            int totalBytesOrCharsRead = 0;

            // we need length in bytes, b/c that is what SQLGetData expects
            long cbLength = (isCharsBuffer) ? checked(bytesOrCharsLength * 2) : bytesOrCharsLength;

            // continue reading from the driver until we fill the user's buffer or until no more data is available
            // the data is pumped first into the internal native buffer and after that copied into the user's one if buffer is not null
            CNativeBuffer internalNativeBuffer = this.Buffer;

            // read the data in loop up to th user's length
            // if the data size is less than requested or in case of error, the while loop will stop in the middle
            while (cbLength > 0)
            {
                // max data to be read, in bytes, not including null-terminator for WCHARs
                int cbReadMax;

                // read from the driver
                bool isNotDbNull;
                int cbTotal;
                // read either bytes or chars, depending on the method called
                if (isCharsBuffer)
                {
                    // for WCHAR buffers, we need to leave space for null-terminator (2 bytes)
                    // reserve 2 bytes for null-terminator and 2 bytes to prevent assert in GetData
                    // if SQL_NO_TOTAL is returned, this ammount is read from the wire, in bytes
                    cbReadMax = (int)Math.Min(cbLength, internalNativeBuffer.Length - 4);

                    // SQLGetData will always append it - we do not to copy it to user's buffer
                    isNotDbNull = GetData(i, ODBC32.SQL_C.WCHAR, cbReadMax + 2, out cbTotal);
                }
                else
                {
                    // reserve 2 bytes to prevent assert in GetData
                    // when querying bytes, no need to reserve space for null
                    cbReadMax = (int)Math.Min(cbLength, internalNativeBuffer.Length - 2);

                    isNotDbNull = GetData(i, ODBC32.SQL_C.BINARY, cbReadMax, out cbTotal);
                }

                if (!isNotDbNull)
                {
                    // DbNull received, neither GetBytes nor GetChars should be used with DbNull value
                    // two options
                    // 1. be consistent with SqlDataReader, raise SqlNullValueException
                    // 2. be consistent with other Get* methods of OdbcDataReader and raise InvalidCastException
                    // after consulting with Himanshu (PM), decided to go with option 2 (raise cast exception)
                    throw ADP.InvalidCast();
                }

                int cbRead; // will hold number of bytes read in this loop
                bool noDataRemained = false;
                if (cbTotal == 0)
                {
                    // no bytes read, stop
                    break;
                }
                else if (ODBC32.SQL_NO_TOTAL == cbTotal)
                {
                    // the driver has filled the internal buffer, but the length of remained data is still unknown
                    // we will continue looping until SQLGetData indicates the end of data or user buffer is fully filled
                    cbRead = cbReadMax;
                }
                else
                {
                    Debug.Assert((cbTotal > 0), "GetData returned negative value, which is not SQL_NO_TOTAL");
                    // GetData uses SQLGetData, which StrLen_or_IndPtr (cbTotal in our case) to the current buf + remained buf (if any)
                    if (cbTotal > cbReadMax)
                    {
                        // in this case the amount of bytes/chars read will be the max requested (and more bytes can be read)
                        cbRead = cbReadMax;
                    }
                    else
                    {
                        // SQLGetData read all the available data, no more remained
                        // continue processing this chunk and stop
                        cbRead = cbTotal;
                        noDataRemained = true;
                    }
                }

                _sequentialBytesRead += cbRead;

                // update internal state and copy the data to user's buffer
                if (isCharsBuffer)
                {
                    int cchRead = cbRead / 2;
                    if (buffer != null)
                    {
                        internalNativeBuffer.ReadChars(0, (char[])buffer, bufferIndex, cchRead);
                        bufferIndex += cchRead;
                    }
                    totalBytesOrCharsRead += cchRead;
                }
                else
                {
                    if (buffer != null)
                    {
                        internalNativeBuffer.ReadBytes(0, (byte[])buffer, bufferIndex, cbRead);
                        bufferIndex += cbRead;
                    }
                    totalBytesOrCharsRead += cbRead;
                }

                cbLength -= cbRead;

                // stop if no data remained
                if (noDataRemained)
                    break;
            }

            return totalBytesOrCharsRead;
        }

        private object internalGetBytes(int i)
        {
            if (_dataCache!.AccessIndex(i) == null)
            {
                // Obtain _ALL_ the bytes...
                // The first time GetData returns the true length (so we have to min it).
                byte[] rgBytes;
                int cbBufferLen = Buffer.Length - 4;
                int cbActual;
                int cbOffset = 0;

                if (GetData(i, ODBC32.SQL_C.BINARY, cbBufferLen, out cbActual))
                {
                    CNativeBuffer buffer = Buffer;

                    if (ODBC32.SQL_NO_TOTAL != cbActual)
                    {
                        rgBytes = new byte[cbActual];
                        Buffer.ReadBytes(0, rgBytes, cbOffset, Math.Min(cbActual, cbBufferLen));

                        // Chunking.  The data may be larger than our native buffer.  In which case
                        // instead of growing the buffer (out of control), we will read in chunks to
                        // reduce memory footprint size.
                        while (cbActual > cbBufferLen)
                        {
                            // The first time GetData returns the true length.  Then successive calls
                            // return the remaining data.
                            bool flag = GetData(i, ODBC32.SQL_C.BINARY, cbBufferLen, out cbActual);
                            Debug.Assert(flag, "internalGetBytes - unexpected invalid result inside if-block");

                            cbOffset += cbBufferLen;
                            buffer.ReadBytes(0, rgBytes, cbOffset, Math.Min(cbActual, cbBufferLen));
                        }
                    }
                    else
                    {
                        List<byte[]> junkArray = new List<byte[]>();
                        int junkSize;
                        int totalSize = 0;
                        do
                        {
                            junkSize = (ODBC32.SQL_NO_TOTAL != cbActual) ? cbActual : cbBufferLen;
                            rgBytes = new byte[junkSize];
                            totalSize += junkSize;
                            buffer.ReadBytes(0, rgBytes, 0, junkSize);
                            junkArray.Add(rgBytes);
                        }
                        while ((ODBC32.SQL_NO_TOTAL == cbActual) && GetData(i, ODBC32.SQL_C.BINARY, cbBufferLen, out cbActual));

                        rgBytes = new byte[totalSize];
                        foreach (byte[] junk in junkArray)
                        {
                            junk.CopyTo(rgBytes, cbOffset);
                            cbOffset += junk.Length;
                        }
                    }

                    // always update the cache
                    _dataCache[i] = rgBytes;
                }
            }
            return _dataCache[i]!;
        }

        // GetColAttribute
        // ---------------
        // [IN] iColumn   ColumnNumber
        // [IN] v3FieldId FieldIdentifier of the attribute for version3 drivers (>=3.0)
        // [IN] v2FieldId FieldIdentifier of the attribute for version2 drivers (<3.0)
        //
        // returns the value of the FieldIdentifier field of the column
        // or -1 if the FieldIdentifier wasn't supported by the driver
        //
        private SQLLEN GetColAttribute(int iColumn, ODBC32.SQL_DESC v3FieldId, ODBC32.SQL_COLUMN v2FieldId, ODBC32.HANDLER handler)
        {
            SQLLEN numericAttribute;
            ODBC32.SQLRETURN retcode;

            // protect against dead connection, dead or canceling command.
            if ((Connection == null) || _cmdWrapper!.Canceling)
            {
                return -1;
            }

            //Ordinals are 1:base in odbc
            OdbcStatementHandle stmt = StatementHandle;
            if (Connection.IsV3Driver)
            {
                retcode = stmt.ColumnAttribute(iColumn + 1, (short)v3FieldId, Buffer, out _, out numericAttribute);
            }
            else if (v2FieldId != (ODBC32.SQL_COLUMN)(-1))
            {
                retcode = stmt.ColumnAttribute(iColumn + 1, (short)v2FieldId, Buffer, out _, out numericAttribute);
            }
            else
            {
                return 0;
            }
            if (retcode != ODBC32.SQLRETURN.SUCCESS)
            {
                if (retcode == ODBC32.SQLRETURN.ERROR)
                {
                    if ("HY091" == Command!.GetDiagSqlState())
                    {
                        Connection.FlagUnsupportedColAttr(v3FieldId, v2FieldId);
                    }
                }
                if (handler == ODBC32.HANDLER.THROW)
                {
                    Connection.HandleError(stmt, retcode);
                }
                return -1;
            }
            return numericAttribute;
        }

        // GetColAttributeStr
        // ---------------
        // [IN] iColumn   ColumnNumber
        // [IN] v3FieldId FieldIdentifier of the attribute for version3 drivers (>=3.0)
        // [IN] v2FieldId FieldIdentifier of the attribute for version2 drivers (<3.0)
        //
        // returns the stringvalue of the FieldIdentifier field of the column
        // or null if the string returned was empty or if the FieldIdentifier wasn't supported by the driver
        //
        private string? GetColAttributeStr(int i, ODBC32.SQL_DESC v3FieldId, ODBC32.SQL_COLUMN v2FieldId, ODBC32.HANDLER handler)
        {
            ODBC32.SQLRETURN retcode;
            short cchNameLength;
            CNativeBuffer buffer = Buffer;
            buffer.WriteInt16(0, 0);

            OdbcStatementHandle? stmt = StatementHandle;

            // protect against dead connection
            if (Connection == null || _cmdWrapper!.Canceling || stmt == null)
            {
                return "";
            }

            if (Connection.IsV3Driver)
            {
                retcode = stmt.ColumnAttribute(i + 1, (short)v3FieldId, buffer, out cchNameLength, out _);
            }
            else if (v2FieldId != (ODBC32.SQL_COLUMN)(-1))
            {
                retcode = stmt.ColumnAttribute(i + 1, (short)v2FieldId, buffer, out cchNameLength, out _);
            }
            else
            {
                return null;
            }
            if ((retcode != ODBC32.SQLRETURN.SUCCESS) || (cchNameLength == 0))
            {
                if (retcode == ODBC32.SQLRETURN.ERROR)
                {
                    if ("HY091" == Command!.GetDiagSqlState())
                    {
                        Connection.FlagUnsupportedColAttr(v3FieldId, v2FieldId);
                    }
                }
                if (handler == ODBC32.HANDLER.THROW)
                {
                    Connection.HandleError(stmt, retcode);
                }
                return null;
            }
            string retval = buffer.PtrToStringUni(0, cchNameLength / 2 /*cch*/);
            return retval;
        }

        // todo: Another 3.0 only attribute that is guaranteed to fail on V2 driver.
        // need to special case this for V2 drivers.
        //
        private string? GetDescFieldStr(int i, ODBC32.SQL_DESC attribute, ODBC32.HANDLER handler)
        {
            int numericAttribute = 0;

            // protect against dead connection, dead or canceling command.
            if ((Connection == null) || _cmdWrapper!.Canceling)
            {
                return "";
            }

            // APP_PARAM_DESC is a (ODBCVER >= 0x0300) attribute
            if (!Connection.IsV3Driver)
            {
                Debug.Fail("Non-V3 driver. Must not call GetDescFieldStr");
                return null;
            }

            ODBC32.SQLRETURN retcode;
            CNativeBuffer buffer = Buffer;

            // Need to set the APP_PARAM_DESC values here
            using (OdbcDescriptorHandle hdesc = new OdbcDescriptorHandle(StatementHandle, ODBC32.SQL_ATTR.APP_PARAM_DESC))
            {
                //SQLGetDescField
                retcode = hdesc.GetDescriptionField(i + 1, attribute, buffer, out numericAttribute);

                //Since there are many attributes (column, statement, etc), that may or may not be
                //supported, we don't want to throw (which obtains all errorinfo, marshals strings,
                //builds exceptions, etc), in common cases, unless we absolutely need this info...
                if ((retcode != ODBC32.SQLRETURN.SUCCESS) || (numericAttribute == 0))
                {
                    if (retcode == ODBC32.SQLRETURN.ERROR)
                    {
                        if ("HY091" == Command!.GetDiagSqlState())
                        {
                            Connection.FlagUnsupportedColAttr(attribute, (ODBC32.SQL_COLUMN)0);
                        }
                    }
                    if (handler == ODBC32.HANDLER.THROW)
                    {
                        Connection.HandleError(StatementHandle, retcode);
                    }
                    return null;
                }
            }
            string retval = buffer.PtrToStringUni(0, numericAttribute / 2 /*cch*/);
            return retval;
        }

        /// <summary>
        /// This methods queries the following field information: isDbNull and remained size/indicator. No data is read from the driver.
        /// If the value is DbNull, this value will be cached. Refer to GetData for more details.
        /// </summary>
        /// <returns>false if value is DbNull, true otherwise</returns>
        private bool QueryFieldInfo(int i, ODBC32.SQL_C sqlctype, out int cbLengthOrIndicator)
        {
            int cb = 0;
            if (sqlctype == ODBC32.SQL_C.WCHAR)
            {
                // SQLBU 266054 - in case of WCHAR data, we need to provide buffer with a space for null terminator (two bytes)
                cb = 2;
            }
            return GetData(i, sqlctype, cb /* no data should be lost */, out cbLengthOrIndicator);
        }

        private bool GetData(int i, ODBC32.SQL_C sqlctype)
        {
            // Never call GetData with anything larger than _buffer.Length-2.
            // We keep reallocating native buffers and it kills performance!!!
            return GetData(i, sqlctype, Buffer.Length - 4, out _);
        }

        /// <summary>
        /// Note: use only this method to call SQLGetData! It caches the null value so the fact that the value is null is kept and no other calls
        /// are made after it.
        ///
        /// retrieves the data into this.Buffer.
        /// * If the data is DbNull, the value be also cached and false is returned.
        /// * if the data is not DbNull, the value is not cached and true is returned
        ///
        /// Note: cbLengthOrIndicator can be either the length of (remained) data or SQL_NO_TOTAL (-4) when the length is not known.
        /// in case of SQL_NO_TOTAL, driver fills the buffer till the end.
        /// The indicator will NOT be SQL_NULL_DATA, GetData will replace it with zero and return false.
        /// </summary>
        /// <returns>false if value is DbNull, true otherwise</returns>
        private bool GetData(int i, ODBC32.SQL_C sqlctype, int cb, out int cbLengthOrIndicator)
        {
            nint cbActual;  // Length or an indicator value

            if (IsCancelingCommand)
            {
                throw ADP.DataReaderNoData();
            }
            Debug.Assert(null != StatementHandle, "Statement handle is null in DateReader");

            // see notes on ODBC32.RetCode.NO_DATA case below.
            Debug.Assert(_dataCache == null || !Convert.IsDBNull(_dataCache[i]), "Cannot call GetData without checking for cache first!");

            // Never call GetData with anything larger than _buffer.Length-2.
            // We keep reallocating native buffers and it kills performance!!!

            Debug.Assert(cb <= Buffer.Length - 2, "GetData needs to Reallocate. Perf bug");

            // SQLGetData
            CNativeBuffer buffer = Buffer;
            ODBC32.SQLRETURN retcode = StatementHandle.GetData(
               (i + 1),    // Column ordinals start at 1 in odbc
               sqlctype,
               buffer,
               cb,
               out cbActual);

            switch (retcode)
            {
                case ODBC32.SQLRETURN.SUCCESS:
                    break;
                case ODBC32.SQLRETURN.SUCCESS_WITH_INFO:
                    if ((int)cbActual == ODBC32.SQL_NO_TOTAL)
                    {
                        break;
                    }
                    // devnote: don't we want to fire an event?
                    break;

                case ODBC32.SQLRETURN.NO_DATA:
                    // SQLBU 266054: System.Data.Odbc: Fails with truncated error when we pass BufferLength  as 0
                    // NO_DATA return value is success value - it means that the driver has fully consumed the current column value
                    // but did not move to the next column yet.
                    // For fixed-size values, we do not expect this to happen because we fully consume the data and store it in cache after the first call.
                    // For variable-length values (any character or binary data), SQLGetData can be called several times on the same column,
                    // to query for the next chunk of value, even after reaching its end!
                    // Thus, ignore NO_DATA for variable length data, but raise exception for fixed-size types
                    if (sqlctype != ODBC32.SQL_C.WCHAR && sqlctype != ODBC32.SQL_C.BINARY)
                    {
                        Connection!.HandleError(StatementHandle, retcode);
                    }

                    if (cbActual == (IntPtr)ODBC32.SQL_NO_TOTAL)
                    {
                        // ensure SQL_NO_TOTAL value gets replaced with zero if the driver has fully consumed the current column
                        cbActual = 0;
                    }
                    break;

                default:
                    Connection!.HandleError(StatementHandle, retcode);
                    break;
            }

            // reset the current row and column
            SetCurrentRowColumnInfo(_row, i);

            // test for SQL_NULL_DATA
            if (cbActual == (IntPtr)ODBC32.SQL_NULL_DATA)
            {
                // Store the DBNull value in cache. Note that if we need to do it, because second call into the SQLGetData returns NO_DATA, which means
                // we already consumed the value (see above) and the NULL information is lost. By storing the null in cache, we avoid second call into the driver
                // for the same row/column.
                _dataCache![i] = DBNull.Value;
                // the indicator is never -1 (and it should not actually be used if the data is DBNull)
                cbLengthOrIndicator = 0;
                return false;
            }
            else
            {
                //Return the actual size (for chunking scenarios)
                // note the return value can be SQL_NO_TOTAL (-4)
                cbLengthOrIndicator = (int)cbActual;
                return true;
            }
        }

        public override bool Read()
        {
            if (IsClosed)
            {
                throw ADP.DataReaderClosed("Read");
            }

            if (IsCancelingCommand)
            {
                _isRead = false;
                return false;
            }

            // HasRows needs to call into Read so we don't want to read on the actual Read call
            if (_skipReadOnce)
            {
                _skipReadOnce = false;
                return _isRead;
            }

            if (_noMoreRows || _noMoreResults || IsCommandBehavior(CommandBehavior.SchemaOnly))
                return false;

            if (!_isValidResult)
            {
                return false;
            }

            ODBC32.SQLRETURN retcode;

            //SQLFetch is only valid to call for row returning queries
            //We get: [24000]Invalid cursor state.  So we could either check the count
            //ahead of time (which is cached), or check on error and compare error states.
            //Note: SQLFetch is also invalid to be called on a prepared (schemaonly) statement
            //SqlFetch
            retcode = StatementHandle.Fetch();

            switch (retcode)
            {
                case ODBC32.SQLRETURN.SUCCESS_WITH_INFO:
                    Connection!.HandleErrorNoThrow(StatementHandle, retcode);
                    _hasRows = HasRowsStatus.HasRows;
                    _isRead = true;
                    break;
                case ODBC32.SQLRETURN.SUCCESS:
                    _hasRows = HasRowsStatus.HasRows;
                    _isRead = true;
                    break;
                case ODBC32.SQLRETURN.NO_DATA:
                    _isRead = false;
                    if (_hasRows == HasRowsStatus.DontKnow)
                    {
                        _hasRows = HasRowsStatus.HasNoRows;
                    }
                    break;
                default:
                    Connection!.HandleError(StatementHandle, retcode);
                    break;
            }
            //Null out previous cached row values.
            _dataCache!.FlushValues();

            // if CommandBehavior == SingleRow we set _noMoreResults to true so that following reads will fail
            if (IsCommandBehavior(CommandBehavior.SingleRow))
            {
                _noMoreRows = true;
                // no more rows, set to -1
                SetCurrentRowColumnInfo(-1, 0);
            }
            else
            {
                // move to the next row
                SetCurrentRowColumnInfo(_row + 1, 0);
            }
            return _isRead;
        }

        // Called by odbccommand when executed for the first time
        internal void FirstResult()
        {
            short cCols;
            SQLLEN cRowsAffected;

            cRowsAffected = GetRowCount();              // get rowcount of the current resultset (if any)
            CalculateRecordsAffected(cRowsAffected);    // update recordsaffected

            ODBC32.SQLRETURN retcode = FieldCountNoThrow(out cCols);
            if ((retcode == ODBC32.SQLRETURN.SUCCESS) && (cCols == 0))
            {
                NextResult();
            }
            else
            {
                _isValidResult = true;
            }
        }

        public override bool NextResult()
        {
            return NextResult(false, false);
        }

        private bool NextResult(bool disposing, bool allresults)
        {
            // if disposing, loop through all the remaining results and ignore error messages
            // if allresults, loop through all results and collect all error messages for a single exception
            // callers are via Close(false, true), Dispose(true, false), NextResult(false,false)
            Debug.Assert(!disposing || !allresults, "both disposing & allresults are true");
            const int MaxConsecutiveFailure = 2000; // see WebData 72126 for why more than 1000

            SQLLEN cRowsAffected;
            short cColsAffected;
            ODBC32.SQLRETURN retcode, firstRetCode = ODBC32.SQLRETURN.SUCCESS;
            bool hasMoreResults;
            bool hasColumns = false;
            bool singleResult = IsCommandBehavior(CommandBehavior.SingleResult);

            if (IsClosed)
            {
                throw ADP.DataReaderClosed("NextResult");
            }
            _fieldNameLookup = null;

            if (IsCancelingCommand || _noMoreResults)
            {
                return false;
            }

            //Blow away the previous cache (since the next result set will have a different shape,
            //different schema data, and different data.
            _isRead = false;
            _hasRows = HasRowsStatus.DontKnow;
            _fieldNameLookup = null;
            _metadata = null;
            _schemaTable = null;

            int loop = 0; // infinite loop protection, max out after 2000 consecutive failed results
            OdbcErrorCollection? errors = null; // SQLBU 342112
            do
            {
                _isValidResult = false;
                retcode = StatementHandle.MoreResults();
                hasMoreResults = ((retcode == ODBC32.SQLRETURN.SUCCESS)
                                || (retcode == ODBC32.SQLRETURN.SUCCESS_WITH_INFO));

                if (retcode == ODBC32.SQLRETURN.SUCCESS_WITH_INFO)
                {
                    Connection!.HandleErrorNoThrow(StatementHandle, retcode);
                }
                else if (!disposing && (retcode != ODBC32.SQLRETURN.NO_DATA) && (ODBC32.SQLRETURN.SUCCESS != retcode))
                {
                    // allow for building comulative error messages.
                    if (null == errors)
                    {
                        firstRetCode = retcode;
                        errors = new OdbcErrorCollection();
                    }
                    ODBC32.GetDiagErrors(errors, null, StatementHandle, retcode);
                    ++loop;
                }

                if (!disposing && hasMoreResults)
                {
                    loop = 0;
                    cRowsAffected = GetRowCount();              // get rowcount of the current resultset (if any)
                    CalculateRecordsAffected(cRowsAffected);    // update recordsaffected
                    if (!singleResult)
                    {
                        // update row- and columncount
                        FieldCountNoThrow(out cColsAffected);
                        hasColumns = (0 != cColsAffected);
                        _isValidResult = hasColumns;
                    }
                }
            } while ((!singleResult && hasMoreResults && !hasColumns)  // repeat for results with no columns
                     || ((ODBC32.SQLRETURN.NO_DATA != retcode) && allresults && (loop < MaxConsecutiveFailure)) // or process all results until done
                     || (singleResult && hasMoreResults));           // or for any result in singelResult mode

            if (retcode == ODBC32.SQLRETURN.NO_DATA)
            {
                _dataCache = null;
                _noMoreResults = true;
            }
            if (null != errors)
            {
                Debug.Assert(!disposing, "errors while disposing");
                errors.SetSource(Connection!.Driver);
                OdbcException exception = OdbcException.CreateException(errors, firstRetCode);
                Connection.ConnectionIsAlive(exception);
                throw exception;
            }
            return (hasMoreResults);
        }

        private void BuildMetaDataInfo()
        {
            int count = FieldCount;
            MetaData[] metaInfos = new MetaData[count];
            List<string>? qrytables;
            bool needkeyinfo = IsCommandBehavior(CommandBehavior.KeyInfo);
            bool isKeyColumn;
            bool isHidden;
            ODBC32.SQL_NULLABILITY nullable;

            if (needkeyinfo)
                qrytables = new List<string>();
            else
                qrytables = null;

            // Find out all the metadata info, not all of this info will be available in all cases
            //
            for (int i = 0; i < count; i++)
            {
                metaInfos[i] = new MetaData();
                metaInfos[i].ordinal = i;
                TypeMap typeMap;

                // for precision and scale we take the SQL_COLUMN_ attributes.
                // Those attributes are supported by all provider versions.
                // for size we use the octet length. We can't use column length because there is an incompatibility with the jet driver.
                // furthermore size needs to be special cased for wchar types
                //
                typeMap = TypeMap.FromSqlType((ODBC32.SQL_TYPE)unchecked((int)GetColAttribute(i, ODBC32.SQL_DESC.CONCISE_TYPE, ODBC32.SQL_COLUMN.TYPE, ODBC32.HANDLER.THROW)));
                if (typeMap._signType)
                {
                    bool sign = (GetColAttribute(i, ODBC32.SQL_DESC.UNSIGNED, ODBC32.SQL_COLUMN.UNSIGNED, ODBC32.HANDLER.THROW).ToInt64() != 0);
                    // sign = true if the column is unsigned
                    typeMap = TypeMap.UpgradeSignedType(typeMap, sign);
                }

                metaInfos[i].typemap = typeMap;
                metaInfos[i].size = GetColAttribute(i, ODBC32.SQL_DESC.OCTET_LENGTH, ODBC32.SQL_COLUMN.LENGTH, ODBC32.HANDLER.IGNORE);

                // special case the 'n' types
                //
                switch (metaInfos[i].typemap._sql_type)
                {
                    case ODBC32.SQL_TYPE.WCHAR:
                    case ODBC32.SQL_TYPE.WLONGVARCHAR:
                    case ODBC32.SQL_TYPE.WVARCHAR:
                        metaInfos[i].size /= 2;
                        break;
                }

                metaInfos[i].precision = (byte)GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_COLUMN.PRECISION, ODBC32.SQL_COLUMN.PRECISION, ODBC32.HANDLER.IGNORE);
                metaInfos[i].scale = (byte)GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_COLUMN.SCALE, ODBC32.SQL_COLUMN.SCALE, ODBC32.HANDLER.IGNORE);

                metaInfos[i].isAutoIncrement = GetColAttribute(i, ODBC32.SQL_DESC.AUTO_UNIQUE_VALUE, ODBC32.SQL_COLUMN.AUTO_INCREMENT, ODBC32.HANDLER.IGNORE) == 1;
                metaInfos[i].isReadOnly = (GetColAttribute(i, ODBC32.SQL_DESC.UPDATABLE, ODBC32.SQL_COLUMN.UPDATABLE, ODBC32.HANDLER.IGNORE) == (int)ODBC32.SQL_UPDATABLE.READONLY);

                nullable = (ODBC32.SQL_NULLABILITY)(int)GetColAttribute(i, ODBC32.SQL_DESC.NULLABLE, ODBC32.SQL_COLUMN.NULLABLE, ODBC32.HANDLER.IGNORE);
                metaInfos[i].isNullable = (nullable == ODBC32.SQL_NULLABILITY.NULLABLE);

                switch (metaInfos[i].typemap._sql_type)
                {
                    case ODBC32.SQL_TYPE.LONGVARCHAR:
                    case ODBC32.SQL_TYPE.WLONGVARCHAR:
                    case ODBC32.SQL_TYPE.LONGVARBINARY:
                        metaInfos[i].isLong = true;
                        break;
                    default:
                        metaInfos[i].isLong = false;
                        break;
                }

                if (IsCommandBehavior(CommandBehavior.KeyInfo))
                {
                    // Note: Following two attributes are SQL Server specific (hence _SS in the name)

                    // SSS_WARNINGS_OFF
                    if (!Connection!.ProviderInfo.NoSqlCASSColumnKey)
                    {
                        isKeyColumn = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_KEY, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE) == 1;
                        if (isKeyColumn)
                        {
                            metaInfos[i].isKeyColumn = isKeyColumn;
                            metaInfos[i].isUnique = true;
                            needkeyinfo = false;
                        }
                    }
                    // SSS_WARNINGS_ON

                    metaInfos[i].baseSchemaName = GetColAttributeStr(i, ODBC32.SQL_DESC.SCHEMA_NAME, ODBC32.SQL_COLUMN.OWNER_NAME, ODBC32.HANDLER.IGNORE);
                    metaInfos[i].baseCatalogName = GetColAttributeStr(i, ODBC32.SQL_DESC.CATALOG_NAME, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE);
                    metaInfos[i].baseTableName = GetColAttributeStr(i, ODBC32.SQL_DESC.BASE_TABLE_NAME, ODBC32.SQL_COLUMN.TABLE_NAME, ODBC32.HANDLER.IGNORE);
                    metaInfos[i].baseColumnName = GetColAttributeStr(i, ODBC32.SQL_DESC.BASE_COLUMN_NAME, ODBC32.SQL_COLUMN.NAME, ODBC32.HANDLER.IGNORE);

                    if (Connection.IsV3Driver)
                    {
                        if (string.IsNullOrEmpty(metaInfos[i].baseTableName))
                        {
                            // Driver didn't return the necessary information from GetColAttributeStr.
                            // Try GetDescField()
                            metaInfos[i].baseTableName = GetDescFieldStr(i, ODBC32.SQL_DESC.BASE_TABLE_NAME, ODBC32.HANDLER.IGNORE);
                        }
                        if (string.IsNullOrEmpty(metaInfos[i].baseColumnName))
                        {
                            // Driver didn't return the necessary information from GetColAttributeStr.
                            // Try GetDescField()
                            metaInfos[i].baseColumnName = GetDescFieldStr(i, ODBC32.SQL_DESC.BASE_COLUMN_NAME, ODBC32.HANDLER.IGNORE);
                        }
                    }
                    if ((metaInfos[i].baseTableName is string baseTableName) && !(qrytables!.Contains(baseTableName)))
                    {
                        qrytables.Add(baseTableName);
                    }
                }

                // If primary key or autoincrement, then must also be unique
                if (metaInfos[i].isKeyColumn || metaInfos[i].isAutoIncrement)
                {
                    if (nullable == ODBC32.SQL_NULLABILITY.UNKNOWN)
                        metaInfos[i].isNullable = false;    // We can safely assume these are not nullable
                }
            }

            // now loop over the hidden columns (if any)

            // SSS_WARNINGS_OFF
            if (!Connection!.ProviderInfo.NoSqlCASSColumnKey)
            {
                for (int i = count; i < count + _hiddenColumns; i++)
                {
                    isKeyColumn = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_KEY, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE) == 1;
                    if (isKeyColumn)
                    {
                        isHidden = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_HIDDEN, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE) == 1;
                        if (isHidden)
                        {
                            for (int j = 0; j < count; j++)
                            {
                                metaInfos[j].isKeyColumn = false;   // downgrade keycolumn
                                metaInfos[j].isUnique = false;      // downgrade uniquecolumn
                            }
                        }
                    }
                }
            }
            // SSS_WARNINGS_ON

            // Blow away the previous metadata
            _metadata = metaInfos;

            // If key info is requested, then we have to make a few more calls to get the
            //  special columns. This may not succeed for all drivers, so ignore errors and
            // fill in as much as possible.
            if (IsCommandBehavior(CommandBehavior.KeyInfo))
            {
                if ((qrytables != null) && (qrytables.Count > 0))
                {
                    List<string>.Enumerator tablesEnum = qrytables.GetEnumerator();
                    QualifiedTableName qualifiedTableName = new QualifiedTableName(Connection.QuoteChar(ADP.GetSchemaTable));
                    while (tablesEnum.MoveNext())
                    {
                        // Find the primary keys, identity and autincrement columns
                        qualifiedTableName.Table = tablesEnum.Current;
                        if (RetrieveKeyInfo(needkeyinfo, qualifiedTableName, false) <= 0)
                        {
                            RetrieveKeyInfo(needkeyinfo, qualifiedTableName, true);
                        }
                    }
                }
                else
                {
                    // Some drivers ( < 3.x ?) do not provide base table information. In this case try to
                    // find it by parsing the statement

                    QualifiedTableName qualifiedTableName = new QualifiedTableName(Connection.QuoteChar(ADP.GetSchemaTable), GetTableNameFromCommandText());
                    if (!string.IsNullOrEmpty(qualifiedTableName.Table))
                    { // fxcop
                        SetBaseTableNames(qualifiedTableName);
                        if (RetrieveKeyInfo(needkeyinfo, qualifiedTableName, false) <= 0)
                        {
                            RetrieveKeyInfo(needkeyinfo, qualifiedTableName, true);
                        }
                    }
                }
            }
        }

        private DataTable NewSchemaTable()
        {
            DataTable schematable = new DataTable("SchemaTable");
            schematable.Locale = CultureInfo.InvariantCulture;
            schematable.MinimumCapacity = this.FieldCount;

            //Schema Columns
            DataColumnCollection columns = schematable.Columns;
            columns.Add(new DataColumn("ColumnName", typeof(string)));
            columns.Add(new DataColumn("ColumnOrdinal", typeof(int))); // UInt32
            columns.Add(new DataColumn("ColumnSize", typeof(int))); // UInt32
            columns.Add(new DataColumn("NumericPrecision", typeof(short))); // UInt16
            columns.Add(new DataColumn("NumericScale", typeof(short)));
            columns.Add(new DataColumn("DataType", typeof(object)));
            columns.Add(new DataColumn("ProviderType", typeof(int)));
            columns.Add(new DataColumn("IsLong", typeof(bool)));
            columns.Add(new DataColumn("AllowDBNull", typeof(bool)));
            columns.Add(new DataColumn("IsReadOnly", typeof(bool)));
            columns.Add(new DataColumn("IsRowVersion", typeof(bool)));
            columns.Add(new DataColumn("IsUnique", typeof(bool)));
            columns.Add(new DataColumn("IsKey", typeof(bool)));
            columns.Add(new DataColumn("IsAutoIncrement", typeof(bool)));
            columns.Add(new DataColumn("BaseSchemaName", typeof(string)));
            columns.Add(new DataColumn("BaseCatalogName", typeof(string)));
            columns.Add(new DataColumn("BaseTableName", typeof(string)));
            columns.Add(new DataColumn("BaseColumnName", typeof(string)));

            // MDAC Bug 79231
            foreach (DataColumn column in columns)
            {
                column.ReadOnly = true;
            }
            return schematable;
        }

        // The default values are already defined in DbSchemaRows (see DbSchemaRows.cs) so there is no need to set any default value
        //

        public override DataTable? GetSchemaTable()
        {
            if (IsClosed)
            { // MDAC 68331
                throw ADP.DataReaderClosed("GetSchemaTable");           // can't use closed connection
            }
            if (_noMoreResults)
            {
                return null;                                            // no more results
            }
            if (null != _schemaTable)
            {
                return _schemaTable;                                // return cached schematable
            }

            //Delegate, to have the base class setup the structure
            DataTable schematable = NewSchemaTable();

            if (FieldCount == 0)
            {
                return schematable;
            }
            if (_metadata == null)
            {
                BuildMetaDataInfo();
            }

            DataColumn columnName = schematable.Columns["ColumnName"]!;
            DataColumn columnOrdinal = schematable.Columns["ColumnOrdinal"]!;
            DataColumn columnSize = schematable.Columns["ColumnSize"]!;
            DataColumn numericPrecision = schematable.Columns["NumericPrecision"]!;
            DataColumn numericScale = schematable.Columns["NumericScale"]!;
            DataColumn dataType = schematable.Columns["DataType"]!;
            DataColumn providerType = schematable.Columns["ProviderType"]!;
            DataColumn isLong = schematable.Columns["IsLong"]!;
            DataColumn allowDBNull = schematable.Columns["AllowDBNull"]!;
            DataColumn isReadOnly = schematable.Columns["IsReadOnly"]!;
            DataColumn isRowVersion = schematable.Columns["IsRowVersion"]!;
            DataColumn isUnique = schematable.Columns["IsUnique"]!;
            DataColumn isKey = schematable.Columns["IsKey"]!;
            DataColumn isAutoIncrement = schematable.Columns["IsAutoIncrement"]!;
            DataColumn baseSchemaName = schematable.Columns["BaseSchemaName"]!;
            DataColumn baseCatalogName = schematable.Columns["BaseCatalogName"]!;
            DataColumn baseTableName = schematable.Columns["BaseTableName"]!;
            DataColumn baseColumnName = schematable.Columns["BaseColumnName"]!;


            //Populate the rows (1 row for each column)
            int count = FieldCount;
            for (int i = 0; i < count; i++)
            {
                DataRow row = schematable.NewRow();

                row[columnName] = GetName(i);        //ColumnName
                row[columnOrdinal] = i;                 //ColumnOrdinal
                row[columnSize] = unchecked((int)Math.Min(Math.Max(int.MinValue, _metadata![i].size.ToInt64()), int.MaxValue));
                row[numericPrecision] = (short)_metadata[i].precision;
                row[numericScale] = (short)_metadata[i].scale;
                row[dataType] = _metadata[i].typemap._type;          //DataType
                row[providerType] = _metadata[i].typemap._odbcType;          // ProviderType
                row[isLong] = _metadata[i].isLong;           // IsLong
                row[allowDBNull] = _metadata[i].isNullable;       //AllowDBNull
                row[isReadOnly] = _metadata[i].isReadOnly;      // IsReadOnly
                row[isRowVersion] = _metadata[i].isRowVersion;    //IsRowVersion
                row[isUnique] = _metadata[i].isUnique;        //IsUnique
                row[isKey] = _metadata[i].isKeyColumn;    // IsKey
                row[isAutoIncrement] = _metadata[i].isAutoIncrement; //IsAutoIncrement

                //BaseSchemaName
                row[baseSchemaName] = _metadata[i].baseSchemaName;
                //BaseCatalogName
                row[baseCatalogName] = _metadata[i].baseCatalogName;
                //BaseTableName
                row[baseTableName] = _metadata[i].baseTableName;
                //BaseColumnName
                row[baseColumnName] = _metadata[i].baseColumnName;

                schematable.Rows.Add(row);
                row.AcceptChanges();
            }
            _schemaTable = schematable;
            return schematable;
        }

        internal int RetrieveKeyInfo(bool needkeyinfo, QualifiedTableName qualifiedTableName, bool quoted)
        {
            Debug.Assert(_metadata != null);

            ODBC32.SQLRETURN retcode;
            string columnname;
            int ordinal;
            int keyColumns = 0;
            nint cbActual;

            if (IsClosed || (_cmdWrapper == null))
            {
                return 0;     // Can't do anything without a second handle
            }
            _cmdWrapper.CreateKeyInfoStatementHandle();

            CNativeBuffer buffer = Buffer;
            bool mustRelease = false;
            Debug.Assert(buffer.Length >= 264, "Native buffer to small (_buffer.Length < 264)");

            try
            {
                buffer.DangerousAddRef(ref mustRelease);

                if (needkeyinfo)
                {
                    if (!Connection!.ProviderInfo.NoSqlPrimaryKeys)
                    {
                        // Get the primary keys
                        retcode = KeyInfoStatementHandle.PrimaryKeys(
                                    qualifiedTableName.Catalog,
                                    qualifiedTableName.Schema,
                                    qualifiedTableName.GetTable(quoted)!);

                        if ((retcode == ODBC32.SQLRETURN.SUCCESS) || (retcode == ODBC32.SQLRETURN.SUCCESS_WITH_INFO))
                        {
                            bool noUniqueKey = false;

                            // We are only interested in column name
                            buffer.WriteInt16(0, 0);
                            retcode = KeyInfoStatementHandle.BindColumn2(
                                           (short)(ODBC32.SQL_PRIMARYKEYS.COLUMNNAME),    // Column Number
                                           ODBC32.SQL_C.WCHAR,
                                           buffer.PtrOffset(0, 256),
                                           (IntPtr)256,
                                           buffer.PtrOffset(256, IntPtr.Size).Handle);
                            while (ODBC32.SQLRETURN.SUCCESS == (retcode = KeyInfoStatementHandle.Fetch()))
                            {
                                cbActual = buffer.ReadIntPtr(256);
                                columnname = buffer.PtrToStringUni(0, (int)cbActual / 2/*cch*/);
                                ordinal = this.GetOrdinalFromBaseColName(columnname);
                                if (ordinal != -1)
                                {
                                    keyColumns++;
                                    _metadata[ordinal].isKeyColumn = true;
                                    _metadata[ordinal].isUnique = true;
                                    _metadata[ordinal].isNullable = false;
                                    _metadata[ordinal].baseTableName = qualifiedTableName.Table;

                                    _metadata[ordinal].baseColumnName ??= columnname;
                                }
                                else
                                {
                                    noUniqueKey = true;
                                    break;  // no need to go over the remaining columns anymore
                                }
                            }
                            //




                            // if we got keyinfo from the column we dont even get to here!
                            //
                            // reset isUnique flag if the key(s) are not unique
                            //
                            if (noUniqueKey)
                            {
                                foreach (MetaData metadata in _metadata)
                                {
                                    metadata.isKeyColumn = false;
                                }
                            }

                            // Unbind the column
                            retcode = KeyInfoStatementHandle.BindColumn3(
                                (short)(ODBC32.SQL_PRIMARYKEYS.COLUMNNAME),      // SQLUSMALLINT ColumnNumber
                                ODBC32.SQL_C.WCHAR,                     // SQLSMALLINT  TargetType
                                buffer.DangerousGetHandle());                                   // SQLLEN *     StrLen_or_Ind
                        }
                        else
                        {
                            if ("IM001" == Command!.GetDiagSqlState())
                            {
                                Connection.ProviderInfo.NoSqlPrimaryKeys = true;
                            }
                        }
                    }

                    if (keyColumns == 0)
                    {
                        // SQLPrimaryKeys did not work. Have to use the slower SQLStatistics to obtain key information
                        KeyInfoStatementHandle.MoreResults();
                        keyColumns += RetrieveKeyInfoFromStatistics(qualifiedTableName, quoted);
                    }
                    KeyInfoStatementHandle.MoreResults();
                }

                // Get the special columns for version
                retcode = KeyInfoStatementHandle.SpecialColumns(qualifiedTableName.GetTable(quoted)!);

                if ((retcode == ODBC32.SQLRETURN.SUCCESS) || (retcode == ODBC32.SQLRETURN.SUCCESS_WITH_INFO))
                {
                    // We are only interested in column name
                    cbActual = IntPtr.Zero;
                    buffer.WriteInt16(0, 0);
                    retcode = KeyInfoStatementHandle.BindColumn2(
                                   (short)(ODBC32.SQL_SPECIALCOLUMNSET.COLUMN_NAME),
                                   ODBC32.SQL_C.WCHAR,
                                   buffer.PtrOffset(0, 256),
                                   (IntPtr)256,
                                   buffer.PtrOffset(256, IntPtr.Size).Handle);

                    while (ODBC32.SQLRETURN.SUCCESS == (retcode = KeyInfoStatementHandle.Fetch()))
                    {
                        cbActual = buffer.ReadIntPtr(256);
                        columnname = buffer.PtrToStringUni(0, (int)cbActual / 2/*cch*/);
                        ordinal = this.GetOrdinalFromBaseColName(columnname);
                        if (ordinal != -1)
                        {
                            _metadata[ordinal].isRowVersion = true;
                            _metadata[ordinal].baseColumnName ??= columnname;
                        }
                    }
                    // Unbind the column
                    retcode = KeyInfoStatementHandle.BindColumn3(
                                   (short)(ODBC32.SQL_SPECIALCOLUMNSET.COLUMN_NAME),
                                   ODBC32.SQL_C.WCHAR,
                                   buffer.DangerousGetHandle());

                    retcode = KeyInfoStatementHandle.MoreResults();
                }
                else
                {
                    //  i've seen "DIAG [HY000] [Microsoft][ODBC SQL Server Driver]Connection is busy with results for another hstmt (0) "
                    //  how did we get here? SqlServer does not allow a second handle (Keyinfostmt) anyway...
                    //
                    /*
                        string msg = "Unexpected failure of SQLSpecialColumns. Code = " + Command.GetDiagSqlState();
                        Debug.Assert (false, msg);
                    */
                }
            }
            finally
            {
                if (mustRelease)
                {
                    buffer.DangerousRelease();
                }
            }
            return keyColumns;
        }

        // Uses SQLStatistics to retrieve key information for a table
        private int RetrieveKeyInfoFromStatistics(QualifiedTableName qualifiedTableName, bool quoted)
        {
            Debug.Assert(_metadata != null);

            ODBC32.SQLRETURN retcode;
            string columnname = string.Empty;
            string indexname;
            string currentindexname = string.Empty;
            int[] indexcolumnordinals = new int[16];
            int[] pkcolumnordinals = new int[16];
            int npkcols = 0;
            int ncols = 0;                  // No of cols in the index
            bool partialcolumnset = false;
            int ordinal;
            int indexordinal;
            nint cbIndexLen;
            nint cbColnameLen;
            int keyColumns = 0;

            // devnote: this test is already done by calling method ...
            // if (IsClosed) return;   // protect against dead connection

            string tablename1 = qualifiedTableName.GetTable(quoted)!;

            // Select only unique indexes
            retcode = KeyInfoStatementHandle.Statistics(tablename1);

            if (retcode != ODBC32.SQLRETURN.SUCCESS)
            {
                // We give up at this point
                return 0;
            }

            CNativeBuffer buffer = Buffer;
            bool mustRelease = false;
            Debug.Assert(buffer.Length >= 544, "Native buffer to small (_buffer.Length < 544)");

            try
            {
                buffer.DangerousAddRef(ref mustRelease);

                const int colnameBufOffset = 0;
                const int indexBufOffset = 256;
                const int ordinalBufOffset = 512;
                const int colnameActualOffset = 520;
                const int indexActualOffset = 528;
                const int ordinalActualOffset = 536;
                HandleRef colnamebuf = buffer.PtrOffset(colnameBufOffset, 256);
                HandleRef indexbuf = buffer.PtrOffset(indexBufOffset, 256);
                HandleRef ordinalbuf = buffer.PtrOffset(ordinalBufOffset, 4);

                IntPtr colnameActual = buffer.PtrOffset(colnameActualOffset, IntPtr.Size).Handle;
                IntPtr indexActual = buffer.PtrOffset(indexActualOffset, IntPtr.Size).Handle;
                IntPtr ordinalActual = buffer.PtrOffset(ordinalActualOffset, IntPtr.Size).Handle;

                //We are interested in index name, column name, and ordinal
                buffer.WriteInt16(indexBufOffset, 0);
                retcode = KeyInfoStatementHandle.BindColumn2(
                            (short)(ODBC32.SQL_STATISTICS.INDEXNAME),
                            ODBC32.SQL_C.WCHAR,
                            indexbuf,
                            (IntPtr)256,
                            indexActual);
                retcode = KeyInfoStatementHandle.BindColumn2(
                            (short)(ODBC32.SQL_STATISTICS.ORDINAL_POSITION),
                            ODBC32.SQL_C.SSHORT,
                            ordinalbuf,
                            (IntPtr)4,
                            ordinalActual);
                buffer.WriteInt16(ordinalBufOffset, 0);
                retcode = KeyInfoStatementHandle.BindColumn2(
                            (short)(ODBC32.SQL_STATISTICS.COLUMN_NAME),
                            ODBC32.SQL_C.WCHAR,
                            colnamebuf,
                            (IntPtr)256,
                            colnameActual);
                // Find the best unique index on the table, use the ones whose columns are
                // completely covered by the query.
                while (ODBC32.SQLRETURN.SUCCESS == (retcode = KeyInfoStatementHandle.Fetch()))
                {
                    cbColnameLen = buffer.ReadIntPtr(colnameActualOffset);
                    cbIndexLen = buffer.ReadIntPtr(indexActualOffset);

                    // If indexname is not returned, skip this row
                    if (0 == buffer.ReadInt16(indexBufOffset))
                        continue;       // Not an index row, get next row.

                    columnname = buffer.PtrToStringUni(colnameBufOffset, (int)cbColnameLen / 2/*cch*/);
                    indexname = buffer.PtrToStringUni(indexBufOffset, (int)cbIndexLen / 2/*cch*/);
                    ordinal = (int)buffer.ReadInt16(ordinalBufOffset);

                    if (SameIndexColumn(currentindexname, indexname, ordinal, ncols))
                    {
                        // We are still working on the same index
                        if (partialcolumnset)
                            continue;       // We don't have all the keys for this index, so we can't use it

                        ordinal = this.GetOrdinalFromBaseColName(columnname, qualifiedTableName.Table);
                        if (ordinal == -1)
                        {
                            partialcolumnset = true;
                        }
                        else
                        {
                            // Add the column to the index column set
                            if (ncols < 16)
                                indexcolumnordinals[ncols++] = ordinal;
                            else    // Can't deal with indexes > 16 columns
                                partialcolumnset = true;
                        }
                    }
                    else
                    {
                        // We got a new index, save the previous index information
                        if (!partialcolumnset && (ncols != 0))
                        {
                            // Choose the unique index with least columns as primary key
                            if ((npkcols == 0) || (npkcols > ncols))
                            {
                                npkcols = ncols;
                                for (int i = 0; i < ncols; i++)
                                    pkcolumnordinals[i] = indexcolumnordinals[i];
                            }
                        }
                        // Reset the parameters for a new index
                        ncols = 0;
                        currentindexname = indexname;
                        partialcolumnset = false;
                        // Add this column to index
                        ordinal = this.GetOrdinalFromBaseColName(columnname, qualifiedTableName.Table);
                        if (ordinal == -1)
                        {
                            partialcolumnset = true;
                        }
                        else
                        {
                            // Add the column to the index column set
                            indexcolumnordinals[ncols++] = ordinal;
                        }
                    }
                    // Go on to the next column
                }
                // Do we have an index?
                if (!partialcolumnset && (ncols != 0))
                {
                    // Choose the unique index with least columns as primary key
                    if ((npkcols == 0) || (npkcols > ncols))
                    {
                        npkcols = ncols;
                        for (int i = 0; i < ncols; i++)
                            pkcolumnordinals[i] = indexcolumnordinals[i];
                    }
                }
                // Mark the chosen index as primary key
                if (npkcols != 0)
                {
                    for (int i = 0; i < npkcols; i++)
                    {
                        indexordinal = pkcolumnordinals[i];
                        keyColumns++;
                        _metadata[indexordinal].isKeyColumn = true;
                        // should we set isNullable = false?
                        // This makes the QuikTest against Jet fail
                        //
                        // test test test - we don't know if this is nulalble or not so why do we want to set it to a value?
                        _metadata[indexordinal].isNullable = false;
                        _metadata[indexordinal].isUnique = true;
                        _metadata[indexordinal].baseTableName ??= qualifiedTableName.Table;
                        _metadata[indexordinal].baseColumnName ??= columnname;
                    }
                }
                // Unbind the columns
                _cmdWrapper!.FreeKeyInfoStatementHandle(ODBC32.STMT.UNBIND);
            }
            finally
            {
                if (mustRelease)
                {
                    buffer.DangerousRelease();
                }
            }
            return keyColumns;
        }

        internal static bool SameIndexColumn(string currentindexname, string indexname, int ordinal, int ncols)
        {
            if (string.IsNullOrEmpty(currentindexname))
            {
                return false;
            }
            if ((currentindexname == indexname) &&
                (ordinal == ncols + 1))
                return true;
            return false;
        }

        internal int GetOrdinalFromBaseColName(string? columnname)
        {
            return GetOrdinalFromBaseColName(columnname, null);
        }

        internal int GetOrdinalFromBaseColName(string? columnname, string? tablename)
        {
            if (string.IsNullOrEmpty(columnname))
            {
                return -1;
            }
            if (_metadata != null)
            {
                int count = FieldCount;
                for (int i = 0; i < count; i++)
                {
                    if ((_metadata[i].baseColumnName != null) &&
                        (columnname == _metadata[i].baseColumnName))
                    {
                        if (!string.IsNullOrEmpty(tablename))
                        {
                            if (tablename == _metadata[i].baseTableName)
                            {
                                return i;
                            } // end if
                        } // end if
                        else
                        {
                            return i;
                        } // end else
                    }
                }
            }
            // We can't find it in base column names, try regular colnames
            return this.IndexOf(columnname);
        }

        // We try parsing the SQL statement to get the table name as a last resort when
        // the driver doesn't return this information back to us.
        //
        // we can't handle multiple tablenames (JOIN)
        // only the first tablename will be returned

        internal string? GetTableNameFromCommandText()
        {
            if (_command == null)
            {
                return null;
            }
            string localcmdtext = _cmdText;
            if (string.IsNullOrEmpty(localcmdtext))
            { // fxcop
                return null;
            }
            string tablename;
            int idx;
            CStringTokenizer tokenstmt = new CStringTokenizer(localcmdtext, Connection!.QuoteChar(ADP.GetSchemaTable)[0], Connection.EscapeChar(ADP.GetSchemaTable));

            if (tokenstmt.StartsWith("select"))
            {
                // select command, search for from clause
                idx = tokenstmt.FindTokenIndex("from");
            }
            else
            {
                if (tokenstmt.StartsWith("insert") ||
                    tokenstmt.StartsWith("update") ||
                    tokenstmt.StartsWith("delete"))
                {
                    // Get the following word
                    idx = tokenstmt.CurrentPosition;
                }
                else
                    idx = -1;
            }
            if (idx == -1)
                return null;
            // The next token is the table name
            tablename = tokenstmt.NextToken();

            localcmdtext = tokenstmt.NextToken();
            if ((localcmdtext.Length > 0) && (localcmdtext[0] == ','))
            {
                return null;        // can't handle multiple tables
            }
            if ((localcmdtext.Length == 2) &&
                ((localcmdtext[0] == 'a') || (localcmdtext[0] == 'A')) &&
                ((localcmdtext[1] == 's') || (localcmdtext[1] == 'S')))
            {
                // aliased table, skip the alias name
                tokenstmt.NextToken();
                localcmdtext = tokenstmt.NextToken();
                if ((localcmdtext.Length > 0) && (localcmdtext[0] == ','))
                {
                    return null;        // Multiple tables
                }
            }
            return tablename;
        }

        internal void SetBaseTableNames(QualifiedTableName qualifiedTableName)
        {
            int count = FieldCount;

            for (int i = 0; i < count; i++)
            {
                if (_metadata![i].baseTableName == null)
                {
                    _metadata[i].baseTableName = qualifiedTableName.Table;
                    _metadata[i].baseSchemaName = qualifiedTableName.Schema;
                    _metadata[i].baseCatalogName = qualifiedTableName.Catalog;
                }
            }
            return;
        }

        internal sealed class QualifiedTableName
        {
            private readonly string? _catalogName;
            private readonly string? _schemaName;
            private string? _tableName;
            private string? _quotedTableName;
            private readonly string _quoteChar;

            internal string? Catalog
            {
                get
                {
                    return _catalogName;
                }
            }

            internal string? Schema
            {
                get
                {
                    return _schemaName;
                }
            }

            internal string? Table
            {
                get
                {
                    return _tableName;
                }
                set
                {
                    _quotedTableName = value;
                    _tableName = UnQuote(value);
                }
            }
            internal string? QuotedTable
            {
                get
                {
                    return _quotedTableName;
                }
            }
            internal string? GetTable(bool flag)
            {
                return (flag ? QuotedTable : Table);
            }
            internal QualifiedTableName(string quoteChar)
            {
                _quoteChar = quoteChar;
            }
            internal QualifiedTableName(string quoteChar, string? qualifiedname)
            {
                _quoteChar = quoteChar;

                string?[] names = ParseProcedureName(qualifiedname, quoteChar, quoteChar);
                _catalogName = UnQuote(names[1]);
                _schemaName = UnQuote(names[2]);
                _quotedTableName = names[3];
                _tableName = UnQuote(names[3]);
            }

            private string? UnQuote(string? str)
            {
                if ((str != null) && (str.Length > 0))
                {
                    char quoteChar = _quoteChar[0];
                    if (str[0] == quoteChar)
                    {
                        Debug.Assert(str.Length > 1, "Illegal string, only one char that is a quote");
                        Debug.Assert(str[str.Length - 1] == quoteChar, "Missing quote at end of string that begins with quote");
                        if (str.Length > 1 && str[str.Length - 1] == quoteChar)
                        {
                            str = str.Substring(1, str.Length - 2);
                        }
                    }
                }
                return str;
            }

            // Note: copy-and pasted from internal DbCommandBuilder implementation
            // Note: Per definition (ODBC reference) the CatalogSeparator comes before and after the
            // catalog name, the SchemaSeparator is undefined. Does it come between Schema and Table?
            internal static string?[] ParseProcedureName(string? name, string? quotePrefix, string? quoteSuffix)
            {
                // Procedure may consist of up to four parts:
                // 0) Server
                // 1) Catalog
                // 2) Schema
                // 3) ProcedureName
                //
                // Parse the string into four parts, allowing the last part to contain '.'s.
                // If less than four period delimited parts, use the parts from procedure backwards.
                //
                const string Separator = ".";

                string?[] qualifiers = new string[4];
                if (!string.IsNullOrEmpty(name))
                {
                    bool useQuotes = !string.IsNullOrEmpty(quotePrefix) && !string.IsNullOrEmpty(quoteSuffix);

                    int currentPos = 0, parts;
                    for (parts = 0; (parts < qualifiers.Length) && (currentPos < name.Length); ++parts)
                    {
                        int startPos = currentPos;

                        // does the part begin with a quotePrefix?
                        if (useQuotes && (name.IndexOf(quotePrefix!, currentPos, quotePrefix!.Length, StringComparison.Ordinal) == currentPos))
                        {
                            Debug.Assert(quotePrefix != null && quoteSuffix != null);

                            currentPos += quotePrefix.Length; // move past the quotePrefix

                            // search for the quoteSuffix (or end of string)
                            while (currentPos < name.Length)
                            {
                                currentPos = name.IndexOf(quoteSuffix, currentPos, StringComparison.Ordinal);
                                if (currentPos < 0)
                                {
                                    // error condition, no quoteSuffix
                                    currentPos = name.Length;
                                    break;
                                }
                                else
                                {
                                    currentPos += quoteSuffix.Length; // move past the quoteSuffix

                                    // is this a double quoteSuffix?
                                    if ((currentPos < name.Length) && (name.IndexOf(quoteSuffix, currentPos, quoteSuffix.Length, StringComparison.Ordinal) == currentPos))
                                    {
                                        // a second quoteSuffix, continue search for terminating quoteSuffix
                                        currentPos += quoteSuffix.Length; // move past the second quoteSuffix
                                    }
                                    else
                                    {
                                        // found the terminating quoteSuffix
                                        break;
                                    }
                                }
                            }
                        }

                        // search for separator (either no quotePrefix or already past quoteSuffix)
                        if (currentPos < name.Length)
                        {
                            currentPos = name.IndexOf(Separator, currentPos, StringComparison.Ordinal);
                            if ((currentPos < 0) || (parts == qualifiers.Length - 1))
                            {
                                // last part that can be found
                                currentPos = name.Length;
                            }
                        }

                        qualifiers[parts] = name.Substring(startPos, currentPos - startPos);
                        currentPos += Separator.Length;
                    }

                    // allign the qualifiers if we had less than MaxQualifiers
                    for (int j = qualifiers.Length - 1; 0 <= j; --j)
                    {
                        qualifiers[j] = ((0 < parts) ? qualifiers[--parts] : null);
                    }
                }
                return qualifiers;
            }
        }

        private sealed class MetaData
        {
            internal int ordinal;
            internal TypeMap typemap = null!; // Lazy-initialized

            internal SQLLEN size;
            internal byte precision;
            internal byte scale;

            internal bool isAutoIncrement;
            internal bool isUnique;
            internal bool isReadOnly;
            internal bool isNullable;
            internal bool isRowVersion;
            internal bool isLong;

            internal bool isKeyColumn;
            internal string? baseSchemaName;
            internal string? baseCatalogName;
            internal string? baseTableName;
            internal string? baseColumnName;
        }
    }
}
