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