1 //------------------------------------------------------------------------------ 2 // <copyright file="OdbcDataReader.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 // <owner current="true" primary="true">Microsoft</owner> 6 // <owner current="true" primary="false">Microsoft</owner> 7 //------------------------------------------------------------------------------ 8 9 using System; 10 using System.Collections; 11 using System.Collections.Generic; 12 using System.ComponentModel; 13 using System.Data; 14 using System.Data.Common; 15 using System.Data.ProviderBase; 16 using System.Diagnostics; 17 using System.Globalization; 18 using System.Runtime.CompilerServices; 19 using System.Runtime.InteropServices; 20 using System.Text; // StringBuilder 21 using System.Threading; 22 23 namespace System.Data.Odbc 24 { 25 public sealed class OdbcDataReader : DbDataReader { 26 27 private OdbcCommand command; 28 29 private int recordAffected = -1; 30 private FieldNameLookup _fieldNameLookup; 31 32 private DbCache dataCache; 33 private enum HasRowsStatus { 34 DontKnow = 0, 35 HasRows = 1, 36 HasNoRows = 2, 37 } 38 private HasRowsStatus _hasRows = HasRowsStatus.DontKnow; 39 private bool _isClosed; 40 private bool _isRead; 41 private bool _isValidResult; 42 private bool _noMoreResults; 43 private bool _noMoreRows; 44 private bool _skipReadOnce; 45 private int _hiddenColumns; // number of hidden columns 46 private CommandBehavior _commandBehavior; 47 48 // track current row and column, will be set on the first Fetch call 49 private int _row = -1; 50 private int _column = -1; 51 52 // used to track position in field for sucessive reads in case of Sequential Access 53 private long _sequentialBytesRead; 54 55 private static int _objectTypeCount; // Bid counter 56 internal readonly int ObjectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); 57 58 // the statement handle here is just a copy of the statement handle owned by the command 59 // the DataReader must not free the statement handle. this is done by the command 60 // 61 62 private MetaData[] metadata; 63 private DataTable schemaTable; // MDAC 68336 64 private string _cmdText; // get a copy in case the command text on the command is changed ... 65 private CMDWrapper _cmdWrapper; 66 OdbcDataReader(OdbcCommand command, CMDWrapper cmdWrapper, CommandBehavior commandbehavior)67 internal OdbcDataReader(OdbcCommand command, CMDWrapper cmdWrapper, CommandBehavior commandbehavior) { 68 OdbcConnection.VerifyExecutePermission(); 69 70 Debug.Assert(command != null, "Command null on OdbcDataReader ctor"); 71 this.command = command; 72 _commandBehavior = commandbehavior; 73 _cmdText = command.CommandText; // get a copy in case the command text on the command is changed ... 74 _cmdWrapper = cmdWrapper; 75 } 76 77 private CNativeBuffer Buffer { 78 get { 79 CNativeBuffer value = _cmdWrapper._dataReaderBuf; 80 if (null == value) { 81 Debug.Assert(false, "object is disposed"); 82 throw new ObjectDisposedException(GetType().Name); 83 } 84 return value; 85 } 86 } 87 88 private OdbcConnection Connection { 89 get { 90 if (null != _cmdWrapper) { 91 return _cmdWrapper.Connection; 92 } 93 else { 94 return null; 95 } 96 } 97 } 98 99 internal OdbcCommand Command { 100 get { 101 return this.command; 102 } 103 set { 104 this.command=value; 105 } 106 } 107 108 private OdbcStatementHandle StatementHandle { 109 get { 110 return _cmdWrapper.StatementHandle; 111 } 112 } 113 114 private OdbcStatementHandle KeyInfoStatementHandle { 115 get { return _cmdWrapper.KeyInfoStatement; } 116 } 117 IsBehavior(CommandBehavior behavior)118 internal bool IsBehavior(CommandBehavior behavior) { 119 return IsCommandBehavior(behavior); 120 } 121 122 internal bool IsCancelingCommand { 123 get { 124 if (this.command != null) { 125 return command.Canceling; 126 } 127 return false; 128 } 129 } 130 131 internal bool IsNonCancelingCommand { 132 get { 133 if (this.command != null) { 134 return !command.Canceling; 135 } 136 return false; 137 } 138 } 139 140 override public int Depth { 141 get { 142 if (IsClosed) { // MDAC 63669 143 throw ADP.DataReaderClosed("Depth"); 144 } 145 return 0; 146 } 147 } 148 149 override public int FieldCount { 150 get { 151 if (IsClosed) { // MDAC 63669 152 throw ADP.DataReaderClosed("FieldCount"); 153 } 154 if (_noMoreResults) { // MDAC 93325 155 return 0; 156 } 157 if (null == this.dataCache) { 158 Int16 cColsAffected; 159 ODBC32.RetCode retcode = this.FieldCountNoThrow(out cColsAffected); 160 if(retcode != ODBC32.RetCode.SUCCESS) { 161 Connection.HandleError(StatementHandle, retcode); 162 } 163 } 164 return ((null != this.dataCache) ? this.dataCache._count : 0); 165 } 166 } 167 168 // HasRows 169 // 170 // Use to detect wheter there are one ore more rows in the result without going through Read 171 // May be called at any time 172 // Basically it calls Read and sets a flag so that the actual Read call will be skipped once 173 // 174 override public bool HasRows { 175 get { 176 if (IsClosed) { 177 throw ADP.DataReaderClosed("HasRows"); 178 } 179 if (_hasRows == HasRowsStatus.DontKnow){ 180 Read(); // 181 _skipReadOnce = true; // need to skip Read once because we just did it 182 } 183 return (_hasRows == HasRowsStatus.HasRows); 184 } 185 } 186 FieldCountNoThrow(out Int16 cColsAffected)187 internal ODBC32.RetCode FieldCountNoThrow(out Int16 cColsAffected) { 188 if (IsCancelingCommand) { 189 cColsAffected = 0; 190 return ODBC32.RetCode.ERROR; 191 } 192 193 ODBC32.RetCode retcode = StatementHandle.NumberOfResultColumns(out cColsAffected); 194 if (retcode == ODBC32.RetCode.SUCCESS) { 195 _hiddenColumns = 0; 196 if (IsCommandBehavior(CommandBehavior.KeyInfo)) { 197 // we need to search for the first hidden column 198 // 199 if (!Connection.ProviderInfo.NoSqlSoptSSNoBrowseTable && !Connection.ProviderInfo.NoSqlSoptSSHiddenColumns) { 200 for (int i=0; i<cColsAffected; i++) 201 { 202 SQLLEN isHidden = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_HIDDEN, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE); 203 if (isHidden.ToInt64() == 1) { 204 _hiddenColumns = (int)cColsAffected-i; 205 cColsAffected = (Int16)i; 206 break; 207 } 208 } 209 } 210 } 211 this.dataCache = new DbCache(this, cColsAffected); 212 } 213 else { 214 cColsAffected = 0; 215 } 216 return retcode; 217 } 218 219 override public bool IsClosed { 220 get { 221 return _isClosed; 222 } 223 } 224 GetRowCount()225 private SQLLEN GetRowCount() { 226 if (!IsClosed) { 227 SQLLEN cRowsAffected; 228 ODBC32.RetCode retcode = StatementHandle.RowCount(out cRowsAffected); 229 if (ODBC32.RetCode.SUCCESS == retcode || ODBC32.RetCode.SUCCESS_WITH_INFO == retcode) { 230 return cRowsAffected; 231 } 232 } 233 return -1; 234 } 235 CalculateRecordsAffected(int cRowsAffected)236 internal int CalculateRecordsAffected(int cRowsAffected) { 237 if (0 <= cRowsAffected) { 238 if (-1 == this.recordAffected) { 239 this.recordAffected = cRowsAffected; 240 } 241 else { 242 this.recordAffected += cRowsAffected; 243 } 244 } 245 return this.recordAffected; 246 } 247 248 249 override public int RecordsAffected { 250 get { 251 return this.recordAffected; 252 } 253 } 254 255 override public object this[int i] { 256 get { 257 return GetValue(i); 258 } 259 } 260 261 override public object this[string value] { 262 get { 263 return GetValue(GetOrdinal(value)); 264 } 265 } 266 Close()267 override public void Close() { 268 Close(false); 269 } 270 Close(bool disposing)271 private void Close(bool disposing) { 272 Exception error = null; 273 274 CMDWrapper wrapper = _cmdWrapper; 275 if (null != wrapper && wrapper.StatementHandle != null) { 276 // disposing 277 // true to release both managed and unmanaged resources; false to release only unmanaged resources. 278 // 279 if (IsNonCancelingCommand) { 280 //Read any remaining results off the wire 281 // some batch statements may not be executed until SQLMoreResults is called. 282 // We want the user to be able to do ExecuteNonQuery or ExecuteReader 283 // and close without having iterate to get params or batch. 284 // 285 NextResult(disposing, !disposing); // false,true or true,false 286 if (null != command) { 287 if (command.HasParameters) { 288 // Output Parameters are not guareenteed to be returned until all the data 289 // from any restssets are read, so we do this after the above NextResult call(s) 290 command.Parameters.GetOutputValues(_cmdWrapper); 291 } 292 wrapper.FreeStatementHandle(ODBC32.STMT.CLOSE); 293 command.CloseFromDataReader(); 294 } 295 } 296 wrapper.FreeKeyInfoStatementHandle(ODBC32.STMT.CLOSE); 297 } 298 299 // if the command is still around we call CloseFromDataReader, 300 // otherwise we need to dismiss the statement handle ourselves 301 // 302 if (null != command) { 303 command.CloseFromDataReader(); 304 305 if(IsCommandBehavior(CommandBehavior.CloseConnection)) { 306 Debug.Assert(null != Connection, "null cmd connection"); 307 command.Parameters.RebindCollection = true; 308 Connection.Close(); 309 } 310 } 311 else if (null != wrapper) { 312 wrapper.Dispose(); 313 } 314 315 this.command = null; 316 _isClosed=true; 317 this.dataCache = null; 318 this.metadata = null; 319 this.schemaTable = null; 320 _isRead = false; 321 _hasRows = HasRowsStatus.DontKnow; 322 _isValidResult = false; 323 _noMoreResults = true; 324 _noMoreRows = true; 325 _fieldNameLookup = null; 326 327 SetCurrentRowColumnInfo(-1, 0); 328 329 if ((null != error) && !disposing) { 330 throw error; 331 } 332 _cmdWrapper = null; 333 } 334 Dispose(bool disposing)335 protected override void Dispose(bool disposing) { 336 if (disposing) { 337 Close(true); 338 } 339 // not delegating to base class because we know it only calls Close 340 //base.Dispose(disposing) 341 } 342 GetDataTypeName(int i)343 override public String GetDataTypeName(int i) { 344 if (null != this.dataCache) { 345 DbSchemaInfo info = this.dataCache.GetSchema(i); 346 if(info._typename == null) { 347 info._typename = GetColAttributeStr(i, ODBC32.SQL_DESC.TYPE_NAME, ODBC32.SQL_COLUMN.TYPE_NAME, ODBC32.HANDLER.THROW); 348 } 349 return info._typename; 350 } 351 throw ADP.DataReaderNoData(); 352 } 353 GetEnumerator()354 override public IEnumerator GetEnumerator() { 355 return new DbEnumerator((IDataReader)this, IsCommandBehavior(CommandBehavior.CloseConnection)); 356 } 357 GetFieldType(int i)358 override public Type GetFieldType(int i) { 359 if (null != this.dataCache) { 360 DbSchemaInfo info = this.dataCache.GetSchema(i); 361 if(info._type == null) { 362 info._type = GetSqlType(i)._type; 363 } 364 return info._type; 365 } 366 throw ADP.DataReaderNoData(); 367 } 368 GetName(int i)369 override public String GetName(int i) { 370 if (null != this.dataCache) { 371 DbSchemaInfo info = this.dataCache.GetSchema(i); 372 if(info._name == null) { 373 info._name = GetColAttributeStr(i, ODBC32.SQL_DESC.NAME, ODBC32.SQL_COLUMN.NAME, ODBC32.HANDLER.THROW); 374 if (null == info._name) { // MDAC 66681 375 info._name = ""; 376 } 377 } 378 return info._name; 379 } 380 throw ADP.DataReaderNoData(); 381 } 382 GetOrdinal(string value)383 override public int GetOrdinal(string value) { 384 if (null == _fieldNameLookup) { 385 if (null == this.dataCache) { 386 throw ADP.DataReaderNoData(); 387 } 388 _fieldNameLookup = new FieldNameLookup(this, -1); 389 } 390 return _fieldNameLookup.GetOrdinal(value); // MDAC 71470 391 } 392 IndexOf(string value)393 private int IndexOf(string value) { 394 if (null == _fieldNameLookup) { 395 if (null == this.dataCache) { 396 throw ADP.DataReaderNoData(); 397 } 398 _fieldNameLookup = new FieldNameLookup(this, -1); 399 } 400 return _fieldNameLookup.IndexOf(value); 401 } 402 IsCommandBehavior(CommandBehavior condition)403 private bool IsCommandBehavior(CommandBehavior condition) { 404 return (condition == (condition & _commandBehavior)); 405 } 406 GetValue(int i, TypeMap typemap)407 internal object GetValue(int i, TypeMap typemap) { 408 switch(typemap._sql_type) { 409 410 case ODBC32.SQL_TYPE.CHAR: 411 case ODBC32.SQL_TYPE.VARCHAR: 412 case ODBC32.SQL_TYPE.LONGVARCHAR: 413 case ODBC32.SQL_TYPE.WCHAR: 414 case ODBC32.SQL_TYPE.WVARCHAR: 415 case ODBC32.SQL_TYPE.WLONGVARCHAR: 416 return internalGetString(i); 417 418 case ODBC32.SQL_TYPE.DECIMAL: 419 case ODBC32.SQL_TYPE.NUMERIC: 420 return internalGetDecimal(i); 421 422 case ODBC32.SQL_TYPE.SMALLINT: 423 return internalGetInt16(i); 424 425 case ODBC32.SQL_TYPE.INTEGER: 426 return internalGetInt32(i); 427 428 case ODBC32.SQL_TYPE.REAL: 429 return internalGetFloat(i); 430 431 case ODBC32.SQL_TYPE.FLOAT: 432 case ODBC32.SQL_TYPE.DOUBLE: 433 return internalGetDouble(i); 434 435 case ODBC32.SQL_TYPE.BIT: 436 return internalGetBoolean(i); 437 438 case ODBC32.SQL_TYPE.TINYINT: 439 return internalGetByte(i); 440 441 case ODBC32.SQL_TYPE.BIGINT: 442 return internalGetInt64(i); 443 444 case ODBC32.SQL_TYPE.BINARY: 445 case ODBC32.SQL_TYPE.VARBINARY: 446 case ODBC32.SQL_TYPE.LONGVARBINARY: 447 return internalGetBytes(i); 448 449 case ODBC32.SQL_TYPE.TYPE_DATE: 450 return internalGetDate(i); 451 452 case ODBC32.SQL_TYPE.TYPE_TIME: 453 return internalGetTime(i); 454 455 // case ODBC32.SQL_TYPE.TIMESTAMP: 456 case ODBC32.SQL_TYPE.TYPE_TIMESTAMP: 457 return internalGetDateTime(i); 458 459 case ODBC32.SQL_TYPE.GUID: 460 return internalGetGuid(i); 461 462 case ODBC32.SQL_TYPE.SS_VARIANT: 463 //Note: SQL Variant is not an ODBC defined type. 464 //Instead of just binding it as a byte[], which is not very useful, 465 //we will actually code this specific for SQL Server. 466 467 //To obtain the sub-type, we need to first load the context (obtaining the length 468 //will work), and then query for a speicial SQLServer specific attribute. 469 if (_isRead) { 470 if(this.dataCache.AccessIndex(i) == null) { 471 int dummy; 472 bool isNotDbNull = QueryFieldInfo(i, ODBC32.SQL_C.BINARY, out dummy); 473 // if the value is DBNull, QueryFieldInfo will cache it 474 if (isNotDbNull) { 475 //Delegate (for the sub type) 476 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); 477 return GetValue(i, TypeMap.FromSqlType(subtype)); 478 } 479 } 480 return this.dataCache[i]; 481 } 482 throw ADP.DataReaderNoData(); 483 484 485 486 default: 487 //Unknown types are bound strictly as binary 488 return internalGetBytes(i); 489 } 490 } 491 GetValue(int i)492 override public object GetValue(int i) { 493 if (_isRead) { 494 if(dataCache.AccessIndex(i) == null) { 495 dataCache[i] = GetValue(i, GetSqlType(i)); 496 } 497 return dataCache[i]; 498 } 499 throw ADP.DataReaderNoData(); 500 } 501 GetValues(object[] values)502 override public int GetValues(object[] values) { 503 if (_isRead) { 504 int nValues = Math.Min(values.Length, FieldCount); 505 for (int i = 0; i < nValues; ++i) { 506 values[i] = GetValue(i); 507 } 508 return nValues; 509 } 510 throw ADP.DataReaderNoData(); 511 } 512 GetSqlType(int i)513 private TypeMap GetSqlType(int i) { 514 //Note: Types are always returned (advertised) from ODBC as SQL_TYPEs, and 515 //are always bound by the user as SQL_C types. 516 TypeMap typeMap; 517 DbSchemaInfo info = this.dataCache.GetSchema(i); 518 if(!info._dbtype.HasValue) { 519 info._dbtype = unchecked((ODBC32.SQL_TYPE)(int)GetColAttribute(i, ODBC32.SQL_DESC.CONCISE_TYPE, ODBC32.SQL_COLUMN.TYPE,ODBC32.HANDLER.THROW)); 520 typeMap = TypeMap.FromSqlType(info._dbtype.Value); 521 if (typeMap._signType == true) { 522 bool sign = (GetColAttribute(i, ODBC32.SQL_DESC.UNSIGNED, ODBC32.SQL_COLUMN.UNSIGNED, ODBC32.HANDLER.THROW).ToInt64() != 0); 523 typeMap = TypeMap.UpgradeSignedType(typeMap, sign); 524 info._dbtype = typeMap._sql_type; 525 } 526 } 527 else { 528 typeMap = TypeMap.FromSqlType(info._dbtype.Value); 529 } 530 Connection.SetSupportedType(info._dbtype.Value); 531 return typeMap; 532 } 533 IsDBNull(int i)534 override public bool IsDBNull(int i) { 535 // Note: ODBC SQLGetData doesn't allow retriving the column value twice. 536 // The reational is that for ForwardOnly access (the default and LCD of drivers) 537 // we cannot obtain the data more than once, and even GetData(0) (to determine is-null) 538 // still obtains data for fixed length types. 539 540 // So simple code like: 541 // if(!rReader.IsDBNull(i)) 542 // rReader.GetInt32(i) 543 // 544 // Would fail, unless we cache on the IsDBNull call, and return the cached 545 // item for GetInt32. This actually improves perf anyway, (even if the driver could 546 // support it), since we are not making a seperate interop call... 547 548 // Bug SQLBUVSTS01:110664 - available cases: 549 // 1. random access - always cache the value (as before the fix), to minimize regression risk 550 // 2. sequential access, fixed-size value: continue caching the value as before, again to minimize regression risk 551 // 3. sequential access, variable-length value: this scenario did not work properly before the fix. Fix 552 // it now by calling GetData(length = 0). 553 // 4. sequential access, cache value exists: just check the cache for DbNull (no validations done, again to minimize regressions) 554 555 if (!IsCommandBehavior(CommandBehavior.SequentialAccess)) 556 return Convert.IsDBNull(GetValue(i)); // case 1, cache the value 557 558 // in 'ideal' Sequential access support, we do not want cache the value in order to check if it is DbNull or not. 559 // But, to minimize regressions, we will continue caching the fixed-size values (case 2), even with SequentialAccess 560 // only in case of SequentialAccess with variable length data types (case 3), we will use GetData with zero length. 561 562 object cachedObj = this.dataCache[i]; 563 if (cachedObj != null) 564 { 565 // case 4 - if cached object was created before, use it 566 return Convert.IsDBNull(cachedObj); 567 } 568 569 // no cache, check for the type (cases 2 and 3) 570 TypeMap typeMap = GetSqlType(i); 571 if (typeMap._bufferSize > 0) { 572 // case 2 - fixed-size types have _bufferSize set to positive value 573 // call GetValue(i) as before the fix of SQLBUVSTS01:110664 574 // note, when SQLGetData is called for a fixed length type, the buffer size is always ignored and 575 // the data will always be read off the wire 576 return Convert.IsDBNull(GetValue(i)); 577 } 578 else { 579 // case 3 - the data has variable-length type, read zero-length data to query for null 580 // QueryFieldInfo will return false only if the object cached as DbNull 581 // QueryFieldInfo will put DbNull in cache only if the SQLGetData returns SQL_NULL_DATA, otherwise it does not change it 582 int dummy; 583 return !QueryFieldInfo(i, typeMap._sql_c, out dummy); 584 } 585 } 586 GetByte(int i)587 override public Byte GetByte(int i) { 588 return (Byte)internalGetByte(i); 589 } 590 internalGetByte(int i)591 private object internalGetByte(int i) { 592 if (_isRead) { 593 if(this.dataCache.AccessIndex(i) == null) { 594 if (GetData(i, ODBC32.SQL_C.UTINYINT)) { 595 this.dataCache[i] = Buffer.ReadByte(0); 596 } 597 } 598 return this.dataCache[i]; 599 } 600 throw ADP.DataReaderNoData(); 601 } 602 GetChar(int i)603 override public Char GetChar(int i) { 604 return (Char)internalGetChar(i); 605 } internalGetChar(int i)606 private object internalGetChar(int i) { 607 if (_isRead) { 608 if(this.dataCache.AccessIndex(i) == null) { 609 if (GetData(i, ODBC32.SQL_C.WCHAR)) { 610 this.dataCache[i] = Buffer.ReadChar(0); 611 } 612 } 613 return this.dataCache[i]; 614 } 615 throw ADP.DataReaderNoData(); 616 } 617 GetInt16(int i)618 override public Int16 GetInt16(int i) { 619 return (Int16)internalGetInt16(i); 620 } internalGetInt16(int i)621 private object internalGetInt16(int i) { 622 if (_isRead) { 623 if(this.dataCache.AccessIndex(i) == null) { 624 if (GetData(i, ODBC32.SQL_C.SSHORT)) { 625 this.dataCache[i] = Buffer.ReadInt16(0); 626 } 627 } 628 return this.dataCache[i]; 629 } 630 throw ADP.DataReaderNoData(); 631 } 632 GetInt32(int i)633 override public Int32 GetInt32(int i) { 634 return (Int32)internalGetInt32(i); 635 } internalGetInt32(int i)636 private object internalGetInt32(int i){ 637 if (_isRead) { 638 if(this.dataCache.AccessIndex(i) == null) { 639 if(GetData(i, ODBC32.SQL_C.SLONG)){ 640 this.dataCache[i] = Buffer.ReadInt32(0); 641 } 642 } 643 return this.dataCache[i]; 644 } 645 throw ADP.DataReaderNoData(); 646 } 647 GetInt64(int i)648 override public Int64 GetInt64(int i) { 649 return (Int64)internalGetInt64(i); 650 } 651 // ---------------------------------------------------------------------------------------------- // 652 // internal internalGetInt64 653 // ------------------------- 654 // Get Value of type SQL_BIGINT 655 // Since the driver refused to accept the type SQL_BIGINT we read that 656 // as SQL_C_WCHAR and convert it back to the Int64 data type 657 // internalGetInt64(int i)658 private object internalGetInt64(int i) { 659 if (_isRead) { 660 if(this.dataCache.AccessIndex(i) == null) { 661 if (GetData(i, ODBC32.SQL_C.WCHAR)) { 662 string value = (string)Buffer.MarshalToManaged(0, ODBC32.SQL_C.WCHAR, ODBC32.SQL_NTS); 663 this.dataCache[i] = Int64.Parse(value, CultureInfo.InvariantCulture); 664 } 665 } 666 return this.dataCache[i]; 667 } 668 throw ADP.DataReaderNoData(); 669 } 670 GetBoolean(int i)671 override public bool GetBoolean(int i) { 672 return (bool) internalGetBoolean(i); 673 } internalGetBoolean(int i)674 private object internalGetBoolean(int i) { 675 if (_isRead) { 676 if(this.dataCache.AccessIndex(i) == null) { 677 if(GetData(i, ODBC32.SQL_C.BIT)){ 678 this.dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.BIT, -1); 679 } 680 } 681 return this.dataCache[i]; 682 } 683 throw ADP.DataReaderNoData(); 684 } 685 GetFloat(int i)686 override public float GetFloat(int i) { 687 return (float)internalGetFloat(i); 688 } internalGetFloat(int i)689 private object internalGetFloat(int i) { 690 if (_isRead) { 691 if(this.dataCache.AccessIndex(i) == null) { 692 if(GetData(i, ODBC32.SQL_C.REAL)){ 693 this.dataCache[i] = Buffer.ReadSingle(0); 694 } 695 } 696 return this.dataCache[i]; 697 } 698 throw ADP.DataReaderNoData(); 699 } 700 GetDate(int i)701 public DateTime GetDate(int i) { 702 return (DateTime)internalGetDate(i); 703 } 704 internalGetDate(int i)705 private object internalGetDate(int i) { 706 if (_isRead) { 707 if(this.dataCache.AccessIndex(i) == null) { 708 if(GetData(i, ODBC32.SQL_C.TYPE_DATE)){ 709 this.dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.TYPE_DATE, -1); 710 } 711 } 712 return this.dataCache[i]; 713 } 714 throw ADP.DataReaderNoData(); 715 } 716 GetDateTime(int i)717 override public DateTime GetDateTime(int i) { 718 return (DateTime)internalGetDateTime(i); 719 } 720 internalGetDateTime(int i)721 private object internalGetDateTime(int i) { 722 if (_isRead) { 723 if(this.dataCache.AccessIndex(i) == null) { 724 if(GetData(i, ODBC32.SQL_C.TYPE_TIMESTAMP)){ 725 this.dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.TYPE_TIMESTAMP, -1); 726 } 727 } 728 return this.dataCache[i]; 729 } 730 throw ADP.DataReaderNoData(); 731 } 732 GetDecimal(int i)733 override public decimal GetDecimal(int i) { 734 return (decimal)internalGetDecimal(i); 735 } 736 737 // ---------------------------------------------------------------------------------------------- // 738 // internal GetDecimal 739 // ------------------- 740 // Get Value of type SQL_DECIMAL or SQL_NUMERIC 741 // Due to provider incompatibilities with SQL_DECIMAL or SQL_NUMERIC types we always read the value 742 // as SQL_C_WCHAR and convert it back to the Decimal data type 743 // internalGetDecimal(int i)744 private object internalGetDecimal(int i) { 745 if (_isRead) { 746 if(this.dataCache.AccessIndex(i) == null) { 747 if(GetData(i, ODBC32.SQL_C.WCHAR )){ 748 string s = null; 749 try { 750 s = (string)Buffer.MarshalToManaged(0, ODBC32.SQL_C.WCHAR, ODBC32.SQL_NTS); 751 this.dataCache[i] = Decimal.Parse(s, System.Globalization.CultureInfo.InvariantCulture); 752 } 753 catch(OverflowException e) { 754 this.dataCache[i] = s; 755 throw e; 756 } 757 } 758 } 759 return this.dataCache[i]; 760 } 761 throw ADP.DataReaderNoData(); 762 } 763 GetDouble(int i)764 override public double GetDouble(int i) { 765 return (double)internalGetDouble(i); 766 } internalGetDouble(int i)767 private object internalGetDouble(int i) { 768 if (_isRead) { 769 if(this.dataCache.AccessIndex(i) == null) { 770 if(GetData(i, ODBC32.SQL_C.DOUBLE)){ 771 this.dataCache[i] = Buffer.ReadDouble(0); 772 } 773 } 774 return this.dataCache[i]; 775 } 776 throw ADP.DataReaderNoData(); 777 } 778 GetGuid(int i)779 override public Guid GetGuid(int i) { 780 return (Guid)internalGetGuid(i); 781 } 782 internalGetGuid(int i)783 private object internalGetGuid(int i) { 784 if (_isRead) { 785 if(this.dataCache.AccessIndex(i) == null) { 786 if(GetData(i, ODBC32.SQL_C.GUID)){ 787 this.dataCache[i] = Buffer.ReadGuid(0); 788 } 789 } 790 return this.dataCache[i]; 791 } 792 throw ADP.DataReaderNoData(); 793 } 794 GetString(int i)795 override public String GetString(int i) { 796 return (String)internalGetString(i); 797 } 798 internalGetString(int i)799 private object internalGetString(int i) { 800 if(_isRead) { 801 if(this.dataCache.AccessIndex(i) == null) { 802 // Obtain _ALL_ the characters 803 // Note: We can bind directly as WCHAR in ODBC and the DM will convert to and 804 // from ANSI if not supported by the driver. 805 // 806 807 // Note: The driver always returns the raw length of the data, minus the 808 // terminator. This means that our buffer length always includes the terminator 809 // charactor, so when determining which characters to count, and if more data 810 // exists, it should not take the terminator into effect. 811 // 812 CNativeBuffer buffer = Buffer; 813 // that does not make sense unless we expect four byte terminators 814 int cbMaxData = buffer.Length - 4; 815 816 // The first time GetData returns the true length (so we have to min it). 817 // We also pass in the true length to the marshal function since there could be 818 // embedded nulls 819 // 820 int lengthOrIndicator; 821 if (GetData(i, ODBC32.SQL_C.WCHAR, buffer.Length - 2, out lengthOrIndicator)) { 822 // RFC 50002644: we do not expect negative values from GetData call except SQL_NO_TOTAL(== -4) 823 // note that in general you should not trust third-party providers so such asserts should be 824 // followed by exception. I did not add it now to avoid breaking change 825 Debug.Assert(lengthOrIndicator >= 0 || lengthOrIndicator == ODBC32.SQL_NO_TOTAL, "unexpected lengthOrIndicator value"); 826 827 if (lengthOrIndicator <= cbMaxData && (ODBC32.SQL_NO_TOTAL != lengthOrIndicator)) { 828 // all data read? good! Directly marshal to a string and we're done 829 // 830 string strdata = buffer.PtrToStringUni(0, Math.Min(lengthOrIndicator, cbMaxData) / 2); 831 this.dataCache[i] = strdata; 832 return strdata; 833 } 834 835 // We need to chunk the data 836 // Char[] buffer for the junks 837 // StringBuilder for the actual string 838 // 839 Char[] rgChars = new Char[cbMaxData / 2]; 840 841 // RFC 50002644: negative value cannot be used for capacity. 842 // in case of SQL_NO_TOTAL, set the capacity to cbMaxData, StringBuilder will automatically reallocate 843 // its internal buffer when appending more data 844 int cbBuilderInitialCapacity = (lengthOrIndicator == ODBC32.SQL_NO_TOTAL) ? cbMaxData : lengthOrIndicator; 845 StringBuilder builder = new StringBuilder(cbBuilderInitialCapacity / 2); 846 847 bool gotData; 848 int cchJunk; 849 int cbActual = cbMaxData; 850 int cbMissing = (ODBC32.SQL_NO_TOTAL == lengthOrIndicator) ? -1 : lengthOrIndicator - cbActual; 851 852 do { 853 cchJunk = cbActual / 2; 854 buffer.ReadChars(0, rgChars, 0, cchJunk); 855 builder.Append(rgChars, 0, cchJunk); 856 857 if(0 == cbMissing) { 858 break; // done 859 } 860 861 gotData = GetData(i, ODBC32.SQL_C.WCHAR, buffer.Length - 2, out lengthOrIndicator); 862 // RFC 50002644: we do not expect negative values from GetData call except SQL_NO_TOTAL(== -4) 863 // note that in general you should not trust third-party providers so such asserts should be 864 // followed by exception. I did not add it now to avoid breaking change 865 Debug.Assert(lengthOrIndicator >= 0 || lengthOrIndicator == ODBC32.SQL_NO_TOTAL, "unexpected lengthOrIndicator value"); 866 867 if (ODBC32.SQL_NO_TOTAL != lengthOrIndicator) { 868 cbActual = Math.Min(lengthOrIndicator, cbMaxData); 869 if(0 < cbMissing) { 870 cbMissing -= cbActual; 871 } 872 else { 873 // it is a last call to SqlGetData that started with SQL_NO_TOTAL 874 // the last call to SqlGetData must always return the length of the 875 // data, not zero or SqlNoTotal (see Odbc Programmers Reference) 876 Debug.Assert(cbMissing == -1 && lengthOrIndicator <= cbMaxData); 877 cbMissing = 0; 878 } 879 } 880 } 881 while(gotData); 882 883 this.dataCache[i] = builder.ToString(); 884 } 885 } 886 return this.dataCache[i]; 887 } 888 throw ADP.DataReaderNoData(); 889 } 890 GetTime(int i)891 public TimeSpan GetTime(int i) { 892 return (TimeSpan)internalGetTime(i); 893 } 894 internalGetTime(int i)895 private object internalGetTime(int i) { 896 if (_isRead) { 897 if(this.dataCache.AccessIndex(i) == null) { 898 if(GetData(i, ODBC32.SQL_C.TYPE_TIME)){ 899 this.dataCache[i] = Buffer.MarshalToManaged(0, ODBC32.SQL_C.TYPE_TIME, -1); 900 } 901 } 902 return this.dataCache[i]; 903 } 904 throw ADP.DataReaderNoData(); 905 } 906 SetCurrentRowColumnInfo(int row, int column)907 private void SetCurrentRowColumnInfo(int row, int column) 908 { 909 if (_row != row || _column != column) 910 { 911 _row = row; 912 _column = column; 913 914 // reset the blob reader when moved to new column 915 _sequentialBytesRead = 0; 916 } 917 } 918 GetBytes(int i, long dataIndex, byte[] buffer, int bufferIndex, int length)919 override public long GetBytes(int i, long dataIndex, byte[] buffer, int bufferIndex, int length) { 920 return GetBytesOrChars(i, dataIndex, buffer, false /* bytes buffer */, bufferIndex, length); 921 } GetChars(int i, long dataIndex, char[] buffer, int bufferIndex, int length)922 override public long GetChars(int i, long dataIndex, char[] buffer, int bufferIndex, int length) { 923 return GetBytesOrChars(i, dataIndex, buffer, true /* chars buffer */, bufferIndex, length); 924 } 925 926 // unify the implementation of GetChars and GetBytes to prevent code duplicate GetBytesOrChars(int i, long dataIndex, Array buffer, bool isCharsBuffer, int bufferIndex, int length)927 private long GetBytesOrChars(int i, long dataIndex, Array buffer, bool isCharsBuffer, int bufferIndex, int length) { 928 if (IsClosed) { 929 throw ADP.DataReaderNoData(); 930 } 931 if(!_isRead) { 932 throw ADP.DataReaderNoData(); 933 } 934 if(dataIndex < 0 ) { 935 // test only for negative value here, Int32.MaxValue will be validated only in case of random access 936 throw ADP.ArgumentOutOfRange("dataIndex"); 937 } 938 if(bufferIndex < 0) { 939 throw ADP.ArgumentOutOfRange("bufferIndex"); 940 } 941 if(length < 0) { 942 throw ADP.ArgumentOutOfRange("length"); 943 } 944 945 string originalMethodName = isCharsBuffer ? "GetChars" : "GetBytes"; 946 947 // row/column info will be reset only if changed 948 SetCurrentRowColumnInfo(_row, i); 949 950 // Possible cases: 951 // 1. random access, user asks for the value first time: bring it and cache the value 952 // 2. random access, user already queried the value: use the cache 953 // 3. sequential access, cache exists: user already read this value using different method (it becomes cached) 954 // use the cache - preserve the original behavior to minimize regression risk 955 // 4. sequential access, no cache: (fixed now) user reads the bytes/chars in sequential order (no cache) 956 957 object cachedObj = null; // The cached object (if there is one) 958 959 // Get cached object, ensure the correct type using explicit cast, to preserve same behavior as before 960 if (isCharsBuffer) 961 cachedObj = (string)this.dataCache[i]; 962 else 963 cachedObj = (byte[])this.dataCache[i]; 964 965 bool isRandomAccess = !IsCommandBehavior(CommandBehavior.SequentialAccess); 966 967 if (isRandomAccess || (cachedObj != null)) 968 { 969 // random access (cases 1 or 2) and sequential access with cache (case 3) 970 // preserve the original behavior as before the fix 971 972 if (Int32.MaxValue < dataIndex) 973 { 974 // indices greater than allocable size are not supported in random access 975 // (negative value is already tested in the beginning of ths function) 976 throw ADP.ArgumentOutOfRange("dataIndex"); 977 } 978 979 if(cachedObj == null) { 980 // case 1, get the value and cache it 981 // internalGetString/internalGetBytes will get the entire value and cache it, 982 // since we are not in SequentialAccess (isRandomAccess is true), it is OK 983 984 if (isCharsBuffer) { 985 cachedObj = (String)internalGetString(i); 986 Debug.Assert((cachedObj != null), "internalGetString should always return non-null or raise exception"); 987 } 988 else { 989 cachedObj = (byte[])internalGetBytes(i); 990 Debug.Assert((cachedObj != null), "internalGetBytes should always return non-null or raise exception"); 991 } 992 993 // continue to case 2 994 } 995 996 // after this point the value is cached (case 2 or 3) 997 // if it is DbNull, cast exception will be raised (same as before the 110664 fix) 998 int cachedObjectLength = isCharsBuffer ? ((string)cachedObj).Length : ((byte[])cachedObj).Length; 999 1000 // the user can ask for the length of the field by passing in a null pointer for the buffer 1001 if (buffer == null) { 1002 // return the length if that's all what user needs 1003 return cachedObjectLength; 1004 } 1005 1006 // user asks for bytes 1007 1008 if (length == 0) { 1009 return 0; // Nothing to do ... 1010 } 1011 1012 if (dataIndex >= cachedObjectLength) 1013 { 1014 // no more bytes to read 1015 // see also MDAC bug 73298 1016 return 0; 1017 } 1018 1019 int lengthFromDataIndex = cachedObjectLength - (int)dataIndex; 1020 int lengthOfCopy = Math.Min(lengthFromDataIndex, length); 1021 1022 // silently reduce the length to avoid regression from EVERETT 1023 lengthOfCopy = Math.Min(lengthOfCopy, buffer.Length - bufferIndex); 1024 if (lengthOfCopy <= 0) return 0; // MDAC Bug 73298 1025 1026 if (isCharsBuffer) 1027 ((string)cachedObj).CopyTo((int)dataIndex, (char[])buffer, bufferIndex, lengthOfCopy); 1028 else 1029 Array.Copy((byte[])cachedObj, (int)dataIndex, (byte[])buffer, bufferIndex, lengthOfCopy); 1030 1031 return lengthOfCopy; 1032 } 1033 else { 1034 // sequential access, case 4 1035 1036 // SQLBU:532243 -- For SequentialAccess we need to read a chunk of 1037 // data and not cache it. 1038 // Note: If the object was previous cached (see case 3 above), the function will go thru 'if' path, to minimize 1039 // regressions 1040 1041 // the user can ask for the length of the field by passing in a null pointer for the buffer 1042 if (buffer == null) { 1043 // Get len. of remaining data from provider 1044 ODBC32.SQL_C sqlctype; 1045 int cbLengthOrIndicator; 1046 bool isDbNull; 1047 1048 sqlctype = isCharsBuffer ? ODBC32.SQL_C.WCHAR : ODBC32.SQL_C.BINARY; 1049 isDbNull = !QueryFieldInfo(i, sqlctype, out cbLengthOrIndicator); 1050 1051 if (isDbNull) { 1052 // SQLBU 266054: 1053 1054 // GetChars: 1055 // in Orcas RTM: GetChars has always raised InvalidCastException. 1056 // in Orcas SP1: GetChars returned 0 if DbNull is not cached yet and InvalidCastException if it is in cache (from case 3). 1057 // Managed Providers team has decided to fix the GetChars behavior and raise InvalidCastException, as it was in RTM 1058 // Reason: returing 0 is wrong behavior since it conflicts with return value in case of empty data 1059 1060 // GetBytes: 1061 // In Orcas RTM: GetBytes(null buffer) returned -1 for null value if DbNull is not cached yet. 1062 // But, after calling IsDBNull, GetBytes(null) raised InvalidCastException. 1063 // In Orcas SP1: GetBytes always raises InvalidCastException for null value. 1064 // Managed Providers team has decided to keep the behavior of RTM for this case to fix the RTM's breaking change. 1065 // Reason: while -1 is wrong behavior, people might be already relying on it, so we should not be changing it. 1066 // Note: this will happen only on the first call to GetBytes(with null buffer). 1067 // If IsDbNull has already been called before or for second call to query for size, 1068 // DBNull is cached and GetBytes raises InvalidCastException in case 3 (see the cases above in this method). 1069 1070 if (isCharsBuffer) { 1071 throw ADP.InvalidCast(); 1072 } 1073 else { 1074 return -1; 1075 } 1076 } 1077 else { 1078 // the value is not null 1079 1080 // SQLBU 266054: 1081 // If cbLengthOrIndicator is SQL_NO_TOTAL (-4), this call returns -4 or -2, depending on the type (GetChars=>-2, GetBytes=>-4). 1082 // This is the Orcas RTM and SP1 behavior, changing this would be a breaking change. 1083 // SQL_NO_TOTAL means that the driver does not know what is the remained lenght of the data, so we cannot really guess the value here. 1084 // Reason: while returning different negative values depending on the type seems inconsistent, 1085 // 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. 1086 if (isCharsBuffer) { 1087 return cbLengthOrIndicator / 2; // return length in wide characters or -2 if driver returns SQL_NO_TOTAL 1088 } 1089 else { 1090 return cbLengthOrIndicator; // return length in bytes or -4 if driver returns SQL_NO_TOTAL 1091 } 1092 } 1093 } 1094 else { 1095 // buffer != null, read the data 1096 1097 // check if user tries to read data that was already received 1098 // if yes, this violates 'sequential access' 1099 if ((isCharsBuffer && dataIndex < _sequentialBytesRead / 2) || 1100 (!isCharsBuffer && dataIndex < _sequentialBytesRead)) { 1101 // backward reading is not allowed in sequential access 1102 throw ADP.NonSeqByteAccess( 1103 dataIndex, 1104 _sequentialBytesRead, 1105 originalMethodName 1106 ); 1107 } 1108 1109 // note that it is actually not possible to read with an offset (dataIndex) 1110 // therefore, adjust the data index relative to number of bytes already read 1111 if (isCharsBuffer) 1112 dataIndex -= _sequentialBytesRead / 2; 1113 else 1114 dataIndex -= _sequentialBytesRead; 1115 1116 if (dataIndex > 0) 1117 { 1118 // user asked to skip bytes - it is OK, even in case of sequential access 1119 // forward the stream by dataIndex bytes/chars 1120 int charsOrBytesRead = readBytesOrCharsSequentialAccess(i, null, isCharsBuffer, 0, dataIndex); 1121 if (charsOrBytesRead < dataIndex) 1122 { 1123 // the stream ended before we forwarded to the requested index, stop now 1124 return 0; 1125 } 1126 } 1127 1128 // ODBC driver now points to the correct position, start filling the user buffer from now 1129 1130 // Make sure we don't overflow the user provided buffer 1131 // Note: SqlDataReader will raise exception if there is no enough room for length requested. 1132 // In case of ODBC, I decided to keep this consistent with random access after consulting with PM. 1133 length = Math.Min(length, buffer.Length - bufferIndex); 1134 if (length <= 0) { 1135 // SQLBU 266054: 1136 // if the data is null, the ideal behavior here is to raise InvalidCastException. But, 1137 // * GetBytes returned 0 in Orcas RTM and SP1, continue to do so to avoid breaking change from Orcas RTM and SP1. 1138 // * GetChars raised exception in RTM, and returned 0 in SP1: we decided to revert back to the RTM's behavior and raise InvalidCast 1139 if (isCharsBuffer) { 1140 // for GetChars, ensure data is not null 1141 // 2 bytes for '\0' termination, no data is actually read from the driver 1142 int cbLengthOrIndicator; 1143 bool isDbNull = !QueryFieldInfo(i, ODBC32.SQL_C.WCHAR, out cbLengthOrIndicator); 1144 if (isDbNull) { 1145 throw ADP.InvalidCast(); 1146 } 1147 } 1148 // else - GetBytes - return now 1149 return 0; 1150 } 1151 1152 // fill the user's buffer 1153 return readBytesOrCharsSequentialAccess(i, buffer, isCharsBuffer, bufferIndex, length); 1154 } 1155 } 1156 } 1157 1158 // fill the user's buffer (char[] or byte[], depending on isCharsBuffer) 1159 // if buffer is null, just skip the bytesOrCharsLength bytes or chars readBytesOrCharsSequentialAccess(int i, Array buffer, bool isCharsBuffer, int bufferIndex, long bytesOrCharsLength)1160 private int readBytesOrCharsSequentialAccess(int i, Array buffer, bool isCharsBuffer, int bufferIndex, long bytesOrCharsLength) { 1161 Debug.Assert(bufferIndex >= 0, "Negative buffer index"); 1162 Debug.Assert(bytesOrCharsLength >= 0, "Negative number of bytes or chars to read"); 1163 1164 // validated by the caller 1165 Debug.Assert(buffer == null || bytesOrCharsLength <= (buffer.Length - bufferIndex), "Not enough space in user's buffer"); 1166 1167 int totalBytesOrCharsRead = 0; 1168 string originalMethodName = isCharsBuffer ? "GetChars" : "GetBytes"; 1169 1170 // we need length in bytes, b/c that is what SQLGetData expects 1171 long cbLength = (isCharsBuffer)? checked(bytesOrCharsLength * 2) : bytesOrCharsLength; 1172 1173 // continue reading from the driver until we fill the user's buffer or until no more data is available 1174 // the data is pumped first into the internal native buffer and after that copied into the user's one if buffer is not null 1175 CNativeBuffer internalNativeBuffer = this.Buffer; 1176 1177 // read the data in loop up to th user's length 1178 // if the data size is less than requested or in case of error, the while loop will stop in the middle 1179 while (cbLength > 0) 1180 { 1181 // max data to be read, in bytes, not including null-terminator for WCHARs 1182 int cbReadMax; 1183 1184 // read from the driver 1185 bool isNotDbNull; 1186 int cbTotal; 1187 // read either bytes or chars, depending on the method called 1188 if (isCharsBuffer) { 1189 // for WCHAR buffers, we need to leave space for null-terminator (2 bytes) 1190 // reserve 2 bytes for null-terminator and 2 bytes to prevent assert in GetData 1191 // if SQL_NO_TOTAL is returned, this ammount is read from the wire, in bytes 1192 cbReadMax = (int)Math.Min(cbLength, internalNativeBuffer.Length - 4); 1193 1194 // SQLGetData will always append it - we do not to copy it to user's buffer 1195 isNotDbNull = GetData(i, ODBC32.SQL_C.WCHAR, cbReadMax + 2, out cbTotal); 1196 } 1197 else { 1198 // reserve 2 bytes to prevent assert in GetData 1199 // when querying bytes, no need to reserve space for null 1200 cbReadMax = (int)Math.Min(cbLength, internalNativeBuffer.Length - 2); 1201 1202 isNotDbNull = GetData(i, ODBC32.SQL_C.BINARY, cbReadMax, out cbTotal); 1203 } 1204 1205 if (!isNotDbNull) 1206 { 1207 // DbNull received, neither GetBytes nor GetChars should be used with DbNull value 1208 // two options 1209 // 1. be consistent with SqlDataReader, raise SqlNullValueException 1210 // 2. be consistent with other Get* methods of OdbcDataReader and raise InvalidCastException 1211 // after consulting with Himanshu (PM), decided to go with option 2 (raise cast exception) 1212 throw ADP.InvalidCast(); 1213 } 1214 1215 int cbRead; // will hold number of bytes read in this loop 1216 bool noDataRemained = false; 1217 if (cbTotal == 0) 1218 { 1219 // no bytes read, stop 1220 break; 1221 } 1222 else if (ODBC32.SQL_NO_TOTAL == cbTotal) 1223 { 1224 // the driver has filled the internal buffer, but the length of remained data is still unknown 1225 // we will continue looping until SQLGetData indicates the end of data or user buffer is fully filled 1226 cbRead = cbReadMax; 1227 } 1228 else 1229 { 1230 Debug.Assert((cbTotal > 0), "GetData returned negative value, which is not SQL_NO_TOTAL"); 1231 // GetData uses SQLGetData, which StrLen_or_IndPtr (cbTotal in our case) to the current buf + remained buf (if any) 1232 if (cbTotal > cbReadMax) 1233 { 1234 // in this case the amount of bytes/chars read will be the max requested (and more bytes can be read) 1235 cbRead = cbReadMax; 1236 } 1237 else 1238 { 1239 // SQLGetData read all the available data, no more remained 1240 // continue processing this chunk and stop 1241 cbRead = cbTotal; 1242 noDataRemained = true; 1243 } 1244 } 1245 1246 _sequentialBytesRead += cbRead; 1247 1248 // update internal state and copy the data to user's buffer 1249 if (isCharsBuffer) 1250 { 1251 int cchRead = cbRead / 2; 1252 if (buffer != null) { 1253 internalNativeBuffer.ReadChars(0, (char[])buffer, bufferIndex, cchRead); 1254 bufferIndex += cchRead; 1255 } 1256 totalBytesOrCharsRead += cchRead; 1257 1258 } 1259 else 1260 { 1261 if (buffer != null) { 1262 internalNativeBuffer.ReadBytes(0, (byte[])buffer, bufferIndex, cbRead); 1263 bufferIndex += cbRead; 1264 } 1265 totalBytesOrCharsRead += cbRead; 1266 } 1267 1268 cbLength -= cbRead; 1269 1270 // stop if no data remained 1271 if (noDataRemained) 1272 break; 1273 } 1274 1275 return totalBytesOrCharsRead; 1276 } 1277 internalGetBytes(int i)1278 private object internalGetBytes(int i) { 1279 if(this.dataCache.AccessIndex(i) == null) { 1280 // Obtain _ALL_ the bytes... 1281 // The first time GetData returns the true length (so we have to min it). 1282 Byte[] rgBytes; 1283 int cbBufferLen = Buffer.Length - 4; 1284 int cbActual; 1285 int cbOffset = 0; 1286 1287 if(GetData(i, ODBC32.SQL_C.BINARY, cbBufferLen, out cbActual)) { 1288 CNativeBuffer buffer = Buffer; 1289 1290 if(ODBC32.SQL_NO_TOTAL != cbActual) { 1291 rgBytes = new Byte[cbActual]; 1292 Buffer.ReadBytes(0, rgBytes, cbOffset, Math.Min(cbActual, cbBufferLen)); 1293 1294 // Chunking. The data may be larger than our native buffer. In which case 1295 // instead of growing the buffer (out of control), we will read in chunks to 1296 // reduce memory footprint size. 1297 while(cbActual > cbBufferLen) { 1298 // The first time GetData returns the true length. Then successive calls 1299 // return the remaining data. 1300 bool flag = GetData(i, ODBC32.SQL_C.BINARY, cbBufferLen, out cbActual); 1301 Debug.Assert(flag, "internalGetBytes - unexpected invalid result inside if-block"); 1302 1303 cbOffset += cbBufferLen; 1304 buffer.ReadBytes(0, rgBytes, cbOffset, Math.Min(cbActual, cbBufferLen)); 1305 } 1306 } 1307 else { 1308 List<Byte[]> junkArray = new List<Byte[]>(); 1309 int junkSize; 1310 int totalSize = 0; 1311 do { 1312 junkSize = (ODBC32.SQL_NO_TOTAL != cbActual) ? cbActual : cbBufferLen; 1313 rgBytes = new Byte[junkSize]; 1314 totalSize += junkSize; 1315 buffer.ReadBytes(0, rgBytes, 0, junkSize); 1316 junkArray.Add(rgBytes); 1317 } 1318 while ((ODBC32.SQL_NO_TOTAL == cbActual) && GetData(i, ODBC32.SQL_C.BINARY, cbBufferLen, out cbActual)); 1319 1320 rgBytes = new Byte[totalSize]; 1321 foreach(Byte[] junk in junkArray) { 1322 junk.CopyTo(rgBytes, cbOffset); 1323 cbOffset += junk.Length; 1324 } 1325 } 1326 1327 // always update the cache 1328 this.dataCache[i] = rgBytes; 1329 } 1330 } 1331 return this.dataCache[i]; 1332 } 1333 1334 // GetColAttribute 1335 // --------------- 1336 // [IN] iColumn ColumnNumber 1337 // [IN] v3FieldId FieldIdentifier of the attribute for version3 drivers (>=3.0) 1338 // [IN] v2FieldId FieldIdentifier of the attribute for version2 drivers (<3.0) 1339 // 1340 // returns the value of the FieldIdentifier field of the column 1341 // or -1 if the FieldIdentifier wasn't supported by the driver 1342 // GetColAttribute(int iColumn, ODBC32.SQL_DESC v3FieldId, ODBC32.SQL_COLUMN v2FieldId, ODBC32.HANDLER handler)1343 private SQLLEN GetColAttribute(int iColumn, ODBC32.SQL_DESC v3FieldId, ODBC32.SQL_COLUMN v2FieldId, ODBC32.HANDLER handler) { 1344 Int16 cchNameLength = 0; 1345 SQLLEN numericAttribute; 1346 ODBC32.RetCode retcode; 1347 1348 // protect against dead connection, dead or canceling command. 1349 if ((Connection == null) || _cmdWrapper.Canceling) { 1350 return -1; 1351 } 1352 1353 //Ordinals are 1:base in odbc 1354 OdbcStatementHandle stmt = StatementHandle; 1355 if (Connection.IsV3Driver) { 1356 retcode = stmt.ColumnAttribute(iColumn+1, (short)v3FieldId, Buffer, out cchNameLength, out numericAttribute); 1357 } 1358 else if (v2FieldId != (ODBC32.SQL_COLUMN)(-1)) { 1359 retcode = stmt.ColumnAttribute(iColumn+1, (short)v2FieldId, Buffer, out cchNameLength, out numericAttribute); 1360 } 1361 else { 1362 return 0; 1363 } 1364 if (retcode != ODBC32.RetCode.SUCCESS) 1365 { 1366 if (retcode == ODBC32.RetCode.ERROR) { 1367 if ("HY091" == Command.GetDiagSqlState()) { 1368 Connection.FlagUnsupportedColAttr(v3FieldId, v2FieldId); 1369 } 1370 } 1371 if(handler == ODBC32.HANDLER.THROW) { 1372 Connection.HandleError(stmt, retcode); 1373 } 1374 return -1; 1375 } 1376 return numericAttribute; 1377 } 1378 1379 // GetColAttributeStr 1380 // --------------- 1381 // [IN] iColumn ColumnNumber 1382 // [IN] v3FieldId FieldIdentifier of the attribute for version3 drivers (>=3.0) 1383 // [IN] v2FieldId FieldIdentifier of the attribute for version2 drivers (<3.0) 1384 // 1385 // returns the stringvalue of the FieldIdentifier field of the column 1386 // or null if the string returned was empty or if the FieldIdentifier wasn't supported by the driver 1387 // GetColAttributeStr(int i, ODBC32.SQL_DESC v3FieldId, ODBC32.SQL_COLUMN v2FieldId, ODBC32.HANDLER handler)1388 private String GetColAttributeStr(int i, ODBC32.SQL_DESC v3FieldId, ODBC32.SQL_COLUMN v2FieldId, ODBC32.HANDLER handler) { 1389 ODBC32.RetCode retcode; 1390 Int16 cchNameLength = 0; 1391 SQLLEN numericAttribute; 1392 CNativeBuffer buffer = Buffer; 1393 buffer.WriteInt16(0, 0); 1394 1395 OdbcStatementHandle stmt = StatementHandle; 1396 1397 // protect against dead connection 1398 if (Connection == null || _cmdWrapper.Canceling || stmt == null) { 1399 return ""; 1400 } 1401 1402 if (Connection.IsV3Driver) { 1403 retcode = stmt.ColumnAttribute(i+1, (short)v3FieldId, buffer, out cchNameLength, out numericAttribute); 1404 } 1405 else if (v2FieldId != (ODBC32.SQL_COLUMN)(-1)) { 1406 retcode = stmt.ColumnAttribute(i+1, (short)v2FieldId, buffer, out cchNameLength, out numericAttribute); 1407 } 1408 else { 1409 return null; 1410 } 1411 if((retcode != ODBC32.RetCode.SUCCESS) || (cchNameLength == 0)) 1412 { 1413 if (retcode == ODBC32.RetCode.ERROR) { 1414 if ("HY091" == Command.GetDiagSqlState()) { 1415 Connection.FlagUnsupportedColAttr(v3FieldId, v2FieldId); 1416 } 1417 } 1418 if(handler == ODBC32.HANDLER.THROW) { 1419 Connection.HandleError(stmt, retcode); 1420 } 1421 return null; 1422 } 1423 string retval = buffer.PtrToStringUni(0, cchNameLength/2 /*cch*/); 1424 return retval; 1425 } 1426 1427 // todo: Another 3.0 only attribute that is guaranteed to fail on V2 driver. 1428 // need to special case this for V2 drivers. 1429 // GetDescFieldStr(int i, ODBC32.SQL_DESC attribute, ODBC32.HANDLER handler)1430 private String GetDescFieldStr(int i, ODBC32.SQL_DESC attribute, ODBC32.HANDLER handler) { 1431 Int32 numericAttribute = 0; 1432 1433 // protect against dead connection, dead or canceling command. 1434 if ((Connection == null) || _cmdWrapper.Canceling) { 1435 return ""; 1436 } 1437 1438 // APP_PARAM_DESC is a (ODBCVER >= 0x0300) attribute 1439 if (!Connection.IsV3Driver) { 1440 Debug.Assert (false, "Non-V3 driver. Must not call GetDescFieldStr"); 1441 return null; 1442 } 1443 1444 ODBC32.RetCode retcode; 1445 CNativeBuffer buffer = Buffer; 1446 1447 // Need to set the APP_PARAM_DESC values here 1448 using(OdbcDescriptorHandle hdesc = new OdbcDescriptorHandle(StatementHandle, ODBC32.SQL_ATTR.APP_PARAM_DESC)) { 1449 //SQLGetDescField 1450 retcode = hdesc.GetDescriptionField(i+1, attribute, buffer, out numericAttribute); 1451 1452 //Since there are many attributes (column, statement, etc), that may or may not be 1453 //supported, we don't want to throw (which obtains all errorinfo, marshals strings, 1454 //builds exceptions, etc), in common cases, unless we absolutly need this info... 1455 if((retcode != ODBC32.RetCode.SUCCESS) || (numericAttribute == 0)) 1456 { 1457 if (retcode == ODBC32.RetCode.ERROR) { 1458 if ("HY091" == Command.GetDiagSqlState()) { 1459 Connection.FlagUnsupportedColAttr(attribute, (ODBC32.SQL_COLUMN)0); 1460 } 1461 } 1462 if(handler == ODBC32.HANDLER.THROW) { 1463 Connection.HandleError(StatementHandle, retcode); 1464 } 1465 return null; 1466 } 1467 } 1468 string retval = buffer.PtrToStringUni(0, numericAttribute/2 /*cch*/); 1469 return retval; 1470 } 1471 1472 /// <summary> 1473 /// This methods queries the following field information: isDbNull and remained size/indicator. No data is read from the driver. 1474 /// If the value is DbNull, this value will be cached. Refer to GetData for more details. 1475 /// </summary> 1476 /// <returns>false if value is DbNull, true otherwise</returns> QueryFieldInfo(int i, ODBC32.SQL_C sqlctype, out int cbLengthOrIndicator)1477 private bool QueryFieldInfo(int i, ODBC32.SQL_C sqlctype, out int cbLengthOrIndicator) { 1478 int cb = 0; 1479 if (sqlctype == ODBC32.SQL_C.WCHAR) { 1480 // SQLBU 266054 - in case of WCHAR data, we need to provide buffer with a space for null terminator (two bytes) 1481 cb = 2; 1482 } 1483 return GetData(i, sqlctype, cb /* no data should be lost */, out cbLengthOrIndicator); 1484 } 1485 GetData(int i, ODBC32.SQL_C sqlctype)1486 private bool GetData(int i, ODBC32.SQL_C sqlctype) { 1487 // Never call GetData with anything larger than _buffer.Length-2. 1488 // We keep reallocating native buffers and it kills performance!!! 1489 int dummy; 1490 return GetData(i, sqlctype, Buffer.Length - 4, out dummy); 1491 } 1492 1493 /// <summary> 1494 /// 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 1495 /// are made after it. 1496 /// 1497 /// retrieves the data into this.Buffer. 1498 /// * If the data is DbNull, the value be also cached and false is returned. 1499 /// * if the data is not DbNull, the value is not cached and true is returned 1500 /// 1501 /// Note: cbLengthOrIndicator can be either the length of (remained) data or SQL_NO_TOTAL (-4) when the length is not known. 1502 /// in case of SQL_NO_TOTAL, driver fills the buffer till the end. 1503 /// The indicator will NOT be SQL_NULL_DATA, GetData will replace it with zero and return false. 1504 /// </summary> 1505 /// <returns>false if value is DbNull, true otherwise</returns> GetData(int i, ODBC32.SQL_C sqlctype, int cb, out int cbLengthOrIndicator)1506 private bool GetData(int i, ODBC32.SQL_C sqlctype, int cb, out int cbLengthOrIndicator) { 1507 IntPtr cbActual = IntPtr.Zero; // Length or an indicator value 1508 1509 if (IsCancelingCommand){ 1510 throw ADP.DataReaderNoData(); 1511 } 1512 Debug.Assert (null != StatementHandle, "Statement handle is null in DateReader"); 1513 1514 // see notes on ODBC32.RetCode.NO_DATA case below. 1515 Debug.Assert(this.dataCache == null || !Convert.IsDBNull(this.dataCache[i]), "Cannot call GetData without checking for cache first!"); 1516 1517 // Never call GetData with anything larger than _buffer.Length-2. 1518 // We keep reallocating native buffers and it kills performance!!! 1519 1520 Debug.Assert(cb <= Buffer.Length-2, "GetData needs to Reallocate. Perf bug"); 1521 1522 // SQLGetData 1523 CNativeBuffer buffer = Buffer; 1524 ODBC32.RetCode retcode = StatementHandle.GetData( 1525 (i+1), // Column ordinals start at 1 in odbc 1526 sqlctype, 1527 buffer, 1528 cb, 1529 out cbActual); 1530 1531 switch (retcode) { 1532 case ODBC32.RetCode.SUCCESS: 1533 break; 1534 case ODBC32.RetCode.SUCCESS_WITH_INFO: 1535 if ((Int32)cbActual == ODBC32.SQL_NO_TOTAL) { 1536 break; 1537 } 1538 // devnote: don't we want to fire an event? 1539 break; 1540 1541 case ODBC32.RetCode.NO_DATA: 1542 // SQLBU 266054: System.Data.Odbc: Fails with truncated error when we pass BufferLength as 0 1543 // NO_DATA return value is success value - it means that the driver has fully consumed the current column value 1544 // but did not move to the next column yet. 1545 // 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. 1546 // For variable-length values (any character or binary data), SQLGetData can be called several times on the same column, 1547 // to query for the next chunk of value, even after reaching its end! 1548 // Thus, ignore NO_DATA for variable length data, but raise exception for fixed-size types 1549 if (sqlctype != ODBC32.SQL_C.WCHAR && sqlctype != ODBC32.SQL_C.BINARY) { 1550 Connection.HandleError(StatementHandle, retcode); 1551 } 1552 1553 if (cbActual == (IntPtr)ODBC32.SQL_NO_TOTAL) { 1554 // ensure SQL_NO_TOTAL value gets replaced with zero if the driver has fully consumed the current column 1555 cbActual = (IntPtr)0; 1556 } 1557 break; 1558 1559 default: 1560 Connection.HandleError(StatementHandle, retcode); 1561 break; 1562 } 1563 1564 // reset the current row and column 1565 SetCurrentRowColumnInfo(_row, i); 1566 1567 // test for SQL_NULL_DATA 1568 if (cbActual == (IntPtr)ODBC32.SQL_NULL_DATA) { 1569 // 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 1570 // 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 1571 // for the same row/column. 1572 this.dataCache[i] = DBNull.Value; 1573 // the indicator is never -1 (and it should not actually be used if the data is DBNull) 1574 cbLengthOrIndicator = 0; 1575 return false; 1576 } 1577 else { 1578 //Return the actual size (for chunking scenarios) 1579 // note the return value can be SQL_NO_TOTAL (-4) 1580 cbLengthOrIndicator = (int)cbActual; 1581 return true; 1582 } 1583 } 1584 Read()1585 override public bool Read() { 1586 if (IsClosed) { 1587 throw ADP.DataReaderClosed("Read"); 1588 } 1589 1590 if (IsCancelingCommand) { 1591 _isRead = false; 1592 return false; 1593 } 1594 1595 // HasRows needs to call into Read so we don't want to read on the actual Read call 1596 if (_skipReadOnce){ 1597 _skipReadOnce = false; 1598 return _isRead; 1599 } 1600 1601 if (_noMoreRows || _noMoreResults || IsCommandBehavior(CommandBehavior.SchemaOnly)) 1602 return false; 1603 1604 if (!_isValidResult) { 1605 return false; 1606 } 1607 1608 ODBC32.RetCode retcode; 1609 1610 //SQLFetch is only valid to call for row returning queries 1611 //We get: [24000]Invalid cursor state. So we could either check the count 1612 //ahead of time (which is cached), or check on error and compare error states. 1613 //Note: SQLFetch is also invalid to be called on a prepared (schemaonly) statement 1614 //SqlFetch 1615 retcode = StatementHandle.Fetch(); 1616 1617 switch(retcode) { 1618 case ODBC32.RetCode.SUCCESS_WITH_INFO: 1619 Connection.HandleErrorNoThrow(StatementHandle, retcode); 1620 _hasRows = HasRowsStatus.HasRows; 1621 _isRead = true; 1622 break; 1623 case ODBC32.RetCode.SUCCESS: 1624 _hasRows = HasRowsStatus.HasRows; 1625 _isRead = true; 1626 break; 1627 case ODBC32.RetCode.NO_DATA: 1628 _isRead = false; 1629 if (_hasRows == HasRowsStatus.DontKnow) { 1630 _hasRows = HasRowsStatus.HasNoRows; 1631 } 1632 break; 1633 default: 1634 Connection.HandleError(StatementHandle, retcode); 1635 break; 1636 } 1637 //Null out previous cached row values. 1638 this.dataCache.FlushValues(); 1639 1640 // if CommandBehavior == SingleRow we set _noMoreResults to true so that following reads will fail 1641 if (IsCommandBehavior(CommandBehavior.SingleRow)) { 1642 _noMoreRows = true; 1643 // no more rows, set to -1 1644 SetCurrentRowColumnInfo(-1, 0); 1645 } 1646 else { 1647 // move to the next row 1648 SetCurrentRowColumnInfo(_row + 1, 0); 1649 } 1650 return _isRead; 1651 } 1652 1653 // Called by odbccommand when executed for the first time FirstResult()1654 internal void FirstResult() { 1655 Int16 cCols; 1656 SQLLEN cRowsAffected; 1657 1658 cRowsAffected = GetRowCount(); // get rowcount of the current resultset (if any) 1659 CalculateRecordsAffected(cRowsAffected); // update recordsaffected 1660 1661 ODBC32.RetCode retcode = FieldCountNoThrow(out cCols); 1662 if ((retcode == ODBC32.RetCode.SUCCESS) && (cCols == 0)) { 1663 NextResult(); 1664 } 1665 else { 1666 this._isValidResult = true; 1667 } 1668 } 1669 NextResult()1670 override public bool NextResult() { 1671 return NextResult(false, false); 1672 } 1673 NextResult(bool disposing, bool allresults)1674 private bool NextResult(bool disposing, bool allresults) { 1675 // if disposing, loop through all the remaining results and ignore error messages 1676 // if allresults, loop through all results and collect all error messages for a single exception 1677 // callers are via Close(false, true), Dispose(true, false), NextResult(false,false) 1678 Debug.Assert(!disposing || !allresults, "both disposing & allresults are true"); 1679 const int MaxConsecutiveFailure = 2000; // see WebData 72126 for why more than 1000 1680 1681 SQLLEN cRowsAffected; 1682 Int16 cColsAffected; 1683 ODBC32.RetCode retcode, firstRetCode = ODBC32.RetCode.SUCCESS; 1684 bool hasMoreResults; 1685 bool hasColumns = false; 1686 bool singleResult = IsCommandBehavior(CommandBehavior.SingleResult); 1687 1688 if (IsClosed) { 1689 throw ADP.DataReaderClosed("NextResult"); 1690 } 1691 _fieldNameLookup = null; 1692 1693 if (IsCancelingCommand || _noMoreResults) { 1694 return false; 1695 } 1696 1697 //Blow away the previous cache (since the next result set will have a different shape, 1698 //different schema data, and different data. 1699 _isRead = false; 1700 _hasRows = HasRowsStatus.DontKnow; 1701 _fieldNameLookup = null; 1702 this.metadata = null; 1703 this.schemaTable = null; 1704 1705 int loop = 0; // infinite loop protection, max out after 2000 consecutive failed results 1706 OdbcErrorCollection errors = null; // SQLBU 342112 1707 do { 1708 _isValidResult = false; 1709 retcode = StatementHandle.MoreResults(); 1710 hasMoreResults = ((retcode == ODBC32.RetCode.SUCCESS) 1711 ||(retcode == ODBC32.RetCode.SUCCESS_WITH_INFO)); 1712 1713 if (retcode == ODBC32.RetCode.SUCCESS_WITH_INFO) { 1714 Connection.HandleErrorNoThrow(StatementHandle, retcode); 1715 } 1716 else if (!disposing && (retcode != ODBC32.RetCode.NO_DATA) && (ODBC32.RetCode.SUCCESS != retcode)) { 1717 // allow for building comulative error messages. 1718 if (null == errors) { 1719 firstRetCode = retcode; 1720 errors = new OdbcErrorCollection(); 1721 } 1722 ODBC32.GetDiagErrors(errors, null, StatementHandle, retcode); 1723 ++loop; 1724 } 1725 1726 if (!disposing && hasMoreResults) { 1727 loop = 0; 1728 cRowsAffected = GetRowCount(); // get rowcount of the current resultset (if any) 1729 CalculateRecordsAffected(cRowsAffected); // update recordsaffected 1730 if (!singleResult) { 1731 // update row- and columncount 1732 FieldCountNoThrow(out cColsAffected); 1733 hasColumns = (0 != cColsAffected); 1734 _isValidResult = hasColumns; 1735 } 1736 } 1737 } while ((!singleResult && hasMoreResults && !hasColumns) // repeat for results with no columns 1738 || ((ODBC32.RetCode.NO_DATA != retcode) && allresults && (loop < MaxConsecutiveFailure)) // or process all results until done 1739 || (singleResult && hasMoreResults)); // or for any result in singelResult mode 1740 if (MaxConsecutiveFailure <= loop) { 1741 Bid.Trace("<odbc.OdbcDataReader.NextResult|INFO> 2000 consecutive failed results"); 1742 } 1743 1744 if(retcode == ODBC32.RetCode.NO_DATA) { 1745 this.dataCache = null; 1746 _noMoreResults = true; 1747 } 1748 if (null != errors) { 1749 Debug.Assert(!disposing, "errors while disposing"); 1750 errors.SetSource(Connection.Driver); 1751 OdbcException exception = OdbcException.CreateException(errors, firstRetCode); 1752 Connection.ConnectionIsAlive(exception); 1753 throw exception; 1754 } 1755 return (hasMoreResults); 1756 } 1757 BuildMetaDataInfo()1758 private void BuildMetaDataInfo() { 1759 int count = FieldCount; 1760 MetaData[] metaInfos = new MetaData[count]; 1761 List<string> qrytables; 1762 bool needkeyinfo = IsCommandBehavior(CommandBehavior.KeyInfo); 1763 bool isKeyColumn; 1764 bool isHidden; 1765 ODBC32.SQL_NULLABILITY nullable; 1766 1767 if (needkeyinfo) 1768 qrytables = new List<string>(); 1769 else 1770 qrytables = null; 1771 1772 // Find out all the metadata info, not all of this info will be available in all cases 1773 // 1774 for(int i=0; i<count; i++) 1775 { 1776 metaInfos[i] = new MetaData(); 1777 metaInfos[i].ordinal = i; 1778 TypeMap typeMap; 1779 1780 // for precision and scale we take the SQL_COLUMN_ attributes. 1781 // Those attributes are supported by all provider versions. 1782 // for size we use the octet length. We can't use column length because there is an incompatibility with the jet driver. 1783 // furthermore size needs to be special cased for wchar types 1784 // 1785 typeMap = TypeMap.FromSqlType((ODBC32.SQL_TYPE)unchecked((int) GetColAttribute(i, ODBC32.SQL_DESC.CONCISE_TYPE, ODBC32.SQL_COLUMN.TYPE, ODBC32.HANDLER.THROW))); 1786 if (typeMap._signType == true) { 1787 bool sign = (GetColAttribute(i, ODBC32.SQL_DESC.UNSIGNED, ODBC32.SQL_COLUMN.UNSIGNED, ODBC32.HANDLER.THROW).ToInt64() != 0); 1788 // sign = true if the column is unsigned 1789 typeMap = TypeMap.UpgradeSignedType(typeMap, sign); 1790 } 1791 1792 metaInfos[i].typemap = typeMap; 1793 metaInfos[i].size = GetColAttribute(i, ODBC32.SQL_DESC.OCTET_LENGTH, ODBC32.SQL_COLUMN.LENGTH, ODBC32.HANDLER.IGNORE); 1794 1795 // special case the 'n' types 1796 // 1797 switch(metaInfos[i].typemap._sql_type) { 1798 case ODBC32.SQL_TYPE.WCHAR: 1799 case ODBC32.SQL_TYPE.WLONGVARCHAR: 1800 case ODBC32.SQL_TYPE.WVARCHAR: 1801 metaInfos[i].size /= 2; 1802 break; 1803 } 1804 1805 metaInfos[i].precision = (byte) GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_COLUMN.PRECISION, ODBC32.SQL_COLUMN.PRECISION, ODBC32.HANDLER.IGNORE); 1806 metaInfos[i].scale = (byte) GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_COLUMN.SCALE, ODBC32.SQL_COLUMN.SCALE, ODBC32.HANDLER.IGNORE); 1807 1808 metaInfos[i].isAutoIncrement = GetColAttribute(i, ODBC32.SQL_DESC.AUTO_UNIQUE_VALUE, ODBC32.SQL_COLUMN.AUTO_INCREMENT, ODBC32.HANDLER.IGNORE) == 1; 1809 metaInfos[i].isReadOnly = (GetColAttribute(i, ODBC32.SQL_DESC.UPDATABLE, ODBC32.SQL_COLUMN.UPDATABLE, ODBC32.HANDLER.IGNORE) == (Int32)ODBC32.SQL_UPDATABLE.READONLY); 1810 1811 nullable = (ODBC32.SQL_NULLABILITY) (int) GetColAttribute(i, ODBC32.SQL_DESC.NULLABLE, ODBC32.SQL_COLUMN.NULLABLE, ODBC32.HANDLER.IGNORE); 1812 metaInfos[i].isNullable = (nullable == ODBC32.SQL_NULLABILITY.NULLABLE); 1813 1814 switch(metaInfos[i].typemap._sql_type) { 1815 case ODBC32.SQL_TYPE.LONGVARCHAR: 1816 case ODBC32.SQL_TYPE.WLONGVARCHAR: 1817 case ODBC32.SQL_TYPE.LONGVARBINARY: 1818 metaInfos[i].isLong = true; 1819 break; 1820 default: 1821 metaInfos[i].isLong = false; 1822 break; 1823 } 1824 1825 if(IsCommandBehavior(CommandBehavior.KeyInfo)) 1826 { 1827 // Note: Following two attributes are SQL Server specific (hence _SS in the name) 1828 1829 // SSS_WARNINGS_OFF 1830 if (!Connection.ProviderInfo.NoSqlCASSColumnKey) { 1831 isKeyColumn = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_KEY, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE) == 1; 1832 if (isKeyColumn) { 1833 metaInfos[i].isKeyColumn = isKeyColumn; 1834 metaInfos[i].isUnique = true; 1835 needkeyinfo = false; 1836 } 1837 } 1838 // SSS_WARNINGS_ON 1839 1840 metaInfos[i].baseSchemaName = GetColAttributeStr(i, ODBC32.SQL_DESC.SCHEMA_NAME, ODBC32.SQL_COLUMN.OWNER_NAME, ODBC32.HANDLER.IGNORE); 1841 metaInfos[i].baseCatalogName = GetColAttributeStr(i, ODBC32.SQL_DESC.CATALOG_NAME, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE); 1842 metaInfos[i].baseTableName = GetColAttributeStr(i, ODBC32.SQL_DESC.BASE_TABLE_NAME, ODBC32.SQL_COLUMN.TABLE_NAME, ODBC32.HANDLER.IGNORE); 1843 metaInfos[i].baseColumnName = GetColAttributeStr(i, ODBC32.SQL_DESC.BASE_COLUMN_NAME, ODBC32.SQL_COLUMN.NAME, ODBC32.HANDLER.IGNORE); 1844 1845 if (Connection.IsV3Driver) { 1846 if ((metaInfos[i].baseTableName == null) ||(metaInfos[i].baseTableName.Length == 0)) { 1847 // Driver didn't return the necessary information from GetColAttributeStr. 1848 // Try GetDescField() 1849 metaInfos[i].baseTableName = GetDescFieldStr(i, ODBC32.SQL_DESC.BASE_TABLE_NAME, ODBC32.HANDLER.IGNORE); 1850 } 1851 if ((metaInfos[i].baseColumnName == null) ||(metaInfos[i].baseColumnName.Length == 0)) { 1852 // Driver didn't return the necessary information from GetColAttributeStr. 1853 // Try GetDescField() 1854 metaInfos[i].baseColumnName = GetDescFieldStr(i, ODBC32.SQL_DESC.BASE_COLUMN_NAME, ODBC32.HANDLER.IGNORE); 1855 } 1856 } 1857 if ((metaInfos[i].baseTableName != null) && !(qrytables.Contains(metaInfos[i].baseTableName))) { 1858 qrytables.Add(metaInfos[i].baseTableName); 1859 } 1860 } 1861 1862 // If primary key or autoincrement, then must also be unique 1863 if (metaInfos[i].isKeyColumn || metaInfos[i].isAutoIncrement ) { 1864 if (nullable == ODBC32.SQL_NULLABILITY.UNKNOWN) 1865 metaInfos[i].isNullable = false; // We can safely assume these are not nullable 1866 } 1867 } 1868 1869 // now loop over the hidden columns (if any) 1870 1871 // SSS_WARNINGS_OFF 1872 if (!Connection.ProviderInfo.NoSqlCASSColumnKey) { 1873 for (int i=count; i<count+_hiddenColumns; i++) { 1874 isKeyColumn = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_KEY, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE) == 1; 1875 if (isKeyColumn) { 1876 isHidden = GetColAttribute(i, (ODBC32.SQL_DESC)ODBC32.SQL_CA_SS.COLUMN_HIDDEN, (ODBC32.SQL_COLUMN)(-1), ODBC32.HANDLER.IGNORE) == 1; 1877 if (isHidden) { 1878 for (int j=0; j<count; j++) { 1879 metaInfos[j].isKeyColumn = false; // downgrade keycolumn 1880 metaInfos[j].isUnique = false; // downgrade uniquecolumn 1881 } 1882 } 1883 } 1884 } 1885 } 1886 // SSS_WARNINGS_ON 1887 1888 // Blow away the previous metadata 1889 this.metadata = metaInfos; 1890 1891 // If key info is requested, then we have to make a few more calls to get the 1892 // special columns. This may not succeed for all drivers, so ignore errors and 1893 // fill in as much as possible. 1894 if (IsCommandBehavior(CommandBehavior.KeyInfo)) { 1895 if((qrytables != null) && (qrytables.Count > 0) ) { 1896 List<string>.Enumerator tablesEnum = qrytables.GetEnumerator(); 1897 QualifiedTableName qualifiedTableName = new QualifiedTableName(Connection.QuoteChar(ADP.GetSchemaTable)); 1898 while(tablesEnum.MoveNext()) { 1899 // Find the primary keys, identity and autincrement columns 1900 qualifiedTableName.Table = tablesEnum.Current; 1901 if (RetrieveKeyInfo(needkeyinfo, qualifiedTableName, false) <= 0) { 1902 RetrieveKeyInfo(needkeyinfo, qualifiedTableName, true); 1903 } 1904 } 1905 } 1906 else { 1907 // Some drivers ( < 3.x ?) do not provide base table information. In this case try to 1908 // find it by parsing the statement 1909 1910 QualifiedTableName qualifiedTableName = new QualifiedTableName(Connection.QuoteChar(ADP.GetSchemaTable), GetTableNameFromCommandText()); 1911 if (!ADP.IsEmpty(qualifiedTableName.Table)) { // fxcop 1912 SetBaseTableNames(qualifiedTableName); 1913 if (RetrieveKeyInfo(needkeyinfo, qualifiedTableName, false) <= 0) { 1914 RetrieveKeyInfo(needkeyinfo, qualifiedTableName, true); 1915 } 1916 } 1917 } 1918 } 1919 } 1920 NewSchemaTable()1921 private DataTable NewSchemaTable() { 1922 DataTable schematable = new DataTable("SchemaTable"); 1923 schematable.Locale = CultureInfo.InvariantCulture; 1924 schematable.MinimumCapacity = this.FieldCount; 1925 1926 //Schema Columns 1927 DataColumnCollection columns = schematable.Columns; 1928 columns.Add(new DataColumn("ColumnName", typeof(System.String))); 1929 columns.Add(new DataColumn("ColumnOrdinal", typeof(System.Int32))); // UInt32 1930 columns.Add(new DataColumn("ColumnSize", typeof(System.Int32))); // UInt32 1931 columns.Add(new DataColumn("NumericPrecision", typeof(System.Int16))); // UInt16 1932 columns.Add(new DataColumn("NumericScale", typeof(System.Int16))); 1933 columns.Add(new DataColumn("DataType", typeof(System.Object))); 1934 columns.Add(new DataColumn("ProviderType", typeof(System.Int32))); 1935 columns.Add(new DataColumn("IsLong", typeof(System.Boolean))); 1936 columns.Add(new DataColumn("AllowDBNull", typeof(System.Boolean))); 1937 columns.Add(new DataColumn("IsReadOnly", typeof(System.Boolean))); 1938 columns.Add(new DataColumn("IsRowVersion", typeof(System.Boolean))); 1939 columns.Add(new DataColumn("IsUnique", typeof(System.Boolean))); 1940 columns.Add(new DataColumn("IsKey", typeof(System.Boolean))); 1941 columns.Add(new DataColumn("IsAutoIncrement", typeof(System.Boolean))); 1942 columns.Add(new DataColumn("BaseSchemaName", typeof(System.String))); 1943 columns.Add(new DataColumn("BaseCatalogName", typeof(System.String))); 1944 columns.Add(new DataColumn("BaseTableName", typeof(System.String))); 1945 columns.Add(new DataColumn("BaseColumnName", typeof(System.String))); 1946 1947 // MDAC Bug 79231 1948 foreach (DataColumn column in columns) { 1949 column.ReadOnly = true; 1950 } 1951 return schematable; 1952 } 1953 1954 // The default values are already defined in DbSchemaRows (see DbSchemaRows.cs) so there is no need to set any default value 1955 // 1956 GetSchemaTable()1957 override public DataTable GetSchemaTable() { 1958 if (IsClosed) { // MDAC 68331 1959 throw ADP.DataReaderClosed("GetSchemaTable"); // can't use closed connection 1960 } 1961 if (_noMoreResults) { 1962 return null; // no more results 1963 } 1964 if (null != this.schemaTable) { 1965 return this.schemaTable; // return cached schematable 1966 } 1967 1968 //Delegate, to have the base class setup the structure 1969 DataTable schematable = NewSchemaTable(); 1970 1971 if (FieldCount == 0) { 1972 return schematable; 1973 } 1974 if (this.metadata == null) { 1975 BuildMetaDataInfo(); 1976 } 1977 1978 DataColumn columnName = schematable.Columns["ColumnName"]; 1979 DataColumn columnOrdinal = schematable.Columns["ColumnOrdinal"]; 1980 DataColumn columnSize = schematable.Columns["ColumnSize"]; 1981 DataColumn numericPrecision = schematable.Columns["NumericPrecision"]; 1982 DataColumn numericScale = schematable.Columns["NumericScale"]; 1983 DataColumn dataType = schematable.Columns["DataType"]; 1984 DataColumn providerType = schematable.Columns["ProviderType"]; 1985 DataColumn isLong = schematable.Columns["IsLong"]; 1986 DataColumn allowDBNull = schematable.Columns["AllowDBNull"]; 1987 DataColumn isReadOnly = schematable.Columns["IsReadOnly"]; 1988 DataColumn isRowVersion = schematable.Columns["IsRowVersion"]; 1989 DataColumn isUnique = schematable.Columns["IsUnique"]; 1990 DataColumn isKey = schematable.Columns["IsKey"]; 1991 DataColumn isAutoIncrement = schematable.Columns["IsAutoIncrement"]; 1992 DataColumn baseSchemaName = schematable.Columns["BaseSchemaName"]; 1993 DataColumn baseCatalogName = schematable.Columns["BaseCatalogName"]; 1994 DataColumn baseTableName = schematable.Columns["BaseTableName"]; 1995 DataColumn baseColumnName = schematable.Columns["BaseColumnName"]; 1996 1997 1998 //Populate the rows (1 row for each column) 1999 int count = FieldCount; 2000 for(int i=0; i<count; i++) { 2001 DataRow row = schematable.NewRow(); 2002 2003 row[columnName] = GetName(i); //ColumnName 2004 row[columnOrdinal] = i; //ColumnOrdinal 2005 row[columnSize] = unchecked((int)Math.Min(Math.Max(Int32.MinValue, metadata[i].size.ToInt64()), Int32.MaxValue)); 2006 row[numericPrecision] = (Int16) metadata[i].precision; 2007 row[numericScale] = (Int16) metadata[i].scale; 2008 row[dataType] = metadata[i].typemap._type; //DataType 2009 row[providerType] = metadata[i].typemap._odbcType; // ProviderType 2010 row[isLong] = metadata[i].isLong; // IsLong 2011 row[allowDBNull] = metadata[i].isNullable; //AllowDBNull 2012 row[isReadOnly] = metadata[i].isReadOnly; // IsReadOnly 2013 row[isRowVersion] = metadata[i].isRowVersion; //IsRowVersion 2014 row[isUnique] = metadata[i].isUnique; //IsUnique 2015 row[isKey] = metadata[i].isKeyColumn; // IsKey 2016 row[isAutoIncrement] = metadata[i].isAutoIncrement; //IsAutoIncrement 2017 2018 //BaseSchemaName 2019 row[baseSchemaName] = metadata[i].baseSchemaName; 2020 //BaseCatalogName 2021 row[baseCatalogName] = metadata[i].baseCatalogName; 2022 //BaseTableName 2023 row[baseTableName] = metadata[i].baseTableName ; 2024 //BaseColumnName 2025 row[baseColumnName] = metadata[i].baseColumnName; 2026 2027 schematable.Rows.Add(row); 2028 row.AcceptChanges(); 2029 } 2030 this.schemaTable = schematable; 2031 return schematable; 2032 } 2033 RetrieveKeyInfo(bool needkeyinfo, QualifiedTableName qualifiedTableName, bool quoted)2034 internal int RetrieveKeyInfo(bool needkeyinfo, QualifiedTableName qualifiedTableName, bool quoted) { 2035 ODBC32.RetCode retcode; 2036 string columnname; 2037 int ordinal; 2038 int keyColumns = 0; 2039 IntPtr cbActual = IntPtr.Zero; 2040 2041 if (IsClosed || (_cmdWrapper == null)) { 2042 return 0; // Can't do anything without a second handle 2043 } 2044 _cmdWrapper.CreateKeyInfoStatementHandle(); 2045 2046 CNativeBuffer buffer = Buffer; 2047 bool mustRelease = false; 2048 Debug.Assert(buffer.Length >= 264, "Native buffer to small (_buffer.Length < 264)"); 2049 2050 RuntimeHelpers.PrepareConstrainedRegions(); 2051 try { 2052 buffer.DangerousAddRef(ref mustRelease); 2053 2054 if (needkeyinfo) { 2055 if (!Connection.ProviderInfo.NoSqlPrimaryKeys) { 2056 // Get the primary keys 2057 retcode = KeyInfoStatementHandle.PrimaryKeys( 2058 qualifiedTableName.Catalog, 2059 qualifiedTableName.Schema, 2060 qualifiedTableName.GetTable(quoted)); 2061 2062 if ((retcode == ODBC32.RetCode.SUCCESS) || (retcode == ODBC32.RetCode.SUCCESS_WITH_INFO)) { 2063 bool noUniqueKey = false; 2064 2065 // We are only interested in column name 2066 buffer.WriteInt16(0, 0); 2067 retcode = KeyInfoStatementHandle.BindColumn2( 2068 (short)(ODBC32.SQL_PRIMARYKEYS.COLUMNNAME), // Column Number 2069 ODBC32.SQL_C.WCHAR, 2070 buffer.PtrOffset(0, 256), 2071 (IntPtr)256, 2072 buffer.PtrOffset(256, IntPtr.Size).Handle); 2073 while (ODBC32.RetCode.SUCCESS == (retcode = KeyInfoStatementHandle.Fetch())) { 2074 cbActual = buffer.ReadIntPtr(256); 2075 columnname = buffer.PtrToStringUni(0, (int)cbActual/2/*cch*/); 2076 ordinal = this.GetOrdinalFromBaseColName(columnname); 2077 if (ordinal != -1) { 2078 keyColumns ++; 2079 this.metadata[ordinal].isKeyColumn = true; 2080 this.metadata[ordinal].isUnique = true; 2081 this.metadata[ordinal].isNullable = false; 2082 this.metadata[ordinal].baseTableName = qualifiedTableName.Table; 2083 2084 if (this.metadata[ordinal].baseColumnName == null) { 2085 this.metadata[ordinal].baseColumnName = columnname; 2086 } 2087 } 2088 else { 2089 noUniqueKey = true; 2090 break; // no need to go over the remaining columns anymore 2091 } 2092 } 2093 // 2094 2095 2096 2097 2098 // if we got keyinfo from the column we dont even get to here! 2099 // 2100 // reset isUnique flag if the key(s) are not unique 2101 // 2102 if (noUniqueKey) { 2103 foreach (MetaData metadata in this.metadata) { 2104 metadata.isKeyColumn = false; 2105 } 2106 } 2107 2108 // Unbind the column 2109 retcode = KeyInfoStatementHandle.BindColumn3( 2110 (short)(ODBC32.SQL_PRIMARYKEYS.COLUMNNAME), // SQLUSMALLINT ColumnNumber 2111 ODBC32.SQL_C.WCHAR, // SQLSMALLINT TargetType 2112 buffer.DangerousGetHandle()); // SQLLEN * StrLen_or_Ind 2113 } 2114 else { 2115 if ("IM001" == Command.GetDiagSqlState()) { 2116 Connection.ProviderInfo.NoSqlPrimaryKeys = true; 2117 } 2118 } 2119 } 2120 2121 if (keyColumns == 0) { 2122 // SQLPrimaryKeys did not work. Have to use the slower SQLStatistics to obtain key information 2123 KeyInfoStatementHandle.MoreResults(); 2124 keyColumns += RetrieveKeyInfoFromStatistics(qualifiedTableName, quoted); 2125 } 2126 KeyInfoStatementHandle.MoreResults(); 2127 } 2128 2129 // Get the special columns for version 2130 retcode = KeyInfoStatementHandle.SpecialColumns(qualifiedTableName.GetTable(quoted)); 2131 2132 if ((retcode == ODBC32.RetCode.SUCCESS) || (retcode == ODBC32.RetCode.SUCCESS_WITH_INFO)) { 2133 // We are only interested in column name 2134 cbActual = IntPtr.Zero; 2135 buffer.WriteInt16(0, 0); 2136 retcode = KeyInfoStatementHandle.BindColumn2( 2137 (short)(ODBC32.SQL_SPECIALCOLUMNSET.COLUMN_NAME), 2138 ODBC32.SQL_C.WCHAR, 2139 buffer.PtrOffset(0, 256), 2140 (IntPtr)256, 2141 buffer.PtrOffset(256, IntPtr.Size).Handle); 2142 2143 while (ODBC32.RetCode.SUCCESS == (retcode = KeyInfoStatementHandle.Fetch())) { 2144 cbActual = buffer.ReadIntPtr(256); 2145 columnname = buffer.PtrToStringUni(0, (int)cbActual/2/*cch*/); 2146 ordinal = this.GetOrdinalFromBaseColName(columnname); 2147 if (ordinal != -1) { 2148 this.metadata[ordinal].isRowVersion = true; 2149 if (this.metadata[ordinal].baseColumnName == null) { 2150 this.metadata[ordinal].baseColumnName = columnname; 2151 } 2152 } 2153 } 2154 // Unbind the column 2155 retcode = KeyInfoStatementHandle.BindColumn3( 2156 (short)(ODBC32.SQL_SPECIALCOLUMNSET.COLUMN_NAME), 2157 ODBC32.SQL_C.WCHAR, 2158 buffer.DangerousGetHandle()); 2159 2160 retcode = KeyInfoStatementHandle.MoreResults(); 2161 } 2162 else { 2163 // i've seen "DIAG [HY000] [Microsoft][ODBC SQL Server Driver]Connection is busy with results for another hstmt (0) " 2164 // how did we get here? SqlServer does not allow a second handle (Keyinfostmt) anyway... 2165 // 2166 /* 2167 string msg = "Unexpected failure of SQLSpecialColumns. Code = " + Command.GetDiagSqlState(); 2168 Debug.Assert (false, msg); 2169 */ 2170 } 2171 } 2172 finally { 2173 if (mustRelease) { 2174 buffer.DangerousRelease(); 2175 } 2176 } 2177 return keyColumns; 2178 } 2179 2180 // Uses SQLStatistics to retrieve key information for a table RetrieveKeyInfoFromStatistics(QualifiedTableName qualifiedTableName, bool quoted)2181 private int RetrieveKeyInfoFromStatistics(QualifiedTableName qualifiedTableName, bool quoted) { 2182 ODBC32.RetCode retcode; 2183 String columnname = String.Empty; 2184 String indexname = String.Empty; 2185 String currentindexname = String.Empty; 2186 int[] indexcolumnordinals = new int[16]; 2187 int[] pkcolumnordinals = new int[16]; 2188 int npkcols = 0; 2189 int ncols = 0; // No of cols in the index 2190 bool partialcolumnset = false; 2191 int ordinal; 2192 int indexordinal; 2193 IntPtr cbIndexLen = IntPtr.Zero; 2194 IntPtr cbColnameLen = IntPtr.Zero; 2195 int keyColumns = 0; 2196 2197 // devnote: this test is already done by calling method ... 2198 // if (IsClosed) return; // protect against dead connection 2199 2200 // MDAC Bug 75928 - SQLStatisticsW damages the string passed in 2201 // To protect the tablename we need to pass in a copy of that string 2202 String tablename1 = String.Copy(qualifiedTableName.GetTable(quoted)); 2203 2204 // Select only unique indexes 2205 retcode = KeyInfoStatementHandle.Statistics(tablename1); 2206 2207 if (retcode != ODBC32.RetCode.SUCCESS) { 2208 // We give up at this point 2209 return 0; 2210 } 2211 2212 CNativeBuffer buffer = Buffer; 2213 bool mustRelease = false; 2214 Debug.Assert(buffer.Length >= 544, "Native buffer to small (_buffer.Length < 544)"); 2215 2216 RuntimeHelpers.PrepareConstrainedRegions(); 2217 try { 2218 buffer.DangerousAddRef(ref mustRelease); 2219 2220 const int colnameBufOffset = 0; 2221 const int indexBufOffset = 256; 2222 const int ordinalBufOffset = 512; 2223 const int colnameActualOffset = 520; 2224 const int indexActualOffset = 528; 2225 const int ordinalActualOffset = 536; 2226 HandleRef colnamebuf = buffer.PtrOffset(colnameBufOffset, 256); 2227 HandleRef indexbuf = buffer.PtrOffset(indexBufOffset, 256); 2228 HandleRef ordinalbuf = buffer.PtrOffset(ordinalBufOffset, 4); 2229 2230 IntPtr colnameActual = buffer.PtrOffset(colnameActualOffset, IntPtr.Size).Handle; 2231 IntPtr indexActual = buffer.PtrOffset(indexActualOffset, IntPtr.Size).Handle; 2232 IntPtr ordinalActual = buffer.PtrOffset(ordinalActualOffset, IntPtr.Size).Handle; 2233 2234 //We are interested in index name, column name, and ordinal 2235 buffer.WriteInt16(indexBufOffset, 0); 2236 retcode = KeyInfoStatementHandle.BindColumn2( 2237 (short)(ODBC32.SQL_STATISTICS.INDEXNAME), 2238 ODBC32.SQL_C.WCHAR, 2239 indexbuf, 2240 (IntPtr)256, 2241 indexActual); 2242 retcode = KeyInfoStatementHandle.BindColumn2( 2243 (short)(ODBC32.SQL_STATISTICS.ORDINAL_POSITION), 2244 ODBC32.SQL_C.SSHORT, 2245 ordinalbuf, 2246 (IntPtr)4, 2247 ordinalActual); 2248 buffer.WriteInt16(ordinalBufOffset, 0); 2249 retcode = KeyInfoStatementHandle.BindColumn2( 2250 (short)(ODBC32.SQL_STATISTICS.COLUMN_NAME), 2251 ODBC32.SQL_C.WCHAR, 2252 colnamebuf, 2253 (IntPtr)256, 2254 colnameActual); 2255 // Find the best unique index on the table, use the ones whose columns are 2256 // completely covered by the query. 2257 while (ODBC32.RetCode.SUCCESS == (retcode = KeyInfoStatementHandle.Fetch())) { 2258 2259 cbColnameLen = buffer.ReadIntPtr(colnameActualOffset); 2260 cbIndexLen = buffer.ReadIntPtr(indexActualOffset); 2261 2262 // If indexname is not returned, skip this row 2263 if (0 == buffer.ReadInt16(indexBufOffset)) 2264 continue; // Not an index row, get next row. 2265 2266 columnname = buffer.PtrToStringUni(colnameBufOffset, (int)cbColnameLen/2/*cch*/); 2267 indexname = buffer.PtrToStringUni(indexBufOffset, (int)cbIndexLen/2/*cch*/); 2268 ordinal = (int) buffer.ReadInt16(ordinalBufOffset); 2269 2270 if (SameIndexColumn(currentindexname, indexname, ordinal, ncols)) { 2271 // We are still working on the same index 2272 if (partialcolumnset) 2273 continue; // We don't have all the keys for this index, so we can't use it 2274 2275 ordinal = this.GetOrdinalFromBaseColName(columnname, qualifiedTableName.Table); 2276 if (ordinal == -1) { 2277 partialcolumnset = true; 2278 } 2279 else { 2280 // Add the column to the index column set 2281 if (ncols < 16) 2282 indexcolumnordinals[ncols++] = ordinal; 2283 else // Can't deal with indexes > 16 columns 2284 partialcolumnset = true; 2285 } 2286 } 2287 else { 2288 // We got a new index, save the previous index information 2289 if (!partialcolumnset && (ncols != 0)) { 2290 // Choose the unique index with least columns as primary key 2291 if ((npkcols == 0) || (npkcols > ncols)){ 2292 npkcols = ncols; 2293 for (int i = 0 ; i < ncols ; i++) 2294 pkcolumnordinals[i] = indexcolumnordinals[i]; 2295 } 2296 } 2297 // Reset the parameters for a new index 2298 ncols = 0; 2299 currentindexname = indexname; 2300 partialcolumnset = false; 2301 // Add this column to index 2302 ordinal = this.GetOrdinalFromBaseColName(columnname, qualifiedTableName.Table); 2303 if (ordinal == -1) { 2304 partialcolumnset = true; 2305 } 2306 else { 2307 // Add the column to the index column set 2308 indexcolumnordinals[ncols++] = ordinal; 2309 } 2310 } 2311 // Go on to the next column 2312 } 2313 // Do we have an index? 2314 if (!partialcolumnset && (ncols != 0)) { 2315 // Choose the unique index with least columns as primary key 2316 if ((npkcols == 0) || (npkcols > ncols)){ 2317 npkcols = ncols; 2318 for (int i = 0 ; i < ncols ; i++) 2319 pkcolumnordinals[i] = indexcolumnordinals[i]; 2320 } 2321 } 2322 // Mark the chosen index as primary key 2323 if (npkcols != 0) { 2324 for (int i = 0 ; i < npkcols ; i++) { 2325 indexordinal = pkcolumnordinals[i]; 2326 keyColumns++; 2327 this.metadata[indexordinal].isKeyColumn = true; 2328 // should we set isNullable = false? 2329 // This makes the QuikTest against Jet fail 2330 // 2331 // test test test - we don't know if this is nulalble or not so why do we want to set it to a value? 2332 this.metadata[indexordinal].isNullable = false; 2333 this.metadata[indexordinal].isUnique = true; 2334 if (this.metadata[indexordinal].baseTableName == null) { 2335 this.metadata[indexordinal].baseTableName = qualifiedTableName.Table; 2336 } 2337 if (this.metadata[indexordinal].baseColumnName == null) { 2338 this.metadata[indexordinal].baseColumnName = columnname; 2339 } 2340 } 2341 } 2342 // Unbind the columns 2343 _cmdWrapper.FreeKeyInfoStatementHandle(ODBC32.STMT.UNBIND); 2344 } 2345 finally { 2346 if (mustRelease) { 2347 buffer.DangerousRelease(); 2348 } 2349 } 2350 return keyColumns; 2351 } 2352 SameIndexColumn(String currentindexname, String indexname, int ordinal, int ncols)2353 internal bool SameIndexColumn(String currentindexname, String indexname, int ordinal, int ncols) 2354 { 2355 if (ADP.IsEmpty(currentindexname)){ 2356 return false; 2357 } 2358 if ((currentindexname == indexname) && 2359 (ordinal == ncols+1)) 2360 return true; 2361 return false; 2362 } 2363 GetOrdinalFromBaseColName(String columnname)2364 internal int GetOrdinalFromBaseColName(String columnname) { 2365 return GetOrdinalFromBaseColName(columnname, null); 2366 } 2367 GetOrdinalFromBaseColName(String columnname, String tablename)2368 internal int GetOrdinalFromBaseColName(String columnname, String tablename) 2369 { 2370 if (ADP.IsEmpty(columnname)) { 2371 return -1; 2372 } 2373 if (this.metadata != null) { 2374 int count = FieldCount; 2375 for (int i = 0 ; i < count ; i++) { 2376 if ( (this.metadata[i].baseColumnName != null) && 2377 (columnname == this.metadata[i].baseColumnName)) { 2378 if (!ADP.IsEmpty(tablename)) { 2379 if (tablename == this.metadata[i].baseTableName) { 2380 return i; 2381 } // end if 2382 } // end if 2383 else { 2384 return i; 2385 } // end else 2386 } 2387 } 2388 } 2389 // We can't find it in base column names, try regular colnames 2390 return this.IndexOf(columnname); 2391 } 2392 2393 // We try parsing the SQL statement to get the table name as a last resort when 2394 // the driver doesn't return this information back to us. 2395 // 2396 // we can't handle multiple tablenames (JOIN) 2397 // only the first tablename will be returned 2398 GetTableNameFromCommandText()2399 internal string GetTableNameFromCommandText() 2400 { 2401 if (command == null){ 2402 return null; 2403 } 2404 String localcmdtext = _cmdText; 2405 if (ADP.IsEmpty(localcmdtext)) { // fxcop 2406 return null; 2407 } 2408 String tablename; 2409 int idx; 2410 CStringTokenizer tokenstmt = new CStringTokenizer(localcmdtext, Connection.QuoteChar(ADP.GetSchemaTable)[0], Connection.EscapeChar(ADP.GetSchemaTable)); 2411 2412 if (tokenstmt.StartsWith("select") == true) { 2413 // select command, search for from clause 2414 idx = tokenstmt.FindTokenIndex("from"); 2415 } 2416 else { 2417 if ((tokenstmt.StartsWith("insert") == true) || 2418 (tokenstmt.StartsWith("update") == true) || 2419 (tokenstmt.StartsWith("delete") == true) ) { 2420 // Get the following word 2421 idx = tokenstmt.CurrentPosition; 2422 } 2423 else 2424 idx = -1; 2425 } 2426 if (idx == -1) 2427 return null; 2428 // The next token is the table name 2429 tablename = tokenstmt.NextToken(); 2430 2431 localcmdtext = tokenstmt.NextToken(); 2432 if ((localcmdtext.Length > 0) && (localcmdtext[0] == ',')) { 2433 return null; // can't handle multiple tables 2434 } 2435 if ((localcmdtext.Length == 2) && 2436 ((localcmdtext[0] == 'a') || (localcmdtext[0] == 'A')) && 2437 ((localcmdtext[1] == 's') || (localcmdtext[1] == 'S'))) { 2438 // aliased table, skip the alias name 2439 localcmdtext = tokenstmt.NextToken(); 2440 localcmdtext = tokenstmt.NextToken(); 2441 if ((localcmdtext.Length > 0) && (localcmdtext[0] == ',')) { 2442 return null; // Multiple tables 2443 } 2444 } 2445 return tablename; 2446 } 2447 SetBaseTableNames(QualifiedTableName qualifiedTableName)2448 internal void SetBaseTableNames(QualifiedTableName qualifiedTableName) 2449 { 2450 int count = FieldCount; 2451 2452 for(int i=0; i<count; i++) 2453 { 2454 if (metadata[i].baseTableName == null) { 2455 metadata[i].baseTableName = qualifiedTableName.Table; 2456 metadata[i].baseSchemaName = qualifiedTableName.Schema; 2457 metadata[i].baseCatalogName = qualifiedTableName.Catalog; 2458 } 2459 } 2460 return; 2461 } 2462 2463 sealed internal class QualifiedTableName { 2464 private string _catalogName; 2465 private string _schemaName; 2466 private string _tableName; 2467 private string _quotedTableName; 2468 private string _quoteChar; 2469 2470 internal string Catalog { 2471 get { 2472 return _catalogName; 2473 } 2474 } 2475 2476 internal string Schema { 2477 get { 2478 return _schemaName; 2479 } 2480 } 2481 2482 internal string Table { 2483 get { 2484 return _tableName; 2485 } 2486 set { 2487 _quotedTableName = value; 2488 _tableName = UnQuote(value); 2489 } 2490 } 2491 internal string QuotedTable { 2492 get { 2493 return _quotedTableName; 2494 } 2495 } GetTable(bool flag)2496 internal string GetTable(bool flag) { 2497 return (flag ? QuotedTable : Table); 2498 } QualifiedTableName(string quoteChar)2499 internal QualifiedTableName (string quoteChar) { 2500 _quoteChar = quoteChar; 2501 } QualifiedTableName(string quoteChar, string qualifiedname)2502 internal QualifiedTableName (string quoteChar, string qualifiedname) { 2503 _quoteChar = quoteChar; 2504 2505 string[] names = DbCommandBuilder.ParseProcedureName (qualifiedname, quoteChar, quoteChar); 2506 _catalogName = UnQuote(names[1]); 2507 _schemaName = UnQuote(names[2]); 2508 _quotedTableName = names[3]; 2509 _tableName = UnQuote(names[3]); 2510 } 2511 UnQuote(string str)2512 private string UnQuote (string str) { 2513 if ((str != null) && (str.Length > 0)) { 2514 char quoteChar = _quoteChar[0]; 2515 if (str[0] == quoteChar) { 2516 Debug.Assert (str.Length > 1, "Illegal string, only one char that is a quote"); 2517 Debug.Assert (str[str.Length-1] == quoteChar, "Missing quote at end of string that begins with quote"); 2518 if (str.Length > 1 && str[str.Length-1] == quoteChar) { 2519 str = str.Substring(1, str.Length-2); 2520 } 2521 } 2522 } 2523 return str; 2524 } 2525 } 2526 2527 sealed private class MetaData { 2528 2529 internal int ordinal; 2530 internal TypeMap typemap; 2531 2532 internal SQLLEN size; 2533 internal byte precision; 2534 internal byte scale; 2535 2536 internal bool isAutoIncrement; 2537 internal bool isUnique; 2538 internal bool isReadOnly; 2539 internal bool isNullable; 2540 internal bool isRowVersion; 2541 internal bool isLong; 2542 2543 internal bool isKeyColumn; 2544 internal string baseSchemaName; 2545 internal string baseCatalogName; 2546 internal string baseTableName; 2547 internal string baseColumnName; 2548 } 2549 } 2550 } 2551