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