2 // System.Data.Common.DbCommandBuilder
5 // Tim Coleman (tim@timcoleman.com)
7 // Copyright (C) Tim Coleman, 2003
11 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
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:
21 // The above copyright notice and this permission notice shall be
22 // included in all copies or substantial portions of the Software.
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.
33 #if NET_2_0 || TARGET_JVM
35 using System.ComponentModel;
37 using System.Globalization;
40 namespace System.Data.Common {
41 public abstract class DbCommandBuilder : Component
46 DataTable _dbSchemaTable;
47 DbDataAdapter _dbDataAdapter;
48 private CatalogLocation _catalogLocation = CatalogLocation.Start;
49 private ConflictOption _conflictOption = ConflictOption.CompareAllSearchableValues;
51 private string _tableName;
52 private string _catalogSeparator;
53 private string _quotePrefix;
54 private string _quoteSuffix;
55 private string _schemaSeparator;
56 private DbCommand _dbCommand;
58 DbCommand _deleteCommand;
59 DbCommand _insertCommand;
60 DbCommand _updateCommand;
62 static readonly string SEPARATOR_DEFAULT = ".";
63 // Used to construct WHERE clauses
64 static readonly string clause1 = "({0} = 1 AND {1} IS NULL)";
65 static readonly string clause2 = "({0} = {1})";
67 protected DbCommandBuilder ()
71 private void BuildCache (bool closeConnection)
73 DbCommand sourceCommand = SourceCommand;
74 if (sourceCommand == null)
75 throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
76 DbConnection connection = sourceCommand.Connection;
77 if (connection == null)
78 throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
80 if (_dbSchemaTable == null) {
81 if (connection.State == ConnectionState.Open)
82 closeConnection = false;
86 DbDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
87 _dbSchemaTable = reader.GetSchemaTable ();
91 BuildInformation (_dbSchemaTable);
95 private string QuotedTableName {
96 get { return GetQuotedString (_tableName); }
99 bool IsCommandGenerated {
101 return (_insertCommand != null || _updateCommand != null || _deleteCommand != null);
105 private string GetQuotedString (string value)
107 if (value == String.Empty || value == null)
110 string prefix = QuotePrefix;
111 string suffix = QuoteSuffix;
113 if (prefix.Length == 0 && suffix.Length == 0)
115 return String.Format ("{0}{1}{2}", prefix, value, suffix);
118 private void BuildInformation (DataTable schemaTable)
120 _tableName = String.Empty;
121 foreach (DataRow schemaRow in schemaTable.Rows) {
122 if (schemaRow.IsNull ("BaseTableName") || (string) schemaRow ["BaseTableName"] == String.Empty)
125 if (_tableName == String.Empty)
126 _tableName = (string) schemaRow ["BaseTableName"];
127 else if (_tableName != (string) schemaRow["BaseTableName"])
128 throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
130 if (_tableName == String.Empty)
131 throw new InvalidOperationException ("Dynamic SQL generation is not supported with no base table.");
132 _dbSchemaTable = schemaTable;
135 private bool IncludedInInsert (DataRow schemaRow)
137 // If the parameter has one of these properties, then we don't include it in the insert:
138 // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
140 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
142 // if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
144 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
146 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
148 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
153 private bool IncludedInUpdate (DataRow schemaRow)
155 // If the parameter has one of these properties, then we don't include it in the insert:
156 // AutoIncrement, Hidden, RowVersion
158 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
160 // if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
162 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
164 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
166 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
172 private bool IncludedInWhereClause (DataRow schemaRow)
174 if ((bool) schemaRow ["IsLong"])
179 private DbCommand CreateDeleteCommand (bool option)
181 // If no table was found, then we can't do an delete
182 if (QuotedTableName == String.Empty)
185 CreateNewCommand (ref _deleteCommand);
187 string command = String.Format ("DELETE FROM {0}", QuotedTableName);
188 StringBuilder whereClause = new StringBuilder ();
189 bool keyFound = false;
192 foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
193 if (!schemaRow.IsNull ("IsExpression") && (bool)schemaRow["IsExpression"] == true)
195 if (!IncludedInWhereClause (schemaRow))
198 if (whereClause.Length > 0)
199 whereClause.Append (" AND ");
201 bool isKey = (bool) schemaRow ["IsKey"];
202 DbParameter parameter = null;
203 string sourceColumnName;
208 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
209 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
210 //following the 2.0 approach
211 bool allowNull = (bool) schemaRow ["AllowDBNull"];
212 if (!isKey && allowNull) {
213 parameter = _deleteCommand.CreateParameter ();
215 parameter.ParameterName = String.Format ("@IsNull_{0}",
216 schemaRow ["BaseColumnName"]);
218 parameter.ParameterName = String.Format ("@p{0}", parmIndex++);
221 parameter.DbType = DbType.Int32;
222 // This should be set for nullcheckparam
223 sourceColumnName = (string) schemaRow ["BaseColumnName"];
224 parameter.SourceColumn = sourceColumnName;
225 parameter.SourceColumnNullMapping = true;
226 parameter.SourceVersion = DataRowVersion.Original;
227 _deleteCommand.Parameters.Add (parameter);
229 whereClause.Append ("(");
230 whereClause.Append (String.Format (clause1, parameter.ParameterName,
231 GetQuotedString (sourceColumnName)));
232 whereClause.Append (" OR ");
236 parameter = CreateParameter (_deleteCommand, schemaRow, true);
238 parameter = CreateParameter (_deleteCommand, parmIndex++, schemaRow);
240 parameter.SourceVersion = DataRowVersion.Original;
241 ApplyParameterInfo (parameter, schemaRow, StatementType.Delete, true);
242 //parameter.IsNullable = allowNull;
244 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
246 if (!isKey && allowNull)
247 whereClause.Append (")");
250 throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
252 // We're all done, so bring it on home
253 string sql = String.Format ("{0} WHERE ({1})", command, whereClause.ToString ());
254 _deleteCommand.CommandText = sql;
255 _dbCommand = _deleteCommand;
256 return _deleteCommand;
259 private DbCommand CreateInsertCommand (bool option, DataRow row)
261 if (QuotedTableName == String.Empty)
264 CreateNewCommand (ref _insertCommand);
266 string command = String.Format ("INSERT INTO {0}", QuotedTableName);
268 StringBuilder columns = new StringBuilder ();
269 StringBuilder values = new StringBuilder ();
272 DbParameter parameter = null;
273 foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
274 if (!IncludedInInsert (schemaRow))
277 if (columns.Length > 0) {
278 columns.Append (", ");
279 values.Append (", ");
283 parameter = CreateParameter (_insertCommand, schemaRow, false);
285 parameter = CreateParameter (_insertCommand, parmIndex++, schemaRow);
286 parameter.SourceVersion = DataRowVersion.Current;
287 ApplyParameterInfo (parameter, schemaRow, StatementType.Insert, false);
289 columns.Append (GetQuotedString (parameter.SourceColumn));
291 // Workaround for columns that may have a default/bound value and for now,
292 // the framework, don't provide a mechanism to read these values yet
293 // AllowDBNull and DataRow is used to workaround #385028 by using DEFAULT
294 string colName = schemaRow ["ColumnName"] as string;
295 bool allowDBNull = !schemaRow.IsNull ("AllowDBNull") & (bool) schemaRow ["AllowDBNull"];
296 if (!allowDBNull && row != null &&
297 (row [colName] == DBNull.Value || row [colName] == null)) {
298 values.Append ("DEFAULT");
300 values.Append (parameter.ParameterName);
304 sql = String.Format ("{0} ({1}) VALUES ({2})", command, columns.ToString (), values.ToString ());
305 _insertCommand.CommandText = sql;
306 _dbCommand = _insertCommand;
307 return _insertCommand;
310 private void CreateNewCommand (ref DbCommand command)
312 DbCommand sourceCommand = SourceCommand;
313 if (command == null) {
314 command = sourceCommand.Connection.CreateCommand ();
315 command.CommandTimeout = sourceCommand.CommandTimeout;
316 command.Transaction = sourceCommand.Transaction;
318 command.CommandType = CommandType.Text;
319 command.UpdatedRowSource = UpdateRowSource.None;
320 command.Parameters.Clear ();
323 private DbCommand CreateUpdateCommand (bool option)
325 // If no table was found, then we can't do an update
326 if (QuotedTableName == String.Empty)
329 CreateNewCommand (ref _updateCommand);
331 string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
332 StringBuilder columns = new StringBuilder ();
333 StringBuilder whereClause = new StringBuilder ();
335 bool keyFound = false;
336 DbParameter parameter = null;
338 // First, create the X=Y list for UPDATE
339 foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
340 if (!IncludedInUpdate (schemaRow))
342 if (columns.Length > 0)
343 columns.Append (", ");
346 parameter = CreateParameter (_updateCommand, schemaRow, false);
348 parameter = CreateParameter (_updateCommand, parmIndex++, schemaRow);
349 parameter.SourceVersion = DataRowVersion.Current;
350 ApplyParameterInfo (parameter, schemaRow, StatementType.Update, false);
351 //parameter.IsNullable = (bool) schemaRow ["AllowDBNull"];
352 columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
355 // Now, create the WHERE clause. This may be optimizable, but it would be ugly to incorporate
356 // into the loop above. "Premature optimization is the root of all evil." -- Knuth
357 foreach (DataRow schemaRow in _dbSchemaTable.Rows) {
358 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"] == true)
361 if (!IncludedInWhereClause (schemaRow))
364 if (whereClause.Length > 0)
365 whereClause.Append (" AND ");
367 bool isKey = (bool) schemaRow ["IsKey"];
371 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
372 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
373 //following the 2.0 approach
374 bool allowNull = (bool) schemaRow ["AllowDBNull"];
375 if (!isKey && allowNull) {
376 parameter = _updateCommand.CreateParameter ();
378 parameter.ParameterName = String.Format ("@IsNull_{0}",
379 schemaRow ["BaseColumnName"]);
381 parameter.ParameterName = String.Format ("@p{0}", parmIndex++);
383 parameter.DbType = DbType.Int32;
385 parameter.SourceColumn = (string) schemaRow ["BaseColumnName"];
386 parameter.SourceColumnNullMapping = true;
387 parameter.SourceVersion = DataRowVersion.Original;
388 whereClause.Append ("(");
389 whereClause.Append (String.Format (clause1, parameter.ParameterName,
390 GetQuotedString ((string) schemaRow ["BaseColumnName"])));
391 whereClause.Append (" OR ");
392 _updateCommand.Parameters.Add (parameter);
396 parameter = CreateParameter (_updateCommand, schemaRow, true);
398 parameter = CreateParameter (_updateCommand, parmIndex++, schemaRow);
399 parameter.SourceVersion = DataRowVersion.Original;
400 //parameter.IsNullable = allowNull;
401 ApplyParameterInfo (parameter, schemaRow, StatementType.Update, true);
403 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
405 if (!isKey && allowNull)
406 whereClause.Append (")");
409 throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
411 // We're all done, so bring it on home
412 string sql = String.Format ("{0}{1} WHERE ({2})", command, columns.ToString (), whereClause.ToString ());
413 _updateCommand.CommandText = sql;
414 _dbCommand = _updateCommand;
415 return _updateCommand;
418 private DbParameter CreateParameter (DbCommand _dbCommand, DataRow schemaRow, bool whereClause)
420 string sourceColumn = (string) schemaRow ["BaseColumnName"];
421 DbParameter parameter = _dbCommand.CreateParameter ();
423 parameter.ParameterName = GetParameterName ("Original_" + sourceColumn);
425 parameter.ParameterName = GetParameterName (sourceColumn);
426 parameter.SourceColumn = sourceColumn;
427 //parameter.Size = (int) schemaRow ["ColumnSize"];
428 _dbCommand.Parameters.Add (parameter);
432 private DbParameter CreateParameter (DbCommand _dbCommand, int paramIndex, DataRow schemaRow)
434 string sourceColumn = (string) schemaRow ["BaseColumnName"];
435 DbParameter parameter = _dbCommand.CreateParameter ();
436 parameter.ParameterName = GetParameterName (paramIndex);
437 parameter.SourceColumn = sourceColumn;
438 //parameter.Size = (int) schemaRow ["ColumnSize"];
439 _dbCommand.Parameters.Add (parameter);
443 [DefaultValue (CatalogLocation.Start)]
444 public virtual CatalogLocation CatalogLocation {
445 get { return _catalogLocation; }
447 CheckEnumValue (typeof (CatalogLocation),
449 _catalogLocation = value;
454 public virtual string CatalogSeparator {
456 if (_catalogSeparator == null || _catalogSeparator.Length == 0)
457 return SEPARATOR_DEFAULT;
458 return _catalogSeparator;
460 set { _catalogSeparator = value; }
463 [DefaultValue (ConflictOption.CompareAllSearchableValues)]
464 public virtual ConflictOption ConflictOption {
465 get { return _conflictOption; }
467 CheckEnumValue (typeof (ConflictOption),
469 _conflictOption = value;
473 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
475 public DbDataAdapter DataAdapter {
476 get { return _dbDataAdapter; }
477 set { if (value != null)
478 SetRowUpdatingHandler (value);
479 _dbDataAdapter = value;
484 public virtual string QuotePrefix {
486 if (_quotePrefix == null)
491 if (IsCommandGenerated)
492 throw new InvalidOperationException (
493 "QuotePrefix cannot be set after " +
494 "an Insert, Update or Delete command " +
495 "has been generated.");
496 _quotePrefix = value;
501 public virtual string QuoteSuffix {
503 if (_quoteSuffix == null)
508 if (IsCommandGenerated)
509 throw new InvalidOperationException (
510 "QuoteSuffix cannot be set after " +
511 "an Insert, Update or Delete command " +
512 "has been generated.");
513 _quoteSuffix = value;
518 public virtual string SchemaSeparator {
520 if (_schemaSeparator == null || _schemaSeparator.Length == 0)
521 return SEPARATOR_DEFAULT;
522 return _schemaSeparator;
524 set { _schemaSeparator = value; }
527 [DefaultValue (false)]
528 public bool SetAllValues {
529 get { return _setAllValues; }
530 set { _setAllValues = value; }
533 private DbCommand SourceCommand {
535 if (_dbDataAdapter != null)
536 return _dbDataAdapter.SelectCommand;
541 protected abstract void ApplyParameterInfo (DbParameter parameter,
543 StatementType statementType,
546 protected override void Dispose (bool disposing)
550 if (_insertCommand != null)
551 _insertCommand.Dispose ();
552 if (_deleteCommand != null)
553 _deleteCommand.Dispose ();
554 if (_updateCommand != null)
555 _updateCommand.Dispose ();
556 if (_dbSchemaTable != null)
557 _dbSchemaTable.Dispose ();
563 public DbCommand GetDeleteCommand ()
565 return GetDeleteCommand (false);
568 public DbCommand GetDeleteCommand (bool useColumnsForParameterNames)
571 if (_deleteCommand == null || useColumnsForParameterNames)
572 return CreateDeleteCommand (useColumnsForParameterNames);
573 return _deleteCommand;
576 public DbCommand GetInsertCommand ()
578 return GetInsertCommand (false, null);
581 public DbCommand GetInsertCommand (bool useColumnsForParameterNames)
583 return GetInsertCommand (useColumnsForParameterNames, null);
586 internal DbCommand GetInsertCommand (bool useColumnsForParameterNames, DataRow row)
589 if (_insertCommand == null || useColumnsForParameterNames)
590 return CreateInsertCommand (useColumnsForParameterNames, row);
591 return _insertCommand;
594 public DbCommand GetUpdateCommand ()
596 return GetUpdateCommand (false);
599 public DbCommand GetUpdateCommand (bool useColumnsForParameterNames)
602 if (_updateCommand == null || useColumnsForParameterNames)
603 return CreateUpdateCommand (useColumnsForParameterNames);
604 return _updateCommand;
607 protected virtual DbCommand InitializeCommand (DbCommand command)
609 if (_dbCommand == null) {
610 _dbCommand = SourceCommand;
612 _dbCommand.CommandTimeout = 30;
613 _dbCommand.Transaction = null;
614 _dbCommand.CommandType = CommandType.Text;
615 _dbCommand.UpdatedRowSource = UpdateRowSource.None;
621 public virtual string QuoteIdentifier (string unquotedIdentifier)
623 throw new NotSupportedException ();
626 public virtual string UnquoteIdentifier (string quotedIdentifier)
628 if (quotedIdentifier == null) {
629 throw new ArgumentNullException ("Quoted identifier parameter cannot be null");
631 string unquotedIdentifier = quotedIdentifier.Trim ();
632 if (unquotedIdentifier.StartsWith (this.QuotePrefix)) {
633 unquotedIdentifier = unquotedIdentifier.Remove (0, 1);
635 if (unquotedIdentifier.EndsWith (this.QuoteSuffix)) {
636 unquotedIdentifier = unquotedIdentifier.Remove (unquotedIdentifier.Length - 1, 1);
638 return unquotedIdentifier;
641 public virtual void RefreshSchema ()
643 _tableName = String.Empty;
644 _dbSchemaTable = null;
645 _deleteCommand = null;
646 _updateCommand = null;
647 _insertCommand = null;
650 protected void RowUpdatingHandler (RowUpdatingEventArgs rowUpdatingEvent)
652 if (rowUpdatingEvent.Command != null)
655 switch (rowUpdatingEvent.StatementType) {
656 case StatementType.Insert:
657 rowUpdatingEvent.Command = GetInsertCommand (false, rowUpdatingEvent.Row);
659 case StatementType.Update:
660 rowUpdatingEvent.Command = GetUpdateCommand ();
662 case StatementType.Delete:
663 rowUpdatingEvent.Command = GetDeleteCommand ();
666 } catch (Exception e) {
667 rowUpdatingEvent.Errors = e;
668 rowUpdatingEvent.Status = UpdateStatus.ErrorsOccurred;
672 protected abstract string GetParameterName (int parameterOrdinal);
673 protected abstract string GetParameterName (String parameterName);
674 protected abstract string GetParameterPlaceholder (int parameterOrdinal);
676 protected abstract void SetRowUpdatingHandler (DbDataAdapter adapter);
678 protected virtual DataTable GetSchemaTable (DbCommand sourceCommand)
680 using (DbDataReader rdr = sourceCommand.ExecuteReader ())
681 return rdr.GetSchemaTable ();
684 static void CheckEnumValue (Type type, int value)
686 if (Enum.IsDefined (type, value))
689 string typename = type.Name;
690 string msg = string.Format (CultureInfo.CurrentCulture,
691 "Value {0} is not valid for {1}.", value,
693 throw new ArgumentOutOfRangeException (typename, msg);