2003-11-09 Pedro Mart�nez Juli� <yoros@wanadoo.es>
[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                         bool ignoreCase = false;
363                         if (_dataTable.DataSet != null)
364                                 ignoreCase = !_dataTable.DataSet.CaseSensitive;
365                         //TODO: Investigate other ways of speeding up the validation work below.
366
367                         //validate no duplicates exists.
368                         //Only validate when there are at least 2 rows
369                         //so that a duplicate migth exist.
370                         if(tbl.Rows.Count > 1) {
371                                 //get copy of rows collection first so that we do not modify the
372                                 //original.
373                                 DataRow[] rows = new DataRow [tbl.Rows.Count];
374                                 tbl.Rows.CopyTo (rows, 0);
375                                 
376                                 Array.Sort(rows, new RowsComparer(this, ignoreCase));
377                                 
378                                 for (int i = 0 ; i < rows.Length - 1 ; i++) 
379                                 {
380                                         bool match = true;
381                                         // check if the values in the constraints columns are equal
382                                         for (int j = 0; j < _dataColumns.Length; j++)
383                                         {
384                                                 if (_dataColumns[j].DataType == typeof(string))
385                                                 {
386                                                         string origVal = (string)rows[i][_dataColumns[j]];
387                                                         string compVal = (string)rows[i + 1][_dataColumns[j]];
388                                                         if (String.Compare(origVal, compVal, ignoreCase) != 0)
389                                                         {
390                                                                 match = false;
391                                                                 break;
392                                                         }
393                                                 }
394                                                 else if (!rows[i][_dataColumns[j]].Equals(rows[i + 1][_dataColumns[j]]))
395                                                 {
396                                                         match = false;
397                                                         break;
398                                                 }       
399                                         }
400                                         if (match)
401                                                 throw new ConstraintException (String.Format ("Column '{0}' contains non-unique values", this._dataColumns[0]));                                        
402                                 }
403                         }
404
405
406                 }
407
408                 [MonoTODO]
409                 internal override void AssertConstraint(DataRow row)
410                 {
411                         if (_dataTable == null) return; //???
412                         if (_dataColumns == null) return; //???
413
414                         //Unique?
415                         // check that the row has values for all columns in the constarint. 
416                         object val;
417                         for (int i = 0; i < _dataColumns.Length; i++)
418                         {
419
420                                 val = row[_dataColumns[i]];
421                                 if (val == null || val == DBNull.Value)
422                                         throw new NoNullAllowedException("Column '" + _dataColumns[i].ColumnName + "' does not allow nulls.");
423                         }
424
425                         DataTable tbl = _dataTable;
426                         bool isValid;
427                         object[] rowVals = new object[_dataColumns.Length];
428                         for (int i = 0; i < _dataColumns.Length; i++)
429                         {
430                                 if(row.HasVersion(DataRowVersion.Proposed))
431                                         rowVals[i] = row[_dataColumns[i], DataRowVersion.Proposed];
432                                 else
433                                         rowVals[i] = row[_dataColumns[i], DataRowVersion.Current];
434                         }
435                         
436                         bool ignoreCase = false;
437                         if (_dataTable.DataSet != null)
438                                 ignoreCase = !_dataTable.DataSet.CaseSensitive;
439
440                         foreach(DataRow compareRow in tbl.Rows)
441                         {
442                                 if (compareRow.RowState != DataRowState.Deleted)
443                                 {
444                                         isValid = false;
445                                         //skip if it is the same row to be validated
446                                         if(!row.Equals(compareRow))
447                                         {
448                                                 for (int i = 0; i < _dataColumns.Length; i++)
449                                                 {
450                                                         // if the values in the row are not equal to the values of
451                                                         // the original row from the table we can move to the next row.
452                                                         if (_dataColumns[i].DataType == typeof(string))
453                                                         {
454                                                                 string origVal = (string)rowVals[i];
455                                                                 string compVal = (string)compareRow[_dataColumns[i]];
456                                                                 if (String.Compare(origVal, compVal, ignoreCase) != 0)
457                                                                 {
458                                                                         isValid = true;
459                                                                         break;
460                                                                 }
461                                                         }
462                                                         else if (!rowVals[i].Equals( compareRow[_dataColumns[i]]))
463                                                         {
464                                                                 isValid = true;
465                                                                 break;
466                                                         }
467                                                 }
468                                 
469                                                 if (!isValid)
470                                                         throw new ConstraintException(GetErrorMessage(compareRow));
471
472                                         }
473                                 }
474
475                         }
476
477                 }
478
479                 private string GetErrorMessage(DataRow row)
480                 {
481                         int i;
482                          
483                         System.Text.StringBuilder sb = new System.Text.StringBuilder(row[_dataColumns[0]].ToString());
484                         for (i = 1; i < _dataColumns.Length; i++)
485                                 sb = sb.Append(", ").Append(row[_dataColumns[i].ColumnName]);
486                         string valStr = sb.ToString();
487                         sb = new System.Text.StringBuilder(_dataColumns[0].ColumnName);
488                         for (i = 1; i < _dataColumns.Length; i++)
489                                 sb = sb.Append(", ").Append(_dataColumns[i].ColumnName);
490                         string colStr = sb.ToString();
491                         return "Column '" + colStr + "' is constrained to be unique.  Value '" + valStr + "' is already present.";
492                 }
493                 
494                 // generates a hash key for a given row based on the constraints columns.
495                 internal int CalcHashValue(DataRow row, bool ignoreCase)
496                 {
497                         object o;
498                         int retVal = 0;
499                         CaseInsensitiveHashCodeProvider ciProvider = null;
500                         if (ignoreCase)
501                                 ciProvider = new CaseInsensitiveHashCodeProvider(_dataTable.Locale);
502                         for (int i = 0; i < _dataColumns.Length; i++)
503                         {
504                                 o = row[_dataColumns[i]];
505                                 if (o != null)
506                                 {
507                                         if (ciProvider != null)
508                                                 retVal += ciProvider.GetHashCode(o);
509                                         else
510                                                 retVal += o.GetHashCode();
511
512                                 }
513                         }
514                         return retVal;
515                 }
516
517                 #endregion // Methods
518
519                 private class RowsComparer : IComparer
520                 {
521                         private UniqueConstraint _uc;
522                         private bool _ignoreCase;
523                         
524                         public RowsComparer(UniqueConstraint uc, bool ignoreCase)
525                         {
526                                 _ignoreCase = ignoreCase;
527                                 _uc = uc;
528                         }
529
530                         public int Compare(object o1, object o2)
531                         {
532                                 DataRow row1 = (DataRow) o1;
533                                 DataRow row2 = (DataRow) o2;
534                                 int val1 = _uc.CalcHashValue(row1, _ignoreCase);
535                                 int val2 = _uc.CalcHashValue(row2, _ignoreCase);
536                                 
537                                 if (val1 > val2)
538                                         return 1;
539                                 if (val1 == val2)
540                                         return 0;
541                                 return -1;
542                         }
543                 }
544         }
545
546         
547 }