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