In Test/System.Data:
[mono.git] / mcs / class / System.Data / System.Data / DataColumnCollection.cs
1 //
2 // System.Data.DataColumnCollection.cs
3 //
4 // Author:
5 //   Christopher Podurgiel (cpodurgiel@msn.com)
6 //   Stuart Caborn      <stuart.caborn@virgin.net>
7 //   Tim Coleman (tim@timcoleman.com)
8 //
9 // (C) Chris Podurgiel
10 // Copyright (C) Tim Coleman, 2002
11 // Copyright (C) Daniel Morgan, 2003
12 //
13
14 //
15 // Copyright (C) 2004 Novell, Inc (http://www.novell.com)
16 //
17 // Permission is hereby granted, free of charge, to any person obtaining
18 // a copy of this software and associated documentation files (the
19 // "Software"), to deal in the Software without restriction, including
20 // without limitation the rights to use, copy, modify, merge, publish,
21 // distribute, sublicense, and/or sell copies of the Software, and to
22 // permit persons to whom the Software is furnished to do so, subject to
23 // the following conditions:
24 // 
25 // The above copyright notice and this permission notice shall be
26 // included in all copies or substantial portions of the Software.
27 // 
28 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
29 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
30 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
31 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
32 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
33 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
34 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35 //
36
37 using System;
38 using System.Collections;
39 using System.ComponentModel;
40
41 namespace System.Data {
42         [Editor]
43         [Serializable]
44         [DefaultEvent ("CollectionChanged")]
45         public class DataColumnCollection : InternalDataCollectionBase
46         {
47                 //This hashtable maps between column name to DataColumn object.
48                 private Hashtable columnFromName = new Hashtable();
49                 //This ArrayList contains the auto-increment columns names
50                 private ArrayList autoIncrement = new ArrayList();
51                 //This holds the next index to use for default column name.
52                 private int defaultColumnIndex = 1;
53                 //table should be the DataTable this DataColumnCollection belongs to.
54                 private DataTable parentTable = null;
55
56                 // Internal Constructor.  This Class can only be created from other classes in this assembly.
57                 internal DataColumnCollection(DataTable table):base()
58                 {
59                         parentTable = table;
60                 }
61
62                 /// <summary>
63                 /// Gets the DataColumn from the collection at the specified index.
64                 /// </summary>
65                 public virtual DataColumn this[int index]
66                 {
67                         get
68                         {
69                                 return (DataColumn) base.List[index];
70                         }
71                 }
72
73                 /// <summary>
74                 /// Gets the DataColumn from the collection with the specified name.
75                 /// </summary>
76                 public virtual DataColumn this[string name]
77                 {
78                         get
79                         {
80                                 DataColumn dc = columnFromName[name] as DataColumn;
81                                 
82                                 if (dc != null)
83                                         return dc;
84
85                                 int tmp = IndexOf(name, true);
86                                 if (tmp == -1)
87                                         return null;
88                                 return this[tmp]; 
89                         }
90                 }
91
92                 /// <summary>
93                 /// Gets a list of the DataColumnCollection items.
94                 /// </summary>
95                 protected override ArrayList List 
96                 {
97                         get
98                         {
99                                 return base.List;
100                         }
101                 }
102
103                 internal ArrayList AutoIncrmentColumns 
104                 {
105                         get
106                         {
107                                 return autoIncrement;
108                         }
109                 }
110
111                 //Add Logic
112                 //
113                 //Changing Event
114                 //DefaultValue set and AutoInc set check
115                 //?Validate Expression??
116                 //Name check and creation
117                 //Set Table
118                 //Check Unique if true then add a unique constraint
119                 //?Notify Rows of new column ?
120                 //Add to collection
121                 //Changed Event
122
123                 /// <summary>
124                 /// Creates and adds a DataColumn object to the DataColumnCollection.
125                 /// </summary>
126                 /// <returns></returns>
127                 public virtual DataColumn Add()
128                 {
129                         string defaultName = GetNextDefaultColumnName ();
130                         DataColumn column = new DataColumn (defaultName);
131                         Add (column);
132                         return column;
133                 }
134
135                 private void RegisterName(string name, DataColumn column)
136                 {
137                         if (columnFromName.Contains(name))
138                                 throw new DuplicateNameException("A DataColumn named '" + name + "' already belongs to this DataTable.");
139
140                         columnFromName[name] = column;
141
142                         if (name.StartsWith("Column") && name == MakeName(defaultColumnIndex + 1))
143                         {
144                                 do
145                                 {
146                                         defaultColumnIndex++;
147                                 }
148                                 while (Contains(MakeName(defaultColumnIndex + 1)));
149                         }
150                 }
151
152                 private void UnregisterName(string name)
153                 {
154                         if (columnFromName.Contains(name))
155                                 columnFromName.Remove(name);
156
157                         if (name.StartsWith("Column") && name == MakeName(defaultColumnIndex - 1))
158                         {
159                                 do
160                                 {
161                                         defaultColumnIndex--;
162                                 }
163                                 while (!Contains(MakeName(defaultColumnIndex - 1)) && defaultColumnIndex > 1);
164                         }
165                 }
166
167                 private string GetNextDefaultColumnName ()
168                 {
169                         string defColumnName = MakeName(defaultColumnIndex);
170                         for (int index = defaultColumnIndex + 1; Contains(defColumnName); ++index) {
171                                 defColumnName = MakeName(index);
172                                 defaultColumnIndex++;
173                         }
174                         defaultColumnIndex++;
175                         return defColumnName;
176                 }
177
178                 static readonly string[] TenColumns = { "Column0", "Column1", "Column2", "Column3", "Column4", "Column5", "Column6", "Column7", "Column8", "Column9" };
179
180                 private string MakeName(int index)
181                 {
182                         if (index < 10)
183                                 return TenColumns[index];
184
185                         return String.Concat("Column", index.ToString());
186                 }
187
188                 /// <summary>
189                 /// Creates and adds the specified DataColumn object to the DataColumnCollection.
190                 /// </summary>
191                 /// <param name="column">The DataColumn to add.</param>
192                 public void Add(DataColumn column)
193                 {
194
195                         if (column == null)
196                                 throw new ArgumentNullException ("column", "'column' argument cannot be null.");
197
198                         if (column.ColumnName.Equals(String.Empty))
199                         {
200                                 column.ColumnName = GetNextDefaultColumnName ();
201                         }
202
203 //                      if (Contains(column.ColumnName))
204 //                              throw new DuplicateNameException("A DataColumn named '" + column.ColumnName + "' already belongs to this DataTable.");
205
206                         if (column.Table != null)
207                                 throw new ArgumentException ("Column '" + column.ColumnName + "' already belongs to this or another DataTable.");
208
209                         CollectionChangeEventArgs e = new CollectionChangeEventArgs(CollectionChangeAction.Add, this);
210
211                         column.SetTable (parentTable);
212                         RegisterName(column.ColumnName, column);
213                         int ordinal = base.List.Add(column);
214                         column.SetOrdinal (ordinal);
215
216                         // if table already has rows we need to allocate space 
217                         // in the column data container 
218                         if ( parentTable.Rows.Count > 0 ) {
219                                 column.DataContainer.Capacity = parentTable.RecordCache.CurrentCapacity;
220                         }
221
222                         if (column.AutoIncrement) {
223                                 DataRowCollection rows = column.Table.Rows;
224                                 for (int i = 0; i < rows.Count; i++)
225                                         rows [i] [ordinal] = column.AutoIncrementValue ();
226                         }
227
228                         if (column.AutoIncrement)
229                                 autoIncrement.Add(column);
230
231                         OnCollectionChanged (e);
232                 }
233
234                 /// <summary>
235                 /// Creates and adds a DataColumn object with the specified name to the DataColumnCollection.
236                 /// </summary>
237                 /// <param name="columnName">The name of the column.</param>
238                 /// <returns>The newly created DataColumn.</returns>
239                 public virtual DataColumn Add(string columnName)
240                 {
241                         if (columnName == null || columnName == String.Empty)
242                         {
243                                 columnName = GetNextDefaultColumnName();
244                         }
245                         
246                         DataColumn column = new DataColumn(columnName);
247                         Add (column);
248                         return column;
249                 }
250
251                 /// <summary>
252                 /// Creates and adds a DataColumn object with the specified name and type to the DataColumnCollection.
253                 /// </summary>
254                 /// <param name="columnName">The ColumnName to use when cretaing the column.</param>
255                 /// <param name="type">The DataType of the new column.</param>
256                 /// <returns>The newly created DataColumn.</returns>
257                 public virtual DataColumn Add(string columnName, Type type)
258                 {
259                         if (columnName == null || columnName == "")
260                         {
261                                 columnName = GetNextDefaultColumnName ();
262                         }
263                         
264                         DataColumn column = new DataColumn(columnName, type);
265                         Add (column);
266                         return column;
267                 }
268
269                 /// <summary>
270                 /// Creates and adds a DataColumn object with the specified name, type, and expression to the DataColumnCollection.
271                 /// </summary>
272                 /// <param name="columnName">The name to use when creating the column.</param>
273                 /// <param name="type">The DataType of the new column.</param>
274                 /// <param name="expression">The expression to assign to the Expression property.</param>
275                 /// <returns>The newly created DataColumn.</returns>
276                 public virtual DataColumn Add(string columnName, Type type, string expression)
277                 {
278                         if (columnName == null || columnName == "")
279                         {
280                                 columnName = GetNextDefaultColumnName ();
281                         }
282                         
283                         DataColumn column = new DataColumn(columnName, type, expression);
284                         Add (column);
285                         return column;
286                 }
287
288                 /// <summary>
289                 /// Copies the elements of the specified DataColumn array to the end of the collection.
290                 /// </summary>
291                 /// <param name="columns">The array of DataColumn objects to add to the collection.</param>
292                 public void AddRange(DataColumn[] columns)
293                 {
294                         foreach (DataColumn column in columns)
295                         {
296                                 Add(column);
297                         }
298                         return;
299                 }
300
301                 /// <summary>
302                 /// Checks whether a given column can be removed from the collection.
303                 /// </summary>
304                 /// <param name="column">A DataColumn in the collection.</param>
305                 /// <returns>true if the column can be removed; otherwise, false.</returns>
306                 public bool CanRemove(DataColumn column)
307                 {
308                                                 
309                         //Check that the column does not have a null reference.
310                         if (column == null)
311                                 return false;
312
313                         
314                         //Check that the column is part of this collection.
315                         if (!Contains(column.ColumnName))
316                         {
317                                 return false;
318                         }
319
320
321                         
322                         //Check if this column is part of a relationship. (this could probably be written better)
323                         foreach (DataRelation childRelation in parentTable.ChildRelations)
324                         {
325                                 foreach (DataColumn childColumn in childRelation.ChildColumns)
326                                 {
327                                         if (childColumn == column)
328                                         {
329                                                 return false;
330                                         }
331                                 }
332
333                                 foreach (DataColumn parentColumn in childRelation.ParentColumns)
334                                 {
335                                         if (parentColumn == column)
336                                         {
337                                                 return false;
338                                         }
339                                 }
340                         }
341
342                         //Check if this column is part of a relationship. (this could probably be written better)
343                         foreach (DataRelation parentRelation in parentTable.ParentRelations)
344                         {
345                                 foreach (DataColumn childColumn in parentRelation.ChildColumns)
346                                 {
347                                         if (childColumn == column)
348                                         {
349                                                 return false;
350                                         }
351                                 }
352
353                                 foreach (DataColumn parentColumn in parentRelation.ParentColumns)
354                                 {
355                                         if (parentColumn == column)
356                                         {
357                                                 return false;
358                                         }
359                                 }
360                         }
361
362                         
363                         //Check if another column's expression depends on this column.
364                         
365                         foreach (DataColumn dataColumn in List)
366                         {
367                                 if (dataColumn.Expression.ToString().IndexOf(column.ColumnName) > 0)
368                                 {
369                                         return false;
370                                 }
371                         }
372                         
373                         // check for part of pk
374                         UniqueConstraint uc = UniqueConstraint.GetPrimaryKeyConstraint (parentTable.Constraints);
375                         if (uc != null && uc.Contains (column)) 
376                                 throw new ArgumentException (String.Format ("Cannot remove column {0}, because" +
377                                                              " it is part of primarykey",
378                                                              column.ColumnName));
379                         // check for part of fk
380                         DataSet ds = parentTable.DataSet;
381                         
382                         if (ds != null) {
383                                 foreach (DataTable t in ds.Tables) {
384                                         if (t == parentTable)
385                                                 continue;
386                                         foreach (Constraint c in t.Constraints) {
387                                                 if (! (c is ForeignKeyConstraint))
388                                                         continue;
389                                                 ForeignKeyConstraint fk = (ForeignKeyConstraint) c;
390                                                 if (fk.Contains (column, true)      // look in parent
391                                                     || fk.Contains (column, false)) // look in children
392                                                         throw new ArgumentException (String.Format ("Cannot remove column {0}, because" +
393                                                                                      " it is part of foreign key constraint",
394                                                                                      column.ColumnName));
395                                                 
396                                         }
397                                         
398                                 }
399                                 
400                                 
401                         }
402                         
403                         return true;
404                 }
405
406                 /// <summary>
407                 /// Clears the collection of any columns.
408                 /// </summary>
409                 public void Clear()
410                 {
411                         CollectionChangeEventArgs e = new CollectionChangeEventArgs(CollectionChangeAction.Refresh, this);
412
413
414                         // FIXME: Hmm... This loop could look little nicer :)
415                         foreach (DataColumn Col in List) {
416
417                                 foreach (DataRelation Rel in Col.Table.ParentRelations) {
418
419                                         foreach (DataColumn Col2 in Rel.ParentColumns) {
420                                                 if (Object.ReferenceEquals (Col, Col2))
421                                                         throw new ArgumentException ("Cannot remove this column, because " + 
422                                                                                      "it is part of the parent key for relationship " + 
423                                                                                      Rel.RelationName + ".");
424                                         }
425
426                                         foreach (DataColumn Col2 in Rel.ChildColumns) {
427                                                 if (Object.ReferenceEquals (Col, Col2))
428                                                         throw new ArgumentException ("Cannot remove this column, because " + 
429                                                                                      "it is part of the parent key for relationship " + 
430                                                                                      Rel.RelationName + ".");
431
432                                         }
433                                 }
434
435                                 foreach (DataRelation Rel in Col.Table.ChildRelations) {
436
437                                         foreach (DataColumn Col2 in Rel.ParentColumns) {
438                                                 if (Object.ReferenceEquals (Col, Col2))
439                                                         throw new ArgumentException ("Cannot remove this column, because " + 
440                                                                                      "it is part of the parent key for relationship " + 
441                                                                                      Rel.RelationName + ".");
442                                         }
443
444                                         foreach (DataColumn Col2 in Rel.ChildColumns) {
445                                                 if (Object.ReferenceEquals (Col, Col2))
446                                                         throw new ArgumentException ("Cannot remove this column, because " + 
447                                                                                      "it is part of the parent key for relationship " + 
448                                                                                      Rel.RelationName + ".");
449                                         }
450                                 }
451
452                         }
453
454                         // whether all columns can be removed
455                         foreach (DataColumn col in this) {
456                                 if (!CanRemove (col))
457                                         throw new ArgumentException ("Cannot remove column {0}", col.ColumnName);
458                         }
459
460                         try {
461                                 columnFromName.Clear();
462                                 autoIncrement.Clear();
463                                 base.List.Clear();
464                                 OnCollectionChanged(e);
465                         } catch (Exception ex) {
466                                 throw new ArgumentException (ex.Message, ex);
467                         }
468                 }
469
470                 /// <summary>
471                 /// Checks whether the collection contains a column with the specified name.
472                 /// </summary>
473                 /// <param name="name">The ColumnName of the column to check for.</param>
474                 /// <returns>true if a column exists with this name; otherwise, false.</returns>
475                 public bool Contains(string name)
476                 {
477                         if (columnFromName.Contains(name))
478                                 return true;
479                         
480                         return (IndexOf(name, false) != -1);
481                 }
482
483                 /// <summary>
484                 /// Gets the index of a column specified by name.
485                 /// </summary>
486                 /// <param name="column">The name of the column to return.</param>
487                 /// <returns>The index of the column specified by column if it is found; otherwise, -1.</returns>
488                 public virtual int IndexOf(DataColumn column)
489                 {
490                         return base.List.IndexOf(column);
491                 }
492
493                 /// <summary>
494                 /// Gets the index of the column with the given name (the name is not case sensitive).
495                 /// </summary>
496                 /// <param name="columnName">The name of the column to find.</param>
497                 /// <returns>The zero-based index of the column with the specified name, or -1 if the column doesn't exist in the collection.</returns>
498                 public int IndexOf(string columnName)
499                 {
500                         DataColumn dc = columnFromName[columnName] as DataColumn;
501                                 
502                         if (dc != null)
503                                 return IndexOf(dc);
504
505                         return IndexOf(columnName, false);
506                 }
507
508                 /// <summary>
509                 /// Raises the OnCollectionChanged event.
510                 /// </summary>
511                 /// <param name="ccevent">A CollectionChangeEventArgs that contains the event data.</param>
512                 protected virtual void OnCollectionChanged(CollectionChangeEventArgs ccevent)
513                 {
514                         if (CollectionChanged != null) 
515                         {
516                                 CollectionChanged(this, ccevent);
517                         }
518                 }
519
520                 /// <summary>
521                 /// Raises the OnCollectionChanging event.
522                 /// </summary>
523                 /// <param name="ccevent">A CollectionChangeEventArgs that contains the event data.</param>
524                 protected internal virtual void OnCollectionChanging(CollectionChangeEventArgs ccevent)
525                 {
526                         if (CollectionChanged != null) 
527                         {
528                                 //FIXME: this is not right
529                                 //CollectionChanged(this, ccevent);
530                                 throw new NotImplementedException();
531                         }
532                 }
533
534                 /// <summary>
535                 /// Removes the specified DataColumn object from the collection.
536                 /// </summary>
537                 /// <param name="column">The DataColumn to remove.</param>
538                 public void Remove(DataColumn column)
539                 {
540                         if (column == null)
541                                 throw new ArgumentNullException ("column", "'column' argument cannot be null.");
542
543                         if (!Contains(column.ColumnName))
544                                 throw new ArgumentException ("Cannot remove a column that doesn't belong to this table.");
545                         //TODO: can remove first with exceptions
546                         //and OnChanging Event
547                         CollectionChangeEventArgs e = new CollectionChangeEventArgs(CollectionChangeAction.Remove, this);
548                         
549                         int ordinal = column.Ordinal;
550                         UnregisterName(column.ColumnName);
551                         base.List.Remove(column);
552                         
553                         //Update the ordinals
554                         for( int i = ordinal ; i < this.Count ; i ++ )
555                         {
556                                 this[i].SetOrdinal( i );
557                         }
558
559                         if (parentTable != null)
560                                 parentTable.OnRemoveColumn(column);
561
562                         if (column.AutoIncrement)
563                                 autoIncrement.Remove(column);
564
565                         OnCollectionChanged(e);
566                         return;
567                 }
568
569                 /// <summary>
570                 /// Removes the DataColumn object with the specified name from the collection.
571                 /// </summary>
572                 /// <param name="name">The name of the column to remove.</param>
573                 public void Remove(string name)
574                 {
575                         DataColumn column = this[name];
576                         
577                         if (column == null)
578                                 throw new ArgumentException ("Column '" + name + "' does not belong to table " + ( parentTable == null ? "" : parentTable.TableName ) + ".");
579                         Remove(column);
580                 }
581
582                 /// <summary>
583                 /// Removes the column at the specified index from the collection.
584                 /// </summary>
585                 /// <param name="index">The index of the column to remove.</param>
586                 public void RemoveAt(int index)
587                 {
588                         if (Count <= index)
589                                 throw new IndexOutOfRangeException ("Cannot find column " + index + ".");
590
591                         DataColumn column = this[index];
592                         Remove(column);
593                 }
594
595
596                 /// <summary>
597                 ///  Do the same as Constains -method but case sensitive
598                 /// </summary>
599                 private bool CaseSensitiveContains(string columnName)
600                 {
601                         DataColumn column = this[columnName];
602                         
603                         if (column != null)
604                                 return string.Compare(column.ColumnName, columnName, false) == 0; 
605
606                         return false;
607                 }
608
609                 internal void UpdateAutoIncrement(DataColumn col,bool isAutoIncrement)
610                 {
611                         if (isAutoIncrement)
612                         {
613                                 if (!autoIncrement.Contains(col))
614                                         autoIncrement.Add(col);
615                         }
616                         else
617                         {
618                                 if (autoIncrement.Contains(col))
619                                         autoIncrement.Remove(col);
620                         }
621                 }
622
623                 private int IndexOf (string name, bool error)
624                 {
625                         int count = 0, match = -1;
626                         for (int i = 0; i < List.Count; i++)
627                         {
628                                 String name2 = ((DataColumn) List[i]).ColumnName;
629                                 if (String.Compare (name, name2, true) == 0)
630                                 {
631                                         if (String.Compare (name, name2, false) == 0)
632                                                 return i;
633                                         match = i;
634                                         count++;
635                                 }
636                         }
637                         if (count == 1)
638                                 return match;
639                         if (count > 1 && error)
640                                 throw new ArgumentException ("There is no match for the name in the same case and there are multiple matches in different case.");
641                         return -1;
642                 }
643                 
644                 #region Events
645
646                 /// <summary>
647                 /// Occurs when the columns collection changes, either by adding or removing a column.
648                 /// </summary>
649                 [ResDescriptionAttribute ("Occurs whenever this collection's membership changes.")] 
650                 public event CollectionChangeEventHandler CollectionChanged;
651
652                 #endregion 
653         }
654 }