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;
58 string _catalogSeperator = ".";
59 string _schemaSeperator = ".";
60 CatalogLocation _catalogLocation = CatalogLocation.Start;
63 SqlCommand deleteCommand;
64 SqlCommand insertCommand;
65 SqlCommand updateCommand;
67 // Used to construct WHERE clauses
68 static readonly string clause1 = "({0} = 1 AND {1} IS NULL)";
69 static readonly string clause2 = "({0} = {1})";
71 private SqlRowUpdatingEventHandler rowUpdatingHandler;
77 public SqlCommandBuilder ()
85 quoteSuffix = String.Empty;
86 quotePrefix = String.Empty;
90 public SqlCommandBuilder (SqlDataAdapter adapter)
93 DataAdapter = adapter;
96 #endregion // Constructors
101 [DataSysDescription ("The DataAdapter for which to automatically generate SqlCommands")]
103 [DefaultValue (null)]
104 public new SqlDataAdapter DataAdapter {
105 get { return adapter; }
108 adapter.RowUpdating -= new SqlRowUpdatingEventHandler (RowUpdatingHandler);
112 adapter.RowUpdating += new SqlRowUpdatingEventHandler (RowUpdatingHandler);
116 private string QuotedTableName {
117 get { return GetQuotedString (tableName); }
121 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
123 [DataSysDescription ("The character used in a text command as the opening quote for quoting identifiers that contain special characters.")]
125 [EditorBrowsable (EditorBrowsableState.Never)]
132 get { return quotePrefix; }
134 if (dbSchemaTable != null)
135 throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
141 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
143 [DataSysDescription ("The character used in a text command as the closing quote for quoting identifiers that contain special characters. ")]
145 [EditorBrowsable (EditorBrowsableState.Never)]
152 get { return quoteSuffix; }
154 if (dbSchemaTable != null)
155 throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
161 [EditorBrowsable (EditorBrowsableState.Never)]
163 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
167 public override string CatalogSeparator {
168 get { return _catalogSeperator; }
169 set { if (value != null) _catalogSeperator = value; }
172 [EditorBrowsable (EditorBrowsableState.Never)]
174 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
178 public override string SchemaSeparator {
179 get { return _schemaSeperator; }
180 set { if (value != null) _schemaSeperator = value; }
183 [EditorBrowsable (EditorBrowsableState.Never)]
185 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
187 [DefaultValue (CatalogLocation.Start)]
189 public override CatalogLocation CatalogLocation {
190 get { return _catalogLocation; }
191 set { _catalogLocation = value; }
196 private SqlCommand SourceCommand {
199 return adapter.SelectCommand;
204 #endregion // Properties
208 private void BuildCache (bool closeConnection)
210 SqlCommand sourceCommand = SourceCommand;
211 if (sourceCommand == null)
212 throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
213 SqlConnection connection = sourceCommand.Connection;
214 if (connection == null)
215 throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
217 if (dbSchemaTable == null) {
218 if (connection.State == ConnectionState.Open)
219 closeConnection = false;
223 SqlDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
224 dbSchemaTable = reader.GetSchemaTable ();
228 BuildInformation (dbSchemaTable);
232 private void BuildInformation (DataTable schemaTable)
234 tableName = String.Empty;
235 foreach (DataRow schemaRow in schemaTable.Rows) {
236 if (schemaRow.IsNull ("BaseTableName") ||
237 (string) schemaRow ["BaseTableName"] == String.Empty)
240 if (tableName == String.Empty)
241 tableName = (string) schemaRow ["BaseTableName"];
242 else if (tableName != (string) schemaRow["BaseTableName"])
243 throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
245 if (tableName == String.Empty)
246 throw new InvalidOperationException ("Dynamic SQL generation is not supported with no base table.");
247 dbSchemaTable = schemaTable;
250 private SqlCommand CreateDeleteCommand (bool option)
252 // If no table was found, then we can't do an delete
253 if (QuotedTableName == String.Empty)
256 CreateNewCommand (ref deleteCommand);
258 string command = String.Format ("DELETE FROM {0}", QuotedTableName);
259 StringBuilder whereClause = new StringBuilder ();
260 bool keyFound = false;
263 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
264 if ((bool)schemaRow["IsExpression"] == true)
266 if (!IncludedInWhereClause (schemaRow))
269 if (whereClause.Length > 0)
270 whereClause.Append (" AND ");
272 bool isKey = (bool) schemaRow ["IsKey"];
273 SqlParameter parameter = null;
278 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
279 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
280 //following the 2.0 approach
281 bool allowNull = (bool) schemaRow ["AllowDBNull"];
282 if (!isKey && allowNull) {
284 parameter = deleteCommand.Parameters.Add (String.Format ("@{0}",
285 schemaRow ["BaseColumnName"]),
288 parameter = deleteCommand.Parameters.Add (String.Format ("@p{0}", parmIndex++),
291 String sourceColumnName = (string) schemaRow ["BaseColumnName"];
294 whereClause.Append ("(");
295 whereClause.Append (String.Format (clause1, parameter.ParameterName,
296 GetQuotedString (sourceColumnName)));
297 whereClause.Append (" OR ");
301 parameter = deleteCommand.Parameters.Add (CreateParameter (schemaRow));
303 parameter = deleteCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
305 parameter.SourceVersion = DataRowVersion.Original;
307 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
309 if (!isKey && allowNull)
310 whereClause.Append (")");
313 throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
315 // We're all done, so bring it on home
316 string sql = String.Format ("{0} WHERE ({1})", command, whereClause.ToString ());
317 deleteCommand.CommandText = sql;
318 return deleteCommand;
321 private SqlCommand CreateInsertCommand (bool option)
323 if (QuotedTableName == String.Empty)
326 CreateNewCommand (ref insertCommand);
328 string command = String.Format ("INSERT INTO {0}", QuotedTableName);
330 StringBuilder columns = new StringBuilder ();
331 StringBuilder values = new StringBuilder ();
334 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
335 if (!IncludedInInsert (schemaRow))
339 columns.Append (", ");
340 values.Append (", ");
343 SqlParameter parameter = null;
345 parameter = insertCommand.Parameters.Add (CreateParameter (schemaRow));
347 parameter = insertCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
349 parameter.SourceVersion = DataRowVersion.Current;
351 columns.Append (GetQuotedString (parameter.SourceColumn));
352 values.Append (parameter.ParameterName);
355 sql = String.Format ("{0} ({1}) VALUES ({2})", command, columns.ToString (), values.ToString ());
356 insertCommand.CommandText = sql;
357 return insertCommand;
360 private void CreateNewCommand (ref SqlCommand command)
362 SqlCommand sourceCommand = SourceCommand;
363 if (command == null) {
364 command = sourceCommand.Connection.CreateCommand ();
365 command.CommandTimeout = sourceCommand.CommandTimeout;
366 command.Transaction = sourceCommand.Transaction;
368 command.CommandType = CommandType.Text;
369 command.UpdatedRowSource = UpdateRowSource.None;
370 command.Parameters.Clear ();
373 private SqlCommand CreateUpdateCommand (bool option)
375 // If no table was found, then we can't do an update
376 if (QuotedTableName == String.Empty)
379 CreateNewCommand (ref updateCommand);
381 string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
382 StringBuilder columns = new StringBuilder ();
383 StringBuilder whereClause = new StringBuilder ();
385 bool keyFound = false;
387 // First, create the X=Y list for UPDATE
388 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
389 if (!IncludedInUpdate (schemaRow))
391 if (columns.Length > 0)
392 columns.Append (", ");
394 SqlParameter parameter = null;
396 parameter = updateCommand.Parameters.Add (CreateParameter (schemaRow));
398 parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
400 parameter.SourceVersion = DataRowVersion.Current;
402 columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
405 // Now, create the WHERE clause. This may be optimizable, but it would be ugly to incorporate
406 // into the loop above. "Premature optimization is the root of all evil." -- Knuth
407 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
408 if ((bool)schemaRow["IsExpression"] == true)
411 if (!IncludedInWhereClause (schemaRow))
414 if (whereClause.Length > 0)
415 whereClause.Append (" AND ");
417 bool isKey = (bool) schemaRow ["IsKey"];
418 SqlParameter parameter = null;
424 //ms.net 1.1 generates the null check for columns even if AllowDBNull is false
425 //while ms.net 2.0 does not. Anyways, since both forms are logically equivalent
426 //following the 2.0 approach
427 bool allowNull = (bool) schemaRow ["AllowDBNull"];
428 if (!isKey && allowNull) {
430 parameter = updateCommand.Parameters.Add (String.Format ("@{0}",
431 schemaRow ["BaseColumnName"]),
434 parameter = updateCommand.Parameters.Add (String.Format ("@p{0}", parmIndex++),
438 whereClause.Append ("(");
439 whereClause.Append (String.Format (clause1, parameter.ParameterName,
440 GetQuotedString ((string) schemaRow ["BaseColumnName"])));
441 whereClause.Append (" OR ");
445 parameter = updateCommand.Parameters.Add (CreateParameter (schemaRow));
447 parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
449 parameter.SourceVersion = DataRowVersion.Original;
451 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
453 if (!isKey && allowNull)
454 whereClause.Append (")");
457 throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
459 // We're all done, so bring it on home
460 string sql = String.Format ("{0}{1} WHERE ({2})", command, columns.ToString (), whereClause.ToString ());
461 updateCommand.CommandText = sql;
462 return updateCommand;
465 private SqlParameter CreateParameter (DataRow schemaRow)
467 string sourceColumn = (string) schemaRow ["BaseColumnName"];
468 string name = String.Format ("@{0}", sourceColumn);
469 SqlDbType sqlDbType = (SqlDbType) schemaRow ["ProviderType"];
470 int size = (int) schemaRow ["ColumnSize"];
472 return new SqlParameter (name, sqlDbType, size, sourceColumn);
475 private SqlParameter CreateParameter (int parmIndex, DataRow schemaRow)
477 string name = String.Format ("@p{0}", parmIndex);
478 string sourceColumn = (string) schemaRow ["BaseColumnName"];
479 SqlDbType sqlDbType = (SqlDbType) schemaRow ["ProviderType"];
480 int size = (int) schemaRow ["ColumnSize"];
482 return new SqlParameter (name, sqlDbType, size, sourceColumn);
485 public static void DeriveParameters (SqlCommand command)
487 command.DeriveParameters ();
495 void Dispose (bool disposing)
499 if (insertCommand != null)
500 insertCommand.Dispose ();
501 if (deleteCommand != null)
502 deleteCommand.Dispose ();
503 if (updateCommand != null)
504 updateCommand.Dispose ();
505 if (dbSchemaTable != null)
506 dbSchemaTable.Dispose ();
516 SqlCommand GetDeleteCommand ()
519 if (deleteCommand == null)
520 return CreateDeleteCommand (false);
521 return deleteCommand;
528 SqlCommand GetInsertCommand ()
531 if (insertCommand == null)
532 return CreateInsertCommand (false);
533 return insertCommand;
536 private string GetQuotedString (string value)
538 if (value == String.Empty || value == null)
540 if (quotePrefix == String.Empty && quoteSuffix == String.Empty)
542 return String.Format ("{0}{1}{2}", quotePrefix, value, quoteSuffix);
549 SqlCommand GetUpdateCommand ()
552 if (updateCommand == null)
553 return CreateUpdateCommand (false);
554 return updateCommand;
558 public new SqlCommand GetUpdateCommand (bool useColumnsForParameterNames)
561 if (updateCommand == null)
562 return CreateUpdateCommand (useColumnsForParameterNames);
563 return updateCommand;
566 public new SqlCommand GetDeleteCommand (bool useColumnsForParameterNames)
569 if (deleteCommand == null)
570 return CreateDeleteCommand (useColumnsForParameterNames);
571 return deleteCommand;
574 public new SqlCommand GetInsertCommand (bool useColumnsForParameterNames)
577 if (insertCommand == null)
578 return CreateInsertCommand (useColumnsForParameterNames);
579 return insertCommand;
582 public override string QuoteIdentifier (string unquotedIdentifier)
584 return base.QuoteIdentifier (unquotedIdentifier);
587 public override string UnquoteIdentifier (string quotedIdentifier)
589 return base.UnquoteIdentifier (quotedIdentifier);
594 private bool IncludedInInsert (DataRow schemaRow)
596 // If the parameter has one of these properties, then we don't include it in the insert:
597 // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
599 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
601 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
603 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
605 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
607 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
612 private bool IncludedInUpdate (DataRow schemaRow)
614 // If the parameter has one of these properties, then we don't include it in the insert:
615 // AutoIncrement, Hidden, RowVersion
617 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
619 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
621 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
623 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
625 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
631 private bool IncludedInWhereClause (DataRow schemaRow)
633 if ((bool) schemaRow ["IsLong"])
643 void RefreshSchema ()
645 // FIXME: "Figure out what else needs to be cleaned up when we refresh."
646 tableName = String.Empty;
647 dbSchemaTable = null;
648 CreateNewCommand (ref deleteCommand);
649 CreateNewCommand (ref updateCommand);
650 CreateNewCommand (ref insertCommand);
654 protected override void ApplyParameterInfo (DbParameter parameter,
656 StatementType statementType,
659 SqlParameter sqlParam = (SqlParameter) parameter;
660 sqlParam.Size = int.Parse (datarow ["ColumnSize"].ToString ());
661 if (datarow ["NumericPrecision"] != DBNull.Value) {
662 sqlParam.Precision = byte.Parse (datarow ["NumericPrecision"].ToString ());
664 if (datarow ["NumericScale"] != DBNull.Value) {
665 sqlParam.Scale = byte.Parse (datarow ["NumericScale"].ToString ());
667 sqlParam.SqlDbType = (SqlDbType) datarow ["ProviderType"];
670 protected override string GetParameterName (int parameterOrdinal)
672 return String.Format ("@p{0}", parameterOrdinal);
675 protected override string GetParameterName (string parameterName)
677 return String.Format ("@{0}", parameterName);
680 protected override string GetParameterPlaceholder (int parameterOrdinal)
682 return GetParameterName (parameterOrdinal);
686 #endregion // Methods
688 #region Event Handlers
690 private void RowUpdatingHandler (object sender, SqlRowUpdatingEventArgs args)
692 if (args.Command != null)
695 switch (args.StatementType) {
696 case StatementType.Insert:
697 args.Command = GetInsertCommand ();
699 case StatementType.Update:
700 args.Command = GetUpdateCommand ();
702 case StatementType.Delete:
703 args.Command = GetDeleteCommand ();
706 } catch (Exception e) {
708 args.Status = UpdateStatus.ErrorsOccurred;
713 protected override void SetRowUpdatingHandler (DbDataAdapter adapter)
715 if (!(adapter is SqlDataAdapter)) {
716 throw new InvalidOperationException ("Adapter needs to be a SqlDataAdapter");
718 rowUpdatingHandler = new SqlRowUpdatingEventHandler (RowUpdatingHandler);
719 ((SqlDataAdapter) adapter).RowUpdating += rowUpdatingHandler;
722 protected override DataTable GetSchemaTable (DbCommand srcCommand)
724 using (SqlDataReader rdr = (SqlDataReader) srcCommand.ExecuteReader ())
725 return rdr.GetSchemaTable ();
728 protected override DbCommand InitializeCommand (DbCommand command)
730 if (command == null) {
731 command = new SqlCommand ();
733 command.CommandTimeout = 30;
734 command.Transaction = null;
735 command.CommandType = CommandType.Text;
736 command.UpdatedRowSource = UpdateRowSource.None;
742 #endregion // Event Handlers