*** empty log message ***
[mono.git] / mcs / class / System.Data / System.Data / UniqueConstraint.cs
1 //
2 // System.Data.UniqueConstraint.cs
3 //
4 // Author:
5 //   Franklin Wise <gracenote@earthlink.net>
6 //   Daniel Morgan <danmorg@sc.rr.com>
7 //   Tim Coleman (tim@timcoleman.com)
8 //   
9 // (C) 2002 Franklin Wise
10 // (C) 2002 Daniel Morgan
11 // Copyright (C) Tim Coleman, 2002
12
13 using System;
14 using System.Collections;
15 using System.ComponentModel;
16 using System.Runtime.InteropServices;
17
18 namespace System.Data {
19         [Editor]
20         [DefaultProperty ("ConstraintName")]
21         [Serializable]
22         public class UniqueConstraint : Constraint 
23         {
24                 private bool _isPrimaryKey = false;
25                 private bool __isPrimaryKey = false;
26                 private DataTable _dataTable; //set by ctor except when unique case
27                 
28                 private DataColumn [] _dataColumns;
29
30                 //TODO:provide helpers for this case
31                 private string [] _dataColumnNames; //unique case
32                 
33
34                 #region Constructors
35
36                 public UniqueConstraint (DataColumn column) 
37                 {
38                         _uniqueConstraint ("", column, false);
39                 }
40
41                 public UniqueConstraint (DataColumn[] columns) 
42                 {
43                         _uniqueConstraint ("", columns, false);
44                 }
45
46                 public UniqueConstraint (DataColumn column, bool isPrimaryKey) 
47                 {
48                         _uniqueConstraint ("", column, isPrimaryKey);
49                 }
50
51                 public UniqueConstraint (DataColumn[] columns, bool isPrimaryKey) 
52                 {
53                         _uniqueConstraint ("", columns, isPrimaryKey);
54                 }
55
56                 public UniqueConstraint (string name, DataColumn column) 
57                 {
58                         _uniqueConstraint (name, column, false);
59                 }
60
61                 public UniqueConstraint (string name, DataColumn[] columns) 
62                 {
63                         _uniqueConstraint (name, columns, false);
64                 }
65
66                 public UniqueConstraint (string name, DataColumn column, bool isPrimaryKey) 
67                 {
68                         _uniqueConstraint (name, column, isPrimaryKey);
69                 }
70
71                 public UniqueConstraint (string name, DataColumn[] columns, bool isPrimaryKey) 
72                 {
73                         _uniqueConstraint (name, columns, isPrimaryKey);
74                 }
75
76                 //Special case.  Can only be added to the Collection with AddRange
77                 [MonoTODO]
78                 [Browsable (false)]
79                 public UniqueConstraint (string name, string[] columnNames, bool isPrimaryKey) 
80                 {
81                         throw new NotImplementedException(); //need to finish related logic
82                         /*
83                         base.ConstraintName = name;
84                         
85                         //set unique
86                         //must set unique when added to the collection
87
88                         //keep list of names to resolve later
89                         _dataColumnNames = columnNames;
90
91                         _isPrimaryKey = isPrimaryKey;
92                         */
93                 }
94
95                 //helper ctor
96                 private void _uniqueConstraint(string name, DataColumn column, bool isPrimaryKey) 
97                 {
98                         //validate
99                         _validateColumn (column);
100
101                         //Set Constraint Name
102                         base.ConstraintName = name;
103
104                         __isPrimaryKey = isPrimaryKey;
105
106                         //keep reference 
107                         _dataColumns = new DataColumn [] {column};
108                         
109                         //Get table reference
110                         _dataTable = column.Table;
111                 }
112
113                 //helpter ctor  
114                 private void _uniqueConstraint(string name, DataColumn[] columns, bool isPrimaryKey) 
115                 {
116                         //validate
117                         _validateColumns (columns, out _dataTable);
118
119                         //Set Constraint Name
120                         base.ConstraintName = name;
121
122                         //keep reference
123                         _dataColumns = columns;
124
125                         //PK?
126                         __isPrimaryKey = isPrimaryKey;
127                 }
128                 
129                 #endregion // Constructors
130
131                 #region Helpers
132                 
133                 private void _validateColumns(DataColumn [] columns)
134                 {
135                         DataTable table;
136                         _validateColumns(columns, out table);
137                 }
138                 
139                 //Validates a collection of columns with the ctor rules
140                 private void _validateColumns(DataColumn [] columns, out DataTable table) {
141                         table = null;
142
143                         //not null
144                         if (null == columns) throw new ArgumentNullException();
145                         
146                         //check that there is at least one column
147                         //LAMESPEC: not in spec
148                         if (columns.Length < 1)
149                                 throw new InvalidConstraintException("Must be at least one column.");
150                         
151                         DataTable compareTable = columns[0].Table;
152                         //foreach
153                         foreach (DataColumn col in columns){
154                                 
155                                 //check individual column rules
156                                 _validateColumn (col);
157                                 
158                                 
159                                 //check that columns are all from the same table??
160                                 //LAMESPEC: not in spec
161                                 if (compareTable != col.Table)
162                                         throw new InvalidConstraintException("Columns must be from the same table.");
163                                 
164                         }
165
166                         table = compareTable;
167                 }
168                 
169                 //validates a column with the ctor rules
170                 private void _validateColumn(DataColumn column) {
171         
172                         //not null
173                         if (null == column)  // FIXME: This is little weird, but here it goes...
174                                 throw new NullReferenceException("Object reference not set to an instance of an object.");
175
176                         
177                         //column must belong to a table
178                         //LAMESPEC: not in spec
179                         if (null == column.Table)
180                                 throw new ArgumentException ("Column must belong to a table.");                 
181                 }
182
183                 /// <summary>
184                 ///  If IsPrimaryKey is set to be true, this sets it true
185                 /// </summary>
186                 internal void UpdatePrimaryKey ()
187                 {
188                         _isPrimaryKey = __isPrimaryKey;
189                         foreach (DataColumn Col in _dataColumns)
190                                 Col.SetUnique();
191                         
192                 }
193
194                 internal static void SetAsPrimaryKey(ConstraintCollection collection, UniqueConstraint newPrimaryKey)
195                 {
196                         //not null
197                         if (null == collection) throw new ArgumentNullException("ConstraintCollection can't be null.");
198                         
199                         //make sure newPrimaryKey belongs to the collection parm unless it is null
200                         if (  collection.IndexOf(newPrimaryKey) < 0 && (null != newPrimaryKey) ) 
201                                 throw new ArgumentException("newPrimaryKey must belong to collection.");
202                         
203                         //Get existing pk
204                         UniqueConstraint uc = GetPrimaryKeyConstraint(collection);
205                         
206                         //clear existing
207                         if (null != uc) uc._isPrimaryKey = false;
208
209                         //set new key
210                         if (null != newPrimaryKey) newPrimaryKey._isPrimaryKey = true;
211                         
212                         
213                 }
214
215                 internal static UniqueConstraint GetPrimaryKeyConstraint(ConstraintCollection collection)
216                 {
217                         if (null == collection) throw new ArgumentNullException("Collection can't be null.");
218
219                         UniqueConstraint uc;
220                         IEnumerator enumer = collection.GetEnumerator();
221                         while (enumer.MoveNext())
222                         {
223                                 uc = enumer.Current as UniqueConstraint;
224                                 if (null == uc) continue;
225                                 
226                                 if (uc.IsPrimaryKey) return uc; 
227                         }
228
229                         //if we got here there was no pk
230                         return null;
231                         
232                 }
233
234                 internal static UniqueConstraint GetUniqueConstraintForColumnSet(ConstraintCollection collection,
235                                 DataColumn[] columns)
236                 {
237                         if (null == collection) throw new ArgumentNullException("Collection can't be null.");
238                         if (null == columns ) return null;
239                         
240                         UniqueConstraint uniqueConstraint;
241                         IEnumerator enumer = collection.GetEnumerator();
242                         while (enumer.MoveNext())
243                         {
244                                 uniqueConstraint = enumer.Current as UniqueConstraint;
245                                 if (uniqueConstraint != null)
246                                 {
247                                         if ( DataColumn.AreColumnSetsTheSame(uniqueConstraint.Columns, columns) )
248                                         {
249                                                 return uniqueConstraint;
250                                         }
251                                 }
252                         }
253                         return null;
254                 }
255                         
256                 #endregion //Helpers
257
258                 #region Properties
259
260                 [DataCategory ("Data")]
261                 [DataSysDescription ("Indicates the columns of this constraint.")]
262                 [ReadOnly (true)]
263                 public virtual DataColumn[] Columns {
264                         get { return _dataColumns; }
265                 }
266
267                 [DataCategory ("Data")]
268                 [DataSysDescription ("Indicates if this constraint is a primary key.")]
269                 [ReadOnly (true)]
270                 public bool IsPrimaryKey {
271                         get { return _isPrimaryKey; }
272                 }
273
274                 [DataCategory ("Data")]
275                 [DataSysDescription ("Indicates the table of this constraint.")]
276                 [ReadOnly (true)]
277                 public override DataTable Table {
278                         get { return _dataTable; }
279                 }
280
281                 #endregion // Properties
282
283                 #region Methods
284
285                 public override bool Equals(object key2) {
286
287                         UniqueConstraint cst = key2 as UniqueConstraint;
288                         if (null == cst) return false;
289
290                         //according to spec if the cols are equal
291                         //then two UniqueConstraints are equal
292                         return DataColumn.AreColumnSetsTheSame(cst.Columns, this.Columns);      
293
294                 }
295
296                 public override int GetHashCode() 
297                 {
298                         //initialize hash with default value 
299                         int hash = 42;
300                         int i;
301
302                         //derive the hash code from the columns that way
303                         //Equals and GetHashCode return Equal objects to be the
304                         //same
305
306                         //Get the first column hash
307                         if (this.Columns.Length > 0)
308                                 hash ^= this.Columns[0].GetHashCode();
309                         
310                         //get the rest of the column hashes if there any
311                         for (i = 1; i < this.Columns.Length; i++)
312                         {
313                                 hash ^= this.Columns[1].GetHashCode();
314                                 
315                         }
316                         
317                         return hash ;
318                 }
319                 
320                 [MonoTODO]
321                 internal override void AddToConstraintCollectionSetup(
322                                 ConstraintCollection collection)
323                 {
324                         //run Ctor rules again
325                         _validateColumns(_dataColumns);
326                         
327                         //make sure a unique constraint doesn't already exists for these columns
328                         UniqueConstraint uc = UniqueConstraint.GetUniqueConstraintForColumnSet(collection, this.Columns);       
329                         if (null != uc) throw new ArgumentException("Unique constraint already exists for these" +
330                                         " columns. Existing ConstraintName is " + uc.ConstraintName);
331
332                         //Allow only one primary key
333                         if (this.IsPrimaryKey)
334                         {
335                                 uc = GetPrimaryKeyConstraint(collection);
336                                 if (null != uc) uc._isPrimaryKey = false;
337
338                         }
339                                         
340                         //FIXME: ConstraintCollection calls AssertContraint() again rigth after calling
341                         //this method, so that it is executed twice. Need to investigate which
342                         // call to remove as that migth affect other parts of the classes.
343                         AssertConstraint();
344                 }
345                                         
346                 
347                 internal override void RemoveFromConstraintCollectionCleanup( 
348                                 ConstraintCollection collection)
349                 {
350                 }
351
352                 [MonoTODO]
353                 internal override void AssertConstraint()
354                 {
355                         
356                         if (_dataTable == null) return; //???
357                         if (_dataColumns == null) return; //???
358
359                         
360                         //Unique?       
361                         DataTable tbl = _dataTable;
362
363                         //TODO: Investigate other ways of speeding up the validation work below.
364
365                         //validate no duplicates exists.
366                         //Only validate when there are at least 2 rows
367                         //so that a duplicate migth exist.
368                         if(tbl.Rows.Count > 1) {
369                                 //get copy of rows collection first so that we do not modify the
370                                 //original.
371                                 DataRow[] rows = new DataRow [tbl.Rows.Count];
372                                 tbl.Rows.CopyTo (rows, 0);
373                                 
374                                 Array.Sort(rows, new RowsComparer(this));
375                                 for (int i = 0 ; i < rows.Length - 1 ; i++) 
376                                 {\r
377                                         bool match = true;\r
378                                         // check if the values in the constraints columns are equal\r
379                                         for (int j = 0; j < _dataColumns.Length; j++)\r
380                                         {\r
381                                                 if (!rows[i][_dataColumns[j]].Equals(rows[i + 1][_dataColumns[j]]))\r
382                                                 {\r
383                                                         match = false;\r
384                                                         break;\r
385                                                 }       \r
386                                         }\r
387                                         if (match)\r
388                                                 throw new InvalidConstraintException (String.Format ("Column '{0}' contains non-unique values", this._dataColumns[0]));                                 
389                                 }
390                         }
391
392
393                 }
394
395                 [MonoTODO]
396                 internal override void AssertConstraint(DataRow row)
397                 {
398                         if (_dataTable == null) return; //???
399                         if (_dataColumns == null) return; //???
400
401                         //Unique?
402                         // check that the row has values for all columns in the constarint. 
403                         object val;
404                         for (int i = 0; i < _dataColumns.Length; i++)
405                         {
406
407                                 val = row[_dataColumns[i]];
408                                 if (val == null || val == DBNull.Value)
409                                         throw new NoNullAllowedException("Column '" + _dataColumns[i].ColumnName + "' does not allow nulls.");
410                         }
411
412                         DataTable tbl = _dataTable;
413                         bool isValid;
414                         object[] rowVals = new object[_dataColumns.Length];
415                         for (int i = 0; i < _dataColumns.Length; i++)
416                         {
417                                 if(row.HasVersion(DataRowVersion.Proposed))
418                                         rowVals[i] = row[_dataColumns[i], DataRowVersion.Proposed];
419                                 else
420                                         rowVals[i] = row[_dataColumns[i], DataRowVersion.Current];
421                         }
422                         
423                         foreach(DataRow compareRow in tbl.Rows)
424                         {
425                                 if (compareRow.RowState != DataRowState.Deleted)
426                                 {
427                                         isValid = false;
428                                         //skip if it is the same row to be validated
429                                         if(!row.Equals(compareRow))
430                                         {
431                                                 //FIXME: should we compare to compareRow[DataRowVersion.Current]?
432                                                 //FIXME: We need to compare to all columns the constraint is set to.
433                                                 for (int i = 0; i < _dataColumns.Length; i++)
434                                                 {
435                                                         // if the values in the row are not equal to the values of
436                                                         // the original row from the table we can move to the next row.
437                                                         if(!rowVals[i].Equals( compareRow[_dataColumns[i]]))
438                                                         {
439                                                                 isValid = true;
440                                                                 break;
441                                                         }
442                                                 }
443                                 
444                                                 if (!isValid)
445                                                         throw new ConstraintException(GetErrorMessage(compareRow));
446
447                                         }
448                                 }
449
450                         }
451
452                 }
453
454                 private string GetErrorMessage(DataRow row)
455                 {
456                         int i;
457                          
458                         System.Text.StringBuilder sb = new System.Text.StringBuilder(row[_dataColumns[0]].ToString());
459                         for (i = 1; i < _dataColumns.Length; i++)
460                                 sb = sb.Append(", ").Append(row[_dataColumns[i].ColumnName]);
461                         string valStr = sb.ToString();
462                         sb = new System.Text.StringBuilder(_dataColumns[0].ColumnName);
463                         for (i = 1; i < _dataColumns.Length; i++)
464                                 sb = sb.Append(", ").Append(_dataColumns[i].ColumnName);
465                         string colStr = sb.ToString();
466                         return "Column '" + colStr + "' is constrained to be unique.  Value '" + valStr + "' is already present.";
467                 }
468                 
469                 // generates a hash key for a given row based on the constraints columns.
470                 internal int CalcHashValue(DataRow row)
471                 {
472                         object o;
473                         int retVal = 0;
474                         for (int i = 0; i < _dataColumns.Length; i++)
475                         {
476                                 o = row[_dataColumns[i]];
477                                 if (o != null)
478                                         retVal += o.GetHashCode();
479                         }
480                         return retVal;
481                 }
482
483                 #endregion // Methods
484
485                 private class RowsComparer : IComparer
486                 {
487                         private UniqueConstraint _uc;
488                         
489                         public RowsComparer(UniqueConstraint uc)
490                         {
491                                 _uc = uc;
492                         }
493
494                         public int Compare(object o1, object o2)
495                         {
496                                 DataRow row1 = (DataRow) o1;
497                                 DataRow row2 = (DataRow) o2;
498                                 int val1 = _uc.CalcHashValue(row1);
499                                 int val2 = _uc.CalcHashValue(row2);
500                                 
501                                 return val1 - val2;
502                         }
503                 }
504         }
505
506         
507 }