2 // System.Data.SqlClient.SqlCommandBuilder.cs
5 // Tim Coleman (tim@timcoleman.com)
6 // Veerapuram Varadhan (vvaradhan@novell.com)
8 // Copyright (C) Tim Coleman, 2002
12 // Copyright (C) 2004, 2009 Novell, Inc (http://www.novell.com)
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:
22 // The above copyright notice and this permission notice shall be
23 // included in all copies or substantial portions of the Software.
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.
35 using System.Collections;
36 using System.ComponentModel;
38 using System.Data.Common;
39 using System.Data.SqlTypes;
42 namespace System.Data.SqlClient
44 public sealed class SqlCommandBuilder : DbCommandBuilder
51 DataTable dbSchemaTable;
55 SqlDataAdapter adapter;
56 SqlCommand insertCommand;
57 SqlCommand deleteCommand;
58 SqlCommand updateCommand;
59 // Used to construct WHERE clauses
60 static readonly string clause1 = "({0} = 1 AND {1} IS NULL)";
61 static readonly string clause2 = "({0} = {1})";
64 readonly string _catalogSeparator = ".";
65 readonly string _schemaSeparator = ".";
66 readonly CatalogLocation _catalogLocation = CatalogLocation.Start;
73 public SqlCommandBuilder ()
79 public SqlCommandBuilder (SqlDataAdapter adapter)
82 DataAdapter = adapter;
85 #endregion // Constructors
90 public new SqlDataAdapter DataAdapter {
95 return (SqlDataAdapter)base.DataAdapter;
100 adapter.RowUpdating -= new SqlRowUpdatingEventHandler (RowUpdatingHandler);
104 adapter.RowUpdating += new SqlRowUpdatingEventHandler (RowUpdatingHandler);
106 base.DataAdapter = value;
112 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
113 [EditorBrowsable (EditorBrowsableState.Never)]
119 if (quotePrefix == null)
123 return base.QuotePrefix;
128 if (dbSchemaTable != null)
129 throw new InvalidOperationException (
130 "The QuotePrefix and QuoteSuffix " +
131 "properties cannot be changed once " +
132 "an Insert, Update, or Delete " +
133 "command has been generated.");
136 if (value != "[" && value != "\"")
137 throw new ArgumentException ("Only '[' " +
138 "and '\"' are allowed as value " +
139 "for the 'QuoteSuffix' property.");
140 base.QuotePrefix = value;
146 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
147 [EditorBrowsable (EditorBrowsableState.Never)]
153 if (quoteSuffix == null)
157 return base.QuoteSuffix;
162 if (dbSchemaTable != null)
163 throw new InvalidOperationException (
164 "The QuotePrefix and QuoteSuffix " +
165 "properties cannot be changed once " +
166 "an Insert, Update, or Delete " +
167 "command has been generated.");
170 if (value != "]" && value != "\"")
171 throw new ArgumentException ("Only ']' " +
172 "and '\"' are allowed as value " +
173 "for the 'QuoteSuffix' property.");
174 base.QuoteSuffix = value;
179 [EditorBrowsable (EditorBrowsableState.Never)]
181 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
182 public override string CatalogSeparator {
183 get { return _catalogSeparator; }
185 if (value != _catalogSeparator)
186 throw new ArgumentException ("Only " +
187 "'.' is allowed as value " +
188 "for the 'CatalogSeparator' " +
193 [EditorBrowsable (EditorBrowsableState.Never)]
195 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
196 public override string SchemaSeparator {
197 get { return _schemaSeparator; }
199 if (value != _schemaSeparator)
200 throw new ArgumentException ("Only " +
201 "'.' is allowed as value " +
202 "for the 'SchemaSeparator' " +
207 [EditorBrowsable (EditorBrowsableState.Never)]
209 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
210 public override CatalogLocation CatalogLocation {
211 get { return _catalogLocation; }
213 if (value != CatalogLocation.Start)
214 throw new ArgumentException ("Only " +
215 "'Start' is allowed as value " +
216 "for the 'CatalogLocation' " +
223 private SqlCommand SourceCommand {
226 return adapter.SelectCommand;
232 #endregion // Properties
237 private void BuildCache (bool closeConnection)
239 SqlCommand sourceCommand = SourceCommand;
240 if (sourceCommand == null)
241 throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
242 SqlConnection connection = sourceCommand.Connection;
243 if (connection == null)
244 throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
246 if (dbSchemaTable == null) {
247 if (connection.State == ConnectionState.Open)
248 closeConnection = false;
252 SqlDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
253 dbSchemaTable = reader.GetSchemaTable ();
257 BuildInformation (dbSchemaTable);
261 private void BuildInformation (DataTable schemaTable)
263 tableName = String.Empty;
264 foreach (DataRow schemaRow in schemaTable.Rows) {
265 if (schemaRow.IsNull ("BaseTableName") ||
266 (string) schemaRow ["BaseTableName"] == String.Empty)
269 if (tableName == String.Empty)
270 tableName = (string) schemaRow ["BaseTableName"];
271 else if (tableName != (string) schemaRow["BaseTableName"])
272 throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
274 if (tableName == String.Empty)
275 throw new InvalidOperationException ("Dynamic SQL generation is not supported with no base table.");
276 dbSchemaTable = schemaTable;
279 private SqlCommand CreateDeleteCommand (bool useColumnsForParameterNames)
281 // If no table was found, then we can't do an delete
282 if (QuotedTableName == String.Empty)
285 CreateNewCommand (ref deleteCommand);
287 string command = String.Format ("DELETE FROM {0}", QuotedTableName);
288 StringBuilder whereClause = new StringBuilder ();
289 bool keyFound = false;
292 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
293 if ((bool)schemaRow["IsExpression"] == true)
295 if (!IncludedInWhereClause (schemaRow))
298 if (whereClause.Length > 0)
299 whereClause.Append (" AND ");
301 bool isKey = (bool) schemaRow ["IsKey"];
302 SqlParameter parameter = null;
307 bool allowNull = (bool) schemaRow ["AllowDBNull"];
309 string sourceColumnName = (string) schemaRow ["BaseColumnName"];
310 if (useColumnsForParameterNames) {
311 parameter = deleteCommand.Parameters.Add (
312 GetNullCheckParameterName (sourceColumnName),
315 parameter = deleteCommand.Parameters.Add (
316 GetParameterName (parmIndex++),
319 parameter.IsNullable = allowNull;
320 parameter.SourceVersion = DataRowVersion.Current;
323 whereClause.Append ("(");
324 whereClause.Append (String.Format (clause1, parameter.ParameterName,
325 GetQuotedString (sourceColumnName)));
326 whereClause.Append (" OR ");
329 if (useColumnsForParameterNames)
330 parameter = CreateParameter (schemaRow, true);
332 parameter = CreateParameter (parmIndex++, schemaRow);
333 deleteCommand.Parameters.Add (parameter);
334 ApplyParameterInfo (parameter, schemaRow, StatementType.Delete, true);
335 parameter.IsNullable = allowNull;
336 parameter.SourceVersion = DataRowVersion.Original;
338 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
341 whereClause.Append (")");
344 throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
346 // We're all done, so bring it on home
347 string sql = String.Format ("{0} WHERE ( {1} )", command, whereClause.ToString ());
348 deleteCommand.CommandText = sql;
349 return deleteCommand;
352 private SqlCommand CreateInsertCommand (bool useColumnsForParameterNames)
354 if (QuotedTableName == String.Empty)
357 CreateNewCommand (ref insertCommand);
359 string command = String.Format ("INSERT INTO {0}", QuotedTableName);
361 StringBuilder columns = new StringBuilder ();
362 StringBuilder values = new StringBuilder ();
365 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
366 if (!IncludedInInsert (schemaRow))
370 columns.Append (" , ");
371 values.Append (" , ");
374 SqlParameter parameter = null;
375 if (useColumnsForParameterNames) {
376 parameter = CreateParameter (schemaRow, false);
378 parameter = CreateParameter (parmIndex, schemaRow);
381 insertCommand.Parameters.Add (parameter);
382 ApplyParameterInfo (parameter, schemaRow, StatementType.Insert, false);
383 parameter.SourceVersion = DataRowVersion.Current;
384 parameter.IsNullable = (bool) schemaRow ["AllowDBNull"];
386 columns.Append (GetQuotedString (parameter.SourceColumn));
387 values.Append (parameter.ParameterName);
392 sql = String.Format ("{0}( {1} ) VALUES ( {2} )", command, columns.ToString (), values.ToString ());
393 insertCommand.CommandText = sql;
394 return insertCommand;
397 private void CreateNewCommand (ref SqlCommand command)
399 SqlCommand sourceCommand = SourceCommand;
400 if (command == null) {
401 command = sourceCommand.Connection.CreateCommand ();
402 command.CommandTimeout = sourceCommand.CommandTimeout;
403 command.Transaction = sourceCommand.Transaction;
405 command.CommandType = CommandType.Text;
406 command.UpdatedRowSource = UpdateRowSource.None;
407 command.Parameters.Clear ();
410 private SqlCommand CreateUpdateCommand (bool useColumnsForParameterNames)
412 // If no table was found, then we can't do an update
413 if (QuotedTableName == String.Empty)
416 CreateNewCommand (ref updateCommand);
418 string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
419 StringBuilder columns = new StringBuilder ();
420 StringBuilder whereClause = new StringBuilder ();
422 bool keyFound = false;
424 // First, create the X=Y list for UPDATE
425 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
426 if (!IncludedInUpdate (schemaRow))
428 if (columns.Length > 0)
429 columns.Append (" , ");
431 SqlParameter parameter = null;
432 if (useColumnsForParameterNames) {
433 parameter = CreateParameter (schemaRow, false);
435 parameter = CreateParameter (parmIndex++, schemaRow);
437 updateCommand.Parameters.Add (parameter);
438 ApplyParameterInfo (parameter, schemaRow, StatementType.Update, false);
439 parameter.IsNullable = (bool) schemaRow ["AllowDBNull"];
440 parameter.SourceVersion = DataRowVersion.Current;
442 columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
445 // Now, create the WHERE clause. This may be optimizable, but it would be ugly to incorporate
446 // into the loop above. "Premature optimization is the root of all evil." -- Knuth
447 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
448 if ((bool)schemaRow["IsExpression"] == true)
451 if (!IncludedInWhereClause (schemaRow))
454 if (whereClause.Length > 0)
455 whereClause.Append (" AND ");
457 bool isKey = (bool) schemaRow ["IsKey"];
458 SqlParameter parameter = null;
463 bool allowNull = (bool) schemaRow ["AllowDBNull"];
465 string sourceColumnName = (string) schemaRow ["BaseColumnName"];
466 if (useColumnsForParameterNames) {
467 parameter = updateCommand.Parameters.Add (
468 GetNullCheckParameterName (sourceColumnName),
471 parameter = updateCommand.Parameters.Add (
472 GetParameterName (parmIndex++),
475 parameter.IsNullable = allowNull;
476 parameter.SourceVersion = DataRowVersion.Current;
479 whereClause.Append ("(");
480 whereClause.Append (String.Format (clause1, parameter.ParameterName,
481 GetQuotedString (sourceColumnName)));
482 whereClause.Append (" OR ");
486 if (useColumnsForParameterNames) {
487 parameter = CreateParameter (schemaRow, true);
489 parameter = CreateParameter (parmIndex++, schemaRow);
491 updateCommand.Parameters.Add (parameter);
492 ApplyParameterInfo (parameter, schemaRow, StatementType.Update, true);
493 parameter.IsNullable = allowNull;
494 parameter.SourceVersion = DataRowVersion.Original;
496 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
499 whereClause.Append (")");
502 throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
504 // We're all done, so bring it on home
505 string sql = String.Format ("{0}{1} WHERE ( {2} )", command, columns.ToString (), whereClause.ToString ());
506 updateCommand.CommandText = sql;
507 return updateCommand;
510 private SqlParameter CreateParameter (DataRow schemaRow, bool whereClause)
512 string sourceColumn = (string) schemaRow ["BaseColumnName"];
515 name = GetParameterName ("Original_" + sourceColumn);
517 name = GetParameterName (sourceColumn);
519 SqlParameter param = new SqlParameter ();
520 param.ParameterName = name;
521 param.SourceColumn = sourceColumn;
525 private SqlParameter CreateParameter (int paramIndex, DataRow schemaRow)
527 string sourceColumn = (string) schemaRow ["BaseColumnName"];
528 string name = GetParameterName (paramIndex);
530 SqlParameter param = new SqlParameter ();
531 param.ParameterName = name;
532 param.SourceColumn = sourceColumn;
537 public static void DeriveParameters (SqlCommand command)
539 command.DeriveParameters ();
543 protected override void Dispose (bool disposing)
547 if (insertCommand != null)
548 insertCommand.Dispose ();
549 if (deleteCommand != null)
550 deleteCommand.Dispose ();
551 if (updateCommand != null)
552 updateCommand.Dispose ();
553 if (dbSchemaTable != null)
554 dbSchemaTable.Dispose ();
563 SqlCommand GetDeleteCommand ()
565 return (SqlCommand) base.GetDeleteCommand (false);
570 SqlCommand GetInsertCommand ()
572 return (SqlCommand) base.GetInsertCommand (false);
577 SqlCommand GetUpdateCommand ()
579 return (SqlCommand) base.GetUpdateCommand (false);
582 public new SqlCommand GetUpdateCommand (bool useColumnsForParameterNames)
584 return (SqlCommand) base.GetUpdateCommand (useColumnsForParameterNames);
587 public new SqlCommand GetDeleteCommand (bool useColumnsForParameterNames)
589 return (SqlCommand) base.GetDeleteCommand (useColumnsForParameterNames);
592 public new SqlCommand GetInsertCommand (bool useColumnsForParameterNames)
594 return (SqlCommand) base.GetInsertCommand (useColumnsForParameterNames);
597 public override string QuoteIdentifier (string unquotedIdentifier)
599 if (unquotedIdentifier == null)
600 throw new ArgumentNullException ("unquotedIdentifier");
602 string prefix = QuotePrefix;
603 string suffix = QuoteSuffix;
605 if ((prefix == "[" && suffix != "]") || (prefix == "\"" && suffix != "\""))
606 throw new ArgumentException ("The QuotePrefix " +
607 "and QuoteSuffix properties do not match.");
609 string escaped = unquotedIdentifier.Replace (suffix,
611 return string.Concat (prefix, escaped, suffix);
614 public override string UnquoteIdentifier (string quotedIdentifier)
616 return base.UnquoteIdentifier (quotedIdentifier);
619 private bool IncludedInInsert (DataRow schemaRow)
621 // If the parameter has one of these properties, then we don't include it in the insert:
622 // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
624 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
626 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
628 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
630 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
632 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
637 private bool IncludedInUpdate (DataRow schemaRow)
639 // If the parameter has one of these properties, then we don't include it in the insert:
640 // AutoIncrement, Hidden, RowVersion
642 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
644 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
646 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
648 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
650 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
656 private bool IncludedInWhereClause (DataRow schemaRow)
658 if ((bool) schemaRow ["IsLong"])
664 private string GetQuotedString (string value)
666 if (value == null || value.Length == 0)
669 string prefix = QuotePrefix;
670 string suffix = QuoteSuffix;
672 if (prefix.Length == 0 && suffix.Length == 0)
674 return String.Format ("{0}{1}{2}", prefix, value, suffix);
677 string GetNullCheckParameterName (string parameterName)
679 return GetParameterName ("IsNull_" + parameterName);
683 private string QuotedTableName {
684 get { return GetQuotedString (tableName); }
687 public void RefreshSchema ()
689 // FIXME: "Figure out what else needs to be cleaned up when we refresh."
690 tableName = String.Empty;
691 dbSchemaTable = null;
692 deleteCommand = null;
693 insertCommand = null;
694 updateCommand = null;
698 protected override void ApplyParameterInfo (DbParameter parameter,
700 StatementType statementType,
703 SqlParameter sqlParam = (SqlParameter) parameter;
704 sqlParam.SqlDbType = (SqlDbType) datarow ["ProviderType"];
706 object precision = datarow ["NumericPrecision"];
707 if (precision != DBNull.Value) {
708 short val = (short) precision;
709 if (val < byte.MaxValue && val >= byte.MinValue)
710 sqlParam.Precision = (byte) val;
713 object scale = datarow ["NumericScale"];
714 if (scale != DBNull.Value) {
715 short val = ((short) scale);
716 if (val < byte.MaxValue && val >= byte.MinValue)
717 sqlParam.Scale = (byte) val;
722 string GetParameterName (int parameterOrdinal)
724 return String.Format ("@p{0}", parameterOrdinal);
728 string GetParameterName (string parameterName)
730 return String.Format ("@{0}", parameterName);
733 protected override string GetParameterPlaceholder (int parameterOrdinal)
735 return GetParameterName (parameterOrdinal);
738 #endregion // Methods
740 #region Event Handlers
742 void RowUpdatingHandler (object sender, SqlRowUpdatingEventArgs args)
744 base.RowUpdatingHandler (args);
747 protected override void SetRowUpdatingHandler (DbDataAdapter adapter)
749 SqlDataAdapter sda = adapter as SqlDataAdapter;
751 throw new InvalidOperationException ("Adapter needs to be a SqlDataAdapter");
754 if (sda != base.DataAdapter)
755 sda.RowUpdating += new SqlRowUpdatingEventHandler (RowUpdatingHandler);
757 sda.RowUpdating -= new SqlRowUpdatingEventHandler (RowUpdatingHandler);;
760 protected override DataTable GetSchemaTable (DbCommand srcCommand)
762 using (SqlDataReader rdr = (SqlDataReader) srcCommand.ExecuteReader (CommandBehavior.KeyInfo | CommandBehavior.SchemaOnly))
763 return rdr.GetSchemaTable ();
766 protected override DbCommand InitializeCommand (DbCommand command)
768 if (command == null) {
769 command = new SqlCommand ();
771 command.CommandTimeout = 30;
772 command.Transaction = null;
773 command.CommandType = CommandType.Text;
774 command.UpdatedRowSource = UpdateRowSource.None;
779 #endregion // Event Handlers