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