BindingFlags.Public needed here as Exception.HResult is now public in .NET 4.5. This...
[mono.git] / mcs / class / System.Data / System.Data / ForeignKeyConstraint.cs
1 //
2 // System.Data.ForeignKeyConstraint.cs
3 //
4 // Author:
5 //   Franklin Wise <gracenote@earthlink.net>
6 //   Daniel Morgan <danmorg@sc.rr.com>
7 //
8 // (C) 2002 Franklin Wise
9 // (C) 2002 Daniel Morgan
10 //
11
12 //
13 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
14 //
15 // Permission is hereby granted, free of charge, to any person obtaining
16 // a copy of this software and associated documentation files (the
17 // "Software"), to deal in the Software without restriction, including
18 // without limitation the rights to use, copy, modify, merge, publish,
19 // distribute, sublicense, and/or sell copies of the Software, and to
20 // permit persons to whom the Software is furnished to do so, subject to
21 // the following conditions:
22 // 
23 // The above copyright notice and this permission notice shall be
24 // included in all copies or substantial portions of the Software.
25 // 
26 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
29 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
30 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
31 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
32 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
33 //
34
35 using System;
36 using System.Collections;
37 using System.ComponentModel;
38 using System.Runtime.InteropServices;
39 using System.Data.Common;
40
41 namespace System.Data {
42         [Editor ("Microsoft.VSDesigner.Data.Design.ForeignKeyConstraintEditor, " + Consts.AssemblyMicrosoft_VSDesigner,
43                  "System.Drawing.Design.UITypeEditor, " + Consts.AssemblySystem_Drawing)]
44         [DefaultProperty ("ConstraintName")]
45 #if !NET_2_0
46         [Serializable]
47 #endif
48         public class ForeignKeyConstraint : Constraint 
49         {
50                 private UniqueConstraint _parentUniqueConstraint;
51                 //FIXME: create a class which will wrap this collection
52                 private DataColumn [] _parentColumns;
53                 //FIXME: create a class which will wrap this collection
54                 private DataColumn [] _childColumns;
55                 private Rule _deleteRule = Rule.Cascade;
56                 private Rule _updateRule = Rule.Cascade;
57                 private AcceptRejectRule _acceptRejectRule = AcceptRejectRule.None;
58                 private string _parentTableName;
59 #if NET_2_0
60                 private string _parentTableNamespace;
61 #endif
62                 private string _childTableName;
63
64                 //FIXME: remove those; and use only DataColumns[]
65                 private string [] _parentColumnNames;
66                 private string [] _childColumnNames;
67                         
68                 #region Constructors
69
70                 public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn) 
71                 {
72                         if (null == parentColumn || null == childColumn) {
73                                 throw new NullReferenceException("Neither parentColumn or" +
74                                         " childColumn can be null.");
75                         }
76                         _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
77                                         new DataColumn[] {childColumn});
78                 }
79
80                 public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns) 
81                 {
82                         _foreignKeyConstraint(null, parentColumns, childColumns);
83                 }
84
85                 public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn) 
86                 {
87                         if (null == parentColumn || null == childColumn) {
88                                 throw new NullReferenceException("Neither parentColumn or" +
89                                         " childColumn can be null.");
90                         }
91
92                         _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
93                                         new DataColumn[] {childColumn});
94                 }
95
96                 public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns) 
97                 {
98                         _foreignKeyConstraint(constraintName, parentColumns, childColumns);
99                 }
100
101                 //special case
102                 [Browsable (false)]
103                 public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule) 
104                 {
105                         int i;
106                         InitInProgress = true;
107                         base.ConstraintName = constraintName;
108
109                         // "parentTableName" is searched in the "DataSet" to which the "DataTable"
110                         // from which AddRange() is called
111                         // childTable is the "DataTable" which calls AddRange()
112
113                         // Keep reference to parentTableName to resolve later
114                         _parentTableName = parentTableName;
115
116                         // Keep a copy of parentColumnNames to resolve later
117                         _parentColumnNames = new string [parentColumnNames.Length];
118                         for (i = 0; i < parentColumnNames.Length; i++)
119                                _parentColumnNames[i] = parentColumnNames[i];
120                         
121                         // Keep a copy of childColumnNames to resolve later
122                         _childColumnNames = new string [childColumnNames.Length];
123                         for (i = 0; i < childColumnNames.Length; i++)
124                                _childColumnNames[i] = childColumnNames[i];
125                        
126
127                         _acceptRejectRule = acceptRejectRule;
128                         _deleteRule = deleteRule;
129                         _updateRule = updateRule;
130                 }
131
132                 internal override void FinishInit (DataTable childTable)
133                 {
134                         if (childTable.DataSet == null)
135                                 throw new InvalidConstraintException ("ChildTable : " + childTable.TableName + " does not belong to any DataSet");
136
137                         DataSet dataSet = childTable.DataSet;
138                         _childTableName = childTable.TableName;
139
140                         if (!dataSet.Tables.Contains (_parentTableName))
141                                 throw new InvalidConstraintException ("Table : " + _parentTableName + "does not exist in DataSet : " + dataSet);
142
143                         DataTable parentTable = dataSet.Tables [_parentTableName];
144
145                         int i = 0, j = 0;
146
147                         if (_parentColumnNames.Length < 0 || _childColumnNames.Length < 0)
148                                 throw new InvalidConstraintException ("Neither parent nor child columns can be zero length");
149
150                         if (_parentColumnNames.Length != _childColumnNames.Length)
151                                 throw new InvalidConstraintException ("Both parent and child columns must be of same length");                                                                                                    
152                         DataColumn[] parentColumns = new DataColumn [_parentColumnNames.Length];
153                         DataColumn[] childColumns = new DataColumn [_childColumnNames.Length];
154
155                         foreach (string parentCol in _parentColumnNames){
156                                 if (!parentTable.Columns.Contains (parentCol))
157                                         throw new InvalidConstraintException ("Table : " + _parentTableName + "does not contain the column :" + parentCol);
158                                 parentColumns [i++] = parentTable. Columns [parentCol];
159                         }
160
161                         foreach (string childCol in _childColumnNames){
162                                 if (!childTable.Columns.Contains (childCol))
163                                         throw new InvalidConstraintException ("Table : " + _childTableName + "does not contain the column : " + childCol);
164                                 childColumns [j++] = childTable.Columns [childCol];
165                         }
166                         _validateColumns (parentColumns, childColumns);
167                         _parentColumns = parentColumns;
168                         _childColumns = childColumns;
169 #if NET_2_0
170                         parentTable.Namespace = _parentTableNamespace;
171 #endif
172                         InitInProgress = false;
173                 }
174
175 #if NET_2_0
176                 [Browsable (false)]
177                 public ForeignKeyConstraint (string constraintName, string parentTableName, string parentTableNamespace, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
178                 {
179                         InitInProgress = true;
180                         base.ConstraintName = constraintName;
181
182                         // "parentTableName" is searched in the "DataSet" to which the "DataTable"
183                         // from which AddRange() is called
184                         // childTable is the "DataTable" which calls AddRange()
185
186                         // Keep reference to parentTableName to resolve later
187                         _parentTableName = parentTableName;
188                         _parentTableNamespace = parentTableNamespace;
189
190                         // Keep reference to parentColumnNames to resolve later
191                         _parentColumnNames = parentColumnNames;
192
193                         // Keep reference to childColumnNames to resolve later
194                         _childColumnNames = childColumnNames;
195
196                         _acceptRejectRule = acceptRejectRule;
197                         _deleteRule = deleteRule;
198                         _updateRule = updateRule;
199                 }
200 #endif
201
202                 private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
203                                 DataColumn[] childColumns)
204                 {
205                         int i;
206                         //Validate 
207                         _validateColumns(parentColumns, childColumns);
208
209                         //Set Constraint Name
210                         base.ConstraintName = constraintName;   
211
212                         //copy the columns - Do not keep reference #672113
213                         _parentColumns = new DataColumn [parentColumns.Length];
214                         for (i = 0; i < parentColumns.Length; i++)
215                                _parentColumns[i] = parentColumns[i];
216                         
217                         _childColumns = new DataColumn [childColumns.Length];                   
218                         for (i = 0; i < childColumns.Length; i++)
219                                _childColumns[i] = childColumns[i];
220                 }
221
222 #endregion // Constructors
223
224 #region Helpers
225
226                 private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
227                 {
228                         //not null
229                         if (null == parentColumns || null == childColumns) 
230                                 throw new ArgumentNullException();
231
232                         //at least one element in each array
233                         if (parentColumns.Length < 1 || childColumns.Length < 1)
234                                 throw new ArgumentException("Neither ParentColumns or ChildColumns can't be" +
235                                                 " zero length.");
236                                 
237                         //same size arrays
238                         if (parentColumns.Length != childColumns.Length)
239                                 throw new ArgumentException("Parent columns and child columns must be the same length.");
240                         
241
242                         DataTable ptable = parentColumns[0].Table;
243                         DataTable ctable = childColumns[0].Table;
244
245                         for (int i = 0; i < parentColumns.Length; i++) {
246                                 DataColumn pc = parentColumns[i];
247                                 DataColumn cc = childColumns[i];
248
249                                 //not null check
250                                 if (null == pc.Table) 
251                                         throw new ArgumentException("All columns must belong to a table." + 
252                                                 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
253                                 
254                                 //All columns must belong to the same table
255                                 if (ptable != pc.Table)
256                                         throw new InvalidConstraintException("Parent columns must all belong to the same table.");
257
258                                 //not null check
259                                 if (null == cc.Table) 
260                                         throw new ArgumentException("All columns must belong to a table." + 
261                                                 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
262
263                                 //All columns must belong to the same table.
264                                 if (ctable != cc.Table)
265                                         throw new InvalidConstraintException("Child columns must all belong to the same table.");
266
267                                 if (pc.CompiledExpression != null)
268                                         throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", pc.ColumnName));
269
270                                 if (cc.CompiledExpression != null)
271                                         throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", cc.ColumnName));
272                                 
273                         }
274
275                         //Same dataset.  If both are null it's ok
276                         if (ptable.DataSet != ctable.DataSet)
277                         {
278                                 //LAMESPEC: spec says InvalidConstraintExceptoin
279                                 //      impl does InvalidOperationException
280                                 throw new InvalidOperationException("Parent column and child column must belong to" + 
281                                                 " tables that belong to the same DataSet.");
282                                                 
283                         }       
284
285
286                         int identicalCols = 0;
287                         for (int i = 0; i < parentColumns.Length; i++)
288                         {
289                                 DataColumn pc = parentColumns[i];
290                                 DataColumn cc = childColumns[i];
291                                 
292                                 if (pc == cc) {
293                                         identicalCols++;
294                                         continue;
295                                 }
296
297                                 if (!pc.DataTypeMatches (cc)) {
298                                         //LAMESPEC: spec says throw InvalidConstraintException
299                                         //              implementation throws InvalidOperationException
300                                         throw new InvalidOperationException ("Parent column is not type compatible with it's child"
301                                                 + " column.");
302                                 }
303                         }
304                         if (identicalCols == parentColumns.Length)
305                                 throw new InvalidOperationException ("Property not accessible because 'ParentKey and ChildKey are identical.'.");
306                         
307                 }
308                 
309                 //Checks to see if a related unique constraint exists
310                 //if it doesn't then a unique constraint is created.
311                 //if a unique constraint can't be created an exception will be thrown
312                 private void _ensureUniqueConstraintExists(ConstraintCollection collection,
313                                 DataColumn [] parentColumns)
314                 {
315                         //not null
316                         if (null == parentColumns) throw new ArgumentNullException(
317                                         "ParentColumns can't be null");
318
319                         UniqueConstraint uc = null;
320                         
321                         //see if unique constraint already exists
322                         //if not create unique constraint
323                         if(parentColumns[0] != null)
324                                 uc = UniqueConstraint.GetUniqueConstraintForColumnSet(parentColumns[0].Table.Constraints, parentColumns);
325
326                         if (null == uc) {
327                                 uc = new UniqueConstraint(parentColumns, false); //could throw
328                                 parentColumns [0].Table.Constraints.Add (uc);
329                         }
330
331                         //keep reference
332                         _parentUniqueConstraint = uc;
333                         _parentUniqueConstraint.ChildConstraint = this;
334                 }
335                 
336                 
337                 #endregion //Helpers
338                 
339                 #region Properties
340
341                 [DataCategory ("Data")]
342 #if !NET_2_0
343                 [DataSysDescription ("For accept and reject changes, indicates what kind of cascading should take place across this relation.")]
344 #endif
345                 [DefaultValue (AcceptRejectRule.None)]
346                 public virtual AcceptRejectRule AcceptRejectRule {
347                         get { return _acceptRejectRule; }
348                         set { _acceptRejectRule = value; }
349                 }
350
351                 [DataCategory ("Data")]
352 #if !NET_2_0
353                 [DataSysDescription ("Indicates the child columns of this constraint.")]
354 #endif
355                 [ReadOnly (true)]
356                 public virtual DataColumn[] Columns {
357                         get { return _childColumns; }
358                 }
359
360                 [DataCategory ("Data")]
361 #if !NET_2_0
362                 [DataSysDescription ("For deletions, indicates what kind of cascading should take place across this relation.")]
363 #endif
364                 [DefaultValue (Rule.Cascade)]
365                 public virtual Rule DeleteRule {
366                         get { return _deleteRule; }
367                         set { _deleteRule = value; }
368                 }
369
370                 [DataCategory ("Data")]
371 #if !NET_2_0
372                 [DataSysDescription ("For updates, indicates what kind of cascading should take place across this relation.")]
373 #endif
374                 [DefaultValue (Rule.Cascade)]
375                 public virtual Rule UpdateRule {
376                         get { return _updateRule; }
377                         set { _updateRule = value; }
378                 }
379
380                 [DataCategory ("Data")] 
381 #if !NET_2_0
382                 [DataSysDescription ("Indicates the parent columns of this constraint.")]
383 #endif
384                 [ReadOnly (true)]
385                 public virtual DataColumn[] RelatedColumns {
386                         get { return _parentColumns; }
387                 }
388
389                 [DataCategory ("Data")] 
390 #if !NET_2_0
391                 [DataSysDescription ("Indicates the child table of this constraint.")]
392 #endif
393                 [ReadOnly (true)]
394                 public virtual DataTable RelatedTable {
395                         get {
396                                 if (_parentColumns != null)
397                                         if (_parentColumns.Length > 0)
398                                                 return _parentColumns[0].Table;
399
400                                 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
401                         }
402                 }
403
404                 [DataCategory ("Data")]
405 #if !NET_2_0
406                 [DataSysDescription ("Indicates the table of this constraint.")]
407 #endif
408                 [ReadOnly (true)]
409                 public override DataTable Table {
410                         get {
411                                 if (_childColumns != null)
412                                         if (_childColumns.Length > 0)
413                                                 return _childColumns[0].Table;
414
415                                 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
416                         }
417                 }
418
419                 internal UniqueConstraint ParentConstraint {
420                         get { return _parentUniqueConstraint; }
421                 }
422
423                 #endregion // Properties
424
425                 #region Methods
426
427                 public override bool Equals(object key) 
428                 {
429                         ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
430                         if (null == fkc) return false;
431
432                         //if the fk constrains the same columns then they are equal
433                         if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
434                                 return false;
435                         if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
436                                 return false;
437
438                         return true;
439                 }
440
441                 public override int GetHashCode()
442                 {
443                         //initialize hash1 and hash2 with default hashes
444                         //any two DIFFERENT numbers will do here
445                         int hash1 = 32, hash2 = 88;
446                         int i;
447
448                         //derive the hash code from the columns that way
449                         //Equals and GetHashCode return Equal objects to be the
450                         //same
451
452                         //Get the first parent column hash
453                         if (this.Columns.Length > 0)
454                                 hash1 ^= this.Columns[0].GetHashCode();
455                         
456                         //get the rest of the parent column hashes if there any
457                         for (i = 1; i < this.Columns.Length; i++)
458                         {
459                                 hash1 ^= this.Columns[1].GetHashCode();
460                                 
461                         }
462                         
463                         //Get the child column hash
464                         if (this.RelatedColumns.Length > 0)
465                                 hash2 ^= this.Columns[0].GetHashCode();
466                         
467                         for (i = 1; i < this.RelatedColumns.Length; i++)
468                         {
469                                 hash2 ^= this.RelatedColumns[1].GetHashCode();
470                         }
471
472                         //combine the two hashes
473                         return hash1 ^ hash2;
474                 }
475
476                 internal override void AddToConstraintCollectionSetup(
477                                 ConstraintCollection collection)
478                 {                       
479                         if (collection.Table != Table)
480                                 throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
481
482                         //run Ctor rules again
483                         _validateColumns(_parentColumns, _childColumns);
484                         
485                         //we must have a unique constraint on the parent
486                         _ensureUniqueConstraintExists(collection, _parentColumns);
487                         
488                         if ( (Table.DataSet != null && Table.DataSet.EnforceConstraints)
489                              || (Table.DataSet == null && Table.EnforceConstraints)) {
490                                 if (IsConstraintViolated())
491                                         throw new ArgumentException("This constraint cannot be enabled as not all values have corresponding parent values.");
492                         }
493                         //FIXME : if this fails and we created a unique constraint
494                         //we should probably roll it back
495                         // and remove index form Table                  
496                 }
497                                         
498                 internal override void RemoveFromConstraintCollectionCleanup( 
499                                 ConstraintCollection collection)
500                 {
501                         _parentUniqueConstraint.ChildConstraint = null;
502                         Index = null;
503                 }
504                 
505                 internal override bool IsConstraintViolated()
506                 {
507                         if (Table.DataSet == null || RelatedTable.DataSet == null) 
508                                 return false;
509                         
510                         bool hasErrors = false;
511                         foreach (DataRow row in Table.Rows) {
512                                 // first we check if all values in _childColumns place are nulls.
513                                 // if yes we return.
514                                 if (row.IsNullColumns(_childColumns))
515                                         continue;
516
517                                 // check whenever there is (at least one) parent row  in RelatedTable
518                                 if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) { 
519                                         // if no parent row exists - constraint is violated
520                                         hasErrors = true;
521                                         string[] values = new string[_childColumns.Length];
522                                         for (int i = 0; i < _childColumns.Length; i++){
523                                                 DataColumn col = _childColumns[i];
524                                                 values[i] = row[col].ToString();
525                                         }
526
527                                         row.RowError = String.Format("ForeignKeyConstraint {0} requires the child key values ({1}) to exist in the parent table.",
528                                                 ConstraintName, String.Join(",", values));
529                                 }
530                         }
531
532                         if (hasErrors)
533                                 //throw new ConstraintException("Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.");
534                                 return true;
535
536                         return false;
537                 }
538                 
539                 internal override void AssertConstraint(DataRow row)
540                 {
541                         // first we check if all values in _childColumns place are nulls.
542                         // if yes we return.
543                         if (row.IsNullColumns(_childColumns))
544                                 return;
545
546                         // check whenever there is (at least one) parent row  in RelatedTable
547                         if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) { 
548                                 // if no parent row exists - constraint is violated
549                                 throw new InvalidConstraintException(GetErrorMessage(row));
550                         }
551                 }
552
553                 internal override bool IsColumnContained(DataColumn column)
554                 {
555                         for (int i = 0; i < _parentColumns.Length; i++)
556                                 if (column == _parentColumns[i])
557                                         return true;
558
559                         for (int i = 0; i < _childColumns.Length; i++)
560                                 if (column == _childColumns[i])
561                                         return true;
562
563                         return false;
564                 }
565
566                 internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
567                         return true;
568                 }
569
570                 private string GetErrorMessage(DataRow row)
571                 {
572                         System.Text.StringBuilder sb = new System.Text.StringBuilder();
573                         for (int i = 0; i < _childColumns.Length; i++) {
574                                 sb.Append(row[_childColumns[0]].ToString());
575                                 if (i != _childColumns.Length - 1) {
576                                         sb.Append(',');
577                                 }
578                         }
579                         string valStr = sb.ToString();
580                         return "ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + valStr + ") to exist in the parent table.";
581                 }               
582                 
583                 #endregion // Methods
584         }
585
586 }