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