Merge pull request #2353 from ludovic-henry/fix-servicemodel-15153
[mono.git] / mcs / class / Mono.Data.Sqlite / Mono.Data.Sqlite_2.0 / SQLiteDataReader.cs
1 /********************************************************\r
2  * ADO.NET 2.0 Data Provider for SQLite Version 3.X\r
3  * Written by Robert Simpson (robert@blackcastlesoft.com)\r
4  * \r
5  * Released to the public domain, use at your own risk!\r
6  ********************************************************/\r
7 \r
8 namespace Mono.Data.Sqlite\r
9 {\r
10   using System;\r
11   using System.Data;\r
12   using System.Data.Common;\r
13   using System.Collections.Generic;\r
14   using System.Globalization;\r
15   using System.Reflection;\r
16 \r
17   /// <summary>\r
18   /// SQLite implementation of DbDataReader.\r
19   /// </summary>\r
20   public sealed class SqliteDataReader : DbDataReader\r
21   {\r
22     /// <summary>\r
23     /// Underlying command this reader is attached to\r
24     /// </summary>\r
25     private SqliteCommand _command;\r
26     /// <summary>\r
27     /// Index of the current statement in the command being processed\r
28     /// </summary>\r
29     private int _activeStatementIndex;\r
30     /// <summary>\r
31     /// Current statement being Read()\r
32     /// </summary>\r
33     private SqliteStatement _activeStatement;\r
34     /// <summary>\r
35     /// State of the current statement being processed.\r
36     /// -1 = First Step() executed, so the first Read() will be ignored\r
37     ///  0 = Actively reading\r
38     ///  1 = Finished reading\r
39     ///  2 = Non-row-returning statement, no records\r
40     /// </summary>\r
41     private int _readingState;\r
42     /// <summary>\r
43     /// Number of records affected by the insert/update statements executed on the command\r
44     /// </summary>\r
45     private int _rowsAffected;\r
46     /// <summary>\r
47     /// Count of fields (columns) in the row-returning statement currently being processed\r
48     /// </summary>\r
49     private int _fieldCount;\r
50     /// <summary>\r
51     /// Datatypes of active fields (columns) in the current statement, used for type-restricting data\r
52     /// </summary>\r
53     private SQLiteType[] _fieldTypeArray;\r
54 \r
55     /// <summary>\r
56     /// The behavior of the datareader\r
57     /// </summary>\r
58     private CommandBehavior _commandBehavior;\r
59 \r
60     /// <summary>\r
61     /// If set, then dispose of the command object when the reader is finished\r
62     /// </summary>\r
63     internal bool _disposeCommand;\r
64 \r
65     /// <summary>\r
66     /// An array of rowid's for the active statement if CommandBehavior.KeyInfo is specified\r
67     /// </summary>\r
68     private SqliteKeyReader _keyInfo;\r
69 \r
70     internal long _version; // Matches the version of the connection\r
71 \r
72     /// <summary>\r
73     /// Internal constructor, initializes the datareader and sets up to begin executing statements\r
74     /// </summary>\r
75     /// <param name="cmd">The SqliteCommand this data reader is for</param>\r
76     /// <param name="behave">The expected behavior of the data reader</param>\r
77     internal SqliteDataReader(SqliteCommand cmd, CommandBehavior behave)\r
78     {\r
79       _command = cmd;\r
80       _version = _command.Connection._version;\r
81 \r
82       _commandBehavior = behave;\r
83       _activeStatementIndex = -1;\r
84       _activeStatement = null;\r
85       _rowsAffected = -1;\r
86       _fieldCount = 0;\r
87 \r
88       if (_command != null)\r
89         NextResult();\r
90     }\r
91 \r
92     internal void Cancel()\r
93     {\r
94       _version = 0;\r
95     }\r
96 \r
97     /// <summary>\r
98     /// Closes the datareader, potentially closing the connection as well if CommandBehavior.CloseConnection was specified.\r
99     /// </summary>\r
100     public override void Close()\r
101     {\r
102       try\r
103       {\r
104         if (_command != null)\r
105         {\r
106           try\r
107           {\r
108             try\r
109             {\r
110               // Make sure we've not been canceled\r
111               if (_version != 0)\r
112               {\r
113                 try\r
114                 {\r
115                   while (NextResult())\r
116                   {\r
117                   }\r
118                 }\r
119                 catch\r
120                 {\r
121                 }\r
122               }\r
123               _command.ClearDataReader();\r
124             }\r
125             finally\r
126             {\r
127               // If the datareader's behavior includes closing the connection, then do so here.\r
128               if ((_commandBehavior & CommandBehavior.CloseConnection) != 0 && _command.Connection != null) {\r
129                 // We need to call Dispose on the command before we call Dispose on the Connection,\r
130                 // otherwise we'll get a SQLITE_LOCKED exception.\r
131                 var conn = _command.Connection;\r
132                 _command.Dispose ();\r
133                 conn.Close();\r
134                 _disposeCommand = false;\r
135               }\r
136             }\r
137           }\r
138           finally\r
139           {\r
140             if (_disposeCommand)\r
141               _command.Dispose();\r
142           }\r
143         }\r
144 \r
145         _command = null;\r
146         _activeStatement = null;\r
147         _fieldTypeArray = null;\r
148       }\r
149       finally\r
150       {\r
151         if (_keyInfo != null)\r
152         {\r
153           _keyInfo.Dispose();\r
154           _keyInfo = null;\r
155         }\r
156       }\r
157     }\r
158 \r
159     /// <summary>\r
160     /// Throw an error if the datareader is closed\r
161     /// </summary>\r
162     private void CheckClosed()\r
163     {\r
164       if (_command == null)\r
165         throw new InvalidOperationException("DataReader has been closed");\r
166 \r
167       if (_version == 0)\r
168         throw new SqliteException((int)SQLiteErrorCode.Abort, "Execution was aborted by the user");\r
169 \r
170       if (_command.Connection.State != ConnectionState.Open || _command.Connection._version != _version)\r
171         throw new InvalidOperationException("Connection was closed, statement was terminated");\r
172     }\r
173 \r
174     /// <summary>\r
175     /// Throw an error if a row is not loaded\r
176     /// </summary>\r
177     private void CheckValidRow()\r
178     {\r
179       if (_readingState != 0)\r
180         throw new InvalidOperationException("No current row");\r
181     }\r
182 \r
183     /// <summary>\r
184     /// Enumerator support\r
185     /// </summary>\r
186     /// <returns>Returns a DbEnumerator object.</returns>\r
187     public override global::System.Collections.IEnumerator GetEnumerator()\r
188     {\r
189       return new DbEnumerator(this, ((_commandBehavior & CommandBehavior.CloseConnection) == CommandBehavior.CloseConnection));\r
190     }\r
191 \r
192     /// <summary>\r
193     /// Not implemented.  Returns 0\r
194     /// </summary>\r
195     public override int Depth\r
196     {\r
197       get\r
198       {\r
199         CheckClosed();\r
200         return 0;\r
201       }\r
202     }\r
203 \r
204     /// <summary>\r
205     /// Returns the number of columns in the current resultset\r
206     /// </summary>\r
207     public override int FieldCount\r
208     {\r
209       get\r
210       {\r
211         CheckClosed();\r
212         if (_keyInfo == null)\r
213           return _fieldCount;\r
214 \r
215         return _fieldCount + _keyInfo.Count;\r
216       }\r
217     }\r
218 \r
219     /// <summary>\r
220     /// Returns the number of visible fielsd in the current resultset\r
221     /// </summary>\r
222     public override int VisibleFieldCount\r
223     {\r
224       get\r
225       {\r
226         CheckClosed();\r
227         return _fieldCount;\r
228       }\r
229     }\r
230 \r
231     /// <summary>\r
232     /// SQLite is inherently un-typed.  All datatypes in SQLite are natively strings.  The definition of the columns of a table\r
233     /// and the affinity of returned types are all we have to go on to type-restrict data in the reader.\r
234     /// \r
235     /// This function attempts to verify that the type of data being requested of a column matches the datatype of the column.  In\r
236     /// the case of columns that are not backed into a table definition, we attempt to match up the affinity of a column (int, double, string or blob)\r
237     /// to a set of known types that closely match that affinity.  It's not an exact science, but its the best we can do.\r
238     /// </summary>\r
239     /// <returns>\r
240     /// This function throws an InvalidTypeCast() exception if the requested type doesn't match the column's definition or affinity.\r
241     /// </returns>\r
242     /// <param name="i">The index of the column to type-check</param>\r
243     /// <param name="typ">The type we want to get out of the column</param>\r
244     private TypeAffinity VerifyType(int i, DbType typ)\r
245     {\r
246       CheckClosed();\r
247       CheckValidRow();\r
248       TypeAffinity affinity = GetSQLiteType(i).Affinity;\r
249 \r
250       switch (affinity)\r
251       {\r
252         case TypeAffinity.Int64:\r
253           if (typ == DbType.Int16) return affinity;\r
254           if (typ == DbType.Int32) return affinity;\r
255           if (typ == DbType.Int64) return affinity;\r
256           if (typ == DbType.Boolean) return affinity;\r
257           if (typ == DbType.Byte) return affinity;\r
258           if (typ == DbType.DateTime) return affinity;\r
259           if (typ == DbType.Single) return affinity;\r
260           if (typ == DbType.Double) return affinity;\r
261           if (typ == DbType.Decimal) return affinity;\r
262           break;\r
263         case TypeAffinity.Double:\r
264           if (typ == DbType.Single) return affinity;\r
265           if (typ == DbType.Double) return affinity;\r
266           if (typ == DbType.Decimal) return affinity;\r
267           if (typ == DbType.DateTime) return affinity;\r
268           break;\r
269         case TypeAffinity.Text:\r
270           if (typ == DbType.SByte) return affinity;\r
271           if (typ == DbType.String) return affinity;\r
272           if (typ == DbType.SByte) return affinity;\r
273           if (typ == DbType.Guid) return affinity;\r
274           if (typ == DbType.DateTime) return affinity;\r
275           if (typ == DbType.Decimal) return affinity;\r
276           break;\r
277         case TypeAffinity.Blob:\r
278           if (typ == DbType.Guid) return affinity;\r
279           if (typ == DbType.String) return affinity;\r
280           if (typ == DbType.Binary) return affinity;\r
281           break;\r
282       }\r
283 \r
284       throw new InvalidCastException();\r
285     }\r
286 \r
287     /// <summary>\r
288     /// Retrieves the column as a boolean value\r
289     /// </summary>\r
290     /// <param name="i">The index of the column to retrieve</param>\r
291     /// <returns>bool</returns>\r
292     public override bool GetBoolean(int i)\r
293     {\r
294       if (i >= VisibleFieldCount && _keyInfo != null)\r
295         return _keyInfo.GetBoolean(i - VisibleFieldCount);\r
296 \r
297       VerifyType(i, DbType.Boolean);\r
298       return Convert.ToBoolean(GetValue(i), CultureInfo.CurrentCulture);\r
299     }\r
300 \r
301     /// <summary>\r
302     /// Retrieves the column as a single byte value\r
303     /// </summary>\r
304     /// <param name="i">The index of the column to retrieve</param>\r
305     /// <returns>byte</returns>\r
306     public override byte GetByte(int i)\r
307     {\r
308       if (i >= VisibleFieldCount && _keyInfo != null)\r
309         return _keyInfo.GetByte(i - VisibleFieldCount);\r
310 \r
311       VerifyType(i, DbType.Byte);\r
312       return Convert.ToByte(_activeStatement._sql.GetInt32(_activeStatement, i));\r
313     }\r
314 \r
315     /// <summary>\r
316     /// Retrieves a column as an array of bytes (blob)\r
317     /// </summary>\r
318     /// <param name="i">The index of the column to retrieve</param>\r
319     /// <param name="fieldOffset">The zero-based index of where to begin reading the data</param>\r
320     /// <param name="buffer">The buffer to write the bytes into</param>\r
321     /// <param name="bufferoffset">The zero-based index of where to begin writing into the array</param>\r
322     /// <param name="length">The number of bytes to retrieve</param>\r
323     /// <returns>The actual number of bytes written into the array</returns>\r
324     /// <remarks>\r
325     /// To determine the number of bytes in the column, pass a null value for the buffer.  The total length will be returned.\r
326     /// </remarks>\r
327     public override long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length)\r
328     {\r
329       if (i >= VisibleFieldCount && _keyInfo != null)\r
330         return _keyInfo.GetBytes(i - VisibleFieldCount, fieldOffset, buffer, bufferoffset, length);\r
331 \r
332       VerifyType(i, DbType.Binary);\r
333       return _activeStatement._sql.GetBytes(_activeStatement, i, (int)fieldOffset, buffer, bufferoffset, length);\r
334     }\r
335 \r
336     /// <summary>\r
337     /// Returns the column as a single character\r
338     /// </summary>\r
339     /// <param name="i">The index of the column to retrieve</param>\r
340     /// <returns>char</returns>\r
341     public override char GetChar(int i)\r
342     {\r
343       if (i >= VisibleFieldCount && _keyInfo != null)\r
344         return _keyInfo.GetChar(i - VisibleFieldCount);\r
345 \r
346       VerifyType(i, DbType.SByte);\r
347       return Convert.ToChar(_activeStatement._sql.GetInt32(_activeStatement, i));\r
348     }\r
349 \r
350     /// <summary>\r
351     /// Retrieves a column as an array of chars (blob)\r
352     /// </summary>\r
353     /// <param name="i">The index of the column to retrieve</param>\r
354     /// <param name="fieldoffset">The zero-based index of where to begin reading the data</param>\r
355     /// <param name="buffer">The buffer to write the characters into</param>\r
356     /// <param name="bufferoffset">The zero-based index of where to begin writing into the array</param>\r
357     /// <param name="length">The number of bytes to retrieve</param>\r
358     /// <returns>The actual number of characters written into the array</returns>\r
359     /// <remarks>\r
360     /// To determine the number of characters in the column, pass a null value for the buffer.  The total length will be returned.\r
361     /// </remarks>\r
362     public override long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length)\r
363     {\r
364       if (i >= VisibleFieldCount && _keyInfo != null)\r
365         return _keyInfo.GetChars(i - VisibleFieldCount, fieldoffset, buffer, bufferoffset, length);\r
366 \r
367       VerifyType(i, DbType.String);\r
368       return _activeStatement._sql.GetChars(_activeStatement, i, (int)fieldoffset, buffer, bufferoffset, length);\r
369     }\r
370 \r
371     /// <summary>\r
372     /// Retrieves the name of the back-end datatype of the column\r
373     /// </summary>\r
374     /// <param name="i">The index of the column to retrieve</param>\r
375     /// <returns>string</returns>\r
376     public override string GetDataTypeName(int i)\r
377     {\r
378       if (i >= VisibleFieldCount && _keyInfo != null)\r
379         return _keyInfo.GetDataTypeName(i - VisibleFieldCount);\r
380 \r
381       SQLiteType typ = GetSQLiteType(i);\r
382       return _activeStatement._sql.ColumnType(_activeStatement, i, out typ.Affinity);\r
383     }\r
384 \r
385     /// <summary>\r
386     /// Retrieve the column as a date/time value\r
387     /// </summary>\r
388     /// <param name="i">The index of the column to retrieve</param>\r
389     /// <returns>DateTime</returns>\r
390     public override DateTime GetDateTime(int i)\r
391     {\r
392       if (i >= VisibleFieldCount && _keyInfo != null)\r
393         return _keyInfo.GetDateTime(i - VisibleFieldCount);\r
394 \r
395       VerifyType(i, DbType.DateTime);\r
396       return _activeStatement._sql.GetDateTime(_activeStatement, i);\r
397     }\r
398 \r
399     /// <summary>\r
400     /// Retrieve the column as a decimal value\r
401     /// </summary>\r
402     /// <param name="i">The index of the column to retrieve</param>\r
403     /// <returns>decimal</returns>\r
404     public override decimal GetDecimal(int i)\r
405     {\r
406       if (i >= VisibleFieldCount && _keyInfo != null)\r
407         return _keyInfo.GetDecimal(i - VisibleFieldCount);\r
408 \r
409       VerifyType(i, DbType.Decimal);\r
410       return Decimal.Parse(_activeStatement._sql.GetText(_activeStatement, i), NumberStyles.AllowDecimalPoint | NumberStyles.AllowExponent  | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture);\r
411     }\r
412 \r
413     /// <summary>\r
414     /// Returns the column as a double\r
415     /// </summary>\r
416     /// <param name="i">The index of the column to retrieve</param>\r
417     /// <returns>double</returns>\r
418     public override double GetDouble(int i)\r
419     {\r
420       if (i >= VisibleFieldCount && _keyInfo != null)\r
421         return _keyInfo.GetDouble(i - VisibleFieldCount);\r
422 \r
423       VerifyType(i, DbType.Double);\r
424       return _activeStatement._sql.GetDouble(_activeStatement, i);\r
425     }\r
426 \r
427     /// <summary>\r
428     /// Returns the .NET type of a given column\r
429     /// </summary>\r
430     /// <param name="i">The index of the column to retrieve</param>\r
431     /// <returns>Type</returns>\r
432     public override Type GetFieldType(int i)\r
433     {\r
434       if (i >= VisibleFieldCount && _keyInfo != null)\r
435         return _keyInfo.GetFieldType(i - VisibleFieldCount);\r
436 \r
437       return SqliteConvert.SQLiteTypeToType(GetSQLiteType(i));\r
438     }\r
439 \r
440     /// <summary>\r
441     /// Returns a column as a float value\r
442     /// </summary>\r
443     /// <param name="i">The index of the column to retrieve</param>\r
444     /// <returns>float</returns>\r
445     public override float GetFloat(int i)\r
446     {\r
447       if (i >= VisibleFieldCount && _keyInfo != null)\r
448         return _keyInfo.GetFloat(i - VisibleFieldCount);\r
449 \r
450       VerifyType(i, DbType.Single);\r
451       return Convert.ToSingle(_activeStatement._sql.GetDouble(_activeStatement, i));\r
452     }\r
453 \r
454     /// <summary>\r
455     /// Returns the column as a Guid\r
456     /// </summary>\r
457     /// <param name="i">The index of the column to retrieve</param>\r
458     /// <returns>Guid</returns>\r
459     public override Guid GetGuid(int i)\r
460     {\r
461       if (i >= VisibleFieldCount && _keyInfo != null)\r
462         return _keyInfo.GetGuid(i - VisibleFieldCount);\r
463 \r
464       TypeAffinity affinity = VerifyType(i, DbType.Guid);\r
465       if (affinity == TypeAffinity.Blob)\r
466       {\r
467         byte[] buffer = new byte[16];\r
468         _activeStatement._sql.GetBytes(_activeStatement, i, 0, buffer, 0, 16);\r
469         return new Guid(buffer);\r
470       }\r
471       else\r
472         return new Guid(_activeStatement._sql.GetText(_activeStatement, i));\r
473     }\r
474 \r
475     /// <summary>\r
476     /// Returns the column as a short\r
477     /// </summary>\r
478     /// <param name="i">The index of the column to retrieve</param>\r
479     /// <returns>Int16</returns>\r
480     public override Int16 GetInt16(int i)\r
481     {\r
482       if (i >= VisibleFieldCount && _keyInfo != null)\r
483         return _keyInfo.GetInt16(i - VisibleFieldCount);\r
484 \r
485       VerifyType(i, DbType.Int16);\r
486       return Convert.ToInt16(_activeStatement._sql.GetInt32(_activeStatement, i));\r
487     }\r
488 \r
489     /// <summary>\r
490     /// Retrieves the column as an int\r
491     /// </summary>\r
492     /// <param name="i">The index of the column to retrieve</param>\r
493     /// <returns>Int32</returns>\r
494     public override Int32 GetInt32(int i)\r
495     {\r
496       if (i >= VisibleFieldCount && _keyInfo != null)\r
497         return _keyInfo.GetInt32(i - VisibleFieldCount);\r
498 \r
499       VerifyType(i, DbType.Int32);\r
500       return _activeStatement._sql.GetInt32(_activeStatement, i);\r
501     }\r
502 \r
503     /// <summary>\r
504     /// Retrieves the column as a long\r
505     /// </summary>\r
506     /// <param name="i">The index of the column to retrieve</param>\r
507     /// <returns>Int64</returns>\r
508     public override Int64 GetInt64(int i)\r
509     {\r
510       if (i >= VisibleFieldCount && _keyInfo != null)\r
511         return _keyInfo.GetInt64(i - VisibleFieldCount);\r
512 \r
513       VerifyType(i, DbType.Int64);\r
514       return _activeStatement._sql.GetInt64(_activeStatement, i);\r
515     }\r
516 \r
517     /// <summary>\r
518     /// Retrieves the name of the column\r
519     /// </summary>\r
520     /// <param name="i">The index of the column to retrieve</param>\r
521     /// <returns>string</returns>\r
522     public override string GetName(int i)\r
523     {\r
524       if (i >= VisibleFieldCount && _keyInfo != null)\r
525         return _keyInfo.GetName(i - VisibleFieldCount);\r
526 \r
527       return _activeStatement._sql.ColumnName(_activeStatement, i);\r
528     }\r
529 \r
530     /// <summary>\r
531     /// Retrieves the i of a column, given its name\r
532     /// </summary>\r
533     /// <param name="name">The name of the column to retrieve</param>\r
534     /// <returns>The int i of the column</returns>\r
535     public override int GetOrdinal(string name)\r
536     {\r
537       CheckClosed();\r
538       int r = _activeStatement._sql.ColumnIndex(_activeStatement, name);\r
539       if (r == -1 && _keyInfo != null)\r
540       {\r
541         r = _keyInfo.GetOrdinal(name);\r
542         if (r > -1) r += VisibleFieldCount;\r
543       }\r
544 \r
545       return r;\r
546     }\r
547 \r
548     /// <summary>\r
549     /// Schema information in SQLite is difficult to map into .NET conventions, so a lot of work must be done\r
550     /// to gather the necessary information so it can be represented in an ADO.NET manner.\r
551     /// </summary>\r
552     /// <returns>Returns a DataTable containing the schema information for the active SELECT statement being processed.</returns>\r
553     public override DataTable GetSchemaTable()\r
554     {\r
555       return GetSchemaTable(true, false);\r
556     }\r
557 \r
558     static bool hasColumnMetadataSupport = true;
559
560     internal DataTable GetSchemaTable(bool wantUniqueInfo, bool wantDefaultValue)\r
561     {\r
562       CheckClosed();\r
563 \r
564       DataTable tbl = new DataTable("SchemaTable");\r
565       DataTable tblIndexes = null;\r
566       DataTable tblIndexColumns;\r
567       DataRow row;\r
568       string temp;\r
569       string strCatalog = "";\r
570       string strTable = "";\r
571       string strColumn = "";\r
572 \r
573       tbl.Locale = CultureInfo.InvariantCulture;\r
574       tbl.Columns.Add(SchemaTableColumn.ColumnName, typeof(String));\r
575       tbl.Columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(int));\r
576       tbl.Columns.Add(SchemaTableColumn.ColumnSize, typeof(int));\r
577       tbl.Columns.Add(SchemaTableColumn.NumericPrecision, typeof(short));\r
578       tbl.Columns.Add(SchemaTableColumn.NumericScale, typeof(short));\r
579       tbl.Columns.Add(SchemaTableColumn.IsUnique, typeof(Boolean));\r
580       tbl.Columns.Add(SchemaTableColumn.IsKey, typeof(Boolean));\r
581       tbl.Columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(string));\r
582       tbl.Columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(String));\r
583       tbl.Columns.Add(SchemaTableColumn.BaseColumnName, typeof(String));\r
584       tbl.Columns.Add(SchemaTableColumn.BaseSchemaName, typeof(String));\r
585       tbl.Columns.Add(SchemaTableColumn.BaseTableName, typeof(String));\r
586       tbl.Columns.Add(SchemaTableColumn.DataType, typeof(Type));\r
587       tbl.Columns.Add(SchemaTableColumn.AllowDBNull, typeof(Boolean));\r
588       tbl.Columns.Add(SchemaTableColumn.ProviderType, typeof(int));\r
589       tbl.Columns.Add(SchemaTableColumn.IsAliased, typeof(Boolean));\r
590       tbl.Columns.Add(SchemaTableColumn.IsExpression, typeof(Boolean));\r
591       tbl.Columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(Boolean));\r
592       tbl.Columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(Boolean));\r
593       tbl.Columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(Boolean));\r
594       tbl.Columns.Add(SchemaTableColumn.IsLong, typeof(Boolean));\r
595       tbl.Columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(Boolean));\r
596       tbl.Columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(Type));\r
597       tbl.Columns.Add(SchemaTableOptionalColumn.DefaultValue, typeof(object));\r
598       tbl.Columns.Add("DataTypeName", typeof(string));\r
599       tbl.Columns.Add("CollationType", typeof(string));\r
600       tbl.BeginLoadData();\r
601 \r
602       for (int n = 0; n < _fieldCount; n++)\r
603       {\r
604         row = tbl.NewRow();\r
605 \r
606         DbType typ = GetSQLiteType(n).Type;\r
607 \r
608         // Default settings for the column\r
609         row[SchemaTableColumn.ColumnName] = GetName(n);\r
610         row[SchemaTableColumn.ColumnOrdinal] = n;\r
611         row[SchemaTableColumn.ColumnSize] = SqliteConvert.DbTypeToColumnSize(typ);\r
612         row[SchemaTableColumn.NumericPrecision] = SqliteConvert.DbTypeToNumericPrecision(typ);\r
613         row[SchemaTableColumn.NumericScale] = SqliteConvert.DbTypeToNumericScale(typ);\r
614         row[SchemaTableColumn.ProviderType] = GetSQLiteType(n).Type;\r
615         row[SchemaTableColumn.IsLong] = false;\r
616         row[SchemaTableColumn.AllowDBNull] = true;\r
617         row[SchemaTableOptionalColumn.IsReadOnly] = false;\r
618         row[SchemaTableOptionalColumn.IsRowVersion] = false;\r
619         row[SchemaTableColumn.IsUnique] = false;\r
620         row[SchemaTableColumn.IsKey] = false;\r
621         row[SchemaTableOptionalColumn.IsAutoIncrement] = false;\r
622         row[SchemaTableColumn.DataType] = GetFieldType(n);\r
623         row[SchemaTableOptionalColumn.IsHidden] = false;\r
624 \r
625         // HACK: Prevent exploding if Sqlite was built without the SQLITE_ENABLE_COLUMN_METADATA option.
626         //
627         // This code depends on sqlite3_column_origin_name, which only exists if Sqlite was built with
628         // the SQLITE_ENABLE_COLUMN_METADATA option. This is not the case on iOS, MacOS or (most?)
629         // Androids, so we exclude it from the MONOTOUCH build, and degrade on other systems by simply
630         //  omitting the metadata from the result.
631         //
632         // TODO: we could implement better fallbacks as proposed in
633         // https://bugzilla.xamarin.com/show_bug.cgi?id=2128
634         //
635 #if !MONOTOUCH
636         if (hasColumnMetadataSupport) {
637             try {
638                 strColumn = _command.Connection._sql.ColumnOriginalName(_activeStatement, n);
639                 if (String.IsNullOrEmpty(strColumn) == false) row[SchemaTableColumn.BaseColumnName] = strColumn;
640
641                 row[SchemaTableColumn.IsExpression] = String.IsNullOrEmpty(strColumn);
642                 row[SchemaTableColumn.IsAliased] = (String.Compare(GetName(n), strColumn, true, CultureInfo.InvariantCulture) != 0);
643
644                 temp = _command.Connection._sql.ColumnTableName(_activeStatement, n);
645                 if (String.IsNullOrEmpty(temp) == false) row[SchemaTableColumn.BaseTableName] = temp;
646
647                 temp = _command.Connection._sql.ColumnDatabaseName(_activeStatement, n);
648                 if (String.IsNullOrEmpty(temp) == false) row[SchemaTableOptionalColumn.BaseCatalogName] = temp;
649             } catch (EntryPointNotFoundException) {
650                 hasColumnMetadataSupport = false;
651             }
652         }
653 #endif\r
654 \r
655         string dataType = null;\r
656         // If we have a table-bound column, extract the extra information from it\r
657         if (String.IsNullOrEmpty(strColumn) == false)\r
658         {\r
659           string collSeq;\r
660           bool bNotNull;\r
661           bool bPrimaryKey;\r
662           bool bAutoIncrement;\r
663           string[] arSize;\r
664 \r
665           // Get the column meta data\r
666           _command.Connection._sql.ColumnMetaData(\r
667             (string)row[SchemaTableOptionalColumn.BaseCatalogName],\r
668             (string)row[SchemaTableColumn.BaseTableName],\r
669             strColumn,\r
670             out dataType, out collSeq, out bNotNull, out bPrimaryKey, out bAutoIncrement);\r
671 \r
672           if (bNotNull || bPrimaryKey) row[SchemaTableColumn.AllowDBNull] = false;\r
673 \r
674           row[SchemaTableColumn.IsKey] = bPrimaryKey;\r
675           row[SchemaTableOptionalColumn.IsAutoIncrement] = bAutoIncrement;\r
676           row["CollationType"] = collSeq;\r
677 \r
678           // For types like varchar(50) and such, extract the size\r
679           arSize = dataType.Split('(');\r
680           if (arSize.Length > 1)\r
681           {\r
682             dataType = arSize[0];\r
683             arSize = arSize[1].Split(')');\r
684             if (arSize.Length > 1)\r
685             {\r
686               arSize = arSize[0].Split(',', '.');\r
687               if (GetSQLiteType(n).Type == DbType.String || GetSQLiteType(n).Type == DbType.Binary)\r
688               {\r
689                 row[SchemaTableColumn.ColumnSize] = Convert.ToInt32(arSize[0], CultureInfo.InvariantCulture);\r
690               }\r
691               else\r
692               {\r
693                 row[SchemaTableColumn.NumericPrecision] = Convert.ToInt32(arSize[0], CultureInfo.InvariantCulture);\r
694                 if (arSize.Length > 1)\r
695                   row[SchemaTableColumn.NumericScale] = Convert.ToInt32(arSize[1], CultureInfo.InvariantCulture);\r
696               }\r
697             }\r
698           }\r
699 \r
700           if (wantDefaultValue)\r
701           {\r
702             // Determine the default value for the column, which sucks because we have to query the schema for each column\r
703             using (SqliteCommand cmdTable = new SqliteCommand(String.Format(CultureInfo.InvariantCulture, "PRAGMA [{0}].TABLE_INFO([{1}])",\r
704               row[SchemaTableOptionalColumn.BaseCatalogName],\r
705               row[SchemaTableColumn.BaseTableName]\r
706               ), _command.Connection))\r
707             using (DbDataReader rdTable = cmdTable.ExecuteReader())\r
708             {\r
709               // Find the matching column\r
710               while (rdTable.Read())\r
711               {\r
712                 if (String.Compare((string)row[SchemaTableColumn.BaseColumnName], rdTable.GetString(1), true, CultureInfo.InvariantCulture) == 0)\r
713                 {\r
714                   if (rdTable.IsDBNull(4) == false)\r
715                     row[SchemaTableOptionalColumn.DefaultValue] = rdTable[4];\r
716 \r
717                   break;\r
718                 }\r
719               }\r
720             }\r
721           }\r
722 \r
723           // Determine IsUnique properly, which is a pain in the butt!\r
724           if (wantUniqueInfo)\r
725           {\r
726             if ((string)row[SchemaTableOptionalColumn.BaseCatalogName] != strCatalog\r
727               || (string)row[SchemaTableColumn.BaseTableName] != strTable)\r
728             {\r
729               strCatalog = (string)row[SchemaTableOptionalColumn.BaseCatalogName];\r
730               strTable = (string)row[SchemaTableColumn.BaseTableName];\r
731 \r
732               tblIndexes = _command.Connection.GetSchema("Indexes", new string[] {\r
733                 (string)row[SchemaTableOptionalColumn.BaseCatalogName],\r
734                 null,\r
735                 (string)row[SchemaTableColumn.BaseTableName],\r
736                 null });\r
737             }\r
738 \r
739             foreach (DataRow rowIndexes in tblIndexes.Rows)\r
740             {\r
741               tblIndexColumns = _command.Connection.GetSchema("IndexColumns", new string[] {\r
742                 (string)row[SchemaTableOptionalColumn.BaseCatalogName],\r
743                 null,\r
744                 (string)row[SchemaTableColumn.BaseTableName],\r
745                 (string)rowIndexes["INDEX_NAME"],\r
746                 null\r
747                 });\r
748               foreach (DataRow rowColumnIndex in tblIndexColumns.Rows)\r
749               {\r
750                 if (String.Compare((string)rowColumnIndex["COLUMN_NAME"], strColumn, true, CultureInfo.InvariantCulture) == 0)\r
751                 {\r
752                   if (tblIndexColumns.Rows.Count == 1 && (bool)row[SchemaTableColumn.AllowDBNull] == false)\r
753                     row[SchemaTableColumn.IsUnique] = rowIndexes["UNIQUE"];\r
754 \r
755                   // If its an integer primary key and the only primary key in the table, then its a rowid alias and is autoincrement\r
756                   // NOTE:  Currently commented out because this is not always the desired behavior.  For example, a 1:1 relationship with\r
757                   //        another table, where the other table is autoincrement, but this one is not, and uses the rowid from the other.\r
758                   //        It is safer to only set Autoincrement on tables where we're SURE the user specified AUTOINCREMENT, even if its a rowid column.\r
759 \r
760                   if (tblIndexColumns.Rows.Count == 1 && (bool)rowIndexes["PRIMARY_KEY"] == true && String.IsNullOrEmpty(dataType) == false &&\r
761                     String.Compare(dataType, "integer", true, CultureInfo.InvariantCulture) == 0)\r
762                   {\r
763                     //  row[SchemaTableOptionalColumn.IsAutoIncrement] = true;\r
764                   }\r
765 \r
766                   break;\r
767                 }\r
768               }\r
769             }\r
770           }\r
771 \r
772           if (String.IsNullOrEmpty(dataType))\r
773           {\r
774             TypeAffinity affin;\r
775             dataType = _activeStatement._sql.ColumnType(_activeStatement, n, out affin);\r
776           }\r
777 \r
778           if (String.IsNullOrEmpty(dataType) == false)\r
779             row["DataTypeName"] = dataType;\r
780         }\r
781         tbl.Rows.Add(row);\r
782       }\r
783 \r
784       if (_keyInfo != null)\r
785         _keyInfo.AppendSchemaTable(tbl);\r
786 \r
787       tbl.AcceptChanges();\r
788       tbl.EndLoadData();\r
789 \r
790       return tbl;\r
791     }\r
792 \r
793     /// <summary>\r
794     /// Retrieves the column as a string\r
795     /// </summary>\r
796     /// <param name="i">The index of the column to retrieve</param>\r
797     /// <returns>string</returns>\r
798     public override string GetString(int i)\r
799     {\r
800       if (i >= VisibleFieldCount && _keyInfo != null)\r
801         return _keyInfo.GetString(i - VisibleFieldCount);\r
802 \r
803       VerifyType(i, DbType.String);\r
804       return _activeStatement._sql.GetText(_activeStatement, i);\r
805     }\r
806 \r
807     /// <summary>\r
808     /// Retrieves the column as an object corresponding to the underlying datatype of the column\r
809     /// </summary>\r
810     /// <param name="i">The index of the column to retrieve</param>\r
811     /// <returns>object</returns>\r
812     public override object GetValue(int i)\r
813     {\r
814       if (i >= VisibleFieldCount && _keyInfo != null)\r
815         return _keyInfo.GetValue(i - VisibleFieldCount);\r
816 \r
817       SQLiteType typ = GetSQLiteType(i);\r
818 \r
819       return _activeStatement._sql.GetValue(_activeStatement, i, typ);\r
820     }\r
821 \r
822     /// <summary>\r
823     /// Retreives the values of multiple columns, up to the size of the supplied array\r
824     /// </summary>\r
825     /// <param name="values">The array to fill with values from the columns in the current resultset</param>\r
826     /// <returns>The number of columns retrieved</returns>\r
827     public override int GetValues(object[] values)\r
828     {\r
829       int nMax = FieldCount;\r
830       if (values.Length < nMax) nMax = values.Length;\r
831 \r
832       for (int n = 0; n < nMax; n++)\r
833       {\r
834         values[n] = GetValue(n);\r
835       }\r
836 \r
837       return nMax;\r
838     }\r
839 \r
840     /// <summary>\r
841     /// Returns True if the resultset has rows that can be fetched\r
842     /// </summary>\r
843     public override bool HasRows\r
844     {\r
845       get\r
846       {\r
847         CheckClosed();\r
848         return (_readingState != 1);\r
849       }\r
850     }\r
851 \r
852     /// <summary>\r
853     /// Returns True if the data reader is closed\r
854     /// </summary>\r
855     public override bool IsClosed\r
856     {\r
857       get { return (_command == null); }\r
858     }\r
859 \r
860     /// <summary>\r
861     /// Returns True if the specified column is null\r
862     /// </summary>\r
863     /// <param name="i">The index of the column to retrieve</param>\r
864     /// <returns>True or False</returns>\r
865     public override bool IsDBNull(int i)\r
866     {\r
867       if (i >= VisibleFieldCount && _keyInfo != null)\r
868         return _keyInfo.IsDBNull(i - VisibleFieldCount);\r
869 \r
870       return _activeStatement._sql.IsNull(_activeStatement, i);\r
871     }\r
872 \r
873     /// <summary>\r
874     /// Moves to the next resultset in multiple row-returning SQL command.\r
875     /// </summary>\r
876     /// <returns>True if the command was successful and a new resultset is available, False otherwise.</returns>\r
877     public override bool NextResult()\r
878     {\r
879       CheckClosed();\r
880 \r
881       SqliteStatement stmt = null;\r
882       int fieldCount;\r
883 \r
884       while (true)\r
885       {\r
886         if (_activeStatement != null && stmt == null)\r
887         {\r
888           // Reset the previously-executed statement\r
889           _activeStatement._sql.Reset(_activeStatement);\r
890 \r
891           // If we're only supposed to return a single rowset, step through all remaining statements once until\r
892           // they are all done and return false to indicate no more resultsets exist.\r
893           if ((_commandBehavior & CommandBehavior.SingleResult) != 0)\r
894           {\r
895             for (; ; )\r
896             {\r
897               stmt = _command.GetStatement(_activeStatementIndex + 1);\r
898               if (stmt == null) break;\r
899               _activeStatementIndex++;\r
900 \r
901               stmt._sql.Step(stmt);\r
902               if (stmt._sql.ColumnCount(stmt) == 0)\r
903               {\r
904                 if (_rowsAffected == -1) _rowsAffected = 0;\r
905                 _rowsAffected += stmt._sql.Changes;\r
906               }\r
907               stmt._sql.Reset(stmt); // Gotta reset after every step to release any locks and such!\r
908             }\r
909             return false;\r
910           }\r
911         }\r
912 \r
913         // Get the next statement to execute\r
914         stmt = _command.GetStatement(_activeStatementIndex + 1);\r
915 \r
916         // If we've reached the end of the statements, return false, no more resultsets\r
917         if (stmt == null)\r
918           return false;\r
919 \r
920         // If we were on a current resultset, set the state to "done reading" for it\r
921         if (_readingState < 1)\r
922           _readingState = 1;\r
923 \r
924         _activeStatementIndex++;\r
925 \r
926         fieldCount = stmt._sql.ColumnCount(stmt);\r
927 \r
928         // If the statement is not a select statement or we're not retrieving schema only, then perform the initial step\r
929         if ((_commandBehavior & CommandBehavior.SchemaOnly) == 0 || fieldCount == 0)\r
930         {\r
931           if (stmt._sql.Step(stmt))\r
932           {\r
933             _readingState = -1;\r
934           }\r
935           else if (fieldCount == 0) // No rows returned, if fieldCount is zero, skip to the next statement\r
936           {\r
937             if (_rowsAffected == -1) _rowsAffected = 0;\r
938             _rowsAffected += stmt._sql.Changes;\r
939             stmt._sql.Reset(stmt);\r
940             continue; // Skip this command and move to the next, it was not a row-returning resultset\r
941           }\r
942           else // No rows, fieldCount is non-zero so stop here\r
943           {\r
944             _readingState = 1; // This command returned columns but no rows, so return true, but HasRows = false and Read() returns false\r
945           }\r
946         }\r
947 \r
948         // Ahh, we found a row-returning resultset eligible to be returned!\r
949         _activeStatement = stmt;\r
950         _fieldCount = fieldCount;\r
951         _fieldTypeArray = null;\r
952 \r
953         if ((_commandBehavior & CommandBehavior.KeyInfo) != 0)\r
954           LoadKeyInfo();\r
955 \r
956         return true;\r
957       }\r
958     }\r
959 \r
960     /// <summary>\r
961     /// Retrieves the SQLiteType for a given column, and caches it to avoid repetetive interop calls.\r
962     /// </summary>\r
963     /// <param name="i">The index of the column to retrieve</param>\r
964     /// <returns>A SQLiteType structure</returns>\r
965     private SQLiteType GetSQLiteType(int i)\r
966     {\r
967       SQLiteType typ;\r
968 \r
969       // Initialize the field types array if not already initialized\r
970       if (_fieldTypeArray == null)\r
971         _fieldTypeArray = new SQLiteType[VisibleFieldCount];\r
972 \r
973       // Initialize this column's field type instance\r
974       if (_fieldTypeArray[i] == null) _fieldTypeArray[i] = new SQLiteType();\r
975 \r
976       typ = _fieldTypeArray[i];\r
977 \r
978       // If not initialized, then fetch the declared column datatype and attempt to convert it \r
979       // to a known DbType.\r
980       if (typ.Affinity == TypeAffinity.Uninitialized)\r
981         typ.Type = SqliteConvert.TypeNameToDbType(_activeStatement._sql.ColumnType(_activeStatement, i, out typ.Affinity));\r
982       else\r
983         typ.Affinity = _activeStatement._sql.ColumnAffinity(_activeStatement, i);\r
984 \r
985       return typ;\r
986     }\r
987 \r
988     /// <summary>\r
989     /// Reads the next row from the resultset\r
990     /// </summary>\r
991     /// <returns>True if a new row was successfully loaded and is ready for processing</returns>\r
992     public override bool Read()\r
993     {\r
994       CheckClosed();\r
995 \r
996       if (_readingState == -1) // First step was already done at the NextResult() level, so don't step again, just return true.\r
997       {\r
998         _readingState = 0;\r
999         return true;\r
1000       }\r
1001       else if (_readingState == 0) // Actively reading rows\r
1002       {\r
1003         // Don't read a new row if the command behavior dictates SingleRow.  We've already read the first row.\r
1004         if ((_commandBehavior & CommandBehavior.SingleRow) == 0)\r
1005         {\r
1006           if (_activeStatement._sql.Step(_activeStatement) == true)\r
1007           {\r
1008             if (_keyInfo != null)\r
1009               _keyInfo.Reset();\r
1010 \r
1011             return true;\r
1012           }\r
1013         }\r
1014 \r
1015         _readingState = 1; // Finished reading rows\r
1016       }\r
1017 \r
1018       return false;\r
1019     }\r
1020 \r
1021     /// <summary>\r
1022     /// Retrieve the count of records affected by an update/insert command.  Only valid once the data reader is closed!\r
1023     /// </summary>\r
1024     public override int RecordsAffected\r
1025     {\r
1026       get { return (_rowsAffected < 0) ? 0 : _rowsAffected; }\r
1027     }\r
1028 \r
1029     /// <summary>\r
1030     /// Indexer to retrieve data from a column given its name\r
1031     /// </summary>\r
1032     /// <param name="name">The name of the column to retrieve data for</param>\r
1033     /// <returns>The value contained in the column</returns>\r
1034     public override object this[string name]\r
1035     {\r
1036       get { return GetValue(GetOrdinal(name)); }\r
1037     }\r
1038 \r
1039     /// <summary>\r
1040     /// Indexer to retrieve data from a column given its i\r
1041     /// </summary>\r
1042     /// <param name="i">The index of the column to retrieve</param>\r
1043     /// <returns>The value contained in the column</returns>\r
1044     public override object this[int i]\r
1045     {\r
1046       get { return GetValue(i); }\r
1047     }\r
1048 \r
1049     private void LoadKeyInfo()\r
1050     {\r
1051       if (_keyInfo != null)\r
1052         _keyInfo.Dispose();\r
1053 \r
1054       _keyInfo = new SqliteKeyReader(_command.Connection, this, _activeStatement);\r
1055     }\r
1056   }\r
1057 }\r