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 {
41 public sealed class SqlCommandBuilder : Component
45 bool disposed = false;
47 DataTable dbSchemaTable;
48 SqlDataAdapter adapter;
54 SqlCommand deleteCommand;
55 SqlCommand insertCommand;
56 SqlCommand updateCommand;
58 // Used to construct WHERE clauses
59 static readonly string clause1 = "({0} IS NULL AND {1} IS NULL)";
60 static readonly string clause2 = "({0} = {1})";
66 public SqlCommandBuilder ()
70 quoteSuffix = String.Empty;
71 quotePrefix = String.Empty;
74 public SqlCommandBuilder (SqlDataAdapter adapter)
77 DataAdapter = adapter;
80 #endregion // Constructors
84 [DataSysDescription ("The DataAdapter for which to automatically generate SqlCommands")]
86 public SqlDataAdapter DataAdapter {
87 get { return adapter; }
91 adapter.RowUpdating += new SqlRowUpdatingEventHandler (RowUpdatingHandler);
95 private string QuotedTableName {
96 get { return GetQuotedString (tableName); }
100 [DataSysDescription ("The character used in a text command as the opening quote for quoting identifiers that contain special characters.")]
101 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
102 public string QuotePrefix {
103 get { return quotePrefix; }
105 if (dbSchemaTable != null)
106 throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
112 [DataSysDescription ("The character used in a text command as the closing quote for quoting identifiers that contain special characters.")]
113 [DesignerSerializationVisibility (DesignerSerializationVisibility.Hidden)]
114 public string QuoteSuffix {
115 get { return quoteSuffix; }
117 if (dbSchemaTable != null)
118 throw new InvalidOperationException ("The QuotePrefix and QuoteSuffix properties cannot be changed once an Insert, Update, or Delete command has been generated.");
123 private SqlCommand SourceCommand {
126 return adapter.SelectCommand;
131 #endregion // Properties
135 private void BuildCache (bool closeConnection)
137 SqlCommand sourceCommand = SourceCommand;
138 if (sourceCommand == null)
139 throw new InvalidOperationException ("The DataAdapter.SelectCommand property needs to be initialized.");
140 SqlConnection connection = sourceCommand.Connection;
141 if (connection == null)
142 throw new InvalidOperationException ("The DataAdapter.SelectCommand.Connection property needs to be initialized.");
144 if (dbSchemaTable == null) {
145 if (connection.State == ConnectionState.Open)
146 closeConnection = false;
150 SqlDataReader reader = sourceCommand.ExecuteReader (CommandBehavior.SchemaOnly | CommandBehavior.KeyInfo);
151 dbSchemaTable = reader.GetSchemaTable ();
155 BuildInformation (dbSchemaTable);
159 private void BuildInformation (DataTable schemaTable)
161 tableName = String.Empty;
162 foreach (DataRow schemaRow in schemaTable.Rows) {
163 if (tableName == String.Empty)
164 tableName = schemaRow.IsNull ("BaseTableName") ? null : (string) schemaRow ["BaseTableName"];
165 else if (schemaRow.IsNull ("BaseTableName")) {
166 if (tableName != null)
167 throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
168 } else if (tableName != (string) schemaRow["BaseTableName"])
169 throw new InvalidOperationException ("Dynamic SQL generation is not supported against multiple base tables.");
171 dbSchemaTable = schemaTable;
174 private SqlCommand CreateDeleteCommand (DataRow row, DataTableMapping tableMapping)
176 // If no table was found, then we can't do an delete
177 if (QuotedTableName == String.Empty)
181 CreateNewCommand (ref deleteCommand);
183 string command = String.Format ("DELETE FROM {0} ", QuotedTableName);
184 StringBuilder columns = new StringBuilder ();
185 StringBuilder whereClause = new StringBuilder ();
186 string dsColumnName = String.Empty;
187 bool keyFound = false;
190 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
191 if (!IncludedInWhereClause (schemaRow))
194 if (whereClause.Length > 0)
195 whereClause.Append (" AND ");
197 bool isKey = (bool) schemaRow ["IsKey"];
198 SqlParameter parameter = null;
201 parameter = deleteCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
203 dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
205 parameter.Value = row [dsColumnName, DataRowVersion.Current];
206 whereClause.Append ("(");
207 whereClause.Append (String.Format (clause1, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
208 whereClause.Append (" OR ");
213 parameter = deleteCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
215 dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
217 parameter.Value = row [dsColumnName, DataRowVersion.Current];
219 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
222 whereClause.Append (")");
225 throw new InvalidOperationException ("Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information.");
227 // We're all done, so bring it on home
228 string sql = String.Format ("{0} WHERE ( {1} )", command, whereClause.ToString ());
229 deleteCommand.CommandText = sql;
230 return deleteCommand;
233 private SqlCommand CreateInsertCommand (DataRow row, DataTableMapping tableMapping)
235 if (QuotedTableName == String.Empty)
238 CreateNewCommand (ref insertCommand);
240 string command = String.Format ("INSERT INTO {0}", QuotedTableName);
242 StringBuilder columns = new StringBuilder ();
243 StringBuilder values = new StringBuilder ();
244 string dsColumnName = String.Empty;
247 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
248 if (!IncludedInInsert (schemaRow))
252 columns.Append (" , ");
253 values.Append (" , ");
256 SqlParameter parameter = insertCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
258 dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
260 parameter.Value = row [dsColumnName];
262 columns.Append (GetQuotedString (parameter.SourceColumn));
263 values.Append (parameter.ParameterName);
266 sql = String.Format ("{0}( {1} ) VALUES ( {2} )", command, columns.ToString (), values.ToString ());
267 insertCommand.CommandText = sql;
268 return insertCommand;
271 private void CreateNewCommand (ref SqlCommand command)
273 SqlCommand sourceCommand = SourceCommand;
274 if (command == null) {
275 command = sourceCommand.Connection.CreateCommand ();
276 command.CommandTimeout = sourceCommand.CommandTimeout;
277 command.Transaction = sourceCommand.Transaction;
279 command.CommandType = CommandType.Text;
280 command.UpdatedRowSource = UpdateRowSource.None;
283 private SqlCommand CreateUpdateCommand (DataRow row, DataTableMapping tableMapping)
285 // If no table was found, then we can't do an update
286 if (QuotedTableName == String.Empty)
289 CreateNewCommand (ref updateCommand);
291 string command = String.Format ("UPDATE {0} SET ", QuotedTableName);
292 StringBuilder columns = new StringBuilder ();
293 StringBuilder whereClause = new StringBuilder ();
295 string dsColumnName = String.Empty;
296 bool keyFound = false;
298 // First, create the X=Y list for UPDATE
299 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
300 if (columns.Length > 0)
301 columns.Append (" , ");
303 SqlParameter parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
305 dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
307 parameter.Value = row [dsColumnName, DataRowVersion.Proposed];
309 columns.Append (String.Format ("{0} = {1}", GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
312 // Now, create the WHERE clause. This may be optimizable, but it would be ugly to incorporate
313 // into the loop above. "Premature optimization is the root of all evil." -- Knuth
314 foreach (DataRow schemaRow in dbSchemaTable.Rows) {
315 if (!IncludedInWhereClause (schemaRow))
318 if (whereClause.Length > 0)
319 whereClause.Append (" AND ");
321 bool isKey = (bool) schemaRow ["IsKey"];
322 SqlParameter parameter = null;
326 parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
328 dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
330 parameter.Value = row [dsColumnName];
332 whereClause.Append ("(");
333 whereClause.Append (String.Format (clause1, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
334 whereClause.Append (" OR ");
339 parameter = updateCommand.Parameters.Add (CreateParameter (parmIndex++, schemaRow));
341 dsColumnName = tableMapping.ColumnMappings [parameter.SourceColumn].DataSetColumn;
343 parameter.Value = row [dsColumnName];
345 whereClause.Append (String.Format (clause2, GetQuotedString (parameter.SourceColumn), parameter.ParameterName));
348 whereClause.Append (")");
351 throw new InvalidOperationException ("Dynamic SQL generation for the UpdateCommand is not supported against a SelectCommand that does not return any key column information.");
353 // We're all done, so bring it on home
354 string sql = String.Format ("{0}{1} WHERE ( {2} )", command, columns.ToString (), whereClause.ToString ());
355 updateCommand.CommandText = sql;
356 return updateCommand;
359 private SqlParameter CreateParameter (int parmIndex, DataRow schemaRow)
361 string name = String.Format ("@p{0}", parmIndex);
362 string sourceColumn = (string) schemaRow ["BaseColumnName"];
363 SqlDbType sqlDbType = (SqlDbType) schemaRow ["ProviderType"];
364 int size = (int) schemaRow ["ColumnSize"];
366 return new SqlParameter (name, sqlDbType, size, sourceColumn);
369 public static void DeriveParameters (SqlCommand command)
371 command.DeriveParameters ();
374 protected override void Dispose (bool disposing)
378 if (insertCommand != null)
379 insertCommand.Dispose ();
380 if (deleteCommand != null)
381 deleteCommand.Dispose ();
382 if (updateCommand != null)
383 updateCommand.Dispose ();
384 if (dbSchemaTable != null)
385 dbSchemaTable.Dispose ();
391 public SqlCommand GetDeleteCommand ()
394 return CreateDeleteCommand (null, null);
397 public SqlCommand GetInsertCommand ()
400 return CreateInsertCommand (null, null);
403 private string GetQuotedString (string value)
405 if (value == String.Empty || value == null)
407 if (quotePrefix == String.Empty && quoteSuffix == String.Empty)
409 return String.Format ("{0}{1}{2}", quotePrefix, value, quoteSuffix);
412 public SqlCommand GetUpdateCommand ()
415 return CreateUpdateCommand (null, null);
418 private bool IncludedInInsert (DataRow schemaRow)
420 // If the parameter has one of these properties, then we don't include it in the insert:
421 // AutoIncrement, Hidden, Expression, RowVersion, ReadOnly
423 if (!schemaRow.IsNull ("IsAutoIncrement") && (bool) schemaRow ["IsAutoIncrement"])
425 if (!schemaRow.IsNull ("IsHidden") && (bool) schemaRow ["IsHidden"])
427 if (!schemaRow.IsNull ("IsExpression") && (bool) schemaRow ["IsExpression"])
429 if (!schemaRow.IsNull ("IsRowVersion") && (bool) schemaRow ["IsRowVersion"])
431 if (!schemaRow.IsNull ("IsReadOnly") && (bool) schemaRow ["IsReadOnly"])
436 private bool IncludedInUpdate (DataRow schemaRow)
438 // If the parameter has one of these properties, then we don't include it in the insert:
439 // AutoIncrement, Hidden, RowVersion
441 if ((bool) schemaRow ["IsAutoIncrement"])
443 if ((bool) schemaRow ["IsHidden"])
445 if ((bool) schemaRow ["IsRowVersion"])
450 private bool IncludedInWhereClause (DataRow schemaRow)
452 if ((bool) schemaRow ["IsLong"])
457 [MonoTODO ("Figure out what else needs to be cleaned up when we refresh.")]
458 public void RefreshSchema ()
460 tableName = String.Empty;
461 dbSchemaTable = null;
464 #endregion // Methods
466 #region Event Handlers
468 private void RowUpdatingHandler (object sender, SqlRowUpdatingEventArgs e)
470 if (e.Status != UpdateStatus.Continue)
473 switch (e.StatementType) {
474 case StatementType.Delete:
475 deleteCommand = e.Command;
477 case StatementType.Insert:
478 insertCommand = e.Command;
480 case StatementType.Update:
481 updateCommand = e.Command;
490 switch (e.StatementType) {
491 case StatementType.Delete:
492 e.Command = CreateDeleteCommand (e.Row, e.TableMapping);
493 e.Status = UpdateStatus.Continue;
495 case StatementType.Insert:
496 e.Command = CreateInsertCommand (e.Row, e.TableMapping);
497 e.Status = UpdateStatus.Continue;
499 case StatementType.Update:
500 e.Command = CreateUpdateCommand (e.Row, e.TableMapping);
501 e.Status = UpdateStatus.Continue;
505 if (e.Command != null && e.Row != null) {
506 e.Row.AcceptChanges ();
507 e.Status = UpdateStatus.SkipCurrentRow;
510 catch (Exception exception) {
511 e.Errors = exception;
512 e.Status = UpdateStatus.ErrorsOccurred;
516 #endregion // Event Handlers