New tests.
[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 //
14 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
15 //
16 // Permission is hereby granted, free of charge, to any person obtaining
17 // a copy of this software and associated documentation files (the
18 // "Software"), to deal in the Software without restriction, including
19 // without limitation the rights to use, copy, modify, merge, publish,
20 // distribute, sublicense, and/or sell copies of the Software, and to
21 // permit persons to whom the Software is furnished to do so, subject to
22 // the following conditions:
23 // 
24 // The above copyright notice and this permission notice shall be
25 // included in all copies or substantial portions of the Software.
26 // 
27 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
28 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
29 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
30 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
31 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
32 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
33 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
34 //
35
36 using System;
37 using System.Collections;
38 using System.ComponentModel;
39 using System.Runtime.InteropServices;
40 using System.Data.Common;
41
42 namespace System.Data {
43         [Editor ("Microsoft.VSDesigner.Data.Design.UniqueConstraintEditor, " + Consts.AssemblyMicrosoft_VSDesigner,
44                  "System.Drawing.Design.UITypeEditor, " + Consts.AssemblySystem_Drawing)]
45         [DefaultProperty ("ConstraintName")]
46 #if !NET_2_0
47         [Serializable]
48 #endif
49         public class UniqueConstraint : Constraint 
50         {
51                 private bool _isPrimaryKey = false;
52                 private bool _belongsToCollection = false;
53                 private DataTable _dataTable; //set by ctor except when unique case
54                 
55                 //FIXME: create a class which will wrap this collection
56                 private DataColumn [] _dataColumns;
57
58                 //TODO:provide helpers for this case
59                 private string [] _dataColumnNames; //unique case
60                 private ForeignKeyConstraint _childConstraint = null;
61
62                 #region Constructors
63
64                 public UniqueConstraint (DataColumn column) 
65                 {
66                         _uniqueConstraint ("", column, false);
67                 }
68
69                 public UniqueConstraint (DataColumn[] columns) 
70                 {
71                         _uniqueConstraint ("", columns, false);
72                 }
73
74                 public UniqueConstraint (DataColumn column, bool isPrimaryKey) 
75                 {
76                         _uniqueConstraint ("", column, isPrimaryKey);
77                 }
78
79                 public UniqueConstraint (DataColumn[] columns, bool isPrimaryKey) 
80                 {
81                         _uniqueConstraint ("", columns, isPrimaryKey);
82                 }
83
84                 public UniqueConstraint (string name, DataColumn column) 
85                 {
86                         _uniqueConstraint (name, column, false);
87                 }
88
89                 public UniqueConstraint (string name, DataColumn[] columns) 
90                 {
91                         _uniqueConstraint (name, columns, false);
92                 }
93
94                 public UniqueConstraint (string name, DataColumn column, bool isPrimaryKey) 
95                 {
96                         _uniqueConstraint (name, column, isPrimaryKey);
97                 }
98
99                 public UniqueConstraint (string name, DataColumn[] columns, bool isPrimaryKey) 
100                 {
101                         _uniqueConstraint (name, columns, isPrimaryKey);
102                 }
103
104                 //Special case.  Can only be added to the Collection with AddRange
105                 [Browsable (false)]
106                 public UniqueConstraint (string name, string[] columnNames, bool isPrimaryKey) 
107                 {
108                         InitInProgress = true;
109
110                         //keep list of names to resolve later
111                         _dataColumnNames = columnNames;
112                         base.ConstraintName = name;
113                         _isPrimaryKey = isPrimaryKey;
114                 }
115
116                 //helper ctor
117                 private void _uniqueConstraint(string name, DataColumn column, bool isPrimaryKey) 
118                 {
119                         //validate
120                         _validateColumn (column);
121
122                         //Set Constraint Name
123                         base.ConstraintName = name;
124
125                         _isPrimaryKey = isPrimaryKey;
126
127                         //keep reference 
128                         _dataColumns = new DataColumn [] {column};
129                         
130                         //Get table reference
131                         _dataTable = column.Table;                      
132                 }
133
134                 //helpter ctor  
135                 private void _uniqueConstraint(string name, DataColumn[] columns, bool isPrimaryKey) 
136                 {
137                         //validate
138                         _validateColumns (columns, out _dataTable);
139
140                         //Set Constraint Name
141                         base.ConstraintName = name;
142
143                         //keep reference
144                         _dataColumns = columns;
145
146                         //PK?
147                         _isPrimaryKey = isPrimaryKey;                   
148                 }
149                 
150                 #endregion // Constructors
151
152                 #region Helpers
153                 
154                 private void _validateColumns(DataColumn [] columns)
155                 {
156                         DataTable table;
157                         _validateColumns(columns, out table);
158                 }
159                 
160                 //Validates a collection of columns with the ctor rules
161                 private void _validateColumns(DataColumn [] columns, out DataTable table) {
162                         table = null;
163
164                         //not null
165                         if (null == columns) throw new ArgumentNullException();
166                         
167                         //check that there is at least one column
168                         //LAMESPEC: not in spec
169                         if (columns.Length < 1)
170                                 throw new InvalidConstraintException("Must be at least one column.");
171                         
172                         DataTable compareTable = columns[0].Table;
173                         //foreach
174                         foreach (DataColumn col in columns){
175                                 
176                                 //check individual column rules
177                                 _validateColumn (col);
178                                 
179                                 
180                                 //check that columns are all from the same table??
181                                 //LAMESPEC: not in spec
182                                 if (compareTable != col.Table)
183                                         throw new InvalidConstraintException("Columns must be from the same table.");
184                                 
185                         }
186
187                         table = compareTable;
188                 }
189                 
190                 //validates a column with the ctor rules
191                 private void _validateColumn(DataColumn column) {
192         
193                         //not null
194                         if (null == column)  // FIXME: This is little weird, but here it goes...
195                                 throw new NullReferenceException("Object reference not set to an instance of an object.");
196
197                         
198                         //column must belong to a table
199                         //LAMESPEC: not in spec
200                         if (null == column.Table)
201                                 throw new ArgumentException ("Column must belong to a table.");                 
202                 }
203
204                 internal static void SetAsPrimaryKey(ConstraintCollection collection, UniqueConstraint newPrimaryKey)
205                 {
206                         //not null
207                         if (null == collection) throw new ArgumentNullException("ConstraintCollection can't be null.");
208                         
209                         //make sure newPrimaryKey belongs to the collection parm unless it is null
210                         if (  collection.IndexOf(newPrimaryKey) < 0 && (null != newPrimaryKey) ) 
211                                 throw new ArgumentException("newPrimaryKey must belong to collection.");
212                         
213                         //Get existing pk
214                         UniqueConstraint uc = GetPrimaryKeyConstraint(collection);
215                         
216                         //clear existing
217                         if (null != uc) uc._isPrimaryKey = false;
218
219                         //set new key
220                         if (null != newPrimaryKey) newPrimaryKey._isPrimaryKey = true;
221                         
222                         
223                 }
224
225                 internal static UniqueConstraint GetPrimaryKeyConstraint(ConstraintCollection collection)
226                 {
227                         if (null == collection) throw new ArgumentNullException("Collection can't be null.");
228
229                         UniqueConstraint uc;
230                         IEnumerator enumer = collection.GetEnumerator();
231                         while (enumer.MoveNext())
232                         {
233                                 uc = enumer.Current as UniqueConstraint;
234                                 if (null == uc) continue;
235                                 
236                                 if (uc.IsPrimaryKey) return uc; 
237                         }
238
239                         //if we got here there was no pk
240                         return null;
241                         
242                 }
243
244                 internal static UniqueConstraint GetUniqueConstraintForColumnSet(ConstraintCollection collection,
245                                 DataColumn[] columns)
246                 {
247                         if (null == collection) throw new ArgumentNullException("Collection can't be null.");
248                         if (null == columns ) return null;
249                         
250                         foreach(Constraint constraint in collection) {
251                                 if (constraint is UniqueConstraint) {
252                                         UniqueConstraint uc = constraint as UniqueConstraint;
253                                         if ( DataColumn.AreColumnSetsTheSame(uc.Columns, columns) ) {
254                                                 return uc;
255                                         }
256                                 }
257                         }
258                         return null;
259                 }
260
261                 internal ForeignKeyConstraint ChildConstraint {
262                         get { return _childConstraint; }
263                         set { _childConstraint = value; }
264                 }
265
266                 // Helper Special Ctor
267                 // Set the _dataTable property to the table to which this instance is bound when AddRange()
268                 // is called with the special constructor.
269                 // Validate whether the named columns exist in the _dataTable
270                 internal override void FinishInit (DataTable _setTable) 
271                 {                
272                         _dataTable = _setTable;
273                         if (_isPrimaryKey == true && _setTable.PrimaryKey.Length != 0)
274                                 throw new ArgumentException ("Cannot add primary key constraint since primary key" +
275                                                 "is already set for the table");
276
277                         DataColumn[] cols = new DataColumn [_dataColumnNames.Length];
278                         int i = 0;
279
280                         foreach (string _columnName in _dataColumnNames) {
281                                 if (_setTable.Columns.Contains (_columnName)) {
282                                         cols [i] = _setTable.Columns [_columnName];
283                                         i++;
284                                         continue;
285                                 }
286                                 throw(new InvalidConstraintException ("The named columns must exist in the table"));
287                         }
288                         _dataColumns = cols;
289                         _validateColumns (cols);
290                         InitInProgress = false;
291                 }
292
293
294                 #endregion //Helpers
295
296                 #region Properties
297
298                 [DataCategory ("Data")]
299 #if !NET_2_0
300                 [DataSysDescription ("Indicates the columns of this constraint.")]
301 #endif
302                 [ReadOnly (true)]
303                 public virtual DataColumn[] Columns {
304                         get { return _dataColumns; }
305                 }
306
307                 [DataCategory ("Data")]
308 #if !NET_2_0
309                 [DataSysDescription ("Indicates if this constraint is a primary key.")]
310 #endif
311                 public bool IsPrimaryKey {
312                         get { 
313                                 if (Table == null || (!_belongsToCollection)) {
314                                         return false;
315                                 }
316                                 return _isPrimaryKey; 
317                         }
318                 }
319
320                 [DataCategory ("Data")]
321 #if !NET_2_0
322                 [DataSysDescription ("Indicates the table of this constraint.")]
323 #endif
324                 [ReadOnly (true)]
325                 public override DataTable Table {
326                         get { return _dataTable; }
327                 }
328
329                 #endregion // Properties
330
331                 #region Methods
332
333                 internal void SetIsPrimaryKey (bool value)
334                 {
335                         _isPrimaryKey = value;
336                 }
337
338                 public override bool Equals(object key2) {
339
340                         UniqueConstraint cst = key2 as UniqueConstraint;
341                         if (null == cst) return false;
342
343                         //according to spec if the cols are equal
344                         //then two UniqueConstraints are equal
345                         return DataColumn.AreColumnSetsTheSame(cst.Columns, this.Columns);      
346
347                 }
348
349                 public override int GetHashCode() 
350                 {
351                         //initialize hash with default value 
352                         int hash = 42;
353                         int i;
354
355                         //derive the hash code from the columns that way
356                         //Equals and GetHashCode return Equal objects to be the
357                         //same
358
359                         //Get the first column hash
360                         if (this.Columns.Length > 0)
361                                 hash ^= this.Columns[0].GetHashCode();
362                         
363                         //get the rest of the column hashes if there any
364                         for (i = 1; i < this.Columns.Length; i++)
365                         {
366                                 hash ^= this.Columns[1].GetHashCode();
367                                 
368                         }
369                         
370                         return hash ;
371                 }
372                 
373                 internal override void AddToConstraintCollectionSetup(
374                                 ConstraintCollection collection)
375                 {
376                         for (int i = 0; i < Columns.Length; i++)
377                                 if (Columns[i].Table != collection.Table)
378                                         throw new ArgumentException("These columns don't point to this table.");
379                         //run Ctor rules again
380                         _validateColumns(_dataColumns);
381                         
382                         //make sure a unique constraint doesn't already exists for these columns
383                         UniqueConstraint uc = UniqueConstraint.GetUniqueConstraintForColumnSet(collection, this.Columns);       
384                         if (null != uc) throw new ArgumentException("Unique constraint already exists for these" +
385                                         " columns. Existing ConstraintName is " + uc.ConstraintName);
386
387                         //Allow only one primary key
388                         if (this.IsPrimaryKey) {
389                                 uc = GetPrimaryKeyConstraint(collection);
390                                 if (null != uc) uc._isPrimaryKey = false;
391                         }
392
393                         // if constraint is based on one column only
394                         // this column becomes unique
395                         if (_dataColumns.Length == 1) {
396                                 _dataColumns[0].SetUnique();
397                         }
398                                         
399                         if (IsConstraintViolated())
400                                 throw new ArgumentException("These columns don't currently have unique values.");
401
402                         _belongsToCollection = true;
403                 }
404                                         
405                 
406                 internal override void RemoveFromConstraintCollectionCleanup( 
407                                 ConstraintCollection collection)
408                 {
409                         if (Columns.Length == 1)
410                                 Columns [0].Unique = false;
411
412                         _belongsToCollection = false;
413                         Index index = Index;
414                         Index = null;
415                 }
416
417                 internal override bool IsConstraintViolated()
418                 {       
419                         if (Index == null) {
420                                 Index = Table.GetIndex(Columns,null,DataViewRowState.None,null,false);
421                         }
422
423                         if (Index.HasDuplicates) {
424                                 int[] dups = Index.Duplicates;
425                                 for (int i = 0; i < dups.Length; i++){
426                                         DataRow row = Table.RecordCache[dups[i]];
427                                         ArrayList columns = new ArrayList();
428                                         ArrayList values = new ArrayList();
429                                         foreach (DataColumn col in Columns){
430                                                 columns.Add(col.ColumnName);
431                                                 values.Add(row[col].ToString());
432                                         }
433
434                                         string columnNames = String.Join(", ", (string[])columns.ToArray(typeof(string)));
435                                         string columnValues = String.Join(", ", (string[])values.ToArray(typeof(string)));
436
437                                         row.RowError = String.Format("Column '{0}' is constrained to be unique.  Value '{1}' is already present.", columnNames, columnValues);
438                                         for (int j=0; j < Columns.Length; ++j)
439                                                 row.SetColumnError (Columns [j], row.RowError);
440                                 }
441                                 return true;
442                         }
443                         return false;
444                 }
445
446                 internal override void AssertConstraint(DataRow row)
447                 {       
448                         if (IsPrimaryKey && row.HasVersion(DataRowVersion.Default)) {
449                                 for (int i = 0; i < Columns.Length; i++) {
450                                         if (row.IsNull(Columns[i])) {
451                                                 throw new NoNullAllowedException("Column '" + Columns[i].ColumnName + "' does not allow nulls.");
452                                         }
453                                 }
454                         }
455                         
456                         if (Index == null) {
457                                 Index = Table.GetIndex(Columns,null,DataViewRowState.None,null,false);
458                         }
459
460                         if (Index.HasDuplicates) {
461                                 throw new ConstraintException(GetErrorMessage(row));
462                         }
463                 }
464
465                 internal override bool IsColumnContained(DataColumn column)
466                 {
467                         for (int i = 0; i < _dataColumns.Length; i++)
468                                 if (column == _dataColumns[i])
469                                         return true;
470
471                         return false;
472                 }
473
474                 internal override bool CanRemoveFromCollection(ConstraintCollection col, bool shouldThrow){
475                         if (IsPrimaryKey) {
476                                 if (shouldThrow)
477                                         throw new ArgumentException("Cannot remove unique constraint since it's the primary key of a table.");
478
479                                 return false;
480                         }
481                         
482                         if (Table.DataSet == null)
483                                 return true;
484
485                         if (ChildConstraint != null) {
486                                 if (!shouldThrow)
487                                         return false;
488                                 throw new ArgumentException (String.Format (
489                                                                 "Cannot remove unique constraint '{0}'." +
490                                                                 "Remove foreign key constraint '{1}' first.",
491                                                                 ConstraintName,ChildConstraint.ConstraintName));
492                         }
493                         return true;
494                 }
495
496                 private string GetErrorMessage(DataRow row)
497                 {
498                         int i;
499                          
500                         System.Text.StringBuilder sb = new System.Text.StringBuilder(row[_dataColumns[0]].ToString());
501                         for (i = 1; i < _dataColumns.Length; i++) {
502                                 sb = sb.Append(", ").Append(row[_dataColumns[i].ColumnName]);
503                         }
504                         string valStr = sb.ToString();
505                         sb = new System.Text.StringBuilder(_dataColumns[0].ColumnName);
506                         for (i = 1; i < _dataColumns.Length; i++) {
507                                 sb = sb.Append(", ").Append(_dataColumns[i].ColumnName);
508                         }
509                         string colStr = sb.ToString();
510                         return "Column '" + colStr + "' is constrained to be unique.  Value '" + valStr + "' is already present.";
511                 }
512                                        
513                 
514                 #endregion // Methods
515
516         }
517
518         
519 }