2003-11-25 Tim Coleman <tim@timcoleman.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 Rule _deleteRule = Rule.Cascade;
27                 private Rule _updateRule = Rule.Cascade;
28                 private AcceptRejectRule _acceptRejectRule = AcceptRejectRule.None;
29                 
30                 #region Constructors
31
32                 public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn) 
33                 {
34                         if (null == parentColumn || null == childColumn) {
35                                 throw new ArgumentNullException("Neither parentColumn or" +
36                                         " childColumn can be null.");
37                         }
38
39                         _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
40                                         new DataColumn[] {childColumn});
41                 }
42
43                 public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns) 
44                 {
45                         _foreignKeyConstraint(null, parentColumns, childColumns);
46                 }
47
48                 public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn) 
49                 {
50                         if (null == parentColumn || null == childColumn) {
51                                 throw new ArgumentNullException("Neither parentColumn or" +
52                                         " childColumn can be null.");
53                         }
54
55                         _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
56                                         new DataColumn[] {childColumn});
57                 }
58
59                 public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns) 
60                 {
61                         _foreignKeyConstraint(constraintName, parentColumns, childColumns);
62                 }
63                 
64                 //special case
65                 [MonoTODO]
66                 [Browsable (false)]
67                 public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule) 
68                 {
69                 }
70
71                 private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
72                                 DataColumn[] childColumns)
73                 {
74
75                         //Validate 
76                         _validateColumns(parentColumns, childColumns);
77
78                         //Set Constraint Name
79                         base.ConstraintName = constraintName;   
80
81                         //Keep reference to columns
82                         _parentColumns = parentColumns;
83                         _childColumns = childColumns;
84                 }
85
86                 #endregion // Constructors
87
88                 #region Helpers
89
90                 private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
91                 {
92                         //not null
93                         if (null == parentColumns || null == childColumns) 
94                                 throw new ArgumentNullException();
95                         
96                         //at least one element in each array
97                         if (parentColumns.Length < 1 || childColumns.Length < 1)
98                                 throw new ArgumentException("Neither ParentColumns or ChildColumns can't be" +
99                                                 " zero length.");
100                                 
101                         //same size arrays
102                         if (parentColumns.Length != childColumns.Length)
103                                 throw new ArgumentException("Parent columns and child columns must be the same length.");
104                         
105
106                         DataTable ptable = parentColumns[0].Table;
107                         DataTable ctable = childColumns[0].Table;
108         
109                         
110                         foreach (DataColumn pc in parentColumns)
111                         {
112                                 //not null check
113                                 if (null == pc.Table) 
114                                 {
115                                         throw new ArgumentException("All columns must belong to a table." + 
116                                                         " ColumnName: " + pc.ColumnName + " does not belong to a table.");
117                                 }
118                                 
119                                 //All columns must belong to the same table
120                                 if (ptable != pc.Table)
121                                         throw new InvalidConstraintException("Parent columns must all belong to the same table.");
122                                 
123                                 foreach (DataColumn cc in childColumns)
124                                 {
125                                         //not null
126                                         if (null == pc.Table) 
127                                         {
128                                                 throw new ArgumentException("All columns must belong to a table." + 
129                                                         " ColumnName: " + pc.ColumnName + " does not belong to a table.");
130                                         }
131                         
132                                         //All columns must belong to the same table.
133                                         if (ctable != cc.Table)
134                                                 throw new InvalidConstraintException("Child columns must all belong to the same table.");
135                                                 
136                                                 
137                                         //Can't be the same column
138                                         if (pc == cc)
139                                                 throw new InvalidOperationException("Parent and child columns can't be the same column.");
140
141                                         foreach (DataColumn c2 in childColumns) {
142                                                 if (!Object.ReferenceEquals (c2.Table, cc.Table))
143                                                         throw new InvalidConstraintException ("Cannot create a Key from Columns thath belong to different tables.");
144                                         }
145
146                                         if (! pc.DataType.Equals(cc.DataType))
147                                         {
148                                                 //LAMESPEC: spec says throw InvalidConstraintException
149                                                 //              implementation throws InvalidOperationException
150                                                 throw new InvalidConstraintException("Parent column is not type compatible with it's child"
151                                                                 + " column.");
152                                         }
153                                 }       
154                         }
155                         
156                         
157                         //Same dataset.  If both are null it's ok
158                         if (ptable.DataSet != ctable.DataSet)
159                         {
160                                 //LAMESPEC: spec says InvalidConstraintExceptoin
161                                 //      impl does InvalidOperationException
162                                 throw new InvalidOperationException("Parent column and child column must belong to" + 
163                                                 " tables that belong to the same DataSet.");
164                                                 
165                         }
166
167                         
168                 }
169                 
170
171
172                 private void _validateRemoveParentConstraint(ConstraintCollection sender, 
173                                 Constraint constraint, ref bool cancel, ref string failReason)
174                 {
175                         //if we hold a reference to the parent then cancel it
176                         if (constraint == _parentUniqueConstraint) 
177                         {
178                                 cancel = true;
179                                 failReason = "Cannot remove UniqueConstraint because the"
180                                         + " ForeignKeyConstraint " + this.ConstraintName + " exists.";
181                         }
182                 }
183                 
184                 //Checks to see if a related unique constraint exists
185                 //if it doesn't then a unique constraint is created.
186                 //if a unique constraint can't be created an exception will be thrown
187                 private void _ensureUniqueConstraintExists(ConstraintCollection collection,
188                                 DataColumn [] parentColumns)
189                 {
190                         //not null
191                         if (null == parentColumns) throw new ArgumentNullException(
192                                         "ParentColumns can't be null");
193
194                         UniqueConstraint uc = null;
195                         
196                         //see if unique constraint already exists
197                         //if not create unique constraint
198                         if(parentColumns[0] != null)
199                                 uc = UniqueConstraint.GetUniqueConstraintForColumnSet(parentColumns[0].Table.Constraints, parentColumns);
200
201                         if (null == uc) {
202                                 uc = new UniqueConstraint(parentColumns, false); //could throw
203                                 parentColumns [0].Table.Constraints.Add (uc);
204                         }
205
206                         //keep reference
207                         _parentUniqueConstraint = uc;
208                         //parentColumns [0].Table.Constraints.Add (uc);
209                         //if this unique constraint is attempted to be removed before us
210                         //we can fail the validation
211                         //collection.ValidateRemoveConstraint += new DelegateValidateRemoveConstraint(
212                         //              _validateRemoveParentConstraint);
213
214                         parentColumns [0].Table.Constraints.ValidateRemoveConstraint += new DelegateValidateRemoveConstraint(
215                                 _validateRemoveParentConstraint);
216
217                 }
218                 
219                 
220                 #endregion //Helpers
221                 
222                 #region Properties
223
224                 [DataCategory ("Data")]
225                 [DataSysDescription ("For accept and reject changes, indicates what kind of cascading should take place across this relation.")]
226                 [DefaultValue (AcceptRejectRule.None)]
227                 public virtual AcceptRejectRule AcceptRejectRule {
228                         get { return _acceptRejectRule; }
229                         set { _acceptRejectRule = value; }
230                 }
231
232                 [DataCategory ("Data")]
233                 [DataSysDescription ("Indicates the child columns of this constraint.")]
234                 [ReadOnly (true)]
235                 public virtual DataColumn[] Columns {
236                         get { return _childColumns; }
237                 }
238
239                 [DataCategory ("Data")]
240                 [DataSysDescription ("For deletions, indicates what kind of cascading should take place across this relation.")]
241                 [DefaultValue (Rule.Cascade)]
242                 public virtual Rule DeleteRule {
243                         get { return _deleteRule; }
244                         set { _deleteRule = value; }
245                 }
246
247                 [DataCategory ("Data")]
248                 [DataSysDescription ("For updates, indicates what kind of cascading should take place across this relation.")]
249                 [DefaultValue (Rule.Cascade)]
250                 public virtual Rule UpdateRule {
251                         get { return _updateRule; }
252                         set { _updateRule = value; }
253                 }
254
255                 [DataCategory ("Data")] 
256                 [DataSysDescription ("Indicates the parent columns of this constraint.")]
257                 [ReadOnly (true)]
258                 public virtual DataColumn[] RelatedColumns {
259                         get { return _parentColumns; }
260                 }
261
262                 [DataCategory ("Data")] 
263                 [DataSysDescription ("Indicates the child table of this constraint.")]
264                 [ReadOnly (true)]
265                 public virtual DataTable RelatedTable {
266                         get {
267                                 if (_parentColumns != null)
268                                         if (_parentColumns.Length > 0)
269                                                 return _parentColumns[0].Table;
270
271                                 return null;
272                         }
273                 }
274
275                 [DataCategory ("Data")]
276                 [DataSysDescription ("Indicates the table of this constraint.")]
277                 [ReadOnly (true)]
278                 public override DataTable Table {
279                         get {
280                                 if (_childColumns != null)
281                                         if (_childColumns.Length > 0)
282                                                 return _childColumns[0].Table;
283
284                                 return null;
285                         }
286                 }
287
288                 #endregion // Properties
289
290                 #region Methods
291
292                 public override bool Equals(object key) 
293                 {
294                         ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
295                         if (null == fkc) return false;
296
297                         //if the fk constrains the same columns then they are equal
298                         if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
299                                 return false;
300                         if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
301                                 return false;
302
303                         return true;
304                 }
305
306                 public override int GetHashCode()
307                 {
308                         //initialize hash1 and hash2 with default hashes
309                         //any two DIFFERENT numbers will do here
310                         int hash1 = 32, hash2 = 88;
311                         int i;
312
313                         //derive the hash code from the columns that way
314                         //Equals and GetHashCode return Equal objects to be the
315                         //same
316
317                         //Get the first parent column hash
318                         if (this.Columns.Length > 0)
319                                 hash1 ^= this.Columns[0].GetHashCode();
320                         
321                         //get the rest of the parent column hashes if there any
322                         for (i = 1; i < this.Columns.Length; i++)
323                         {
324                                 hash1 ^= this.Columns[1].GetHashCode();
325                                 
326                         }
327                         
328                         //Get the child column hash
329                         if (this.RelatedColumns.Length > 0)
330                                 hash2 ^= this.Columns[0].GetHashCode();
331                         
332                         for (i = 1; i < this.RelatedColumns.Length; i++)
333                         {
334                                 hash2 ^= this.RelatedColumns[1].GetHashCode();
335                         }
336
337                         //combine the two hashes
338                         return hash1 ^ hash2;
339                 }
340
341                 internal override void AddToConstraintCollectionSetup(
342                                 ConstraintCollection collection)
343                 {
344                         
345                         //run Ctor rules again
346                         _validateColumns(_parentColumns, _childColumns);
347                         
348                         //we must have a unique constraint on the parent
349                         _ensureUniqueConstraintExists(collection, _parentColumns);
350                         
351                         //Make sure we can create this thing
352                         AssertConstraint(); //TODO:if this fails and we created a unique constraint
353                                                 //we should probably roll it back
354                         if (collection.Table != Table)
355                                 throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
356                 }
357                                         
358         
359                 [MonoTODO]
360                 internal override void RemoveFromConstraintCollectionCleanup( 
361                                 ConstraintCollection collection)
362                 {
363                         return; //no rules yet          
364                 }
365                 
366                 [MonoTODO]
367                 internal override void AssertConstraint()
368                 {
369
370                         //Constraint only works if both tables are part of the same dataset
371                         
372                         //if DataSet ...
373                         if (Table == null || RelatedTable == null) return; //TODO: Do we want this
374
375                         if (Table.DataSet == null || RelatedTable.DataSet == null) return; //   
376                                 
377                         //TODO:
378                         //check for orphaned children
379                         //check for...
380                         
381                 }
382                 
383                 [MonoTODO]
384                 internal override void AssertConstraint(DataRow row)
385                 {
386                         // first we check if all values in _childColumns place are DBNull.
387                         // if yes we return.
388                         bool allNull = true;
389                         for (int i = 0; i < _childColumns.Length; i++)
390                         {
391                                 if (row[_childColumns[i]] != DBNull.Value)
392                                 {
393                                         allNull = false;
394                                         break;
395                                 }
396                         }
397
398                         if (allNull)
399                                 return;
400                         
401                         // check that there is a parent for this row.
402                         foreach (DataRow parentRow in this.RelatedTable.Rows)
403                         {
404                                 if (parentRow.RowState != DataRowState.Deleted)
405                                 {
406                                         bool match = true;\r
407                                         // check if the values in the constraints columns are equal\r
408                                         for (int i = 0; i < _parentColumns.Length; i++)\r
409                                         {\r
410                                                 if (!row[_childColumns[i]].Equals(parentRow[_parentColumns[i]]))\r
411                                                 {\r
412                                                         match = false;\r
413                                                         break;\r
414                                                 }       \r
415                                         }\r
416                                         if (match) // there is a parent row for this row.\r
417                                                 return;
418                                 }
419                         }
420                         
421                         string values = "";
422                         for (int i = 0; i < _childColumns.Length; i++)
423                         {
424                                 values += row[_childColumns[0]].ToString();
425                                 if (i != _childColumns.Length - 1)
426                                         values += ",";
427                         }
428                         throw new InvalidConstraintException("ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + values + ") to exist in the parent table.");
429                 }
430                 
431                 #endregion // Methods
432         }
433
434 }