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")]
48 public class ForeignKeyConstraint : Constraint
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;
60 private string _parentTableNamespace;
62 private string _childTableName;
64 //FIXME: remove those; and use only DataColumns[]
65 private string [] _parentColumnNames;
66 private string [] _childColumnNames;
70 public ForeignKeyConstraint(DataColumn parentColumn, DataColumn childColumn)
72 if (null == parentColumn || null == childColumn) {
73 throw new NullReferenceException("Neither parentColumn or" +
74 " childColumn can be null.");
76 _foreignKeyConstraint(null, new DataColumn[] {parentColumn},
77 new DataColumn[] {childColumn});
80 public ForeignKeyConstraint(DataColumn[] parentColumns, DataColumn[] childColumns)
82 _foreignKeyConstraint(null, parentColumns, childColumns);
85 public ForeignKeyConstraint(string constraintName, DataColumn parentColumn, DataColumn childColumn)
87 if (null == parentColumn || null == childColumn) {
88 throw new NullReferenceException("Neither parentColumn or" +
89 " childColumn can be null.");
92 _foreignKeyConstraint(constraintName, new DataColumn[] {parentColumn},
93 new DataColumn[] {childColumn});
96 public ForeignKeyConstraint(string constraintName, DataColumn[] parentColumns, DataColumn[] childColumns)
98 _foreignKeyConstraint(constraintName, parentColumns, childColumns);
103 public ForeignKeyConstraint(string constraintName, string parentTableName, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
106 InitInProgress = true;
107 base.ConstraintName = constraintName;
109 // "parentTableName" is searched in the "DataSet" to which the "DataTable"
110 // from which AddRange() is called
111 // childTable is the "DataTable" which calls AddRange()
113 // Keep reference to parentTableName to resolve later
114 _parentTableName = parentTableName;
116 // Keep a copy of parentColumnNames to resolve later
117 _parentColumnNames = new string [parentColumnNames.Length];
118 for (i = 0; i < parentColumnNames.Length; i++)
119 _parentColumnNames[i] = parentColumnNames[i];
121 // Keep a copy of childColumnNames to resolve later
122 _childColumnNames = new string [childColumnNames.Length];
123 for (i = 0; i < childColumnNames.Length; i++)
124 _childColumnNames[i] = childColumnNames[i];
127 _acceptRejectRule = acceptRejectRule;
128 _deleteRule = deleteRule;
129 _updateRule = updateRule;
132 internal override void FinishInit (DataTable childTable)
134 if (childTable.DataSet == null)
135 throw new InvalidConstraintException ("ChildTable : " + childTable.TableName + " does not belong to any DataSet");
137 DataSet dataSet = childTable.DataSet;
138 _childTableName = childTable.TableName;
140 if (!dataSet.Tables.Contains (_parentTableName))
141 throw new InvalidConstraintException ("Table : " + _parentTableName + "does not exist in DataSet : " + dataSet);
143 DataTable parentTable = dataSet.Tables [_parentTableName];
147 if (_parentColumnNames.Length < 0 || _childColumnNames.Length < 0)
148 throw new InvalidConstraintException ("Neither parent nor child columns can be zero length");
150 if (_parentColumnNames.Length != _childColumnNames.Length)
151 throw new InvalidConstraintException ("Both parent and child columns must be of same length");
152 DataColumn[] parentColumns = new DataColumn [_parentColumnNames.Length];
153 DataColumn[] childColumns = new DataColumn [_childColumnNames.Length];
155 foreach (string parentCol in _parentColumnNames){
156 if (!parentTable.Columns.Contains (parentCol))
157 throw new InvalidConstraintException ("Table : " + _parentTableName + "does not contain the column :" + parentCol);
158 parentColumns [i++] = parentTable. Columns [parentCol];
161 foreach (string childCol in _childColumnNames){
162 if (!childTable.Columns.Contains (childCol))
163 throw new InvalidConstraintException ("Table : " + _childTableName + "does not contain the column : " + childCol);
164 childColumns [j++] = childTable.Columns [childCol];
166 _validateColumns (parentColumns, childColumns);
167 _parentColumns = parentColumns;
168 _childColumns = childColumns;
170 parentTable.Namespace = _parentTableNamespace;
172 InitInProgress = false;
177 public ForeignKeyConstraint (string constraintName, string parentTableName, string parentTableNamespace, string[] parentColumnNames, string[] childColumnNames, AcceptRejectRule acceptRejectRule, Rule deleteRule, Rule updateRule)
179 InitInProgress = true;
180 base.ConstraintName = constraintName;
182 // "parentTableName" is searched in the "DataSet" to which the "DataTable"
183 // from which AddRange() is called
184 // childTable is the "DataTable" which calls AddRange()
186 // Keep reference to parentTableName to resolve later
187 _parentTableName = parentTableName;
188 _parentTableNamespace = parentTableNamespace;
190 // Keep reference to parentColumnNames to resolve later
191 _parentColumnNames = parentColumnNames;
193 // Keep reference to childColumnNames to resolve later
194 _childColumnNames = childColumnNames;
196 _acceptRejectRule = acceptRejectRule;
197 _deleteRule = deleteRule;
198 _updateRule = updateRule;
202 private void _foreignKeyConstraint(string constraintName, DataColumn[] parentColumns,
203 DataColumn[] childColumns)
207 _validateColumns(parentColumns, childColumns);
209 //Set Constraint Name
210 base.ConstraintName = constraintName;
212 //copy the columns - Do not keep reference #672113
213 _parentColumns = new DataColumn [parentColumns.Length];
214 for (i = 0; i < parentColumns.Length; i++)
215 _parentColumns[i] = parentColumns[i];
217 _childColumns = new DataColumn [childColumns.Length];
218 for (i = 0; i < childColumns.Length; i++)
219 _childColumns[i] = childColumns[i];
222 #endregion // Constructors
226 private void _validateColumns(DataColumn[] parentColumns, DataColumn[] childColumns)
229 if (null == parentColumns || null == childColumns)
230 throw new ArgumentNullException();
232 //at least one element in each array
233 if (parentColumns.Length < 1 || childColumns.Length < 1)
234 throw new ArgumentException("Neither ParentColumns or ChildColumns can't be" +
238 if (parentColumns.Length != childColumns.Length)
239 throw new ArgumentException("Parent columns and child columns must be the same length.");
242 DataTable ptable = parentColumns[0].Table;
243 DataTable ctable = childColumns[0].Table;
245 for (int i = 0; i < parentColumns.Length; i++) {
246 DataColumn pc = parentColumns[i];
247 DataColumn cc = childColumns[i];
250 if (null == pc.Table)
251 throw new ArgumentException("All columns must belong to a table." +
252 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
254 //All columns must belong to the same table
255 if (ptable != pc.Table)
256 throw new InvalidConstraintException("Parent columns must all belong to the same table.");
259 if (null == cc.Table)
260 throw new ArgumentException("All columns must belong to a table." +
261 " ColumnName: " + pc.ColumnName + " does not belong to a table.");
263 //All columns must belong to the same table.
264 if (ctable != cc.Table)
265 throw new InvalidConstraintException("Child columns must all belong to the same table.");
267 if (pc.CompiledExpression != null)
268 throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", pc.ColumnName));
270 if (cc.CompiledExpression != null)
271 throw new ArgumentException(String.Format("Cannot create a constraint based on Expression column {0}.", cc.ColumnName));
275 //Same dataset. If both are null it's ok
276 if (ptable.DataSet != ctable.DataSet)
278 //LAMESPEC: spec says InvalidConstraintExceptoin
279 // impl does InvalidOperationException
280 throw new InvalidOperationException("Parent column and child column must belong to" +
281 " tables that belong to the same DataSet.");
286 int identicalCols = 0;
287 for (int i = 0; i < parentColumns.Length; i++)
289 DataColumn pc = parentColumns[i];
290 DataColumn cc = childColumns[i];
297 if (!pc.DataTypeMatches (cc)) {
298 //LAMESPEC: spec says throw InvalidConstraintException
299 // implementation throws InvalidOperationException
300 throw new InvalidOperationException ("Parent column is not type compatible with it's child"
304 if (identicalCols == parentColumns.Length)
305 throw new InvalidOperationException ("Property not accessible because 'ParentKey and ChildKey are identical.'.");
309 //Checks to see if a related unique constraint exists
310 //if it doesn't then a unique constraint is created.
311 //if a unique constraint can't be created an exception will be thrown
312 private void _ensureUniqueConstraintExists(ConstraintCollection collection,
313 DataColumn [] parentColumns)
316 if (null == parentColumns) throw new ArgumentNullException(
317 "ParentColumns can't be null");
319 UniqueConstraint uc = null;
321 //see if unique constraint already exists
322 //if not create unique constraint
323 if(parentColumns[0] != null)
324 uc = UniqueConstraint.GetUniqueConstraintForColumnSet(parentColumns[0].Table.Constraints, parentColumns);
327 uc = new UniqueConstraint(parentColumns, false); //could throw
328 parentColumns [0].Table.Constraints.Add (uc);
332 _parentUniqueConstraint = uc;
333 _parentUniqueConstraint.ChildConstraint = this;
341 [DataCategory ("Data")]
343 [DataSysDescription ("For accept and reject changes, indicates what kind of cascading should take place across this relation.")]
345 [DefaultValue (AcceptRejectRule.None)]
346 public virtual AcceptRejectRule AcceptRejectRule {
347 get { return _acceptRejectRule; }
348 set { _acceptRejectRule = value; }
351 [DataCategory ("Data")]
353 [DataSysDescription ("Indicates the child columns of this constraint.")]
356 public virtual DataColumn[] Columns {
357 get { return _childColumns; }
360 [DataCategory ("Data")]
362 [DataSysDescription ("For deletions, indicates what kind of cascading should take place across this relation.")]
364 [DefaultValue (Rule.Cascade)]
365 public virtual Rule DeleteRule {
366 get { return _deleteRule; }
367 set { _deleteRule = value; }
370 [DataCategory ("Data")]
372 [DataSysDescription ("For updates, indicates what kind of cascading should take place across this relation.")]
374 [DefaultValue (Rule.Cascade)]
375 public virtual Rule UpdateRule {
376 get { return _updateRule; }
377 set { _updateRule = value; }
380 [DataCategory ("Data")]
382 [DataSysDescription ("Indicates the parent columns of this constraint.")]
385 public virtual DataColumn[] RelatedColumns {
386 get { return _parentColumns; }
389 [DataCategory ("Data")]
391 [DataSysDescription ("Indicates the child table of this constraint.")]
394 public virtual DataTable RelatedTable {
396 if (_parentColumns != null)
397 if (_parentColumns.Length > 0)
398 return _parentColumns[0].Table;
400 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
404 [DataCategory ("Data")]
406 [DataSysDescription ("Indicates the table of this constraint.")]
409 public override DataTable Table {
411 if (_childColumns != null)
412 if (_childColumns.Length > 0)
413 return _childColumns[0].Table;
415 throw new InvalidOperationException ("Property not accessible because 'Object reference not set to an instance of an object'");
419 internal UniqueConstraint ParentConstraint {
420 get { return _parentUniqueConstraint; }
423 #endregion // Properties
427 public override bool Equals(object key)
429 ForeignKeyConstraint fkc = key as ForeignKeyConstraint;
430 if (null == fkc) return false;
432 //if the fk constrains the same columns then they are equal
433 if (! DataColumn.AreColumnSetsTheSame( this.RelatedColumns, fkc.RelatedColumns))
435 if (! DataColumn.AreColumnSetsTheSame( this.Columns, fkc.Columns) )
441 public override int GetHashCode()
443 //initialize hash1 and hash2 with default hashes
444 //any two DIFFERENT numbers will do here
445 int hash1 = 32, hash2 = 88;
448 //derive the hash code from the columns that way
449 //Equals and GetHashCode return Equal objects to be the
452 //Get the first parent column hash
453 if (this.Columns.Length > 0)
454 hash1 ^= this.Columns[0].GetHashCode();
456 //get the rest of the parent column hashes if there any
457 for (i = 1; i < this.Columns.Length; i++)
459 hash1 ^= this.Columns[1].GetHashCode();
463 //Get the child column hash
464 if (this.RelatedColumns.Length > 0)
465 hash2 ^= this.Columns[0].GetHashCode();
467 for (i = 1; i < this.RelatedColumns.Length; i++)
469 hash2 ^= this.RelatedColumns[1].GetHashCode();
472 //combine the two hashes
473 return hash1 ^ hash2;
476 internal override void AddToConstraintCollectionSetup(
477 ConstraintCollection collection)
479 if (collection.Table != Table)
480 throw new InvalidConstraintException("This constraint cannot be added since ForeignKey doesn't belong to table " + RelatedTable.TableName + ".");
482 //run Ctor rules again
483 _validateColumns(_parentColumns, _childColumns);
485 //we must have a unique constraint on the parent
486 _ensureUniqueConstraintExists(collection, _parentColumns);
488 if ( (Table.DataSet != null && Table.DataSet.EnforceConstraints)
489 || (Table.DataSet == null && Table.EnforceConstraints)) {
490 if (IsConstraintViolated())
491 throw new ArgumentException("This constraint cannot be enabled as not all values have corresponding parent values.");
493 //FIXME : if this fails and we created a unique constraint
494 //we should probably roll it back
495 // and remove index form Table
498 internal override void RemoveFromConstraintCollectionCleanup(
499 ConstraintCollection collection)
501 _parentUniqueConstraint.ChildConstraint = null;
505 internal override bool IsConstraintViolated()
507 if (Table.DataSet == null || RelatedTable.DataSet == null)
510 bool hasErrors = false;
511 foreach (DataRow row in Table.Rows) {
512 // first we check if all values in _childColumns place are nulls.
514 if (row.IsNullColumns(_childColumns))
517 // check whenever there is (at least one) parent row in RelatedTable
518 if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) {
519 // if no parent row exists - constraint is violated
521 string[] values = new string[_childColumns.Length];
522 for (int i = 0; i < _childColumns.Length; i++){
523 DataColumn col = _childColumns[i];
524 values[i] = row[col].ToString();
527 row.RowError = String.Format("ForeignKeyConstraint {0} requires the child key values ({1}) to exist in the parent table.",
528 ConstraintName, String.Join(",", values));
533 //throw new ConstraintException("Failed to enable constraints. One or more rows contain values violating non-null, unique, or foreign-key constraints.");
539 internal override void AssertConstraint(DataRow row)
541 // first we check if all values in _childColumns place are nulls.
543 if (row.IsNullColumns(_childColumns))
546 // check whenever there is (at least one) parent row in RelatedTable
547 if(!RelatedTable.RowsExist(_parentColumns,_childColumns,row)) {
548 // if no parent row exists - constraint is violated
549 throw new InvalidConstraintException(GetErrorMessage(row));
553 internal override bool IsColumnContained(DataColumn column)
555 for (int i = 0; i < _parentColumns.Length; i++)
556 if (column == _parentColumns[i])
559 for (int i = 0; i < _childColumns.Length; i++)
560 if (column == _childColumns[i])
566 internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
570 private string GetErrorMessage(DataRow row)
572 System.Text.StringBuilder sb = new System.Text.StringBuilder();
573 for (int i = 0; i < _childColumns.Length; i++) {
574 sb.Append(row[_childColumns[0]].ToString());
575 if (i != _childColumns.Length - 1) {
579 string valStr = sb.ToString();
580 return "ForeignKeyConstraint " + ConstraintName + " requires the child key values (" + valStr + ") to exist in the parent table.";
583 #endregion // Methods