Missing 2.0 APIs
[mono.git] / mcs / class / System.Data / System.Data.Odbc / OdbcCommandBuilder.cs
1 //
2 // System.Data.Odbc.OdbcCommandBuilder
3 //
4 // Author:
5 //   Umadevi S (sumadevi@novell.com)
6 //   Sureshkumar T (tsureshkumar@novell.com)
7 //
8 // Copyright (C) Novell Inc, 2004
9 //
10
11 //
12 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
13 //
14 // Permission is hereby granted, free of charge, to any person obtaining
15 // a copy of this software and associated documentation files (the
16 // "Software"), to deal in the Software without restriction, including
17 // without limitation the rights to use, copy, modify, merge, publish,
18 // distribute, sublicense, and/or sell copies of the Software, and to
19 // permit persons to whom the Software is furnished to do so, subject to
20 // the following conditions:
21 // 
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
24 // 
25 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
26 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
27 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
28 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
29 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
30 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
31 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
32 //
33
34 using System.Text;
35 using System.Data;
36 using System.Data.Common;
37 using System.ComponentModel;
38
39 namespace System.Data.Odbc
40 {
41         /// <summary>
42         /// Provides a means of automatically generating single-table commands used to reconcile changes made to a DataSet with the associated database. This class cannot be inherited.
43         /// </summary>
44
45 #if NET_2_0
46         public sealed class OdbcCommandBuilder : DbCommandBuilder
47 #else // 1_1
48         public sealed class OdbcCommandBuilder : Component
49 #endif // NET_2_0
50         {
51                 #region Fields
52
53                 private OdbcDataAdapter _adapter;
54                 private string                  _quotePrefix;
55                 private string                  _quoteSuffix;
56
57                 private DataTable               _schema;
58                 private string                  _tableName;
59                 private OdbcCommand             _insertCommand;
60                 private OdbcCommand             _updateCommand;
61                 private OdbcCommand             _deleteCommand;
62
63                 bool _disposed;
64
65                 private OdbcRowUpdatingEventHandler rowUpdatingHandler;
66                 
67                 #endregion // Fields
68
69                 #region Constructors
70                 
71                 public OdbcCommandBuilder ()
72                 {
73                         _adapter = null;
74                         _quotePrefix = String.Empty;
75                         _quoteSuffix = String.Empty;
76                         rowUpdatingHandler = null;
77                 }
78
79                 public OdbcCommandBuilder (OdbcDataAdapter adapter) 
80                         : this ()
81                 {
82                         DataAdapter = adapter;
83                 }
84
85                 #endregion // Constructors
86
87                 #region Properties
88
89                 [OdbcDescriptionAttribute ("The DataAdapter for which to automatically generate OdbcCommands")]
90                 [DefaultValue (null)]
91                 public
92 #if NET_2_0
93         new
94 #endif // NET_2_0
95                 OdbcDataAdapter DataAdapter {
96                         get {
97                                 return _adapter;
98                         }
99                         set {
100                                 if (_adapter == value)
101                                         return;
102
103                                 if (rowUpdatingHandler != null)
104                                         rowUpdatingHandler = new OdbcRowUpdatingEventHandler (OnRowUpdating);
105                                 
106                                 if (_adapter != null)
107                                         _adapter.RowUpdating -= rowUpdatingHandler;
108                                 _adapter = value;
109                                 if (_adapter != null)
110                                         _adapter.RowUpdating += rowUpdatingHandler;
111                         }
112                 }
113
114                 private OdbcCommand SelectCommand
115                 {
116                         get {
117                                 if (DataAdapter == null)
118                                         return null;
119                                 return DataAdapter.SelectCommand;
120                         }
121                 }
122
123                 private DataTable Schema 
124                 {
125                         get {
126                                 if (_schema == null)
127                                         RefreshSchema ();
128                                 return _schema;
129                         }
130                 }
131                 
132                 private string TableName 
133                 {
134                         get {
135                                 if (_tableName != String.Empty)
136                                         return _tableName;
137
138                                 DataRow [] schemaRows = Schema.Select ("BaseTableName is not null and BaseTableName <> ''");
139                                 if (schemaRows.Length > 1) {
140                                         string tableName = (string) schemaRows [0] ["BaseTableName"];
141                                         foreach (DataRow schemaRow in schemaRows) {
142                                                 if ( (string) schemaRow ["BaseTableName"] != tableName)
143                                                         throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
144                                         }
145                                 }
146                                 if (schemaRows.Length == 0)
147                                         throw new InvalidOperationException ("Cannot determine the base table name. Cannot proceed");
148                                 _tableName = schemaRows [0] ["BaseTableName"].ToString ();
149                                 return _tableName;
150                         }
151                 }
152
153                 [BrowsableAttribute (false)]
154                 [OdbcDescriptionAttribute ("The prefix string wrapped around sql objects")]
155                 [DesignerSerializationVisibilityAttribute (DesignerSerializationVisibility.Hidden)]
156 #if ONLY_1_1
157                 public
158 #else
159                 new
160 #endif
161                 string QuotePrefix {
162                         get {
163                                 return _quotePrefix;
164                         }
165                         set {
166                                 _quotePrefix = value;
167                         }
168                 }
169
170                 [BrowsableAttribute (false)]
171                 [OdbcDescriptionAttribute ("The suffix string wrapped around sql objects")]
172                 [DesignerSerializationVisibilityAttribute (DesignerSerializationVisibility.Hidden)]
173 #if ONLY_1_1
174                 public
175 #else
176                 new
177 #endif // NET_2_0
178                 string QuoteSuffix {
179                         get {
180                                 return _quoteSuffix;
181                         }
182                         set {
183                                 _quoteSuffix = value;
184                         }
185                 }
186
187                 #endregion // Properties
188
189                 #region Methods
190
191                 [MonoTODO]
192                 public static void DeriveParameters (OdbcCommand command) 
193                 {
194                         throw new NotImplementedException ();
195                 }
196
197 #if ONLY_1_1
198                 protected override
199 #else
200                 new
201 #endif
202                 void Dispose (bool disposing) 
203                 {
204                         if (_disposed)
205                                 return;
206                         
207                         if (disposing) {
208                                 // dispose managed resource
209                                 if (_insertCommand != null) _insertCommand.Dispose ();
210                                 if (_updateCommand != null) _updateCommand.Dispose ();
211                                 if (_deleteCommand != null) _deleteCommand.Dispose ();
212                                 if (_schema != null) _insertCommand.Dispose ();
213
214                                 _insertCommand = null;
215                                 _updateCommand = null;
216                                 _deleteCommand = null;
217                                 _schema = null;
218                         }
219                         _disposed = true;
220                 }
221
222                 private bool IsUpdatable (DataRow schemaRow)
223                 {
224                         if ( (! schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
225                              || (! schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
226                              || (! schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
227                              || (! schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
228                              || (! schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
229                              )
230                                 return false;
231                         return true;
232                 }
233                 
234                 private string GetColumnName (DataRow schemaRow)
235                 {
236                         string columnName = schemaRow.IsNull ("BaseColumnName") ? String.Empty : (string) schemaRow ["BaseColumnName"];
237                         if (columnName == String.Empty)
238                                 columnName = schemaRow.IsNull ("ColumnName") ? String.Empty : (string) schemaRow ["ColumnName"];
239                         return columnName;
240                 }
241                 
242                 private OdbcParameter AddParameter (OdbcCommand cmd, string paramName, OdbcType odbcType,
243                                                     int length, string sourceColumnName, DataRowVersion rowVersion)
244                 {
245                         OdbcParameter param;
246                         if (length >= 0 && sourceColumnName != String.Empty)
247                                 param = cmd.Parameters.Add (paramName, odbcType, length, sourceColumnName);
248                         else
249                                 param = cmd.Parameters.Add (paramName, odbcType);
250                         param.SourceVersion = rowVersion;
251                         return param;
252                 }
253
254                 /*
255                  * creates where clause for optimistic concurrency
256                  */
257                 private string CreateOptWhereClause (OdbcCommand command, bool option)
258                 {
259                         string [] whereClause = new string [Schema.Rows.Count];
260
261                         int count = 0;
262
263                         foreach (DataRow schemaRow in Schema.Rows) {
264                                 
265                                 // exclude non updatable columns
266                                 if (! IsUpdatable (schemaRow))
267                                         continue;
268
269                                 string columnName = null;
270                                 if (option)
271                                         columnName = GetColumnName (schemaRow);
272                                 else
273                                         columnName = String.Format ("@p{0}", count);
274                                 
275                                 if (columnName == String.Empty)
276                                         throw new InvalidOperationException ("Cannot form delete command. Column name is missing!");
277
278                                 bool    allowNull  = schemaRow.IsNull ("AllowDBNull") || (bool) schemaRow ["AllowDBNull"];
279                                 OdbcType sqlDbType = schemaRow.IsNull ("ProviderType") ? OdbcType.VarChar : (OdbcType) schemaRow ["ProviderType"];
280                                 int     length     = schemaRow.IsNull ("ColumnSize") ? -1 : (int) schemaRow ["ColumnSize"];
281
282                                 if (allowNull) {
283                                         whereClause [count] = String.Format ("((? = 1 AND {0} IS NULL) OR ({0} = ?))",
284                                                                               columnName);
285                                         AddParameter (command, columnName, sqlDbType, length, columnName, DataRowVersion.Original);
286                                         AddParameter (command, columnName, sqlDbType, length, columnName, DataRowVersion.Original);
287                                 } else {
288                                         whereClause [count] = String.Format ( "({0} = ?)", columnName);
289                                         AddParameter (command, columnName, sqlDbType, length, columnName, DataRowVersion.Original);
290                                 }
291
292                                 count++;
293                         }
294
295                         return String.Join (" AND ", whereClause, 0, count);
296                 }
297
298                 private void CreateNewCommand (ref OdbcCommand command)
299                 {
300                         OdbcCommand sourceCommand = SelectCommand;
301                         if (command == null) {
302                                 command = new OdbcCommand ();
303                                 command.Connection = sourceCommand.Connection;
304                                 command.CommandTimeout = sourceCommand.CommandTimeout;
305                                 command.Transaction = sourceCommand.Transaction;
306                         }
307                         command.CommandType = CommandType.Text;
308                         command.UpdatedRowSource = UpdateRowSource.None;
309                         command.Parameters.Clear ();
310                 }
311                 
312                 private OdbcCommand CreateInsertCommand (bool option)
313                 {
314                         CreateNewCommand (ref _insertCommand);
315                         
316                         string query = String.Format ("INSERT INTO {0}", QuoteIdentifier (TableName));
317                         string [] columns = new string [Schema.Rows.Count];
318                         string [] values  = new string [Schema.Rows.Count];
319
320                         int count = 0;
321
322                         foreach (DataRow schemaRow in Schema.Rows) {
323                                 
324                                 // exclude non updatable columns
325                                 if (! IsUpdatable (schemaRow))
326                                         continue;
327
328                                 string columnName = null;
329                                 
330                                 if (option)
331                                         columnName = GetColumnName (schemaRow);
332                                 else
333                                         columnName = String.Format ("@p{0}", count); 
334                                 
335                                 if (columnName == String.Empty)
336                                         throw new InvalidOperationException ("Cannot form insert command. Column name is missing!");
337
338                                 // create column string & value string
339                                 columns [count] = QuoteIdentifier(columnName);
340                                 values [count++] = "?";
341
342                                 // create parameter and add
343                                 OdbcType sqlDbType = schemaRow.IsNull ("ProviderType") ? OdbcType.VarChar : (OdbcType) schemaRow ["ProviderType"];
344                                 int length = schemaRow.IsNull ("ColumnSize") ? -1 : (int) schemaRow ["ColumnSize"];
345
346                                 AddParameter (_insertCommand, columnName, sqlDbType, length, columnName, DataRowVersion.Current);
347                         }
348
349                         query = String.Format ("{0} ({1}) VALUES ({2})", 
350                                                query, 
351                                                String.Join (", ", columns, 0, count),
352                                                String.Join (", ", values, 0, count) );
353                         _insertCommand.CommandText = query;
354                         return _insertCommand;                  
355                 }
356
357                 public
358 #if NET_2_0
359                         new
360 #endif // NET_2_0
361                                 OdbcCommand GetInsertCommand ()
362                 {
363                         // FIXME: check validity of adapter
364                         if (_insertCommand != null)
365                                 return _insertCommand;
366
367                         if (_schema == null)
368                                 RefreshSchema ();
369                                         
370                         return CreateInsertCommand (false);
371                 }
372
373 #if NET_2_0
374                 public new OdbcCommand GetInsertCommand (bool option)
375                 {
376                         // FIXME: check validity of adapter
377                         if (_insertCommand != null)
378                                 return _insertCommand;
379
380                         if (_schema == null)
381                                 RefreshSchema ();
382
383                         return CreateInsertCommand (option);
384                 }
385 #endif // NET_2_0
386                         
387                 private OdbcCommand CreateUpdateCommand (bool option)
388                 {
389                         CreateNewCommand (ref _updateCommand);
390
391                         string query = String.Format ("UPDATE {0} SET", QuoteIdentifier (TableName));
392                         string [] setClause = new string [Schema.Rows.Count];
393
394                         int count = 0;
395
396                         foreach (DataRow schemaRow in Schema.Rows) {
397                                 
398                                 // exclude non updatable columns
399                                 if (! IsUpdatable (schemaRow))
400                                         continue;
401
402                                 string columnName = null; 
403                                 if (option)
404                                         columnName = GetColumnName (schemaRow);
405                                 else
406                                         columnName = String.Format ("@p{0}", count);
407                                 
408                                 if (columnName == String.Empty)
409                                         throw new InvalidOperationException ("Cannot form update command. Column name is missing!");
410
411                                 OdbcType sqlDbType = schemaRow.IsNull ("ProviderType") ? OdbcType.VarChar : (OdbcType) schemaRow ["ProviderType"];
412                                 int length = schemaRow.IsNull ("ColumnSize") ? -1 : (int) schemaRow ["ColumnSize"];
413
414                                 // create column = value string
415                                 setClause [count] = String.Format ("{0} = ?", QuoteIdentifier(columnName));
416                                 AddParameter (_updateCommand, columnName, sqlDbType, length, columnName, DataRowVersion.Current);
417                                 count++;
418                         }
419
420                         // create where clause. odbc uses positional parameters. so where class
421                         // is created seperate from the above loop.
422                         string whereClause = CreateOptWhereClause (_updateCommand, option);
423                         
424                         query = String.Format ("{0} {1} WHERE ({2})", 
425                                                query, 
426                                                String.Join (", ", setClause, 0, count),
427                                                whereClause);
428                         _updateCommand.CommandText = query;
429                         return _updateCommand;                  
430                 }
431                 
432                 public
433 #if NET_2_0
434                         new
435 #endif // NET_2_0
436                                 OdbcCommand GetUpdateCommand ()
437                 {
438                         // FIXME: check validity of adapter
439                         if (_updateCommand != null)
440                                 return _updateCommand;
441
442                         if (_schema == null)
443                                 RefreshSchema ();
444
445                         return CreateUpdateCommand (false);
446                 }
447
448 #if NET_2_0
449                 public new OdbcCommand GetUpdateCommand (bool option)
450                 {
451                         // FIXME: check validity of adapter
452                         if (_updateCommand != null)
453                                 return _updateCommand;
454
455                         if (_schema == null)
456                                 RefreshSchema ();
457
458                         return CreateUpdateCommand (option);
459                 }
460 #endif // NET_2_0
461                 
462                 private OdbcCommand CreateDeleteCommand (bool option)
463                 {
464                         CreateNewCommand (ref _deleteCommand);
465
466                         string query = String.Format ("DELETE FROM {0}", QuoteIdentifier (TableName));
467                         string whereClause = CreateOptWhereClause (_deleteCommand, option);
468                         
469                         query = String.Format ("{0} WHERE ({1})", query, whereClause);
470                         _deleteCommand.CommandText = query;
471                         return _deleteCommand;                  
472                 }
473
474                 public
475 #if NET_2_0
476                         new
477 #endif // NET_2_0
478                                 OdbcCommand GetDeleteCommand ()
479                 {
480                         // FIXME: check validity of adapter
481                         if (_deleteCommand != null)
482                                 return _deleteCommand;
483
484                         if (_schema == null)
485                                 RefreshSchema ();
486                         
487                         return CreateDeleteCommand (false);
488                 }
489
490 #if NET_2_0
491                 public new OdbcCommand GetDeleteCommand (bool option)
492                 {
493                         // FIXME: check validity of adapter
494                         if (_deleteCommand != null)
495                                 return _deleteCommand;
496
497                         if (_schema == null)
498                                 RefreshSchema ();
499
500                         return CreateDeleteCommand (option);
501                 }
502 #endif // NET_2_0
503
504 #if ONLY_1_1
505                 public
506 #else
507         new
508 #endif // NET_2_0
509                 void RefreshSchema ()
510                 {
511                         // creates metadata
512                         if (SelectCommand == null)
513                                 throw new InvalidOperationException ("SelectCommand should be valid");
514                         if (SelectCommand.Connection == null)
515                                 throw new InvalidOperationException ("SelectCommand's Connection should be valid");
516                         
517                         CommandBehavior behavior = CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo;
518                         if (SelectCommand.Connection.State != ConnectionState.Open) {
519                                 SelectCommand.Connection.Open ();
520                                 behavior |= CommandBehavior.CloseConnection;
521                         }
522                         
523                         OdbcDataReader reader = SelectCommand.ExecuteReader (behavior);
524                         _schema = reader.GetSchemaTable ();
525                         reader.Close ();
526                         
527                         // force creation of commands
528                         _insertCommand  = null;
529                         _updateCommand  = null;
530                         _deleteCommand  = null;
531                         _tableName      = String.Empty;
532                 }
533                 
534 #if NET_2_0
535                 protected override void ApplyParameterInfo (DbParameter dbParameter,
536                                                             DataRow row,
537                                                             StatementType statementType,
538                                                             bool whereClause)
539                 {
540                         OdbcParameter parameter = (OdbcParameter) dbParameter;
541                         parameter.Size = int.Parse (row ["ColumnSize"].ToString ());
542                         if (row ["NumericPrecision"] != DBNull.Value) {
543                                 parameter.Precision = byte.Parse (row ["NumericPrecision"].ToString ());
544                         }
545                         if (row ["NumericScale"] != DBNull.Value) {
546                                 parameter.Scale = byte.Parse (row ["NumericScale"].ToString ());
547                         }
548                         parameter.DbType = (DbType) row ["ProviderType"];
549                 }
550
551                 protected override string GetParameterName (int position)
552                 {
553                         return String.Format("@p{0}", position);
554                 }
555
556                 protected override string GetParameterName (string parameterName)
557                 {
558                         return String.Format("@{0}", parameterName);                       
559                 }
560                 
561                 protected override string GetParameterPlaceholder (int position)
562                 {
563                         return GetParameterName (position);
564                 }
565             
566                 // FIXME: According to MSDN - "if this method is called again with
567                 // the same DbDataAdapter, the DbCommandBuilder is unregistered for 
568                 // that DbDataAdapter's RowUpdating event" - this behaviour is yet
569                 // to be verified               
570                 protected override void SetRowUpdatingHandler (DbDataAdapter adapter)
571                 {
572                         if (!(adapter is OdbcDataAdapter)) {
573                                 throw new InvalidOperationException ("Adapter needs to be a SqlDataAdapter");
574                         }
575                         
576                         if (rowUpdatingHandler == null)
577                                 rowUpdatingHandler = new OdbcRowUpdatingEventHandler (OnRowUpdating);
578                         
579                         ((OdbcDataAdapter) adapter).RowUpdating += rowUpdatingHandler;
580                 }
581
582 #endif // NET_2_0
583                 
584 #if NET_2_0
585                 public override
586 #else
587                 private
588 #endif          
589                 string QuoteIdentifier (string unquotedIdentifier)
590                 {
591                         if (unquotedIdentifier == null || unquotedIdentifier == String.Empty)
592                                 return unquotedIdentifier;
593                         return String.Format ("{0}{1}{2}", QuotePrefix, 
594                                                               unquotedIdentifier, QuoteSuffix);
595                 }
596
597 #if NET_2_0
598                 // FIXME:  Not sure what the extra "connection" param does!
599                 public string QuoteIdentifier (string unquotedIdentifier, OdbcConnection connection)
600                 {
601                         return QuoteIdentifier (unquotedIdentifier);
602                 }
603
604                 public string UnquoteIdentifier (string quotedIdentifier, OdbcConnection connection)
605                 {
606                         return UnquoteIdentifier (quotedIdentifier);
607                 }
608 #endif          
609
610 #if NET_2_0
611                 public override
612 #else
613                 private
614 #endif          
615                 string UnquoteIdentifier (string quotedIdentifier)
616                 {
617                         if (quotedIdentifier == null || quotedIdentifier == String.Empty)
618                                 return quotedIdentifier;
619                         
620                         StringBuilder sb = new StringBuilder (quotedIdentifier.Length);
621                         sb.Append (quotedIdentifier);
622                         if (quotedIdentifier.StartsWith (QuotePrefix))
623                                 sb.Remove (0,QuotePrefix.Length);
624                         if (quotedIdentifier.EndsWith (QuoteSuffix))
625                                 sb.Remove (sb.Length - QuoteSuffix.Length, QuoteSuffix.Length );
626                         return sb.ToString ();
627                 }
628
629                 private void OnRowUpdating (object sender, OdbcRowUpdatingEventArgs args)
630                 {
631                         if (args.Command != null)
632                                 return;
633                         Console.WriteLine (Environment.StackTrace);
634                         try {
635                                 switch (args.StatementType) {
636                                 case StatementType.Insert:
637                                         args.Command = GetInsertCommand ();
638                                         break;
639                                 case StatementType.Update:
640                                         args.Command = GetUpdateCommand ();
641                                         break;
642                                 case StatementType.Delete:
643                                         args.Command = GetDeleteCommand ();
644                                         break;
645                                 }
646                         } catch (Exception e) {
647                                 args.Errors = e;
648                                 args.Status = UpdateStatus.ErrorsOccurred;
649                         }
650                 }
651                 
652
653                 #endregion // Methods
654         }
655 }