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