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 {
42 [Editor ("Microsoft.VSDesigner.Data.Design.ForeignKeyConstraintEditor, " + Consts.AssemblyMicrosoft_VSDesigner,
43 "System.Drawing.Design.UITypeEditor, " + Consts.AssemblySystem_Drawing)]
44 [DefaultProperty ("ConstraintName")]
46 public class ForeignKeyConstraint : Constraint
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;
65 public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn)
67 if (null == parentColumn || null == childColumn) {
68 throw new NullReferenceException("Neither parentColumn or" +
69 " childColumn can be null.");
71 _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
72 new DataColumn[] {childColumn});
75 public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns)
77 _foreignKeyConstraint(null, parentColumns, childColumns);
80 public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn)
82 if (null == parentColumn || null == childColumn) {
83 throw new NullReferenceException("Neither parentColumn or" +
84 " childColumn can be null.");
87 _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
88 new DataColumn[] {childColumn});
91 public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns)
93 _foreignKeyConstraint(constraintName, parentColumns, childColumns);
98 public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
100 _dataColsNotValidated = true;
101 base.ConstraintName = constraintName;
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()
107 // Keep reference to parentTableName to resolve later
108 _parentTableName = parentTableName;
110 // Keep reference to parentColumnNames to resolve later
111 _parentColumnNames = parentColumnNames;
113 // Keep reference to childColumnNames to resolve later
114 _childColumnNames = childColumnNames;
116 _acceptRejectRule = acceptRejectRule;
117 _deleteRule = deleteRule;
118 _updateRule = updateRule;
122 internal void postAddRange (DataTable childTable)
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);
134 // Keep reference to parentTable
135 DataTable parentTable = dataSet.Tables [_parentTableName];
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];
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];
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];
160 _validateColumns (parentColumns, childColumns);
161 _parentColumns = parentColumns;
162 _childColumns = childColumns;
167 public ForeignKeyConstraint (string constraintName, string parentTableName, string parentTableNamespace, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
169 throw new NotImplementedException ();
173 private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
174 DataColumn[] childColumns)
178 _validateColumns(parentColumns, childColumns);
180 //Set Constraint Name
181 base.ConstraintName = constraintName;
183 //Keep reference to columns
184 _parentColumns = parentColumns;
185 _childColumns = childColumns;
188 #endregion // Constructors
192 private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
195 if (null == parentColumns || null == childColumns)
196 throw new ArgumentNullException();
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" +
204 if (parentColumns.Length != childColumns.Length)
205 throw new ArgumentException("Parent columns and child columns must be the same length.");
208 DataTable ptable = parentColumns[0].Table;
209 DataTable ctable = childColumns[0].Table;
211 for (int i = 0; i < parentColumns.Length; i++) {
212 DataColumn pc = parentColumns[i];
213 DataColumn cc = childColumns[i];
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.");
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.");
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.");
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.");
233 if (pc.CompiledExpression != null)
234 throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", pc.ColumnName));
236 if (cc.CompiledExpression != null)
237 throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", cc.ColumnName));
241 //Same dataset. If both are null it's ok
242 if (ptable.DataSet != ctable.DataSet)
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.");
252 for (int i = 0; i < parentColumns.Length; i++)
254 DataColumn pc = parentColumns[i];
255 DataColumn cc = childColumns[i];
257 //Can't be the same column
259 throw new InvalidOperationException("Parent and child columns can't be the same column.");
261 if (! pc.DataType.Equals(cc.DataType))
263 //LAMESPEC: spec says throw InvalidConstraintException
264 // implementation throws InvalidOperationException
265 throw new InvalidConstraintException("Parent column is not type compatible with it's child"
275 private void _validateRemoveParentConstraint(ConstraintCollection sender,
276 Constraint constraint, ref bool cancel, ref string failReason)
279 //if we hold a reference to the parent then cancel it
280 if (constraint == _parentUniqueConstraint)
283 failReason = "Cannot remove UniqueConstraint because the"
284 + " ForeignKeyConstraint " + this.ConstraintName + " exists.";
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)
296 if (null == parentColumns) throw new ArgumentNullException(
297 "ParentColumns can't be null");
299 UniqueConstraint uc = null;
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);
307 uc = new UniqueConstraint(parentColumns, false); //could throw
308 parentColumns [0].Table.Constraints.Add (uc);
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);
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; }
333 [DataCategory ("Data")]
334 [DataSysDescription ("Indicates the child columns of this constraint.")]
336 public virtual DataColumn[] Columns {
337 get { return _childColumns; }
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; }
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; }
356 [DataCategory ("Data")]
357 [DataSysDescription ("Indicates the parent columns of this constraint.")]
359 public virtual DataColumn[] RelatedColumns {
360 get { return _parentColumns; }
363 [DataCategory ("Data")]
364 [DataSysDescription ("Indicates the child table of this constraint.")]
366 public virtual DataTable RelatedTable {
368 if (_parentColumns != null)
369 if (_parentColumns.Length > 0)
370 return _parentColumns[0].Table;
372 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
376 [DataCategory ("Data")]
377 [DataSysDescription ("Indicates the table of this constraint.")]
379 public override DataTable Table {
381 if (_childColumns != null)
382 if (_childColumns.Length > 0)
383 return _childColumns[0].Table;
385 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
389 internal bool DataColsNotValidated
392 return (_dataColsNotValidated);
397 #endregion // Properties
401 public override bool Equals(object key)
403 ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
404 if (null == fkc) return false;
406 //if the fk constrains the same columns then they are equal
407 if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
409 if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
415 public override int GetHashCode()
417 //initialize hash1 and hash2 with default hashes
418 //any two DIFFERENT numbers will do here
419 int hash1 = 32, hash2 = 88;
422 //derive the hash code from the columns that way
423 //Equals and GetHashCode return Equal objects to be the
426 //Get the first parent column hash
427 if (this.Columns.Length > 0)
428 hash1 ^= this.Columns[0].GetHashCode();
430 //get the rest of the parent column hashes if there any
431 for (i = 1; i < this.Columns.Length; i++)
433 hash1 ^= this.Columns[1].GetHashCode();
437 //Get the child column hash
438 if (this.RelatedColumns.Length > 0)
439 hash2 ^= this.Columns[0].GetHashCode();
441 for (i = 1; i < this.RelatedColumns.Length; i++)
443 hash2 ^= this.RelatedColumns[1].GetHashCode();
446 //combine the two hashes
447 return hash1 ^ hash2;
450 internal override void AddToConstraintCollectionSetup(
451 ConstraintCollection collection)
453 if (collection.Table != Table)
454 throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
456 //run Ctor rules again
457 _validateColumns(_parentColumns, _childColumns);
459 //we must have a unique constraint on the parent
460 _ensureUniqueConstraintExists(collection, _parentColumns);
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.");
469 //FIXME : if this fails and we created a unique constraint
470 //we should probably roll it back
471 // and remove index form Table
474 internal override void RemoveFromConstraintCollectionCleanup(
475 ConstraintCollection collection)
480 internal override bool IsConstraintViolated()
482 if (Table.DataSet == null || RelatedTable.DataSet == null)
485 bool hasErrors = false;
486 foreach (DataRow row in Table.Rows) {
487 // first we check if all values in _childColumns place are nulls.
489 if (row.IsNullColumns(_childColumns))
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
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();
502 row.RowError = String.Format("ForeignKeyConstraint {0} requires the child key values ({1}) to exist in the parent table.",
503 ConstraintName, String.Join(",", values));
508 //throw new ConstraintException("Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.");
514 internal override void AssertConstraint(DataRow row)
516 // first we check if all values in _childColumns place are nulls.
518 if (row.IsNullColumns(_childColumns))
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));
528 internal override bool IsColumnContained(DataColumn column)
530 for (int i = 0; i < _parentColumns.Length; i++)
531 if (column == _parentColumns[i])
534 for (int i = 0; i < _childColumns.Length; i++)
535 if (column == _childColumns[i])
541 internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
545 private string GetErrorMessage(DataRow row)
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) {
554 string valStr = sb.ToString();
555 return "ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + valStr + ") to exist in the parent table.";
558 #endregion // Methods