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 using DataContext = System.Data.Linq.DataContext;
\r
42 using DataContext = DbLinq.Data.Linq.DataContext;
\r
45 namespace DbLinq.Data.Linq.Sugar.Implementation
\r
47 internal partial class ExpressionDispatcher
\r
51 /// Returns a registered column, or null if not found
\r
52 /// This method requires the table to be already registered
\r
54 /// <param name="table"></param>
\r
55 /// <param name="name"></param>
\r
56 /// <param name="builderContext"></param>
\r
57 /// <returns></returns>
\r
58 protected virtual ColumnExpression GetRegisteredColumn(TableExpression table, string name,
\r
59 BuilderContext builderContext)
\r
62 (from queryColumn in builderContext.EnumerateScopeColumns()
\r
63 where queryColumn.Table.IsEqualTo(table) && queryColumn.Name == name
\r
64 select queryColumn).SingleOrDefault();
\r
68 /// Returns an existing table or registers the current one
\r
70 /// <param name="tableExpression"></param>
\r
71 /// <param name="builderContext"></param>
\r
72 /// <returns>A registered table or the current newly registered one</returns>
\r
73 public virtual TableExpression RegisterTable(TableExpression tableExpression, BuilderContext builderContext)
\r
75 // 1. Find the table in current scope
\r
76 var foundTableExpression = (from t in builderContext.EnumerateScopeTables()
\r
77 where t.IsEqualTo(tableExpression)
\r
78 select t).SingleOrDefault();
\r
79 if (foundTableExpression != null)
\r
80 return foundTableExpression;
\r
81 // 2. Find it in all scopes, and promote it to current scope.
\r
82 foundTableExpression = PromoteTable(tableExpression, builderContext);
\r
83 if (foundTableExpression != null)
\r
84 return foundTableExpression;
\r
86 builderContext.CurrentSelect.Tables.Add(tableExpression);
\r
87 return tableExpression;
\r
91 /// Promotes a table to a common parent between its current scope and our current scope
\r
93 /// <param name="tableExpression"></param>
\r
94 /// <param name="builderContext"></param>
\r
95 /// <returns></returns>
\r
96 protected virtual TableExpression PromoteTable(TableExpression tableExpression, BuilderContext builderContext)
\r
98 int currentIndex = 0;
\r
99 SelectExpression oldSelect = null;
\r
100 SelectExpression commonScope = null;
\r
101 TableExpression foundTable = null;
\r
105 oldSelect = builderContext.SelectExpressions[currentIndex];
\r
107 // look for a common scope
\r
108 if (oldSelect != builderContext.CurrentSelect)
\r
110 commonScope = FindCommonScope(oldSelect, builderContext.CurrentSelect);
\r
111 if (commonScope != null)
\r
112 // if a common scope exists, look for an equivalent table in that select
\r
113 for (int tableIndex = 0; tableIndex < oldSelect.Tables.Count && foundTable == null; tableIndex++)
\r
115 if (oldSelect.Tables[tableIndex].IsEqualTo(tableExpression))
\r
117 // found a matching table!
\r
118 foundTable = oldSelect.Tables[tableIndex];
\r
124 while (currentIndex < builderContext.SelectExpressions.Count && foundTable == null);
\r
126 if (foundTable != null)
\r
128 oldSelect.Tables.Remove(foundTable);
\r
129 commonScope.Tables.Add(foundTable);
\r
135 /// Find the common ancestor between two ScopeExpressions
\r
137 /// <param name="a"></param>
\r
138 /// <param name="b"></param>
\r
139 /// <returns></returns>
\r
140 protected virtual SelectExpression FindCommonScope(SelectExpression a, SelectExpression b)
\r
142 for (var aScope = a; aScope != null; aScope = aScope.Parent)
\r
144 for (var bScope = b; bScope != null; bScope = bScope.Parent)
\r
146 if (aScope == bScope)
\r
154 /// Registers a column
\r
155 /// This method requires the table to be already registered
\r
157 /// <param name="table"></param>
\r
158 /// <param name="memberInfo"></param>
\r
159 /// <param name="name"></param>
\r
160 /// <param name="builderContext"></param>
\r
161 /// <returns></returns>
\r
162 public ColumnExpression RegisterColumn(TableExpression table,
\r
163 MemberInfo memberInfo, string name,
\r
164 BuilderContext builderContext)
\r
166 if (memberInfo == null)
\r
168 var queryColumn = GetRegisteredColumn(table, name, builderContext);
\r
169 if (queryColumn == null)
\r
171 table = RegisterTable(table, builderContext);
\r
172 queryColumn = CreateColumn(table, memberInfo, builderContext);
\r
173 builderContext.CurrentSelect.Columns.Add(queryColumn);
\r
175 return queryColumn;
\r
179 /// Registers a column with only a table and a MemberInfo (this is the preferred method overload)
\r
181 /// <param name="tableExpression"></param>
\r
182 /// <param name="memberInfo"></param>
\r
183 /// <param name="builderContext"></param>
\r
184 /// <returns></returns>
\r
185 public ColumnExpression RegisterColumn(TableExpression tableExpression, MemberInfo memberInfo,
\r
186 BuilderContext builderContext)
\r
188 var dataMember = builderContext.QueryContext.DataContext.Mapping.GetTable(tableExpression.Type).RowType
\r
189 .GetDataMember(memberInfo);
\r
190 if (dataMember == null)
\r
192 return RegisterColumn(tableExpression, memberInfo, dataMember.MappedName, builderContext);
\r
195 public ColumnExpression CreateColumn(TableExpression table, MemberInfo memberInfo, BuilderContext builderContext)
\r
197 var dataMember = builderContext.QueryContext.DataContext.Mapping.GetTable(table.Type).RowType
\r
198 .GetDataMember(memberInfo);
\r
199 if (dataMember == null)
\r
201 return new ColumnExpression(table, dataMember);
\r
205 /// Creates a default TableExpression
\r
207 /// <param name="tableType"></param>
\r
208 /// <param name="builderContext"></param>
\r
209 /// <returns></returns>
\r
210 public virtual TableExpression CreateTable(Type tableType, BuilderContext builderContext)
\r
212 return new TableExpression(tableType, DataMapper.GetTableName(tableType, builderContext.QueryContext.DataContext));
\r
216 /// Registers an association
\r
218 /// <param name="tableExpression">The table holding the member, to become the joinedTable</param>
\r
219 /// <param name="tableMemberInfo"></param>
\r
220 /// <param name="otherType"></param>
\r
221 /// <param name="builderContext"></param>
\r
222 /// <returns></returns>
\r
223 public virtual TableExpression RegisterAssociation(TableExpression tableExpression, MemberInfo tableMemberInfo,
\r
224 Type otherType, BuilderContext builderContext)
\r
226 IList<MemberInfo> otherKeys;
\r
227 TableJoinType joinType;
\r
229 var theseKeys = DataMapper.GetAssociation(tableExpression, tableMemberInfo, otherType, out otherKeys,
\r
230 out joinType, out joinID, builderContext.QueryContext.DataContext);
\r
231 // if the memberInfo has no corresponding association, we get a null, that we propagate
\r
232 if (theseKeys == null)
\r
235 // the current table has the foreign key, the other table the referenced (usually primary) key
\r
236 if (theseKeys.Count != otherKeys.Count)
\r
237 throw Error.BadArgument("S0128: Association arguments (FK and ref'd PK) don't match");
\r
239 // we first create the table, with the JoinID, and we MUST complete the table later, with the Join() method
\r
240 var otherTableExpression = new TableExpression(otherType, DataMapper.GetTableName(otherType, builderContext.QueryContext.DataContext), joinID);
\r
242 Expression joinExpression = null;
\r
244 var createdColumns = new List<ColumnExpression>();
\r
245 for (int keyIndex = 0; keyIndex < theseKeys.Count; keyIndex++)
\r
247 // joinedKey is registered, even if unused by final select (required columns will be filtered anyway)
\r
248 Expression otherKey = RegisterColumn(otherTableExpression, otherKeys[keyIndex], builderContext);
\r
249 // foreign is created, we will store it later if this assocation is registered too
\r
250 Expression thisKey = CreateColumn(tableExpression, theseKeys[keyIndex], builderContext);
\r
251 createdColumns.Add((ColumnExpression)thisKey);
\r
253 // if the key is nullable, then convert it
\r
254 // TODO: this will probably need to be changed
\r
255 if (otherKey.Type.IsNullable())
\r
256 otherKey = Expression.Convert(otherKey, otherKey.Type.GetNullableType());
\r
257 if (thisKey.Type.IsNullable())
\r
258 thisKey = Expression.Convert(thisKey, thisKey.Type.GetNullableType());
\r
259 // the other key is set as left operand, this must be this way
\r
260 // since some vendors (SQL Server) don't support the opposite
\r
261 var referenceExpression = Expression.Equal(otherKey, thisKey);
\r
263 // if we already have a join expression, then we have a double condition here, so "AND" it
\r
264 if (joinExpression != null)
\r
265 joinExpression = Expression.And(joinExpression, referenceExpression);
\r
267 joinExpression = referenceExpression;
\r
269 // we complete the table here, now that we have all join information
\r
270 otherTableExpression.Join(joinType, tableExpression, joinExpression);
\r
272 // our table is created, with the expressions
\r
273 // now check if we didn't register exactly the same
\r
274 var existingTable = (from t in builderContext.EnumerateScopeTables() where t.IsEqualTo(otherTableExpression) select t).SingleOrDefault();
\r
275 if (existingTable != null)
\r
276 return existingTable;
\r
278 builderContext.CurrentSelect.Tables.Add(otherTableExpression);
\r
279 foreach (var createdColumn in createdColumns)
\r
280 builderContext.CurrentSelect.Columns.Add(createdColumn);
\r
281 return otherTableExpression;
\r
285 /// Registers an external parameter
\r
286 /// Since these can be complex expressions, we don't try to identify them
\r
287 /// and push them every time
\r
288 /// The only loss may be a small memory loss (if anyone can prove me that the same Expression can be used twice)
\r
290 /// <param name="expression"></param>
\r
291 /// <param name="alias"></param>
\r
292 /// <param name="builderContext"></param>
\r
293 /// <returns></returns>
\r
294 public virtual InputParameterExpression RegisterParameter(Expression expression, string alias, BuilderContext builderContext)
\r
296 var queryParameterExpression = new InputParameterExpression(expression, alias);
\r
297 builderContext.ExpressionQuery.Parameters.Add(queryParameterExpression);
\r
298 return queryParameterExpression;
\r
301 public virtual void UnregisterParameter(InputParameterExpression expression, BuilderContext builderContext)
\r
303 builderContext.ExpressionQuery.Parameters.Remove(expression);
\r
307 /// Registers a MetaTable
\r
309 /// <param name="metaTableType"></param>
\r
310 /// <param name="aliases"></param>
\r
311 /// <param name="builderContext"></param>
\r
312 /// <returns></returns>
\r
313 public virtual MetaTableExpression RegisterMetaTable(Type metaTableType, IDictionary<MemberInfo, MutableExpression> aliases,
\r
314 BuilderContext builderContext)
\r
316 MetaTableExpression metaTableExpression;
\r
317 if (!builderContext.MetaTables.TryGetValue(metaTableType, out metaTableExpression))
\r
319 metaTableExpression = new MetaTableExpression(aliases, metaTableType);
\r
320 builderContext.MetaTables[metaTableType] = metaTableExpression;
\r
322 return metaTableExpression;
\r
326 /// Registers a where clause in the current context scope
\r
328 /// <param name="whereExpression"></param>
\r
329 /// <param name="builderContext"></param>
\r
330 public virtual void RegisterWhere(Expression whereExpression, BuilderContext builderContext)
\r
332 builderContext.CurrentSelect.Where.Add(whereExpression);
\r
336 /// Registers all columns of a table.
\r
338 /// <param name="tableExpression"></param>
\r
339 /// <param name="builderContext"></param>
\r
340 /// <returns></returns>
\r
341 public virtual IEnumerable<ColumnExpression> RegisterAllColumns(TableExpression tableExpression, BuilderContext builderContext)
\r
343 foreach (var metaMember in builderContext.QueryContext.DataContext.Mapping.GetTable(tableExpression.Type).RowType.PersistentDataMembers)
\r
345 yield return RegisterColumn(tableExpression, metaMember.Member, builderContext);
\r
350 /// Registers an expression to be returned by main request.
\r
351 /// The strategy is to try to find it in the already registered parameters, and if not found, add it
\r
353 /// <param name="expression">The expression to be registered</param>
\r
354 /// <param name="builderContext"></param>
\r
355 /// <returns>Expression index</returns>
\r
356 public virtual int RegisterOutputParameter(Expression expression, BuilderContext builderContext)
\r
358 var scope = builderContext.CurrentSelect;
\r
359 var operands = scope.Operands.ToList();
\r
360 for (int index = 0; index < operands.Count; index++)
\r
362 if (ExpressionEquals(operands[index], expression))
\r
365 operands.Add(expression);
\r
366 builderContext.CurrentSelect = (SelectExpression)scope.Mutate(operands);
\r
367 return operands.Count - 1;
\r
370 protected virtual bool ExpressionEquals(Expression a, Expression b)
\r
372 // TODO: something smarter, to compare contents and not only references (works fine only for columns)
\r
377 /// Registers the table as returned by the SQL request.
\r
378 /// Actually, the table is split into its columns.
\r
380 /// <param name="tableExpression"></param>
\r
381 /// <param name="dataRecordParameter"></param>
\r
382 /// <param name="mappingContextParameter"></param>
\r
383 /// <param name="builderContext"></param>
\r
384 /// <returns></returns>
\r
385 protected virtual Expression GetOutputTableReader(TableExpression tableExpression,
\r
386 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
387 BuilderContext builderContext)
\r
389 var bindings = new List<MemberBinding>();
\r
391 foreach (ColumnExpression columnExpression in RegisterAllColumns(tableExpression, builderContext))
\r
393 MemberInfo memberInfo = columnExpression.StorageInfo ?? columnExpression.MemberInfo;
\r
394 PropertyInfo propertyInfo = memberInfo as PropertyInfo;
\r
395 if (propertyInfo == null || propertyInfo.CanWrite)
\r
397 var parameterColumn = GetOutputValueReader(columnExpression,
\r
398 dataRecordParameter, mappingContextParameter, builderContext);
\r
399 var binding = Expression.Bind(memberInfo, parameterColumn);
\r
400 bindings.Add(binding);
\r
403 var newExpression = Expression.New(tableExpression.Type);
\r
404 var initExpression = Expression.MemberInit(newExpression, bindings);
\r
405 return initExpression;
\r
409 /// Builds a Row builder, based on a given list of parameters
\r
411 /// <param name="tableType"></param>
\r
412 /// <param name="parameters"></param>
\r
413 /// <param name="builderContext"></param>
\r
414 /// <returns></returns>
\r
415 public virtual LambdaExpression BuildTableReader(Type tableType, IList<string> parameters, BuilderContext builderContext)
\r
417 var dataRecordParameter = Expression.Parameter(typeof(IDataRecord), "dataRecord");
\r
418 var mappingContextParameter = Expression.Parameter(typeof(MappingContext), "mappingContext");
\r
419 //var table = builderContext.QueryContext.DataContext.Mapping.GetTable(tableType);
\r
420 var bindings = new List<MemberBinding>();
\r
421 for (int parameterIndex = 0; parameterIndex < parameters.Count; parameterIndex++)
\r
423 var parameter = parameters[parameterIndex];
\r
424 var memberInfo = tableType.GetTableColumnMember(parameter);
\r
425 if (memberInfo == null)
\r
427 memberInfo = tableType.GetSingleMember(parameter, BindingFlags.Public | BindingFlags.NonPublic
\r
428 | BindingFlags.Instance | BindingFlags.IgnoreCase);
\r
431 if (memberInfo == null)
\r
432 throw new ArgumentException(string.Format("Invalid column '{0}'", parameter));
\r
433 //var column = DataMapper.GetColumnName(tableType, memberInfo, builderContext.QueryContext.DataContext);
\r
434 //var columnName = DataMapper.GetColumnName(tableType, memberInfo, builderContext.QueryContext.DataContext);
\r
435 var invoke = GetOutputValueReader(memberInfo.GetMemberType(), parameterIndex, //GetTableIndex(parameters, columnName),
\r
436 dataRecordParameter, mappingContextParameter);
\r
437 var parameterColumn = GetOutputValueReader(invoke, dataRecordParameter, mappingContextParameter,
\r
439 var binding = Expression.Bind(memberInfo, parameterColumn);
\r
440 bindings.Add(binding);
\r
442 var newExpression = Expression.New(tableType);
\r
443 var initExpression = Expression.MemberInit(newExpression, bindings);
\r
444 return Expression.Lambda(initExpression, dataRecordParameter, mappingContextParameter);
\r
447 protected virtual int GetTableIndex(IList<string> parameters, string columnName)
\r
449 int index = parameters.IndexOf(columnName);
\r
452 for (index = 0; index < parameters.Count; index++)
\r
454 if (string.Compare(parameters[index], columnName, true) == 0)
\r
463 /// Creates an entity set creator, to be used at run-time
\r
465 /// <param name="expression"></param>
\r
466 /// <param name="dataRecordParameter"></param>
\r
467 /// <param name="mappingContextParameter"></param>
\r
468 /// <param name="builderContext"></param>
\r
469 /// <returns></returns>
\r
470 protected virtual Expression GetEntitySetBuilder(EntitySetExpression expression,
\r
471 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
472 BuilderContext builderContext)
\r
474 var entityType = expression.EntitySetType.GetGenericArguments()[0];
\r
475 List<ElementInit> members = new List<ElementInit>();
\r
476 var add = expression.EntitySetType.GetMethod("Add",
\r
477 BindingFlags.NonPublic | BindingFlags.Instance,
\r
479 new Type[] { typeof(KeyValuePair<object, MemberInfo>) },
\r
482 foreach (var info in expression.Columns)
\r
484 var column = info.Key;
\r
485 var tk = info.Value;
\r
486 MemberInfo memberInfo = column.StorageInfo ?? column.MemberInfo;
\r
487 PropertyInfo propertyInfo = memberInfo as PropertyInfo;
\r
488 if (propertyInfo == null || propertyInfo.CanWrite)
\r
490 var parameterColumn = GetOutputValueReader(column,
\r
491 dataRecordParameter, mappingContextParameter, builderContext);
\r
492 members.Add(Expression.ElementInit(add,
\r
494 Expression.New(typeof(KeyValuePair<object, MemberInfo>).GetConstructor(new Type[]{typeof(object), typeof(MemberInfo)}),
\r
495 Expression.Convert(parameterColumn, typeof(object)),
\r
496 Expression.Constant(tk.Member, typeof(MemberInfo)))}));
\r
500 return Expression.ListInit(
\r
502 expression.EntitySetType.GetConstructor(
\r
503 BindingFlags.NonPublic | BindingFlags.Instance,
\r
505 new[] { typeof(DataContext) },
\r
507 Expression.Constant(builderContext.QueryContext.DataContext)),
\r
512 /// Registers the expression as returned by the SQL request.
\r
514 /// <param name="expression"></param>
\r
515 /// <param name="dataRecordParameter"></param>
\r
516 /// <param name="mappingContextParameter"></param>
\r
517 /// <param name="builderContext"></param>
\r
518 /// <returns></returns>
\r
519 protected virtual Expression GetOutputValueReader(Expression expression,
\r
520 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
521 BuilderContext builderContext)
\r
523 int valueIndex = RegisterOutputParameter(expression, builderContext);
\r
524 return GetOutputValueReader(expression.Type, valueIndex, dataRecordParameter, mappingContextParameter);
\r
528 /// Registers the ColumnExpression as returned by the SQL request.
\r
530 /// <param name="expression"></param>
\r
531 /// <param name="dataRecordParameter"></param>
\r
532 /// <param name="mappingContextParameter"></param>
\r
533 /// <param name="builderContext"></param>
\r
534 /// <returns></returns>
\r
535 protected virtual Expression GetOutputValueReader(ColumnExpression expression,
\r
536 ParameterExpression dataRecordParameter, ParameterExpression mappingContextParameter,
\r
537 BuilderContext builderContext)
\r
539 int valueIndex = RegisterOutputParameter(expression, builderContext);
\r
540 Type storageType = expression.StorageInfo != null ? expression.StorageInfo.GetMemberType() : null;
\r
541 return GetOutputValueReader(storageType ?? expression.Type, valueIndex, dataRecordParameter, mappingContextParameter);
\r
546 /// Registers the expression as returned column
\r
548 /// <param name="columnType"></param>
\r
549 /// <param name="valueIndex"></param>
\r
550 /// <param name="dataRecordParameter"></param>
\r
551 /// <param name="mappingContextParameter"></param>
\r
552 /// <returns></returns>
\r
553 protected virtual Expression GetOutputValueReader(Type columnType, int valueIndex, ParameterExpression dataRecordParameter,
\r
554 ParameterExpression mappingContextParameter)
\r
556 var propertyReaderLambda = DataRecordReader.GetPropertyReader(columnType);
\r
557 Expression invoke = new ParameterBinder().BindParams(propertyReaderLambda,
\r
558 dataRecordParameter, mappingContextParameter, Expression.Constant(valueIndex));
\r
559 if (!columnType.IsNullable())
\r
560 invoke = Expression.Convert(invoke, columnType);
\r
565 class ParameterBinder
\r
567 Dictionary<Expression, Expression> map;
\r
569 public Expression BindParams(LambdaExpression expr, params Expression[] args)
\r
571 map = new Dictionary<Expression, Expression>();
\r
573 if (expr.Parameters.Count != args.Length)
\r
574 throw new NotImplementedException();
\r
575 for (int i = 0; i < expr.Parameters.Count; ++i)
\r
576 map[expr.Parameters[i]] = args[i];
\r
577 return Visit(expr.Body);
\r
580 Expression Visit(Expression expr)
\r
582 switch (expr.NodeType)
\r
584 case ExpressionType.Call:
\r
585 MethodCallExpression call = expr as MethodCallExpression;
\r
586 Expression[] new_args = new Expression[call.Arguments.Count];
\r
587 for (int i = 0; i < new_args.Length; ++i)
\r
588 new_args[i] = Visit(call.Arguments[i]);
\r
589 return Expression.Call(call.Object, call.Method, new_args);
\r
590 case ExpressionType.Parameter:
\r
591 Expression new_expr;
\r
592 if (map.TryGetValue(expr, out new_expr))
\r
596 throw new Exception("Can't handle " + expr.NodeType);
\r