2 // System.Data.SqlClient.SqlCommandBuilder.cs
5 // Tim Coleman (tim@timcoleman.com)
7 // Copyright (C) Tim Coleman, 2002
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.
34 using System.Collections;
35 using System.ComponentModel;
37 using System.Data.Common;
40 namespace System.Data.SqlClient
43 public sealed class SqlCommandBuilder : DbCommandBuilder
45 public sealed class SqlCommandBuilder : Component
52 DataTable dbSchemaTable;
53 SqlDataAdapter adapter;
56 string [] columnNames;
59 string _catalogSeperator = ".";
60 string _schemaSeperator = ".";
61 CatalogLocation _catalogLocation = CatalogLocation.Start;
64 SqlCommand deleteCommand;
65 SqlCommand insertCommand;
66 SqlCommand updateCommand;
68 // Used to construct WHERE clauses
69 static readonly string clause1 = "({0} = 1 AND {1} IS NULL)";
70 static readonly string clause2 = "({0} = {1})";
72 private SqlRowUpdatingEventHandler rowUpdatingHandler;
78 public SqlCommandBuilder ()
86 quoteSuffix = String.Empty;
87 quotePrefix = String.Empty;
91 public SqlCommandBuilder (SqlDataAdapter adapter)
94 DataAdapter = adapter;
97 #endregion // Constructors
102 [DataSysDescription ("The DataAdapter for which to automatically generate SqlCommands")]
104 [DefaultValue (null)]
105 public new SqlDataAdapter DataAdapter {
106 get { return adapter; }
109 adapter.RowUpdating -= new SqlRowUpdatingEventHandler (RowUpdatingHandler);
113 adapter.RowUpdating += new SqlRowUpdatingEventHandler (RowUpdatingHandler);
117 private string QuotedTableName {
118 get { return GetQuotedString (tableName); }
122 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
124 [DataSysDescription ("The character used in a text command as the opening quote for quoting identifiers that contain special characters.")]
126 [EditorBrowsable (EditorBrowsableState.Never)]
133 get { return quotePrefix; }
135 if (dbSchemaTable != null)
136 throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
142 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
144 [DataSysDescription ("The character used in a text command as the closing quote for quoting identifiers that contain special characters. ")]
146 [EditorBrowsable (EditorBrowsableState.Never)]
153 get { return quoteSuffix; }
155 if (dbSchemaTable != null)
156 throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
162 [EditorBrowsable (EditorBrowsableState.Never)]
164 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
168 public override string CatalogSeparator {
169 get { return _catalogSeperator; }
170 set { if (value != null) _catalogSeperator = value; }
173 [EditorBrowsable (EditorBrowsableState.Never)]
175 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
179 public override string SchemaSeparator {
180 get { return _schemaSeperator; }
181 set { if (value != null) _schemaSeperator = value; }
184 [EditorBrowsable (EditorBrowsableState.Never)]
186 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
188 [DefaultValue (CatalogLocation.Start)]
190 public override CatalogLocation CatalogLocation {
191 get { return _catalogLocation; }
192 set { _catalogLocation = value; }
197 private SqlCommand SourceCommand {
200 return adapter.SelectCommand;
205 #endregion // Properties
209 private void BuildCache (bool closeConnection)
211 SqlCommand sourceCommand = SourceCommand;
212 if (sourceCommand == null)
213 throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
214 SqlConnection connection = sourceCommand.Connection;
215 if (connection == null)
216 throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
218 if (dbSchemaTable == null) {
219 if (connection.State == ConnectionState.Open)
220 closeConnection = false;
224 SqlDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
225 dbSchemaTable = reader.GetSchemaTable ();
229 BuildInformation (dbSchemaTable);
233 private void BuildInformation (DataTable schemaTable)
235 tableName = String.Empty;
236 foreach (DataRow schemaRow in schemaTable.Rows) {
237 if (schemaRow.IsNull ("BaseTableName") ||
238 (string) schemaRow ["BaseTableName"] == String.Empty)
241 if (tableName == String.Empty)
242 tableName = (string) schemaRow ["BaseTableName"];
243 else if (tableName != (string) schemaRow["BaseTableName"])
244 throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
246 if (tableName == String.Empty)
247 throw new InvalidOperationException ("Dynamic SQL generation is not supported with no base table.");
248 dbSchemaTable = schemaTable;
251 private SqlCommand CreateDeleteCommand (bool option)
253 // If no table was found, then we can't do an delete
254 if (QuotedTableName == String.Empty)
257 CreateNewCommand (ref deleteCommand);
259 string command = String.Format ("DELETE FROM {0}", QuotedTableName);
260 StringBuilder columns = new StringBuilder ();
261 StringBuilder whereClause = new StringBuilder ();
262 string dsColumnName = String.Empty;
263 bool keyFound = false;
266 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
267 if ((bool)schemaRow["IsExpression"] == true)
269 if (!IncludedInWhereClause (schemaRow))
272 if (whereClause.Length > 0)
273 whereClause.Append (" AND ");
275 bool isKey = (bool) schemaRow ["IsKey"];
276 SqlParameter parameter = null;
281 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
282 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
283 //following the 2.0 approach
284 bool allowNull = (bool) schemaRow ["AllowDBNull"];
285 if (!isKey && allowNull) {
287 parameter = deleteCommand.Parameters.Add (String.Format ("@{0}",
288 schemaRow ["BaseColumnName"]),
291 parameter = deleteCommand.Parameters.Add (String.Format ("@p{0}", parmIndex++),
294 String sourceColumnName = (string) schemaRow ["BaseColumnName"];
297 whereClause.Append ("(");
298 whereClause.Append (String.Format (clause1, parameter.ParameterName,
299 GetQuotedString (sourceColumnName)));
300 whereClause.Append (" OR ");
304 parameter = deleteCommand.Parameters.Add (CreateParameter (schemaRow));
306 parameter = deleteCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
308 parameter.SourceVersion = DataRowVersion.Original;
310 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
312 if (!isKey && allowNull)
313 whereClause.Append (")");
316 throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
318 // We're all done, so bring it on home
319 string sql = String.Format ("{0} WHERE ({1})", command, whereClause.ToString ());
320 deleteCommand.CommandText = sql;
321 return deleteCommand;
324 private SqlCommand CreateInsertCommand (bool option)
326 if (QuotedTableName == String.Empty)
329 CreateNewCommand (ref insertCommand);
331 string command = String.Format ("INSERT INTO {0}", QuotedTableName);
333 StringBuilder columns = new StringBuilder ();
334 StringBuilder values = new StringBuilder ();
335 string dsColumnName = String.Empty;
338 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
339 if (!IncludedInInsert (schemaRow))
343 columns.Append (", ");
344 values.Append (", ");
347 SqlParameter parameter = null;
349 parameter = insertCommand.Parameters.Add (CreateParameter (schemaRow));
351 parameter = insertCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
353 parameter.SourceVersion = DataRowVersion.Current;
355 columns.Append (GetQuotedString (parameter.SourceColumn));
356 values.Append (parameter.ParameterName);
359 sql = String.Format ("{0} ({1}) VALUES ({2})", command, columns.ToString (), values.ToString ());
360 insertCommand.CommandText = sql;
361 return insertCommand;
364 private void CreateNewCommand (ref SqlCommand command)
366 SqlCommand sourceCommand = SourceCommand;
367 if (command == null) {
368 command = sourceCommand.Connection.CreateCommand ();
369 command.CommandTimeout = sourceCommand.CommandTimeout;
370 command.Transaction = sourceCommand.Transaction;
372 command.CommandType = CommandType.Text;
373 command.UpdatedRowSource = UpdateRowSource.None;
374 command.Parameters.Clear ();
377 private SqlCommand CreateUpdateCommand (bool option)
379 // If no table was found, then we can't do an update
380 if (QuotedTableName == String.Empty)
383 CreateNewCommand (ref updateCommand);
385 string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
386 StringBuilder columns = new StringBuilder ();
387 StringBuilder whereClause = new StringBuilder ();
389 string dsColumnName = String.Empty;
390 bool keyFound = false;
392 // First, create the X=Y list for UPDATE
393 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
394 if (!IncludedInUpdate (schemaRow))
396 if (columns.Length > 0)
397 columns.Append (", ");
399 SqlParameter parameter = null;
401 parameter = updateCommand.Parameters.Add (CreateParameter (schemaRow));
403 parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
405 parameter.SourceVersion = DataRowVersion.Current;
407 columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
410 // Now, create the WHERE clause. This may be optimizable, but it would be ugly to incorporate
411 // into the loop above. "Premature optimization is the root of all evil." -- Knuth
412 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
413 if ((bool)schemaRow["IsExpression"] == true)
416 if (!IncludedInWhereClause (schemaRow))
419 if (whereClause.Length > 0)
420 whereClause.Append (" AND ");
422 bool isKey = (bool) schemaRow ["IsKey"];
423 SqlParameter parameter = null;
429 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
430 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
431 //following the 2.0 approach
432 bool allowNull = (bool) schemaRow ["AllowDBNull"];
433 if (!isKey && allowNull) {
435 parameter = updateCommand.Parameters.Add (String.Format ("@{0}",
436 schemaRow ["BaseColumnName"]),
439 parameter = updateCommand.Parameters.Add (String.Format ("@p{0}", parmIndex++),
443 whereClause.Append ("(");
444 whereClause.Append (String.Format (clause1, parameter.ParameterName,
445 GetQuotedString ((string) schemaRow ["BaseColumnName"])));
446 whereClause.Append (" OR ");
450 parameter = updateCommand.Parameters.Add (CreateParameter (schemaRow));
452 parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
454 parameter.SourceVersion = DataRowVersion.Original;
456 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
458 if (!isKey && allowNull)
459 whereClause.Append (")");
462 throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
464 // We're all done, so bring it on home
465 string sql = String.Format ("{0}{1} WHERE ({2})", command, columns.ToString (), whereClause.ToString ());
466 updateCommand.CommandText = sql;
467 return updateCommand;
470 private SqlParameter CreateParameter (DataRow schemaRow)
472 string sourceColumn = (string) schemaRow ["BaseColumnName"];
473 string name = String.Format ("@{0}", sourceColumn);
474 SqlDbType sqlDbType = (SqlDbType) schemaRow ["ProviderType"];
475 int size = (int) schemaRow ["ColumnSize"];
477 return new SqlParameter (name, sqlDbType, size, sourceColumn);
480 private SqlParameter CreateParameter (int parmIndex, DataRow schemaRow)
482 string name = String.Format ("@p{0}", parmIndex);
483 string sourceColumn = (string) schemaRow ["BaseColumnName"];
484 SqlDbType sqlDbType = (SqlDbType) schemaRow ["ProviderType"];
485 int size = (int) schemaRow ["ColumnSize"];
487 return new SqlParameter (name, sqlDbType, size, sourceColumn);
490 public static void DeriveParameters (SqlCommand command)
492 command.DeriveParameters ();
500 void Dispose (bool disposing)
504 if (insertCommand != null)
505 insertCommand.Dispose ();
506 if (deleteCommand != null)
507 deleteCommand.Dispose ();
508 if (updateCommand != null)
509 updateCommand.Dispose ();
510 if (dbSchemaTable != null)
511 dbSchemaTable.Dispose ();
521 SqlCommand GetDeleteCommand ()
524 if (deleteCommand == null)
525 return CreateDeleteCommand (false);
526 return deleteCommand;
533 SqlCommand GetInsertCommand ()
536 if (insertCommand == null)
537 return CreateInsertCommand (false);
538 return insertCommand;
541 private string GetQuotedString (string value)
543 if (value == String.Empty || value == null)
545 if (quotePrefix == String.Empty && quoteSuffix == String.Empty)
547 return String.Format ("{0}{1}{2}", quotePrefix, value, quoteSuffix);
554 SqlCommand GetUpdateCommand ()
557 if (updateCommand == null)
558 return CreateUpdateCommand (false);
559 return updateCommand;
563 public new SqlCommand GetUpdateCommand (bool useColumnsForParameterNames)
566 if (updateCommand == null)
567 return CreateUpdateCommand (useColumnsForParameterNames);
568 return updateCommand;
571 public new SqlCommand GetDeleteCommand (bool useColumnsForParameterNames)
574 if (deleteCommand == null)
575 return CreateDeleteCommand (useColumnsForParameterNames);
576 return deleteCommand;
579 public new SqlCommand GetInsertCommand (bool useColumnsForParameterNames)
582 if (insertCommand == null)
583 return CreateInsertCommand (useColumnsForParameterNames);
584 return insertCommand;
587 public override string QuoteIdentifier (string unquotedIdentifier)
589 return base.QuoteIdentifier (unquotedIdentifier);
592 public override string UnquoteIdentifier (string quotedIdentifier)
594 return base.UnquoteIdentifier (quotedIdentifier);
599 private bool IncludedInInsert (DataRow schemaRow)
601 // If the parameter has one of these properties, then we don't include it in the insert:
602 // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
604 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
606 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
608 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
610 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
612 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
617 private bool IncludedInUpdate (DataRow schemaRow)
619 // If the parameter has one of these properties, then we don't include it in the insert:
620 // AutoIncrement, Hidden, RowVersion
622 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
624 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
626 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
628 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
630 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
636 private bool IncludedInWhereClause (DataRow schemaRow)
638 if ((bool) schemaRow ["IsLong"])
648 void RefreshSchema ()
650 // FIXME: "Figure out what else needs to be cleaned up when we refresh."
651 tableName = String.Empty;
652 dbSchemaTable = null;
653 CreateNewCommand (ref deleteCommand);
654 CreateNewCommand (ref updateCommand);
655 CreateNewCommand (ref insertCommand);
659 protected override void ApplyParameterInfo (DbParameter parameter,
661 StatementType statementType,
664 SqlParameter sqlParam = (SqlParameter) parameter;
665 sqlParam.Size = int.Parse (datarow ["ColumnSize"].ToString ());
666 if (datarow ["NumericPrecision"] != DBNull.Value) {
667 sqlParam.Precision = byte.Parse (datarow ["NumericPrecision"].ToString ());
669 if (datarow ["NumericScale"] != DBNull.Value) {
670 sqlParam.Scale = byte.Parse (datarow ["NumericScale"].ToString ());
672 sqlParam.SqlDbType = (SqlDbType) datarow ["ProviderType"];
675 protected override string GetParameterName (int parameterOrdinal)
677 return String.Format ("@p{0}", parameterOrdinal);
680 protected override string GetParameterName (string parameterName)
682 return String.Format ("@{0}", parameterName);
685 protected override string GetParameterPlaceholder (int parameterOrdinal)
687 return GetParameterName (parameterOrdinal);
691 #endregion // Methods
693 #region Event Handlers
695 private void RowUpdatingHandler (object sender, SqlRowUpdatingEventArgs args)
697 if (args.Command != null)
700 switch (args.StatementType) {
701 case StatementType.Insert:
702 args.Command = GetInsertCommand ();
704 case StatementType.Update:
705 args.Command = GetUpdateCommand ();
707 case StatementType.Delete:
708 args.Command = GetDeleteCommand ();
711 } catch (Exception e) {
713 args.Status = UpdateStatus.ErrorsOccurred;
718 protected override void SetRowUpdatingHandler (DbDataAdapter adapter)
720 if (!(adapter is SqlDataAdapter)) {
721 throw new InvalidOperationException ("Adapter needs to be a SqlDataAdapter");
723 rowUpdatingHandler = new SqlRowUpdatingEventHandler (RowUpdatingHandler);
724 ((SqlDataAdapter) adapter).RowUpdating += rowUpdatingHandler;
727 protected override DataTable GetSchemaTable (DbCommand srcCommand)
729 using (SqlDataReader rdr = (SqlDataReader) srcCommand.ExecuteReader ())
730 return rdr.GetSchemaTable ();
733 protected override DbCommand InitializeCommand (DbCommand command)
735 if (command == null) {
736 command = new SqlCommand ();
738 command.CommandTimeout = 30;
739 command.Transaction = null;
740 command.CommandType = CommandType.Text;
741 command.UpdatedRowSource = UpdateRowSource.None;
747 #endregion // Event Handlers