5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne
\r
7 // Permission is hereby granted, free of charge, to any person obtaining a copy
\r
8 // of this software and associated documentation files (the "Software"), to deal
\r
9 // in the Software without restriction, including without limitation the rights
\r
10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\r
11 // copies of the Software, and to permit persons to whom the Software is
\r
12 // furnished to do so, subject to the following conditions:
\r
14 // The above copyright notice and this permission notice shall be included in
\r
15 // all copies or substantial portions of the Software.
\r
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\r
18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\r
19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\r
20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\r
21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\r
22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
\r
28 using System.Collections.Generic;
\r
31 using System.Linq.Expressions;
\r
32 using System.Reflection;
\r
35 using DbLinq.Data.Linq.Mapping;
\r
36 using DbLinq.Data.Linq.Sugar;
\r
37 using DbLinq.Data.Linq.Sugar.Expressions;
\r
40 namespace DbLinq.Data.Linq.Sugar.Implementation
\r
42 internal partial class ExpressionDispatcher
\r
46 /// Returns a registered column, or null if not found
\r
47 /// This method requires the table to be already registered
\r
49 /// <param name="table"></param>
\r
50 /// <param name="name"></param>
\r
51 /// <param name="builderContext"></param>
\r
52 /// <returns></returns>
\r
53 protected virtual ColumnExpression GetRegisteredColumn(TableExpression table, string name,
\r
54 BuilderContext builderContext)
\r
57 (from queryColumn in builderContext.EnumerateScopeColumns()
\r
58 where queryColumn.Table.IsEqualTo(table) && queryColumn.Name == name
\r
59 select queryColumn).SingleOrDefault();
\r
63 /// Returns an existing table or registers the current one
\r
65 /// <param name="tableExpression"></param>
\r
66 /// <param name="builderContext"></param>
\r
67 /// <returns>A registered table or the current newly registered one</returns>
\r
68 public virtual TableExpression RegisterTable(TableExpression tableExpression, BuilderContext builderContext)
\r
70 // 1. Find the table in current scope
\r
71 var foundTableExpression = (from t in builderContext.EnumerateScopeTables()
\r
72 where t.IsEqualTo(tableExpression)
\r
73 select t).SingleOrDefault();
\r
74 if (foundTableExpression != null)
\r
75 return foundTableExpression;
\r
76 // 2. Find it in all scopes, and promote it to current scope.
\r
77 foundTableExpression = PromoteTable(tableExpression, builderContext);
\r
78 if (foundTableExpression != null)
\r
79 return foundTableExpression;
\r
81 builderContext.CurrentSelect.Tables.Add(tableExpression);
\r
82 return tableExpression;
\r
86 /// Promotes a table to a common parent between its current scope and our current scope
\r
88 /// <param name="tableExpression"></param>
\r
89 /// <param name="builderContext"></param>
\r
90 /// <returns></returns>
\r
91 protected virtual TableExpression PromoteTable(TableExpression tableExpression, BuilderContext builderContext)
\r
93 int currentIndex = 0;
\r
94 SelectExpression oldSelect = null;
\r
95 SelectExpression commonScope = null;
\r
96 TableExpression foundTable = null;
\r
100 oldSelect = builderContext.SelectExpressions[currentIndex];
\r
102 // look for a common scope
\r
103 if (oldSelect != builderContext.CurrentSelect)
\r
105 commonScope = FindCommonScope(oldSelect, builderContext.CurrentSelect);
\r
106 if (commonScope != null)
\r
107 // if a common scope exists, look for an equivalent table in that select
\r
108 for (int tableIndex = 0; tableIndex < oldSelect.Tables.Count && foundTable == null; tableIndex++)
\r
110 if (oldSelect.Tables[tableIndex].IsEqualTo(tableExpression))
\r
112 // found a matching table!
\r
113 foundTable = oldSelect.Tables[tableIndex];
\r
119 while (currentIndex < builderContext.SelectExpressions.Count && foundTable == null);
\r
121 if (foundTable != null)
\r
123 oldSelect.Tables.Remove(foundTable);
\r
124 commonScope.Tables.Add(foundTable);
\r
130 /// Find the common ancestor between two ScopeExpressions
\r
132 /// <param name="a"></param>
\r
133 /// <param name="b"></param>
\r
134 /// <returns></returns>
\r
135 protected virtual SelectExpression FindCommonScope(SelectExpression a, SelectExpression b)
\r
137 for (var aScope = a; aScope != null; aScope = aScope.Parent)
\r
139 for (var bScope = b; bScope != null; bScope = bScope.Parent)
\r
141 if (aScope == bScope)
\r
149 /// Registers a column
\r
150 /// This method requires the table to be already registered
\r
152 /// <param name="table"></param>
\r
153 /// <param name="memberInfo"></param>
\r
154 /// <param name="name"></param>
\r
155 /// <param name="builderContext"></param>
\r
156 /// <returns></returns>
\r
157 public ColumnExpression RegisterColumn(TableExpression table,
\r
158 MemberInfo memberInfo, string name,
\r
159 BuilderContext builderContext)
\r
161 if (memberInfo == null)
\r
163 var queryColumn = GetRegisteredColumn(table, name, builderContext);
\r
164 if (queryColumn == null)
\r
166 table = RegisterTable(table, builderContext);
\r
167 queryColumn = CreateColumn(table, memberInfo, builderContext);
\r
168 builderContext.CurrentSelect.Columns.Add(queryColumn);
\r
170 return queryColumn;
\r
174 /// Registers a column with only a table and a MemberInfo (this is the preferred method overload)
\r
176 /// <param name="tableExpression"></param>
\r
177 /// <param name="memberInfo"></param>
\r
178 /// <param name="builderContext"></param>
\r
179 /// <returns></returns>
\r
180 public ColumnExpression RegisterColumn(TableExpression tableExpression, MemberInfo memberInfo,
\r
181 BuilderContext builderContext)
\r
183 var dataMember = builderContext.QueryContext.DataContext.Mapping.GetTable(tableExpression.Type).RowType
\r
184 .GetDataMember(memberInfo);
\r
185 if (dataMember == null)
\r
187 return RegisterColumn(tableExpression, memberInfo, dataMember.MappedName, builderContext);
\r
190 public ColumnExpression CreateColumn(TableExpression table, MemberInfo memberInfo, BuilderContext builderContext)
\r
192 var dataMember = builderContext.QueryContext.DataContext.Mapping.GetTable(table.Type).RowType
\r
193 .GetDataMember(memberInfo);
\r
194 if (dataMember == null)
\r
196 return new ColumnExpression(table, dataMember);
\r
200 /// Creates a default TableExpression
\r
202 /// <param name="tableType"></param>
\r
203 /// <param name="builderContext"></param>
\r
204 /// <returns></returns>
\r
205 public virtual TableExpression CreateTable(Type tableType, BuilderContext builderContext)
\r
207 return new TableExpression(tableType, DataMapper.GetTableName(tableType, builderContext.QueryContext.DataContext));
\r
211 /// Registers an association
\r
213 /// <param name="tableExpression">The table holding the member, to become the joinedTable</param>
\r
214 /// <param name="tableMemberInfo"></param>
\r
215 /// <param name="otherType"></param>
\r
216 /// <param name="builderContext"></param>
\r
217 /// <returns></returns>
\r
218 public virtual TableExpression RegisterAssociation(TableExpression tableExpression, MemberInfo tableMemberInfo,
\r
219 Type otherType, BuilderContext builderContext)
\r
221 IList<MemberInfo> otherKeys;
\r
222 TableJoinType joinType;
\r
224 var theseKeys = DataMapper.GetAssociation(tableExpression, tableMemberInfo, otherType, out otherKeys,
\r
225 out joinType, out joinID, builderContext.QueryContext.DataContext);
\r
226 // if the memberInfo has no corresponding association, we get a null, that we propagate
\r
227 if (theseKeys == null)
\r
230 // the current table has the foreign key, the other table the referenced (usually primary) key
\r
231 if (theseKeys.Count != otherKeys.Count)
\r
232 throw Error.BadArgument("S0128: Association arguments (FK and ref'd PK) don't match");
\r
234 // we first create the table, with the JoinID, and we MUST complete the table later, with the Join() method
\r
235 var otherTableExpression = new TableExpression(otherType, DataMapper.GetTableName(otherType, builderContext.QueryContext.DataContext), joinID);
\r
237 Expression joinExpression = null;
\r
239 var createdColumns = new List<ColumnExpression>();
\r
240 for (int keyIndex = 0; keyIndex < theseKeys.Count; keyIndex++)
\r
242 // joinedKey is registered, even if unused by final select (required columns will be filtered anyway)
\r
243 Expression otherKey = RegisterColumn(otherTableExpression, otherKeys[keyIndex], builderContext);
\r
244 // foreign is created, we will store it later if this assocation is registered too
\r
245 Expression thisKey = CreateColumn(tableExpression, theseKeys[keyIndex], builderContext);
\r
246 createdColumns.Add((ColumnExpression)thisKey);
\r
248 // if the key is nullable, then convert it
\r
249 // TODO: this will probably need to be changed
\r
250 if (otherKey.Type.IsNullable())
\r
251 otherKey = Expression.Convert(otherKey, otherKey.Type.GetNullableType());
\r
252 if (thisKey.Type.IsNullable())
\r
253 thisKey = Expression.Convert(thisKey, thisKey.Type.GetNullableType());
\r
254 // the other key is set as left operand, this must be this way
\r
255 // since some vendors (SQL Server) don't support the opposite
\r
256 var referenceExpression = Expression.Equal(otherKey, thisKey);
\r
258 // if we already have a join expression, then we have a double condition here, so "AND" it
\r
259 if (joinExpression != null)
\r
260 joinExpression = Expression.And(joinExpression, referenceExpression);
\r
262 joinExpression = referenceExpression;
\r
264 // we complete the table here, now that we have all join information
\r
265 otherTableExpression.Join(joinType, tableExpression, joinExpression);
\r
267 // our table is created, with the expressions
\r
268 // now check if we didn't register exactly the same
\r
269 if ((from t in builderContext.EnumerateScopeTables() where t.IsEqualTo(otherTableExpression) select t).SingleOrDefault() == null)
\r
271 builderContext.CurrentSelect.Tables.Add(otherTableExpression);
\r
272 foreach (var createdColumn in createdColumns)
\r
273 builderContext.CurrentSelect.Columns.Add(createdColumn);
\r
276 return otherTableExpression;
\r
280 /// Registers an external parameter
\r
281 /// Since these can be complex expressions, we don't try to identify them
\r
282 /// and push them every time
\r
283 /// The only loss may be a small memory loss (if anyone can prove me that the same Expression can be used twice)
\r
285 /// <param name="expression"></param>
\r
286 /// <param name="alias"></param>
\r
287 /// <param name="builderContext"></param>
\r
288 /// <returns></returns>
\r
289 public virtual InputParameterExpression RegisterParameter(Expression expression, string alias, BuilderContext builderContext)
\r
291 var queryParameterExpression = new InputParameterExpression(expression, alias);
\r
292 builderContext.ExpressionQuery.Parameters.Add(queryParameterExpression);
\r
293 return queryParameterExpression;
\r
296 public virtual void UnregisterParameter(InputParameterExpression expression, BuilderContext builderContext)
\r
298 builderContext.ExpressionQuery.Parameters.Remove(expression);
\r
302 /// Registers a MetaTable
\r
304 /// <param name="metaTableType"></param>
\r
305 /// <param name="aliases"></param>
\r
306 /// <param name="builderContext"></param>
\r
307 /// <returns></returns>
\r
308 public virtual MetaTableExpression RegisterMetaTable(Type metaTableType, IDictionary<MemberInfo, MutableExpression> aliases,
\r
309 BuilderContext builderContext)
\r
311 MetaTableExpression metaTableExpression;
\r
312 if (!builderContext.MetaTables.TryGetValue(metaTableType, out metaTableExpression))
\r
314 metaTableExpression = new MetaTableExpression(aliases, metaTableType);
\r
315 builderContext.MetaTables[metaTableType] = metaTableExpression;
\r
317 return metaTableExpression;
\r
321 /// Registers a where clause in the current context scope
\r
323 /// <param name="whereExpression"></param>
\r
324 /// <param name="builderContext"></param>
\r
325 public virtual void RegisterWhere(Expression whereExpression, BuilderContext builderContext)
\r
327 builderContext.CurrentSelect.Where.Add(whereExpression);
\r
331 /// Registers all columns of a table.
\r
333 /// <param name="tableExpression"></param>
\r
334 /// <param name="builderContext"></param>
\r
335 /// <returns></returns>
\r
336 public virtual IEnumerable<ColumnExpression> RegisterAllColumns(TableExpression tableExpression, BuilderContext builderContext)
\r
338 foreach (var metaMember in builderContext.QueryContext.DataContext.Mapping.GetTable(tableExpression.Type).RowType.PersistentDataMembers)
\r
340 yield return RegisterColumn(tableExpression, metaMember.Member, builderContext);
\r
345 /// Registers an expression to be returned by main request.
\r
346 /// The strategy is to try to find it in the already registered parameters, and if not found, add it
\r
348 /// <param name="expression">The expression to be registered</param>
\r
349 /// <param name="builderContext"></param>
\r
350 /// <returns>Expression index</returns>
\r
351 public virtual int RegisterOutputParameter(Expression expression, BuilderContext builderContext)
\r
353 var scope = builderContext.CurrentSelect;
\r
354 var operands = scope.Operands.ToList();
\r
355 for (int index = 0; index < operands.Count; index++)
\r
357 if (ExpressionEquals(operands[index], expression))
\r
360 operands.Add(expression);
\r
361 builderContext.CurrentSelect = (SelectExpression)scope.Mutate(operands);
\r
362 return operands.Count - 1;
\r
365 protected virtual bool ExpressionEquals(Expression a, Expression b)
\r
367 // TODO: something smarter, to compare contents and not only references (works fine only for columns)
\r
372 /// Registers the table as returned by the SQL request.
\r
373 /// Actually, the table is split into its columns.
\r
375 /// <param name="tableExpression"></param>
\r
376 /// <param name="dataRecordParameter"></param>
\r
377 /// <param name="mappingContextParameter"></param>
\r
378 /// <param name="builderContext"></param>
\r
379 /// <returns></returns>
\r
380 protected virtual Expression GetOutputTableReader(TableExpression tableExpression,
\r
381 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
382 BuilderContext builderContext)
\r
384 var bindings = new List<MemberBinding>();
\r
386 foreach (ColumnExpression columnExpression in RegisterAllColumns(tableExpression, builderContext))
\r
388 MemberInfo memberInfo = columnExpression.StorageInfo ?? columnExpression.MemberInfo;
\r
389 PropertyInfo propertyInfo = memberInfo as PropertyInfo;
\r
390 if (propertyInfo == null || propertyInfo.CanWrite)
\r
392 var parameterColumn = GetOutputValueReader(columnExpression,
\r
393 dataRecordParameter, mappingContextParameter, builderContext);
\r
394 var binding = Expression.Bind(memberInfo, parameterColumn);
\r
395 bindings.Add(binding);
\r
398 var newExpression = Expression.New(tableExpression.Type);
\r
399 var initExpression = Expression.MemberInit(newExpression, bindings);
\r
400 return initExpression;
\r
404 /// Builds a Row builder, based on a given list of parameters
\r
406 /// <param name="tableType"></param>
\r
407 /// <param name="parameters"></param>
\r
408 /// <param name="builderContext"></param>
\r
409 /// <returns></returns>
\r
410 public virtual LambdaExpression BuildTableReader(Type tableType, IList<string> parameters, BuilderContext builderContext)
\r
412 var dataRecordParameter = Expression.Parameter(typeof(IDataRecord), "dataRecord");
\r
413 var mappingContextParameter = Expression.Parameter(typeof(MappingContext), "mappingContext");
\r
414 //var table = builderContext.QueryContext.DataContext.Mapping.GetTable(tableType);
\r
415 var bindings = new List<MemberBinding>();
\r
416 for (int parameterIndex = 0; parameterIndex < parameters.Count; parameterIndex++)
\r
418 var parameter = parameters[parameterIndex];
\r
419 var memberInfo = tableType.GetTableColumnMember(parameter);
\r
420 if (memberInfo == null)
\r
422 memberInfo = tableType.GetSingleMember(parameter, BindingFlags.Public | BindingFlags.NonPublic
\r
423 | BindingFlags.Instance | BindingFlags.IgnoreCase);
\r
426 if (memberInfo == null)
\r
427 throw new ArgumentException(string.Format("Invalid column '{0}'", parameter));
\r
428 //var column = DataMapper.GetColumnName(tableType, memberInfo, builderContext.QueryContext.DataContext);
\r
429 //var columnName = DataMapper.GetColumnName(tableType, memberInfo, builderContext.QueryContext.DataContext);
\r
430 var invoke = GetOutputValueReader(memberInfo.GetMemberType(), parameterIndex, //GetTableIndex(parameters, columnName),
\r
431 dataRecordParameter, mappingContextParameter);
\r
432 var parameterColumn = GetOutputValueReader(invoke, dataRecordParameter, mappingContextParameter,
\r
434 var binding = Expression.Bind(memberInfo, parameterColumn);
\r
435 bindings.Add(binding);
\r
437 var newExpression = Expression.New(tableType);
\r
438 var initExpression = Expression.MemberInit(newExpression, bindings);
\r
439 return Expression.Lambda(initExpression, dataRecordParameter, mappingContextParameter);
\r
442 protected virtual int GetTableIndex(IList<string> parameters, string columnName)
\r
444 int index = parameters.IndexOf(columnName);
\r
447 for (index = 0; index < parameters.Count; index++)
\r
449 if (string.Compare(parameters[index], columnName, true) == 0)
\r
458 /// Creates an entity set creator, to be used at run-time
\r
460 /// <param name="expression"></param>
\r
461 /// <param name="dataRecordParameter"></param>
\r
462 /// <param name="mappingContextParameter"></param>
\r
463 /// <param name="builderContext"></param>
\r
464 /// <returns></returns>
\r
465 protected virtual Expression GetEntitySetBuilder(EntitySetExpression expression,
\r
466 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
467 BuilderContext builderContext)
\r
469 // from here, creating an EntitySet consists in just creating the instance
\r
470 return Expression.New(expression.Type);
\r
474 /// Registers the expression as returned by the SQL request.
\r
476 /// <param name="expression"></param>
\r
477 /// <param name="dataRecordParameter"></param>
\r
478 /// <param name="mappingContextParameter"></param>
\r
479 /// <param name="builderContext"></param>
\r
480 /// <returns></returns>
\r
481 protected virtual Expression GetOutputValueReader(Expression expression,
\r
482 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
483 BuilderContext builderContext)
\r
485 int valueIndex = RegisterOutputParameter(expression, builderContext);
\r
486 return GetOutputValueReader(expression.Type, valueIndex, dataRecordParameter, mappingContextParameter);
\r
490 /// Registers the ColumnExpression as returned by the SQL request.
\r
492 /// <param name="expression"></param>
\r
493 /// <param name="dataRecordParameter"></param>
\r
494 /// <param name="mappingContextParameter"></param>
\r
495 /// <param name="builderContext"></param>
\r
496 /// <returns></returns>
\r
497 protected virtual Expression GetOutputValueReader(ColumnExpression expression,
\r
498 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
499 BuilderContext builderContext)
\r
501 int valueIndex = RegisterOutputParameter(expression, builderContext);
\r
502 Type storageType = expression.StorageInfo != null ? expression.StorageInfo.GetMemberType() : null;
\r
503 return GetOutputValueReader(storageType ?? expression.Type, valueIndex, dataRecordParameter, mappingContextParameter);
\r
508 /// Registers the expression as returned column
\r
510 /// <param name="columnType"></param>
\r
511 /// <param name="valueIndex"></param>
\r
512 /// <param name="dataRecordParameter"></param>
\r
513 /// <param name="mappingContextParameter"></param>
\r
514 /// <returns></returns>
\r
515 protected virtual Expression GetOutputValueReader(Type columnType, int valueIndex, ParameterExpression dataRecordParameter,
\r
516 ParameterExpression mappingContextParameter)
\r
518 var propertyReaderLambda = DataRecordReader.GetPropertyReader(columnType);
\r
519 Expression invoke = Expression.Invoke(propertyReaderLambda, dataRecordParameter,
\r
520 mappingContextParameter, Expression.Constant(valueIndex));
\r
521 if (!columnType.IsNullable())
\r
522 invoke = Expression.Convert(invoke, columnType);
\r