New tests, and fixed buffer mcs -> gmcs switch.
[mono.git] / mcs / class / System.Data / System.Data.Common / DbCommandBuilder.cs
1 //
2 // System.Data.Common.DbCommandBuilder
3 //
4 // Author:
5 //   Tim Coleman (tim@timcoleman.com)
6 //
7 // Copyright (C) Tim Coleman, 2003
8 //
9
10 //
11 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
12 //
13 // Permission is hereby granted, free of charge, to any person obtaining
14 // a copy of this software and associated documentation files (the
15 // "Software"), to deal in the Software without restriction, including
16 // without limitation the rights to use, copy, modify, merge, publish,
17 // distribute, sublicense, and/or sell copies of the Software, and to
18 // permit persons to whom the Software is furnished to do so, subject to
19 // the following conditions:
20 // 
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
23 // 
24 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
27 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
28 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
29 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
30 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
31 //
32
33 #if NET_2_0 || TARGET_JVM
34
35 using System.ComponentModel;
36 using System.Data;
37 using System.Text;
38
39 namespace System.Data.Common {
40         public abstract class DbCommandBuilder : Component
41         {
42                 bool _setAllValues = false;
43                 bool _disposed = false;
44
45                 DataTable _dbSchemaTable;
46                 DbDataAdapter _dbDataAdapter = null;
47                 private CatalogLocation _catalogLocation = CatalogLocation.Start;
48                 private ConflictOption _conflictOption;
49
50                 private string _tableName;
51                 private string _catalogSeperator = ".";
52                 private string _quotePrefix;
53                 private string _quoteSuffix;
54                 private string _schemaSeperator = ".";
55                 private DbCommand _dbCommand = null;
56
57                 // Used to construct WHERE clauses
58                 static readonly string clause1 = "({0} = 1 AND {1} IS NULL)";
59                 static readonly string clause2 = "({0} = {1})";
60
61                 DbCommand _deleteCommand;
62                 DbCommand _insertCommand;
63                 DbCommand _updateCommand;
64
65                 #region Constructors
66
67                 protected DbCommandBuilder ()
68                 {
69                 }
70
71                 #endregion // Constructors
72
73                 #region Properties
74
75                 private void BuildCache (bool closeConnection)
76                 {
77                         DbCommand sourceCommand = SourceCommand;
78                         if (sourceCommand == null)
79                                 throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
80                         DbConnection connection = sourceCommand.Connection;
81                         if (connection == null)
82                                 throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
83
84                         if (_dbSchemaTable == null) {
85                                 if (connection.State == ConnectionState.Open)
86                                         closeConnection = false;        
87                                 else
88                                         connection.Open ();
89         
90                                 DbDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
91                                 _dbSchemaTable = reader.GetSchemaTable ();
92                                 reader.Close ();
93                                 if (closeConnection)
94                                         connection.Close ();    
95                                 BuildInformation (_dbSchemaTable);
96                         }
97                 }
98                 
99                 private string QuotedTableName {
100                         get { return GetQuotedString (_tableName); }
101                 }
102
103                 private string GetQuotedString (string value)
104                 {
105                         if (value == String.Empty || value == null)
106                                 return value;
107                         if (_quotePrefix == String.Empty && _quoteSuffix == String.Empty)
108                                 return value;
109                         return String.Format ("{0}{1}{2}", _quotePrefix, value, _quoteSuffix);
110                 }
111
112                 private void BuildInformation (DataTable schemaTable)
113                 {
114                         _tableName = String.Empty;
115                         foreach (DataRow schemaRow in schemaTable.Rows) {
116                                 if (schemaRow.IsNull ("BaseTableName") || (string) schemaRow ["BaseTableName"] == String.Empty)
117                                         continue;
118
119                                 if (_tableName == String.Empty) 
120                                         _tableName = (string) schemaRow ["BaseTableName"];
121                                 else if (_tableName != (string) schemaRow["BaseTableName"])
122                                         throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
123                         }
124                         if (_tableName == String.Empty)
125                                 throw new InvalidOperationException ("Dynamic SQL generation is not supported with no base table.");
126                         _dbSchemaTable = schemaTable;
127                 }
128
129                 private bool IncludedInInsert (DataRow schemaRow)
130                 {
131                         // If the parameter has one of these properties, then we don't include it in the insert:
132                         // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
133
134                         if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
135                                 return false;
136 //                      if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
137 //                              return false;
138                         if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
139                                 return false;
140                         if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
141                                 return false;
142                         if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
143                                 return false;
144                         return true;
145                 }
146
147                 private bool IncludedInUpdate (DataRow schemaRow)
148                 {
149                         // If the parameter has one of these properties, then we don't include it in the insert:
150                         // AutoIncrement, Hidden, RowVersion
151
152                         if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
153                                 return false;
154 //                      if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
155 //                              return false;
156                         if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
157                                 return false;
158                         if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
159                                 return false;
160                         if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
161                                 return false;
162
163                         return true;
164                 }
165
166                 private bool IncludedInWhereClause (DataRow schemaRow)
167                 {
168                         if ((bool) schemaRow ["IsLong"])
169                                 return false;
170                         return true;
171                 }
172
173                 private DbCommand CreateDeleteCommand (bool option)
174                 {
175                         // If no table was found, then we can't do an delete
176                         if (QuotedTableName == String.Empty)
177                                 return null;
178
179                         CreateNewCommand (ref _deleteCommand);
180
181                         string command = String.Format ("DELETE FROM {0}", QuotedTableName);
182                         StringBuilder whereClause = new StringBuilder ();
183                         bool keyFound = false;
184                         int parmIndex = 1;
185
186                         foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
187                                 if (!schemaRow.IsNull ("IsExpression") && (bool)schemaRow["IsExpression"] == true)
188                                         continue;
189                                 if (!IncludedInWhereClause (schemaRow)) 
190                                         continue;
191
192                                 if (whereClause.Length > 0) 
193                                         whereClause.Append (" AND ");
194
195                                 bool isKey = (bool) schemaRow ["IsKey"];
196                                 DbParameter parameter = null;
197
198                                 if (isKey)
199                                         keyFound = true;
200
201                                 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
202                                 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
203                                 //following the 2.0 approach
204                                 bool allowNull = (bool) schemaRow ["AllowDBNull"];
205                                 if (!isKey && allowNull) {
206                                         parameter = _deleteCommand.CreateParameter ();
207                                         if (option) {
208                                                 parameter.ParameterName = String.Format ("@{0}",
209                                                                                          schemaRow ["BaseColumnName"]);
210                                         } else {
211                                                 parameter.ParameterName = String.Format ("@p{0}", parmIndex++);
212                                         }
213                                         String sourceColumnName = (string) schemaRow ["BaseColumnName"];
214                                         parameter.Value = 1;
215
216                                         whereClause.Append ("(");
217                                         whereClause.Append (String.Format (clause1, parameter.ParameterName, 
218                                                                            GetQuotedString (sourceColumnName)));
219                                         whereClause.Append (" OR ");
220                                 }
221
222                                 parameter = CreateParameter (_deleteCommand, String.Format ("@{0}", option ? schemaRow ["BaseColumnName"] : "p" + parmIndex++), schemaRow);
223                                 parameter.SourceVersion = DataRowVersion.Original;
224
225                                 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
226
227                                 if (!isKey && allowNull)
228                                         whereClause.Append (")");
229                         }
230                         if (!keyFound)
231                                 throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
232
233                         // We're all done, so bring it on home
234                         string sql = String.Format ("{0} WHERE ({1})", command, whereClause.ToString ());
235                         _deleteCommand.CommandText = sql;
236                         _dbCommand = _deleteCommand;
237                         return _deleteCommand;
238                 }
239
240                 private DbCommand CreateInsertCommand (bool option)
241                 {
242                         if (QuotedTableName == String.Empty)
243                                 return null;
244
245                         CreateNewCommand (ref _insertCommand);
246
247                         string command = String.Format ("INSERT INTO {0}", QuotedTableName);
248                         string sql;
249                         StringBuilder columns = new StringBuilder ();
250                         StringBuilder values = new StringBuilder ();
251
252                         int parmIndex = 1;
253                         foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
254                                 if (!IncludedInInsert (schemaRow))
255                                         continue;
256
257                                 if (columns.Length > 0) {
258                                         columns.Append (", ");
259                                         values.Append (", ");
260                                 }
261
262                                 DbParameter parameter = CreateParameter (_insertCommand, String.Format ("@{0}", option ? schemaRow ["BaseColumnName"] : "p" + parmIndex++), schemaRow);
263                                 parameter.SourceVersion = DataRowVersion.Current;
264
265                                 columns.Append (GetQuotedString (parameter.SourceColumn));
266                                 values.Append (parameter.ParameterName);
267                         }
268
269                         sql = String.Format ("{0} ({1}) VALUES ({2})", command, columns.ToString (), values.ToString ());
270                         _insertCommand.CommandText = sql;
271                         _dbCommand = _insertCommand;
272                         return _insertCommand;
273                 }
274
275                 private void CreateNewCommand (ref DbCommand command)
276                 {
277                         DbCommand sourceCommand = SourceCommand;
278                         if (command == null) {
279                                 command = sourceCommand.Connection.CreateCommand ();
280                                 command.CommandTimeout = sourceCommand.CommandTimeout;
281                                 command.Transaction = sourceCommand.Transaction;
282                         }
283                         command.CommandType = CommandType.Text;
284                         command.UpdatedRowSource = UpdateRowSource.None;
285                         command.Parameters.Clear ();
286                 }
287
288                 private DbCommand CreateUpdateCommand (bool option)
289                 {
290                         // If no table was found, then we can't do an update
291                         if (QuotedTableName == String.Empty)
292                                 return null;
293
294                         CreateNewCommand (ref _updateCommand);
295
296                         string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
297                         StringBuilder columns = new StringBuilder ();
298                         StringBuilder whereClause = new StringBuilder ();
299                         int parmIndex = 1;
300                         bool keyFound = false;
301
302                         // First, create the X=Y list for UPDATE
303                         foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
304                                 if (!IncludedInUpdate (schemaRow))
305                                         continue;
306                                 if (columns.Length > 0) 
307                                         columns.Append (", ");
308
309                                 DbParameter parameter = CreateParameter (_updateCommand, String.Format ("@{0}", option ? schemaRow ["BaseColumnName"] : "p" + parmIndex++), schemaRow);
310                                 parameter.SourceVersion = DataRowVersion.Current;
311
312                                 columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
313                         }
314
315                         // Now, create the WHERE clause.  This may be optimizable, but it would be ugly to incorporate
316                         // into the loop above.  "Premature optimization is the root of all evil." -- Knuth
317                         foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
318                                 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"] == true)
319                                         continue;
320
321                                 if (!IncludedInWhereClause (schemaRow)) 
322                                         continue;
323
324                                 if (whereClause.Length > 0) 
325                                         whereClause.Append (" AND ");
326
327                                 bool isKey = (bool) schemaRow ["IsKey"];
328                                 DbParameter parameter = null;
329
330                                 if (isKey)
331                                         keyFound = true;
332
333                                 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
334                                 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
335                                 //following the 2.0 approach
336                                 bool allowNull = (bool) schemaRow ["AllowDBNull"];
337                                 if (!isKey && allowNull) {
338                                         parameter = _updateCommand.CreateParameter ();
339                                         if (option) {
340                                                 parameter.ParameterName = String.Format ("@{0} IS NULL",
341                                                                                          schemaRow ["BaseColumnName"]);
342                                         } else {
343                                                 parameter.ParameterName = String.Format ("@p{0}", parmIndex++);
344                                         }
345                                         parameter.Value = 1;
346                                         whereClause.Append ("(");
347                                         whereClause.Append (String.Format (clause1, parameter.ParameterName,
348                                                                            GetQuotedString ((string) schemaRow ["BaseColumnName"])));
349                                         whereClause.Append (" OR ");
350                                 }
351
352                                 if (option)
353                                         parameter = CreateParameter (_updateCommand, String.Format ("@Original_{0}", schemaRow ["BaseColumnName"]), schemaRow);
354                                 else
355                                         parameter = CreateParameter (_updateCommand, String.Format ("@p{0}", parmIndex++), schemaRow);
356                                 parameter.SourceVersion = DataRowVersion.Original;
357
358                                 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
359
360                                 if (!isKey && allowNull)
361                                         whereClause.Append (")");
362                         }
363                         if (!keyFound)
364                                 throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
365
366                         // We're all done, so bring it on home
367                         string sql = String.Format ("{0}{1} WHERE ({2})", command, columns.ToString (), whereClause.ToString ());
368                         _updateCommand.CommandText = sql;
369                         _dbCommand = _updateCommand;
370                         return _updateCommand;
371                 }
372
373                 private DbParameter CreateParameter (DbCommand _dbCommand, string parameterName, DataRow schemaRow)
374                 {
375                         DbParameter parameter = _dbCommand.CreateParameter ();
376                         parameter.ParameterName = parameterName;
377                         parameter.SourceColumn = (string) schemaRow ["BaseColumnName"];
378                         parameter.Size = (int) schemaRow ["ColumnSize"];
379                         _dbCommand.Parameters.Add (parameter);
380                         return parameter;
381                 }
382
383                 [DefaultValue (CatalogLocation.Start)]
384                 public virtual CatalogLocation CatalogLocation {
385                         get { return _catalogLocation; }
386                         set { _catalogLocation = value; }
387                 }
388
389                 [DefaultValue (".")]
390                 public virtual string CatalogSeparator {
391                         get { return _catalogSeperator; }
392                         set { if (value != null) _catalogSeperator = value; }
393                 }
394
395                 [DefaultValue (ConflictOption.CompareAllSearchableValues)]
396                 public virtual ConflictOption ConflictOption {
397                         get { return _conflictOption; }
398                         set { _conflictOption = value; }
399                 }
400
401                 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
402                 [Browsable (false)]
403                 public DbDataAdapter DataAdapter {
404                         get { return _dbDataAdapter; }
405                         set {  if (value != null) _dbDataAdapter = value; }
406                 }
407
408                 [DefaultValue ("")]
409                 public virtual string QuotePrefix {
410                         get { return _quotePrefix; }
411                         set { if (value != null) _quotePrefix = value; }
412                 }
413
414                 [DefaultValue ("")]
415                 public virtual string QuoteSuffix {
416                         get { return _quoteSuffix; }
417                         set {  if (value != null) _quoteSuffix = value; }
418                 }
419
420                 [DefaultValue (".")]
421                 public virtual string SchemaSeparator {
422                         get { return _schemaSeperator; }
423                         set {  if (value != null) _schemaSeperator = value; }
424                 }
425
426                 [DefaultValue (false)]
427                 public bool SetAllValues {
428                         get { return _setAllValues; }
429                         set { _setAllValues = value; }
430                 }               
431
432                 private DbCommand SourceCommand {
433                         get {
434                                 if (_dbDataAdapter != null)
435                                         return _dbDataAdapter.SelectCommand;
436                                 return null;
437                         }
438                 }
439                 #endregion // Properties
440
441                 #region Methods
442
443                 protected abstract void ApplyParameterInfo (DbParameter parameter, 
444                                                             DataRow row, 
445                                                             StatementType statementType, 
446                                                             bool whereClause);
447
448                 protected override void Dispose (bool disposing)
449                 {
450                         if (!_disposed) {
451                                 if (disposing) {
452                                         if (_insertCommand != null)
453                                                 _insertCommand.Dispose ();
454                                         if (_deleteCommand != null)
455                                                 _deleteCommand.Dispose ();
456                                         if (_updateCommand != null)
457                                                 _updateCommand.Dispose ();
458                                         if (_dbSchemaTable != null)
459                                                 _dbSchemaTable.Dispose ();
460                                 }
461                                 _disposed = true;
462                         }
463                 }
464
465                 public DbCommand GetDeleteCommand ()
466                 {
467                         BuildCache (true);
468                         if (_deleteCommand == null)
469                                 return CreateDeleteCommand (false);
470                         return _deleteCommand;
471                 }
472
473                 public DbCommand GetDeleteCommand (bool option)
474                 {
475                         BuildCache (true);
476                         if (_deleteCommand == null)
477                                 return CreateDeleteCommand (option);
478                         return _deleteCommand;
479                 }
480
481                 public DbCommand GetInsertCommand ()
482                 {
483                         BuildCache (true);
484                         if (_insertCommand == null)
485                                 return CreateInsertCommand (false);
486                         return _insertCommand;
487                 }
488
489                 public DbCommand GetInsertCommand (bool option)
490                 {
491                         BuildCache (true);
492                         if (_insertCommand == null)
493                                 return CreateInsertCommand (option);
494                         return _insertCommand;
495                 }
496
497                 public DbCommand GetUpdateCommand ()
498                 {
499                         BuildCache (true);
500                         if (_updateCommand == null)
501                                 return CreateUpdateCommand (false);
502                         return _updateCommand;
503                 }
504
505                 public DbCommand GetUpdateCommand (bool option)
506                 {
507                         BuildCache (true);
508                         if (_updateCommand == null)
509                                 return CreateUpdateCommand (option);
510                         return _updateCommand;
511                 }
512
513                 protected virtual DbCommand InitializeCommand (DbCommand command)
514                 {
515                         if (_dbCommand == null) {
516                                 _dbCommand = SourceCommand;
517                         } else {
518                                 _dbCommand.CommandTimeout = 30;
519                                 _dbCommand.Transaction = null;
520                                 _dbCommand.CommandType = CommandType.Text;
521                                 _dbCommand.UpdatedRowSource = UpdateRowSource.None;
522                         }
523                         return _dbCommand;
524
525                 }
526
527                 public virtual string QuoteIdentifier (string unquotedIdentifier)
528                 {
529                         if (unquotedIdentifier == null) {
530                                 throw new ArgumentNullException("Unquoted identifier parameter cannot be null");
531                         }
532                         return String.Format ("{0}{1}{2}", this.QuotePrefix, unquotedIdentifier, this.QuoteSuffix);
533                 }
534
535                 public virtual string UnquoteIdentifier (string quotedIdentifier)
536                 {
537                         if (quotedIdentifier == null) {
538                                 throw new ArgumentNullException ("Quoted identifier parameter cannot be null");
539                         }
540                         string unquotedIdentifier = quotedIdentifier.Trim ();
541                         if (unquotedIdentifier.StartsWith (this.QuotePrefix)) {
542                                 unquotedIdentifier = unquotedIdentifier.Remove (0, 1);
543                         }
544                         if (unquotedIdentifier.EndsWith (this.QuoteSuffix)) {
545                                 unquotedIdentifier = unquotedIdentifier.Remove (unquotedIdentifier.Length - 1, 1);
546                         }
547                         return unquotedIdentifier;
548                 }
549
550                 public virtual void RefreshSchema ()
551                 {
552                         _tableName = String.Empty;
553                         _dbSchemaTable = null;
554                         _deleteCommand = null;
555                         _updateCommand = null;
556                         _insertCommand = null;
557                 }
558
559                 protected void RowUpdatingHandler (RowUpdatingEventArgs args)
560                 {
561                         if (args.Command != null)
562                                 return;
563                         try {
564                                 switch (args.StatementType) {
565                                 case StatementType.Insert:
566                                         args.Command = GetInsertCommand ();
567                                         break;
568                                 case StatementType.Update:
569                                         args.Command = GetUpdateCommand ();
570                                         break;
571                                 case StatementType.Delete:
572                                         args.Command = GetDeleteCommand ();
573                                         break;
574                                 }
575                         } catch (Exception e) {
576                                 args.Errors = e;
577                                 args.Status = UpdateStatus.ErrorsOccurred;
578                         }
579                 }
580
581                 protected abstract string GetParameterName (int parameterOrdinal);
582                 protected abstract string GetParameterName (String parameterName);
583                 protected abstract string GetParameterPlaceholder (int parameterOrdinal);
584
585                 protected abstract void SetRowUpdatingHandler (DbDataAdapter adapter);
586
587                 protected virtual DataTable GetSchemaTable (DbCommand cmd)
588                 {
589                         using (DbDataReader rdr = cmd.ExecuteReader ())
590                                 return rdr.GetSchemaTable ();
591                 }
592
593                 #endregion // Methods
594         }
595 }
596
597 #endif