2005-06-13 Sureshkumar T <tsureshkumar@novell.com>
[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]
43         [DefaultProperty ("ConstraintName")]
44         [Serializable]
45         public class ForeignKeyConstraint : Constraint 
46         {
47                 private UniqueConstraint _parentUniqueConstraint;
48                 //FIXME: create a class which will wrap this collection
49                 private DataColumn [] _parentColumns;
50                 //FIXME: create a class which will wrap this collection
51                 private DataColumn [] _childColumns;
52                 private Rule _deleteRule = Rule.Cascade;
53                 private Rule _updateRule = Rule.Cascade;
54                 private AcceptRejectRule _acceptRejectRule = AcceptRejectRule.None;
55             private string _parentTableName;
56         private string _childTableName;
57                 //FIXME: remove those; and use only DataColumns[]
58         private string [] _parentColumnNames;
59         private string [] _childColumnNames;
60         private bool _dataColsNotValidated = false;
61                         
62                 #region Constructors
63
64                 public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn) 
65                 {
66                         if (null == parentColumn || null == childColumn) {
67                                 throw new NullReferenceException("Neither parentColumn or" +
68                                         " childColumn can be null.");
69                         }
70                         _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
71                                         new DataColumn[] {childColumn});
72                 }
73
74                 public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns) 
75                 {
76                         _foreignKeyConstraint(null, parentColumns, childColumns);
77                 }
78
79                 public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn) 
80                 {
81                         if (null == parentColumn || null == childColumn) {
82                                 throw new NullReferenceException("Neither parentColumn or" +
83                                         " childColumn can be null.");
84                         }
85
86                         _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
87                                         new DataColumn[] {childColumn});
88                 }
89
90                 public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns) 
91                 {
92                         _foreignKeyConstraint(constraintName, parentColumns, childColumns);
93                 }
94                 
95                 //special case
96                 [Browsable (false)]
97                 public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule) 
98                 {
99                         _dataColsNotValidated = true;
100             base.ConstraintName = constraintName;
101                                                                                         
102             // "parentTableName" is searched in the "DataSet" to which the "DataTable"
103             // from which AddRange() is called
104             // childTable is the "DataTable" which calls AddRange()
105                                                                                         
106             // Keep reference to parentTableName to resolve later
107             _parentTableName = parentTableName;
108                                                                                         
109             // Keep reference to parentColumnNames to resolve later
110             _parentColumnNames = parentColumnNames;
111                                                                                         
112             // Keep reference to childColumnNames to resolve later
113             _childColumnNames = childColumnNames;
114                                                                                         
115             _acceptRejectRule = acceptRejectRule;
116             _deleteRule = deleteRule;
117             _updateRule = updateRule;
118
119                 }
120
121                 internal void postAddRange (DataTable childTable)
122         {
123             // LAMESPEC - Does not say that this is mandatory
124             // Check whether childTable belongs to a DataSet
125             if (childTable.DataSet == null)
126                     throw new InvalidConstraintException ("ChildTable : " + childTable.TableName + " does not belong to any DataSet");
127             DataSet dataSet = childTable.DataSet;
128             _childTableName = childTable.TableName;
129             // Search for the parentTable in the childTable's DataSet
130             if (!dataSet.Tables.Contains (_parentTableName))
131                     throw new InvalidConstraintException ("Table : " + _parentTableName + "does not exist in DataSet : " + dataSet);
132                                                                                         
133             // Keep reference to parentTable
134             DataTable parentTable = dataSet.Tables [_parentTableName];
135                                                                                         
136             int i = 0, j = 0;
137                                                                                         
138             // LAMESPEC - Does not say which Exception is thrown
139             if (_parentColumnNames.Length < 0 || _childColumnNames.Length < 0)
140                     throw new InvalidConstraintException ("Neither parent nor child columns can be zero length");
141             // LAMESPEC - Does not say which Exception is thrown
142             if (_parentColumnNames.Length != _childColumnNames.Length)
143                                 throw new InvalidConstraintException ("Both parent and child columns must be of same length");                                                                                                    
144             DataColumn []parentColumns = new DataColumn [_parentColumnNames.Length];
145             DataColumn []childColumns = new DataColumn [_childColumnNames.Length];
146                                                                                         
147             // Search for the parentColumns in parentTable
148             foreach (string parentCol in _parentColumnNames){
149                     if (!parentTable.Columns.Contains (parentCol))
150                             throw new InvalidConstraintException ("Table : " + _parentTableName + "does not contain the column :" + parentCol);
151                     parentColumns [i++] = parentTable. Columns [parentCol];
152             }
153             // Search for the childColumns in childTable
154             foreach (string childCol in _childColumnNames){
155                     if (!childTable.Columns.Contains (childCol))
156                             throw new InvalidConstraintException ("Table : " + _childTableName + "does not contain the column : " + childCol);
157                     childColumns [j++] = childTable.Columns [childCol];
158             }
159             _validateColumns (parentColumns, childColumns);
160             _parentColumns = parentColumns;
161             _childColumns = childColumns;
162                 }
163                         
164 #if NET_2_0
165                 [MonoTODO]
166                 public ForeignKeyConstraint (string constraintName, string parentTableName, string parentTableNamespace, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
167                 {
168                         throw new NotImplementedException ();
169                 }
170 #endif
171
172                 private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
173                                 DataColumn[] childColumns)
174                 {
175
176                         //Validate 
177                         _validateColumns(parentColumns, childColumns);
178
179                         //Set Constraint Name
180                         base.ConstraintName = constraintName;   
181
182                         //Keep reference to columns
183                         _parentColumns = parentColumns;
184                         _childColumns = childColumns;
185                 }
186
187                 #endregion // Constructors
188
189                 #region Helpers
190
191                 private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
192                 {
193                         //not null
194                         if (null == parentColumns || null == childColumns) 
195                                 throw new ArgumentNullException();
196                         
197                         //at least one element in each array
198                         if (parentColumns.Length < 1 || childColumns.Length < 1)
199                                 throw new ArgumentException("Neither ParentColumns or ChildColumns can't be" +
200                                                 " zero length.");
201                                 
202                         //same size arrays
203                         if (parentColumns.Length != childColumns.Length)
204                                 throw new ArgumentException("Parent columns and child columns must be the same length.");
205                         
206
207                         DataTable ptable = parentColumns[0].Table;
208                         DataTable ctable = childColumns[0].Table;
209
210                         for (int i = 0; i < parentColumns.Length; i++) {
211                                 DataColumn pc = parentColumns[i];
212                                 DataColumn cc = childColumns[i];
213
214                                 //not null check
215                                 if (null == pc.Table) 
216                                         throw new ArgumentException("All columns must belong to a table." + 
217                                                 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
218                                 
219                                 //All columns must belong to the same table
220                                 if (ptable != pc.Table)
221                                         throw new InvalidConstraintException("Parent columns must all belong to the same table.");
222
223                                 //not null check
224                                 if (null == cc.Table) 
225                                         throw new ArgumentException("All columns must belong to a table." + 
226                                                 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
227
228                                 //All columns must belong to the same table.
229                                 if (ctable != cc.Table)
230                                         throw new InvalidConstraintException("Child columns must all belong to the same table.");
231
232                                 if (pc.CompiledExpression != null)
233                                         throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", pc.ColumnName));
234
235                                 if (cc.CompiledExpression != null)
236                                         throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", cc.ColumnName));
237                                 
238                         }
239
240                         //Same dataset.  If both are null it's ok
241                         if (ptable.DataSet != ctable.DataSet)
242                         {
243                                 //LAMESPEC: spec says InvalidConstraintExceptoin
244                                 //      impl does InvalidOperationException
245                                 throw new InvalidOperationException("Parent column and child column must belong to" + 
246                                                 " tables that belong to the same DataSet.");
247                                                 
248                         }       
249
250
251                         for (int i = 0; i < parentColumns.Length; i++)
252                         {
253                                 DataColumn pc = parentColumns[i];
254                                 DataColumn cc = childColumns[i];
255                                 
256                                 //Can't be the same column
257                                 if (pc == cc)
258                                         throw new InvalidOperationException("Parent and child columns can't be the same column.");
259
260                                 if (! pc.DataType.Equals(cc.DataType))
261                                 {
262                                         //LAMESPEC: spec says throw InvalidConstraintException
263                                         //              implementation throws InvalidOperationException
264                                         throw new InvalidConstraintException("Parent column is not type compatible with it's child"
265                                                 + " column.");
266                                 }
267                                         
268                         }
269                         
270                 }
271                 
272
273
274                 private void _validateRemoveParentConstraint(ConstraintCollection sender, 
275                                 Constraint constraint, ref bool cancel, ref string failReason)
276                 {
277 #if !NET_1_1
278                         //if we hold a reference to the parent then cancel it
279                         if (constraint == _parentUniqueConstraint) 
280                         {
281                                 cancel = true;
282                                 failReason = "Cannot remove UniqueConstraint because the"
283                                         + " ForeignKeyConstraint " + this.ConstraintName + " exists.";
284                         }
285 #endif
286                 }
287                 
288                 //Checks to see if a related unique constraint exists
289                 //if it doesn't then a unique constraint is created.
290                 //if a unique constraint can't be created an exception will be thrown
291                 private void _ensureUniqueConstraintExists(ConstraintCollection collection,
292                                 DataColumn [] parentColumns)
293                 {
294                         //not null
295                         if (null == parentColumns) throw new ArgumentNullException(
296                                         "ParentColumns can't be null");
297
298                         UniqueConstraint uc = null;
299                         
300                         //see if unique constraint already exists
301                         //if not create unique constraint
302                         if(parentColumns[0] != null)
303                                 uc = UniqueConstraint.GetUniqueConstraintForColumnSet(parentColumns[0].Table.Constraints, parentColumns);
304
305                         if (null == uc) {
306                                 uc = new UniqueConstraint(parentColumns, false); //could throw
307                                 parentColumns [0].Table.Constraints.Add (uc);
308                         }
309
310                         //keep reference
311                         _parentUniqueConstraint = uc;
312                         //parentColumns [0].Table.Constraints.Add (uc);
313                         //if this unique constraint is attempted to be removed before us
314                         //we can fail the validation
315                         //collection.ValidateRemoveConstraint += new DelegateValidateRemoveConstraint(
316                         //              _validateRemoveParentConstraint);
317                 }
318                 
319                 
320                 #endregion //Helpers
321                 
322                 #region Properties
323
324                 [DataCategory ("Data")]
325                 [DataSysDescription ("For accept and reject changes, indicates what kind of cascading should take place across this relation.")]
326                 [DefaultValue (AcceptRejectRule.None)]
327                 public virtual AcceptRejectRule AcceptRejectRule {
328                         get { return _acceptRejectRule; }
329                         set { _acceptRejectRule = value; }
330                 }
331
332                 [DataCategory ("Data")]
333                 [DataSysDescription ("Indicates the child columns of this constraint.")]
334                 [ReadOnly (true)]
335                 public virtual DataColumn[] Columns {
336                         get { return _childColumns; }
337                 }
338
339                 [DataCategory ("Data")]
340                 [DataSysDescription ("For deletions, indicates what kind of cascading should take place across this relation.")]
341                 [DefaultValue (Rule.Cascade)]
342                 public virtual Rule DeleteRule {
343                         get { return _deleteRule; }
344                         set { _deleteRule = value; }
345                 }
346
347                 [DataCategory ("Data")]
348                 [DataSysDescription ("For updates, indicates what kind of cascading should take place across this relation.")]
349                 [DefaultValue (Rule.Cascade)]
350                 public virtual Rule UpdateRule {
351                         get { return _updateRule; }
352                         set { _updateRule = value; }
353                 }
354
355                 [DataCategory ("Data")] 
356                 [DataSysDescription ("Indicates the parent columns of this constraint.")]
357                 [ReadOnly (true)]
358                 public virtual DataColumn[] RelatedColumns {
359                         get { return _parentColumns; }
360                 }
361
362                 [DataCategory ("Data")] 
363                 [DataSysDescription ("Indicates the child table of this constraint.")]
364                 [ReadOnly (true)]
365                 public virtual DataTable RelatedTable {
366                         get {
367                                 if (_parentColumns != null)
368                                         if (_parentColumns.Length > 0)
369                                                 return _parentColumns[0].Table;
370
371                                 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
372                         }
373                 }
374
375                 [DataCategory ("Data")]
376                 [DataSysDescription ("Indicates the table of this constraint.")]
377                 [ReadOnly (true)]
378                 public override DataTable Table {
379                         get {
380                                 if (_childColumns != null)
381                                         if (_childColumns.Length > 0)
382                                                 return _childColumns[0].Table;
383
384                                 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
385                         }
386                 }
387
388                 internal bool DataColsNotValidated
389                 {
390             get { 
391                                 return (_dataColsNotValidated); 
392                         }
393         }
394
395
396                 #endregion // Properties
397
398                 #region Methods
399
400                 public override bool Equals(object key) 
401                 {
402                         ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
403                         if (null == fkc) return false;
404
405                         //if the fk constrains the same columns then they are equal
406                         if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
407                                 return false;
408                         if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
409                                 return false;
410
411                         return true;
412                 }
413
414                 public override int GetHashCode()
415                 {
416                         //initialize hash1 and hash2 with default hashes
417                         //any two DIFFERENT numbers will do here
418                         int hash1 = 32, hash2 = 88;
419                         int i;
420
421                         //derive the hash code from the columns that way
422                         //Equals and GetHashCode return Equal objects to be the
423                         //same
424
425                         //Get the first parent column hash
426                         if (this.Columns.Length > 0)
427                                 hash1 ^= this.Columns[0].GetHashCode();
428                         
429                         //get the rest of the parent column hashes if there any
430                         for (i = 1; i < this.Columns.Length; i++)
431                         {
432                                 hash1 ^= this.Columns[1].GetHashCode();
433                                 
434                         }
435                         
436                         //Get the child column hash
437                         if (this.RelatedColumns.Length > 0)
438                                 hash2 ^= this.Columns[0].GetHashCode();
439                         
440                         for (i = 1; i < this.RelatedColumns.Length; i++)
441                         {
442                                 hash2 ^= this.RelatedColumns[1].GetHashCode();
443                         }
444
445                         //combine the two hashes
446                         return hash1 ^ hash2;
447                 }
448
449                 internal override void AddToConstraintCollectionSetup(
450                                 ConstraintCollection collection)
451                 {                       
452                         if (collection.Table != Table)
453                                 throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
454
455                         //run Ctor rules again
456                         _validateColumns(_parentColumns, _childColumns);
457                         
458                         //we must have a unique constraint on the parent
459                         _ensureUniqueConstraintExists(collection, _parentColumns);
460                         
461                         //Make sure we can create this thing
462                         //AssertConstraint(); 
463                         if (IsConstraintViolated())
464                                 throw new ArgumentException("This constraint cannot be enabled as not all values have corresponding parent values.");
465                         //FIXME : if this fails and we created a unique constraint
466                         //we should probably roll it back
467                         // and remove index form Table                  
468                 }
469                                         
470                 internal override void RemoveFromConstraintCollectionCleanup( 
471                                 ConstraintCollection collection)
472                 {
473                         Index = null;
474                 }
475                 
476                 protected override bool IsConstraintViolated()
477                 {
478                         if (Table.DataSet == null || RelatedTable.DataSet == null) 
479                                 return false;
480                         
481                         if (!Table.DataSet.EnforceConstraints)
482                                 return false;
483                                 
484                         bool hasErrors = false;
485                         foreach (DataRow row in Table.Rows) {
486                                 // first we check if all values in _childColumns place are nulls.
487                                 // if yes we return.
488                                 if (row.IsNullColumns(_childColumns))
489                                         continue;
490
491                                 // check whenever there is (at least one) parent row  in RelatedTable
492                                 if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) { 
493                                         // if no parent row exists - constraint is violated
494                                         hasErrors = true;
495                                         string[] values = new string[_childColumns.Length];
496                                         for (int i = 0; i < _childColumns.Length; i++){
497                                                 DataColumn col = _childColumns[i];
498                                                 values[i] = row[col].ToString();
499                                         }
500
501                                         row.RowError = String.Format("ForeignKeyConstraint {0} requires the child key values ({1}) to exist in the parent table.",
502                                                 ConstraintName, String.Join(",", values));
503                                 }
504                         }
505
506                         if (hasErrors)
507                                 //throw new ConstraintException("Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.");
508                                 return true;
509
510                         return false;
511                 }
512                 
513                 internal override void AssertConstraint(DataRow row)
514                 {
515                         // first we check if all values in _childColumns place are nulls.
516                         // if yes we return.
517                         if (row.IsNullColumns(_childColumns))
518                                 return;
519
520                         // check whenever there is (at least one) parent row  in RelatedTable
521                         if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) { 
522                                 // if no parent row exists - constraint is violated
523                                 throw new InvalidConstraintException(GetErrorMessage(row));
524                         }
525                 }
526
527                 internal override bool IsColumnContained(DataColumn column)
528                 {
529                         for (int i = 0; i < _parentColumns.Length; i++)
530                                 if (column == _parentColumns[i])
531                                         return true;
532
533                         for (int i = 0; i < _childColumns.Length; i++)
534                                 if (column == _childColumns[i])
535                                         return true;
536
537                         return false;
538                 }
539
540                 internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
541                         return true;
542                 }
543
544                 private string GetErrorMessage(DataRow row)
545                 {
546                         System.Text.StringBuilder sb = new System.Text.StringBuilder();
547                         for (int i = 0; i < _childColumns.Length; i++) {
548                                 sb.Append(row[_childColumns[0]].ToString());
549                                 if (i != _childColumns.Length - 1) {
550                                         sb.Append(',');
551                                 }
552                         }
553                         string valStr = sb.ToString();
554                         return "ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + valStr + ") to exist in the parent table.";
555                 }               
556                 
557                 #endregion // Methods
558         }
559
560 }