2 // System.Data.ForeignKeyConstraint.cs
5 // Franklin Wise <gracenote@earthlink.net>
6 // Daniel Morgan <danmorg@sc.rr.com>
8 // (C) 2002 Franklin Wise
9 // (C) 2002 Daniel Morgan
13 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
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:
23 // The above copyright notice and this permission notice shall be
24 // included in all copies or substantial portions of the Software.
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.
36 using System.Collections;
37 using System.ComponentModel;
38 using System.Runtime.InteropServices;
39 using System.Data.Common;
41 namespace System.Data {
43 [DefaultProperty ("ConstraintName")]
45 public class ForeignKeyConstraint : Constraint
47 private UniqueConstraint _parentUniqueConstraint;
48 //FIXME: create a class which will wrap this collection
49 private DataColumn [] _parentColumns;
50 //FIXME: create a class which will wrap this collection
51 private DataColumn [] _childColumns;
52 private Rule _deleteRule = Rule.Cascade;
53 private Rule _updateRule = Rule.Cascade;
54 private AcceptRejectRule _acceptRejectRule = AcceptRejectRule.None;
55 private string _parentTableName;
56 private string _childTableName;
57 //FIXME: remove those; and use only DataColumns[]
58 private string [] _parentColumnNames;
59 private string [] _childColumnNames;
60 private bool _dataColsNotValidated = false;
64 public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn)
66 if (null == parentColumn || null == childColumn) {
67 throw new NullReferenceException("Neither parentColumn or" +
68 " childColumn can be null.");
70 _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
71 new DataColumn[] {childColumn});
74 public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns)
76 _foreignKeyConstraint(null, parentColumns, childColumns);
79 public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn)
81 if (null == parentColumn || null == childColumn) {
82 throw new NullReferenceException("Neither parentColumn or" +
83 " childColumn can be null.");
86 _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
87 new DataColumn[] {childColumn});
90 public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns)
92 _foreignKeyConstraint(constraintName, parentColumns, childColumns);
97 public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
99 _dataColsNotValidated = true;
100 base.ConstraintName = constraintName;
102 // "parentTableName" is searched in the "DataSet" to which the "DataTable"
103 // from which AddRange() is called
104 // childTable is the "DataTable" which calls AddRange()
106 // Keep reference to parentTableName to resolve later
107 _parentTableName = parentTableName;
109 // Keep reference to parentColumnNames to resolve later
110 _parentColumnNames = parentColumnNames;
112 // Keep reference to childColumnNames to resolve later
113 _childColumnNames = childColumnNames;
115 _acceptRejectRule = acceptRejectRule;
116 _deleteRule = deleteRule;
117 _updateRule = updateRule;
121 internal void postAddRange (DataTable childTable)
123 // LAMESPEC - Does not say that this is mandatory
124 // Check whether childTable belongs to a DataSet
125 if (childTable.DataSet == null)
126 throw new InvalidConstraintException ("ChildTable : " + childTable.TableName + " does not belong to any DataSet");
127 DataSet dataSet = childTable.DataSet;
128 _childTableName = childTable.TableName;
129 // Search for the parentTable in the childTable's DataSet
130 if (!dataSet.Tables.Contains (_parentTableName))
131 throw new InvalidConstraintException ("Table : " + _parentTableName + "does not exist in DataSet : " + dataSet);
133 // Keep reference to parentTable
134 DataTable parentTable = dataSet.Tables [_parentTableName];
138 // LAMESPEC - Does not say which Exception is thrown
139 if (_parentColumnNames.Length < 0 || _childColumnNames.Length < 0)
140 throw new InvalidConstraintException ("Neither parent nor child columns can be zero length");
141 // LAMESPEC - Does not say which Exception is thrown
142 if (_parentColumnNames.Length != _childColumnNames.Length)
143 throw new InvalidConstraintException ("Both parent and child columns must be of same length");
144 DataColumn []parentColumns = new DataColumn [_parentColumnNames.Length];
145 DataColumn []childColumns = new DataColumn [_childColumnNames.Length];
147 // Search for the parentColumns in parentTable
148 foreach (string parentCol in _parentColumnNames){
149 if (!parentTable.Columns.Contains (parentCol))
150 throw new InvalidConstraintException ("Table : " + _parentTableName + "does not contain the column :" + parentCol);
151 parentColumns [i++] = parentTable. Columns [parentCol];
153 // Search for the childColumns in childTable
154 foreach (string childCol in _childColumnNames){
155 if (!childTable.Columns.Contains (childCol))
156 throw new InvalidConstraintException ("Table : " + _childTableName + "does not contain the column : " + childCol);
157 childColumns [j++] = childTable.Columns [childCol];
159 _validateColumns (parentColumns, childColumns);
160 _parentColumns = parentColumns;
161 _childColumns = childColumns;
166 public ForeignKeyConstraint (string constraintName, string parentTableName, string parentTableNamespace, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
168 throw new NotImplementedException ();
172 private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
173 DataColumn[] childColumns)
177 _validateColumns(parentColumns, childColumns);
179 //Set Constraint Name
180 base.ConstraintName = constraintName;
182 //Keep reference to columns
183 _parentColumns = parentColumns;
184 _childColumns = childColumns;
187 #endregion // Constructors
191 private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
194 if (null == parentColumns || null == childColumns)
195 throw new ArgumentNullException();
197 //at least one element in each array
198 if (parentColumns.Length < 1 || childColumns.Length < 1)
199 throw new ArgumentException("Neither ParentColumns or ChildColumns can't be" +
203 if (parentColumns.Length != childColumns.Length)
204 throw new ArgumentException("Parent columns and child columns must be the same length.");
207 DataTable ptable = parentColumns[0].Table;
208 DataTable ctable = childColumns[0].Table;
210 for (int i = 0; i < parentColumns.Length; i++) {
211 DataColumn pc = parentColumns[i];
212 DataColumn cc = childColumns[i];
215 if (null == pc.Table)
216 throw new ArgumentException("All columns must belong to a table." +
217 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
219 //All columns must belong to the same table
220 if (ptable != pc.Table)
221 throw new InvalidConstraintException("Parent columns must all belong to the same table.");
224 if (null == cc.Table)
225 throw new ArgumentException("All columns must belong to a table." +
226 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
228 //All columns must belong to the same table.
229 if (ctable != cc.Table)
230 throw new InvalidConstraintException("Child columns must all belong to the same table.");
232 if (pc.CompiledExpression != null)
233 throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", pc.ColumnName));
235 if (cc.CompiledExpression != null)
236 throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", cc.ColumnName));
240 //Same dataset. If both are null it's ok
241 if (ptable.DataSet != ctable.DataSet)
243 //LAMESPEC: spec says InvalidConstraintExceptoin
244 // impl does InvalidOperationException
245 throw new InvalidOperationException("Parent column and child column must belong to" +
246 " tables that belong to the same DataSet.");
251 for (int i = 0; i < parentColumns.Length; i++)
253 DataColumn pc = parentColumns[i];
254 DataColumn cc = childColumns[i];
256 //Can't be the same column
258 throw new InvalidOperationException("Parent and child columns can't be the same column.");
260 if (! pc.DataType.Equals(cc.DataType))
262 //LAMESPEC: spec says throw InvalidConstraintException
263 // implementation throws InvalidOperationException
264 throw new InvalidConstraintException("Parent column is not type compatible with it's child"
274 private void _validateRemoveParentConstraint(ConstraintCollection sender,
275 Constraint constraint, ref bool cancel, ref string failReason)
278 //if we hold a reference to the parent then cancel it
279 if (constraint == _parentUniqueConstraint)
282 failReason = "Cannot remove UniqueConstraint because the"
283 + " ForeignKeyConstraint " + this.ConstraintName + " exists.";
288 //Checks to see if a related unique constraint exists
289 //if it doesn't then a unique constraint is created.
290 //if a unique constraint can't be created an exception will be thrown
291 private void _ensureUniqueConstraintExists(ConstraintCollection collection,
292 DataColumn [] parentColumns)
295 if (null == parentColumns) throw new ArgumentNullException(
296 "ParentColumns can't be null");
298 UniqueConstraint uc = null;
300 //see if unique constraint already exists
301 //if not create unique constraint
302 if(parentColumns[0] != null)
303 uc = UniqueConstraint.GetUniqueConstraintForColumnSet(parentColumns[0].Table.Constraints, parentColumns);
306 uc = new UniqueConstraint(parentColumns, false); //could throw
307 parentColumns [0].Table.Constraints.Add (uc);
311 _parentUniqueConstraint = uc;
312 //parentColumns [0].Table.Constraints.Add (uc);
313 //if this unique constraint is attempted to be removed before us
314 //we can fail the validation
315 //collection.ValidateRemoveConstraint += new DelegateValidateRemoveConstraint(
316 // _validateRemoveParentConstraint);
324 [DataCategory ("Data")]
325 [DataSysDescription ("For accept and reject changes, indicates what kind of cascading should take place across this relation.")]
326 [DefaultValue (AcceptRejectRule.None)]
327 public virtual AcceptRejectRule AcceptRejectRule {
328 get { return _acceptRejectRule; }
329 set { _acceptRejectRule = value; }
332 [DataCategory ("Data")]
333 [DataSysDescription ("Indicates the child columns of this constraint.")]
335 public virtual DataColumn[] Columns {
336 get { return _childColumns; }
339 [DataCategory ("Data")]
340 [DataSysDescription ("For deletions, indicates what kind of cascading should take place across this relation.")]
341 [DefaultValue (Rule.Cascade)]
342 public virtual Rule DeleteRule {
343 get { return _deleteRule; }
344 set { _deleteRule = value; }
347 [DataCategory ("Data")]
348 [DataSysDescription ("For updates, indicates what kind of cascading should take place across this relation.")]
349 [DefaultValue (Rule.Cascade)]
350 public virtual Rule UpdateRule {
351 get { return _updateRule; }
352 set { _updateRule = value; }
355 [DataCategory ("Data")]
356 [DataSysDescription ("Indicates the parent columns of this constraint.")]
358 public virtual DataColumn[] RelatedColumns {
359 get { return _parentColumns; }
362 [DataCategory ("Data")]
363 [DataSysDescription ("Indicates the child table of this constraint.")]
365 public virtual DataTable RelatedTable {
367 if (_parentColumns != null)
368 if (_parentColumns.Length > 0)
369 return _parentColumns[0].Table;
371 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
375 [DataCategory ("Data")]
376 [DataSysDescription ("Indicates the table of this constraint.")]
378 public override DataTable Table {
380 if (_childColumns != null)
381 if (_childColumns.Length > 0)
382 return _childColumns[0].Table;
384 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
388 internal bool DataColsNotValidated
391 return (_dataColsNotValidated);
396 #endregion // Properties
400 public override bool Equals(object key)
402 ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
403 if (null == fkc) return false;
405 //if the fk constrains the same columns then they are equal
406 if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
408 if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
414 public override int GetHashCode()
416 //initialize hash1 and hash2 with default hashes
417 //any two DIFFERENT numbers will do here
418 int hash1 = 32, hash2 = 88;
421 //derive the hash code from the columns that way
422 //Equals and GetHashCode return Equal objects to be the
425 //Get the first parent column hash
426 if (this.Columns.Length > 0)
427 hash1 ^= this.Columns[0].GetHashCode();
429 //get the rest of the parent column hashes if there any
430 for (i = 1; i < this.Columns.Length; i++)
432 hash1 ^= this.Columns[1].GetHashCode();
436 //Get the child column hash
437 if (this.RelatedColumns.Length > 0)
438 hash2 ^= this.Columns[0].GetHashCode();
440 for (i = 1; i < this.RelatedColumns.Length; i++)
442 hash2 ^= this.RelatedColumns[1].GetHashCode();
445 //combine the two hashes
446 return hash1 ^ hash2;
449 internal override void AddToConstraintCollectionSetup(
450 ConstraintCollection collection)
452 if (collection.Table != Table)
453 throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
455 //run Ctor rules again
456 _validateColumns(_parentColumns, _childColumns);
458 //we must have a unique constraint on the parent
459 _ensureUniqueConstraintExists(collection, _parentColumns);
461 //Make sure we can create this thing
462 //AssertConstraint();
463 if (IsConstraintViolated())
464 throw new ArgumentException("This constraint cannot be enabled as not all values have corresponding parent values.");
465 //FIXME : if this fails and we created a unique constraint
466 //we should probably roll it back
467 // and remove index form Table
470 internal override void RemoveFromConstraintCollectionCleanup(
471 ConstraintCollection collection)
476 protected override bool IsConstraintViolated()
478 if (Table.DataSet == null || RelatedTable.DataSet == null)
481 if (!Table.DataSet.EnforceConstraints && !Table.EnforceConstraints)
484 bool hasErrors = false;
485 foreach (DataRow row in Table.Rows) {
486 // first we check if all values in _childColumns place are nulls.
488 if (row.IsNullColumns(_childColumns))
491 // check whenever there is (at least one) parent row in RelatedTable
492 if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) {
493 // if no parent row exists - constraint is violated
495 string[] values = new string[_childColumns.Length];
496 for (int i = 0; i < _childColumns.Length; i++){
497 DataColumn col = _childColumns[i];
498 values[i] = row[col].ToString();
501 row.RowError = String.Format("ForeignKeyConstraint {0} requires the child key values ({1}) to exist in the parent table.",
502 ConstraintName, String.Join(",", values));
507 //throw new ConstraintException("Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.");
513 internal override void AssertConstraint(DataRow row)
515 // first we check if all values in _childColumns place are nulls.
517 if (row.IsNullColumns(_childColumns))
520 // check whenever there is (at least one) parent row in RelatedTable
521 if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) {
522 // if no parent row exists - constraint is violated
523 throw new InvalidConstraintException(GetErrorMessage(row));
527 internal override bool IsColumnContained(DataColumn column)
529 for (int i = 0; i < _parentColumns.Length; i++)
530 if (column == _parentColumns[i])
533 for (int i = 0; i < _childColumns.Length; i++)
534 if (column == _childColumns[i])
540 internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
544 private string GetErrorMessage(DataRow row)
546 System.Text.StringBuilder sb = new System.Text.StringBuilder();
547 for (int i = 0; i < _childColumns.Length; i++) {
548 sb.Append(row[_childColumns[0]].ToString());
549 if (i != _childColumns.Length - 1) {
553 string valStr = sb.ToString();
554 return "ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + valStr + ") to exist in the parent table.";
557 #endregion // Methods