b000d40e8729e4c90f77a7e0bcc55ce7af0206e0
[mono.git] / mcs / class / referencesource / System.Data.Entity / System / Data / Query / InternalTrees / ColumnMapFactory.cs
1 //------------------------------------------------------------------------------
2 // <copyright file="ColumnMapFactory.cs" company="Microsoft">
3 //      Copyright (c) Microsoft Corporation.  All rights reserved.
4 // </copyright>
5 // @owner  Microsoft
6 // @backupOwner Microsoft
7 //------------------------------------------------------------------------------
8
9 using System.Collections.Generic;
10 using System.Data.Common;
11 using System.Data.Common.Utils;
12 using System.Data.Entity;
13 using System.Data.Mapping;
14 using System.Data.Metadata.Edm;
15 using System.Data.Objects.ELinq;
16 using System.Diagnostics;
17 using System.Linq;
18 using System.Linq.Expressions;
19 using System.Reflection;
20
21 namespace System.Data.Query.InternalTrees
22 {
23     /// <summary>
24     /// Factory methods for prescriptive column map patterns (includes default
25     /// column maps for -- soon to be -- public materializer services and function
26     /// mappings).
27     /// </summary>
28     internal static class ColumnMapFactory
29     {
30         /// <summary>
31         /// Creates a column map for the given reader and function mapping.
32         /// </summary>
33         internal static CollectionColumnMap CreateFunctionImportStructuralTypeColumnMap(DbDataReader storeDataReader, FunctionImportMappingNonComposable mapping, int resultSetIndex, EntitySet entitySet, StructuralType baseStructuralType)
34         {
35             FunctionImportStructuralTypeMappingKB resultMapping = mapping.GetResultMapping(resultSetIndex);
36             Debug.Assert(resultMapping != null);
37             if (resultMapping.NormalizedEntityTypeMappings.Count == 0) // no explicit mapping; use default non-polymorphic reader
38             {
39                 // if there is no mapping, create default mapping to root entity type or complex type
40                 Debug.Assert(!baseStructuralType.Abstract, "mapping loader must verify abstract types have explicit mapping");
41                
42                 return CreateColumnMapFromReaderAndType(storeDataReader, baseStructuralType, entitySet, resultMapping.ReturnTypeColumnsRenameMapping);
43             }
44             
45             // the section below deals with the polymorphic entity type mapping for return type
46             EntityType baseEntityType = baseStructuralType as EntityType;
47             Debug.Assert(null != baseEntityType, "We should have entity type here");
48
49             // Generate column maps for all discriminators
50             ScalarColumnMap[] discriminatorColumns = CreateDiscriminatorColumnMaps(storeDataReader, mapping, resultSetIndex);
51
52             // Generate default maps for all mapped entity types
53             var mappedEntityTypes = new HashSet<EntityType>(resultMapping.MappedEntityTypes);
54             mappedEntityTypes.Add(baseEntityType); // make sure the base type is represented
55             Dictionary<EntityType, TypedColumnMap> typeChoices = new Dictionary<EntityType, TypedColumnMap>(mappedEntityTypes.Count);
56             ColumnMap[] baseTypeColumnMaps = null;
57             foreach (EntityType entityType in mappedEntityTypes)
58             {                
59                 ColumnMap[] propertyColumnMaps = GetColumnMapsForType(storeDataReader, entityType, resultMapping.ReturnTypeColumnsRenameMapping);
60                 EntityColumnMap entityColumnMap = CreateEntityTypeElementColumnMap(storeDataReader, entityType, entitySet, propertyColumnMaps, resultMapping.ReturnTypeColumnsRenameMapping);
61                 if (!entityType.Abstract)
62                 {
63                     typeChoices.Add(entityType, entityColumnMap);
64                 }
65                 if (entityType == baseStructuralType)
66                 {
67                     baseTypeColumnMaps = propertyColumnMaps;
68                 }
69             }
70
71             // NOTE: We don't have a null sentinel here, because the stored proc won't 
72             //       return one anyway; we'll just presume the data's always there.
73             MultipleDiscriminatorPolymorphicColumnMap polymorphicMap = new MultipleDiscriminatorPolymorphicColumnMap(TypeUsage.Create(baseStructuralType), baseStructuralType.Name, baseTypeColumnMaps, discriminatorColumns, typeChoices, (object[] discriminatorValues) => mapping.Discriminate(discriminatorValues, resultSetIndex));
74             CollectionColumnMap collection = new SimpleCollectionColumnMap(baseStructuralType.GetCollectionType().TypeUsage, baseStructuralType.Name, polymorphicMap, null, null);
75             return collection;
76         }
77
78         /// <summary>
79         /// Build the collectionColumnMap from a store datareader, a type and an entitySet.
80         /// </summary>
81         /// <param name="storeDataReader"></param>
82         /// <param name="edmType"></param>
83         /// <param name="entitySet"></param>
84         /// <returns></returns>
85         internal static CollectionColumnMap CreateColumnMapFromReaderAndType(DbDataReader storeDataReader, EdmType edmType, EntitySet entitySet, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)
86         {
87             Debug.Assert(Helper.IsEntityType(edmType) || null == entitySet, "The specified non-null EntitySet is incompatible with the EDM type specified.");
88
89             // Next, build the ColumnMap directly from the edmType and entitySet provided.
90             ColumnMap[] propertyColumnMaps = GetColumnMapsForType(storeDataReader, edmType, renameList);
91             ColumnMap elementColumnMap = null;
92
93             // NOTE: We don't have a null sentinel here, because the stored proc won't 
94             //       return one anyway; we'll just presume the data's always there.
95             if (Helper.IsRowType(edmType))
96             {
97                 elementColumnMap = new RecordColumnMap(TypeUsage.Create(edmType), edmType.Name, propertyColumnMaps, null);
98             }
99             else if (Helper.IsComplexType(edmType))
100             {
101                 elementColumnMap = new ComplexTypeColumnMap(TypeUsage.Create(edmType), edmType.Name, propertyColumnMaps, null);
102             }
103             else if (Helper.IsScalarType(edmType))
104             {
105                 if (storeDataReader.FieldCount != 1)
106                 {
107                     throw EntityUtil.CommandExecutionDataReaderFieldCountForScalarType();
108                 }
109                 elementColumnMap = new ScalarColumnMap(TypeUsage.Create(edmType), edmType.Name, 0, 0);
110             }
111             else if (Helper.IsEntityType(edmType))
112             {
113                 elementColumnMap = CreateEntityTypeElementColumnMap(storeDataReader, edmType, entitySet, propertyColumnMaps, null/*renameList*/);
114             }
115             else
116             {
117                 Debug.Assert(false, "unexpected edmType?");
118             }
119             CollectionColumnMap collection = new SimpleCollectionColumnMap(edmType.GetCollectionType().TypeUsage, edmType.Name, elementColumnMap, null, null);
120             return collection;
121         }
122
123         /// <summary>
124         /// Requires: a public type with a public, default constructor. Returns a column map initializing the type
125         /// and all properties of the type with a public setter taking a primitive type and having a corresponding 
126         /// column in the reader.
127         /// </summary>
128         internal static CollectionColumnMap CreateColumnMapFromReaderAndClrType(DbDataReader reader, Type type, MetadataWorkspace workspace)
129         {
130             Debug.Assert(null != reader);
131             Debug.Assert(null != type);
132             Debug.Assert(null != workspace);
133
134             // we require a default constructor
135             ConstructorInfo constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
136                 null, Type.EmptyTypes, null);
137             if (type.IsAbstract || (null == constructor && !type.IsValueType))
138             {
139                 throw EntityUtil.InvalidOperation(
140                     Strings.ObjectContext_InvalidTypeForStoreQuery(type));
141             }
142
143             // build a LINQ expression used by result assembly to create results
144             var memberInfo = new List<Tuple<MemberAssignment, int, EdmProperty>>();
145             foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance))
146             {
147                 // for enums unwrap the type if nullable
148                 var propertyUnderlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
149                 Type propType = propertyUnderlyingType.IsEnum ? propertyUnderlyingType.GetEnumUnderlyingType() : prop.PropertyType;
150
151                 EdmType modelType;
152                 int ordinal;
153                 if (TryGetColumnOrdinalFromReader(reader, prop.Name, out ordinal) &&
154                     MetadataHelper.TryDetermineCSpaceModelType(propType, workspace, out modelType) &&
155                     (Helper.IsScalarType(modelType)) &&
156                     prop.CanWrite && prop.GetIndexParameters().Length == 0 && null != prop.GetSetMethod(/* nonPublic */true))
157                 {
158                     memberInfo.Add(Tuple.Create(
159                         Expression.Bind(prop, Expression.Parameter(prop.PropertyType, "placeholder")),
160                         ordinal,
161                         new EdmProperty(prop.Name, TypeUsage.Create(modelType))));
162                 }
163             }
164             // initialize members in the order in which they appear in the reader
165             MemberInfo[] members = new MemberInfo[memberInfo.Count];
166             MemberBinding[] memberBindings = new MemberBinding[memberInfo.Count];
167             ColumnMap[] propertyMaps = new ColumnMap[memberInfo.Count];
168             EdmProperty[] modelProperties = new EdmProperty[memberInfo.Count];
169             int i = 0;
170             foreach (var memberGroup in memberInfo.GroupBy(tuple => tuple.Item2).OrderBy(tuple => tuple.Key))
171             {
172                 // make sure that a single column isn't contributing to multiple properties
173                 if (memberGroup.Count() != 1)
174                 {
175                     throw EntityUtil.InvalidOperation(Strings.ObjectContext_TwoPropertiesMappedToSameColumn(
176                         reader.GetName(memberGroup.Key), 
177                         String.Join(", ", memberGroup.Select(tuple => tuple.Item3.Name).ToArray())));
178                 }
179
180                 var member = memberGroup.Single();
181                 MemberAssignment assignment = member.Item1;
182                 int ordinal = member.Item2;
183                 EdmProperty modelProp = member.Item3;
184                 
185                 members[i] = assignment.Member;
186                 memberBindings[i] = assignment;
187                 propertyMaps[i] = new ScalarColumnMap(modelProp.TypeUsage, modelProp.Name, 0, ordinal);
188                 modelProperties[i] = modelProp;
189                 i++;
190             }
191             NewExpression newExpr = null == constructor ? Expression.New(type) : Expression.New(constructor);
192             MemberInitExpression init = Expression.MemberInit(newExpr, memberBindings);
193             InitializerMetadata initMetadata = InitializerMetadata.CreateProjectionInitializer(
194                 (EdmItemCollection)workspace.GetItemCollection(DataSpace.CSpace), init, members);
195
196             // column map (a collection of rows with InitializerMetadata markup)
197             RowType rowType = new RowType(modelProperties, initMetadata);
198             RecordColumnMap rowMap = new RecordColumnMap(TypeUsage.Create(rowType),
199                 "DefaultTypeProjection", propertyMaps, null);
200             CollectionColumnMap collectionMap = new SimpleCollectionColumnMap(rowType.GetCollectionType().TypeUsage, 
201                 rowType.Name, rowMap, null, null);
202             return collectionMap;
203         }
204
205         /// <summary>
206         /// Build the entityColumnMap from a store datareader, a type and an entitySet and 
207         /// a list ofproperties.
208         /// </summary>
209         /// <param name="storeDataReader"></param>
210         /// <param name="edmType"></param>
211         /// <param name="entitySet"></param>
212         /// <param name="propertyColumnMaps"></param>
213         /// <returns></returns>
214         private static EntityColumnMap CreateEntityTypeElementColumnMap(
215             DbDataReader storeDataReader, EdmType edmType, EntitySet entitySet,
216             ColumnMap[] propertyColumnMaps, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)
217         {
218             EntityType entityType = (EntityType)edmType;
219
220             // The tricky part here is
221             // that the KeyColumns list must point at the same ColumnMap(s) that 
222             // the properties list points to, so we build a quick array of 
223             // ColumnMap(s) that are indexed by their ordinal; then we can walk
224             // the list of keyMembers, and find the ordinal in the reader, and 
225             // pick the same ColumnMap for it.
226
227             // Build the ordinal -> ColumnMap index
228             ColumnMap[] ordinalToColumnMap = new ColumnMap[storeDataReader.FieldCount];
229
230             foreach (ColumnMap propertyColumnMap in propertyColumnMaps)
231             {
232                 int ordinal = ((ScalarColumnMap)propertyColumnMap).ColumnPos;
233                 ordinalToColumnMap[ordinal] = propertyColumnMap;
234             }
235
236             // Now build the list of KeyColumns;
237             IList<EdmMember> keyMembers = entityType.KeyMembers;
238             SimpleColumnMap[] keyColumns = new SimpleColumnMap[keyMembers.Count];
239
240             int keyMemberIndex = 0;
241             foreach (EdmMember keyMember in keyMembers)
242             {
243                 int keyOrdinal = GetMemberOrdinalFromReader(storeDataReader, keyMember, edmType, renameList);
244
245                 Debug.Assert(keyOrdinal >= 0, "keyMember for entity is not found by name in the data reader?");
246
247                 ColumnMap keyColumnMap = ordinalToColumnMap[keyOrdinal];
248
249                 Debug.Assert(null != keyColumnMap, "keyMember for entity isn't in properties collection for the entity?");
250                 keyColumns[keyMemberIndex] = (SimpleColumnMap)keyColumnMap;
251                 keyMemberIndex++;
252             }
253
254             SimpleEntityIdentity entityIdentity = new SimpleEntityIdentity(entitySet, keyColumns);
255
256             EntityColumnMap result = new EntityColumnMap(TypeUsage.Create(edmType), edmType.Name, propertyColumnMaps, entityIdentity);
257             return result;
258         }
259
260         /// <summary>
261         /// For a given edmType, build an array of scalarColumnMaps that map to the columns
262         /// in the store datareader provided.  Note that we're hooking things up by name, not
263         /// by ordinal position.
264         /// </summary>
265         /// <param name="storeDataReader"></param>
266         /// <param name="edmType"></param>
267         /// <returns></returns>
268         private static ColumnMap[] GetColumnMapsForType(DbDataReader storeDataReader, EdmType edmType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)
269         {
270             // First get the list of properties; NOTE: we need to hook up the column by name, 
271             // not by position.
272             IBaseList<EdmMember> members = TypeHelpers.GetAllStructuralMembers(edmType);
273             ColumnMap[] propertyColumnMaps = new ColumnMap[members.Count];
274
275             int index = 0;
276             foreach (EdmMember member in members)
277             {
278                 if (!Helper.IsScalarType(member.TypeUsage.EdmType))
279                 {
280                     throw EntityUtil.InvalidOperation(Strings.ADP_InvalidDataReaderUnableToMaterializeNonScalarType(member.Name, member.TypeUsage.EdmType.FullName));
281                 }
282
283                 int ordinal = GetMemberOrdinalFromReader(storeDataReader, member, edmType, renameList);
284
285                 propertyColumnMaps[index] = new ScalarColumnMap(member.TypeUsage, member.Name, 0, ordinal);
286                 index++;
287             }
288             return propertyColumnMaps;
289         }
290
291         private static ScalarColumnMap[] CreateDiscriminatorColumnMaps(DbDataReader storeDataReader, FunctionImportMappingNonComposable mapping, int resultIndex)
292         {
293             // choose an arbitrary type for discriminator columns -- the type is not
294             // actually statically known
295             EdmType discriminatorType =
296                 MetadataItem.EdmProviderManifest.GetPrimitiveType(PrimitiveTypeKind.String);
297             TypeUsage discriminatorTypeUsage =
298                 TypeUsage.Create(discriminatorType);
299
300             IList<string> discriminatorColumnNames = mapping.GetDiscriminatorColumns(resultIndex);
301             ScalarColumnMap[] discriminatorColumns = new ScalarColumnMap[discriminatorColumnNames.Count];
302             for (int i = 0; i < discriminatorColumns.Length; i++)
303             {
304                 string columnName = discriminatorColumnNames[i];
305                 ScalarColumnMap columnMap = new ScalarColumnMap(discriminatorTypeUsage, columnName, 0,
306                     GetDiscriminatorOrdinalFromReader(storeDataReader, columnName, mapping.FunctionImport));
307                 discriminatorColumns[i] = columnMap;
308             }
309             return discriminatorColumns;
310         }
311
312         /// <summary>
313         /// Given a store datareader and a member of an edmType, find the column ordinal
314         /// in the datareader with the name of the member.
315         /// </summary>
316         /// <param name="storeDataReader"></param>
317         /// <param name="member"></param>
318         /// <returns></returns>
319         private static int GetMemberOrdinalFromReader(DbDataReader storeDataReader, EdmMember member, EdmType currentType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)
320         {
321             int result;
322             string memberName = GetRenameForMember(member, currentType, renameList);
323
324             if (!TryGetColumnOrdinalFromReader(storeDataReader, memberName, out result))
325             {
326                 throw EntityUtil.CommandExecutionDataReaderMissingColumnForType(member, currentType);
327             }
328             return result;
329         }
330
331         private static string GetRenameForMember(EdmMember member, EdmType currentType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)
332         {
333             // if list is null,
334             // or no rename mapping at all,
335             // or partial rename and the member is not specified by the renaming
336             // then we return the original member.Name
337             // otherwise we return the mapped one
338             return renameList == null || renameList.Count == 0 || !renameList.Any(m => m.Key == member.Name) ? member.Name : renameList[member.Name].GetRename(currentType);
339         }
340
341         /// <summary>
342         /// Given a store datareader, a column name, find the column ordinal
343         /// in the datareader with the name of the column.
344         /// 
345         /// We only have the functionImport provided to include it in the exception 
346         /// message.
347         /// </summary>
348         /// <param name="storeDataReader"></param>
349         /// <param name="columnName"></param>
350         /// <param name="functionImport"></param>
351         /// <returns></returns>
352         private static int GetDiscriminatorOrdinalFromReader(DbDataReader storeDataReader, string columnName, EdmFunction functionImport)
353         {
354             int result;
355             if (!TryGetColumnOrdinalFromReader(storeDataReader, columnName, out result))
356             {
357                 throw EntityUtil.CommandExecutionDataReaderMissinDiscriminatorColumn(columnName, functionImport);
358             }
359             return result;
360         }
361
362         /// <summary>
363         /// Given a store datareader and a column name, try to find the column ordinal
364         /// in the datareader with the name of the column.
365         /// </summary>
366         /// <param name="storeDataReader"></param>
367         /// <param name="columnName"></param>
368         /// <param name="ordinal"></param>
369         /// <returns>true if found, false otherwise.</returns>
370         private static bool TryGetColumnOrdinalFromReader(DbDataReader storeDataReader, string columnName, out int ordinal)
371         {
372             if (0 == storeDataReader.FieldCount)
373             {
374                 // If there are no fields, there can't be a match (this check avoids
375                 // an InvalidOperationException on the call to GetOrdinal)
376                 ordinal = default(int);
377                 return false;
378             }
379
380             // Wrap ordinal lookup for the member so that we can throw a nice exception.
381             try
382             {
383                 ordinal = storeDataReader.GetOrdinal(columnName);
384                 return true;
385             }
386             catch (IndexOutOfRangeException)
387             {
388                 // No column matching the column name found
389                 ordinal = default(int);
390                 return false;
391             }
392         }
393     }
394 }