2009-07-11 Michael Barker <mike@middlesoft.co.uk>
[mono.git] / mcs / class / System.Data.Linq / src / DbLinq / Data / Linq / Sugar / Implementation / QueryBuilder.cs
1 #region MIT license\r
2 // \r
3 // MIT license\r
4 //\r
5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne\r
6 // \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
13 // \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
16 // \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
23 // THE SOFTWARE.\r
24 // \r
25 #endregion\r
26 \r
27 using System;\r
28 using System.Collections.Generic;\r
29 using System.Diagnostics;\r
30 using System.Linq;\r
31 using System.Linq.Expressions;\r
32 using System.Text.RegularExpressions;\r
33 \r
34 using DbLinq.Data.Linq.Sugar.ExpressionMutator;\r
35 using DbLinq.Data.Linq.Sugar.Expressions;\r
36 using DbLinq.Factory;\r
37 using DbLinq.Util;\r
38 \r
39 namespace DbLinq.Data.Linq.Sugar.Implementation\r
40 {\r
41     /// <summary>\r
42     /// Full query builder, with cache management\r
43     /// 1. Parses Linq Expression\r
44     /// 2. Generates SQL\r
45     /// </summary>\r
46     internal partial class QueryBuilder : IQueryBuilder\r
47     {\r
48         public IExpressionLanguageParser ExpressionLanguageParser { get; set; }\r
49         public IExpressionDispatcher ExpressionDispatcher { get; set; }\r
50         public IPrequelAnalyzer PrequelAnalyzer { get; set; }\r
51         public IExpressionOptimizer ExpressionOptimizer { get; set; }\r
52         public ISpecialExpressionTranslator SpecialExpressionTranslator { get; set; }\r
53         public ISqlBuilder SqlBuilder { get; set; }\r
54 \r
55         public QueryBuilder()\r
56         {\r
57             ExpressionLanguageParser = ObjectFactory.Get<IExpressionLanguageParser>();\r
58             ExpressionDispatcher = ObjectFactory.Get<IExpressionDispatcher>();\r
59             PrequelAnalyzer = ObjectFactory.Get<IPrequelAnalyzer>();\r
60             ExpressionOptimizer = ObjectFactory.Get<IExpressionOptimizer>();\r
61             SpecialExpressionTranslator = ObjectFactory.Get<ISpecialExpressionTranslator>();\r
62             SqlBuilder = ObjectFactory.Get<ISqlBuilder>();\r
63         }\r
64 \r
65         /// <summary>\r
66         /// Builds the ExpressionQuery:\r
67         /// - parses Expressions and builds row creator\r
68         /// - checks names unicity\r
69         /// </summary>\r
70         /// <param name="expressions"></param>\r
71         /// <param name="queryContext"></param>\r
72         /// <returns></returns>\r
73         protected virtual ExpressionQuery BuildExpressionQuery(ExpressionChain expressions, QueryContext queryContext)\r
74         {\r
75             var builderContext = new BuilderContext(queryContext);\r
76             BuildExpressionQuery(expressions, builderContext);\r
77             CheckTablesAlias(builderContext);\r
78             CheckParametersAlias(builderContext);\r
79             return builderContext.ExpressionQuery;\r
80         }\r
81 \r
82         /// <summary>\r
83         /// Finds all registered tables or columns with the given name.\r
84         /// We exclude parameter because they won't be prefixed/suffixed the same way (well, that's a guess, I hope it's a good one)\r
85         /// </summary>\r
86         /// <param name="name"></param>\r
87         /// <param name="builderContext"></param>\r
88         /// <returns></returns>\r
89         protected virtual IList<Expression> FindExpressionsByName(string name, BuilderContext builderContext)\r
90         {\r
91             var expressions = new List<Expression>();\r
92             expressions.AddRange((from t in builderContext.EnumerateAllTables() where t.Alias == name select (Expression)t).Distinct());\r
93             expressions.AddRange(from c in builderContext.EnumerateScopeColumns() where c.Alias == name select (Expression)c);\r
94             return expressions;\r
95         }\r
96 \r
97         protected virtual string MakeName(string aliasBase, int index, string anonymousBase, BuilderContext builderContext)\r
98         {\r
99             if (string.IsNullOrEmpty(aliasBase))\r
100                 aliasBase = anonymousBase;\r
101             return string.Format("{0}{1}", aliasBase, index);\r
102         }\r
103 \r
104         protected virtual string MakeTableName(string aliasBase, int index, BuilderContext builderContext)\r
105         {\r
106             return MakeName(aliasBase, index, "t", builderContext);\r
107         }\r
108 \r
109         protected virtual string MakeParameterName(string aliasBase, int index, BuilderContext builderContext)\r
110         {\r
111             return MakeName(aliasBase, index, "p", builderContext);\r
112         }\r
113 \r
114         /// <summary>\r
115         /// Give all non-aliased tables a name\r
116         /// </summary>\r
117         /// <param name="builderContext"></param>\r
118         protected virtual void CheckTablesAlias(BuilderContext builderContext)\r
119         {\r
120             var tables = builderContext.EnumerateAllTables().Distinct().ToList();\r
121             // just to be nice: if we have only one table involved, there's no need to alias it\r
122             if (tables.Count == 1)\r
123             {\r
124                 tables[0].Alias = null;\r
125             }\r
126             else\r
127             {\r
128                 foreach (var tableExpression in tables)\r
129                 {\r
130                     // if no alias, or duplicate alias\r
131                     if (string.IsNullOrEmpty(tableExpression.Alias) ||\r
132                         FindExpressionsByName(tableExpression.Alias, builderContext).Count > 1)\r
133                     {\r
134                         int anonymousIndex = 0;\r
135                         var aliasBase = tableExpression.Alias;\r
136                         // we try to assign one until we have a unique alias\r
137                         do\r
138                         {\r
139                             tableExpression.Alias = MakeTableName(aliasBase, ++anonymousIndex, builderContext);\r
140                         } while (FindExpressionsByName(tableExpression.Alias, builderContext).Count != 1);\r
141                     }\r
142                 }\r
143             }\r
144         }\r
145 \r
146         protected virtual IList<InputParameterExpression> FindParametersByName(string name, BuilderContext builderContext)\r
147         {\r
148             return (from p in builderContext.ExpressionQuery.Parameters where p.Alias == name select p).ToList();\r
149         }\r
150 \r
151         /// <summary>\r
152         /// Gives anonymous parameters a name and checks for names unicity\r
153         /// The fact of giving a nice name just helps for readability\r
154         /// </summary>\r
155         /// <param name="builderContext"></param>\r
156         protected virtual void CheckParametersAlias(BuilderContext builderContext)\r
157         {\r
158             foreach (var externalParameterExpression in builderContext.ExpressionQuery.Parameters)\r
159             {\r
160                 if (string.IsNullOrEmpty(externalParameterExpression.Alias)\r
161                     || FindParametersByName(externalParameterExpression.Alias, builderContext).Count > 1)\r
162                 {\r
163                     int anonymousIndex = 0;\r
164                     var aliasBase = externalParameterExpression.Alias;\r
165                     // we try to assign one until we have a unique alias\r
166                     do\r
167                     {\r
168                         externalParameterExpression.Alias = MakeTableName(aliasBase, ++anonymousIndex, builderContext);\r
169                     } while (FindParametersByName(externalParameterExpression.Alias, builderContext).Count > 1);\r
170                 }\r
171             }\r
172         }\r
173 \r
174         /// <summary>\r
175         /// Builds and chains the provided Expressions\r
176         /// </summary>\r
177         /// <param name="expressions"></param>\r
178         /// <param name="builderContext"></param>\r
179         protected virtual void BuildExpressionQuery(ExpressionChain expressions, BuilderContext builderContext)\r
180         {\r
181             var previousExpression = ExpressionDispatcher.CreateTableExpression(expressions.Expressions[0], builderContext);\r
182             previousExpression = BuildExpressionQuery(expressions, previousExpression, builderContext);\r
183             BuildOffsetsAndLimits(builderContext);\r
184             // then prepare Parts for SQL translation\r
185             PrepareSqlOperands(builderContext);\r
186             // now, we optimize anything we can\r
187             OptimizeQuery(builderContext);\r
188             // finally, compile our object creation method\r
189             CompileRowCreator(builderContext);\r
190             // in the very end, we keep the SELECT clause\r
191             builderContext.ExpressionQuery.Select = builderContext.CurrentSelect;\r
192         }\r
193 \r
194         /// <summary>\r
195         /// Builds the ExpressionQuery main Expression, given a Table (or projection) expression\r
196         /// </summary>\r
197         /// <param name="expressions"></param>\r
198         /// <param name="tableExpression"></param>\r
199         /// <param name="builderContext"></param>\r
200         /// <returns></returns>\r
201         protected Expression BuildExpressionQuery(ExpressionChain expressions, Expression tableExpression, BuilderContext builderContext)\r
202         {\r
203             var last = expressions.Last();\r
204             foreach (var expression in expressions)\r
205             {\r
206                 if (expression == last)\r
207                     builderContext.IsExternalInExpressionChain = true;\r
208 \r
209                 // write full debug\r
210 #if DEBUG && !MONO_STRICT\r
211                 var log = builderContext.QueryContext.DataContext.Log;\r
212                 if (log != null)\r
213                     log.WriteExpression(expression);\r
214 #endif\r
215                 // Convert linq Expressions to QueryOperationExpressions and QueryConstantExpressions \r
216                 // Query expressions language identification\r
217                 var currentExpression = ExpressionLanguageParser.Parse(expression, builderContext);\r
218                 // Query expressions query identification \r
219                 currentExpression = ExpressionDispatcher.Analyze(currentExpression, tableExpression, builderContext);\r
220 \r
221                 if(! builderContext.IsExternalInExpressionChain)\r
222                 {\r
223                     EntitySetExpression setExpression = currentExpression as EntitySetExpression;\r
224                     if (setExpression != null)\r
225                         currentExpression = setExpression.TableExpression;\r
226                 }\r
227                 tableExpression = currentExpression;\r
228             }\r
229             ExpressionDispatcher.BuildSelect(tableExpression, builderContext);\r
230             return tableExpression;\r
231         }\r
232 \r
233         public virtual SelectExpression BuildSelectExpression(ExpressionChain expressions, Expression tableExpression, BuilderContext builderContext)\r
234         {\r
235             BuildExpressionQuery(expressions, tableExpression, builderContext);\r
236             return builderContext.CurrentSelect;\r
237         }\r
238 \r
239         /// <summary>\r
240         /// This is a hint for SQL generations\r
241         /// </summary>\r
242         /// <param name="builderContext"></param>\r
243         protected virtual void BuildOffsetsAndLimits(BuilderContext builderContext)\r
244         {\r
245             foreach (var selectExpression in builderContext.SelectExpressions)\r
246             {\r
247                 if (selectExpression.Offset != null && selectExpression.Limit != null)\r
248                 {\r
249                     selectExpression.OffsetAndLimit = Expression.Add(selectExpression.Offset, selectExpression.Limit);\r
250                 }\r
251             }\r
252         }\r
253 \r
254         /// <summary>\r
255         /// Builds the delegate to create a row\r
256         /// </summary>\r
257         /// <param name="builderContext"></param>\r
258         protected virtual void CompileRowCreator(BuilderContext builderContext)\r
259         {\r
260             var reader = builderContext.CurrentSelect.Reader;\r
261             reader = (LambdaExpression)SpecialExpressionTranslator.Translate(reader);\r
262             reader = (LambdaExpression)ExpressionOptimizer.Optimize(reader, builderContext);\r
263             builderContext.ExpressionQuery.RowObjectCreator = reader.Compile();\r
264         }\r
265 \r
266         /// <summary>\r
267         /// Prepares SELECT operands to help SQL transaltion\r
268         /// </summary>\r
269         /// <param name="builderContext"></param>\r
270         protected virtual void PrepareSqlOperands(BuilderContext builderContext)\r
271         {\r
272             ProcessExpressions(PrequelAnalyzer.Analyze, true, builderContext);\r
273         }\r
274 \r
275         /// <summary>\r
276         /// Processes all expressions in query, with the option to process only SQL targetting expressions\r
277         /// This method is generic, it receives a delegate which does the real processing\r
278         /// </summary>\r
279         /// <param name="processor"></param>\r
280         /// <param name="processOnlySqlParts"></param>\r
281         /// <param name="builderContext"></param>\r
282         protected virtual void ProcessExpressions(Func<Expression, BuilderContext, Expression> processor,\r
283                                                   bool processOnlySqlParts, BuilderContext builderContext)\r
284         {\r
285             for (int scopeExpressionIndex = 0; scopeExpressionIndex < builderContext.SelectExpressions.Count; scopeExpressionIndex++)\r
286             {\r
287                 // no need to process the select itself here, all ScopeExpressions that are operands are processed as operands\r
288                 // and the main ScopeExpression (the SELECT) is processed below\r
289                 var scopeExpression = builderContext.SelectExpressions[scopeExpressionIndex];\r
290 \r
291                 // where clauses\r
292                 List<int> whereToRemove = new List<int>(); // List of where clausole evaluating TRUE (could be ignored and so removed)\r
293                 bool falseWhere = false; // true when the full where evaluate to FALSE\r
294                 for (int whereIndex = 0; whereIndex < scopeExpression.Where.Count; whereIndex++)\r
295                 {\r
296                     Expression whereClausole = processor(scopeExpression.Where[whereIndex], builderContext);\r
297                     ConstantExpression constantWhereClausole = whereClausole as ConstantExpression;\r
298                     if (constantWhereClausole != null)\r
299                     {\r
300                         if (constantWhereClausole.Value.Equals(false))\r
301                         {\r
302                             falseWhere = true;\r
303                             break;\r
304                         }\r
305                         else if (constantWhereClausole.Value.Equals(true))\r
306                         {\r
307                             whereToRemove.Add(whereIndex);\r
308                             continue;\r
309                         }\r
310                     }\r
311                     scopeExpression.Where[whereIndex] = whereClausole;\r
312                 }\r
313                 if (scopeExpression.Where.Count > 0)\r
314                 {\r
315                     if (falseWhere)\r
316                     {\r
317                         scopeExpression.Where.Clear();\r
318                         scopeExpression.Where.Add(Expression.Equal(Expression.Constant(true), Expression.Constant(false)));\r
319                     }\r
320                     else\r
321                         foreach (int whereIndex in whereToRemove)\r
322                             scopeExpression.Where.RemoveAt(whereIndex);\r
323                 }\r
324 \r
325                 // limit clauses\r
326                 if (scopeExpression.Offset != null)\r
327                     scopeExpression.Offset = processor(scopeExpression.Offset, builderContext);\r
328                 if (scopeExpression.Limit != null)\r
329                     scopeExpression.Limit = processor(scopeExpression.Limit, builderContext);\r
330                 if (scopeExpression.OffsetAndLimit != null)\r
331                     scopeExpression.OffsetAndLimit = processor(scopeExpression.OffsetAndLimit, builderContext);\r
332 \r
333                 builderContext.SelectExpressions[scopeExpressionIndex] = scopeExpression;\r
334             }\r
335             // now process the main SELECT\r
336             if (processOnlySqlParts)\r
337             {\r
338                 // if we process only the SQL Parts, these are the operands\r
339                 var newOperands = new List<Expression>();\r
340                 foreach (var operand in builderContext.CurrentSelect.Operands)\r
341                     newOperands.Add(processor(operand, builderContext));\r
342                 builderContext.CurrentSelect = builderContext.CurrentSelect.ChangeOperands(newOperands);\r
343             }\r
344             else\r
345             {\r
346                 // the output parameters and result builder\r
347                 builderContext.CurrentSelect = (SelectExpression)processor(builderContext.CurrentSelect, builderContext);\r
348             }\r
349         }\r
350 \r
351         /// <summary>\r
352         /// Optimizes the query by optimizing subexpressions, and preparsing constant expressions\r
353         /// </summary>\r
354         /// <param name="builderContext"></param>\r
355         protected virtual void OptimizeQuery(BuilderContext builderContext)\r
356         {\r
357             ProcessExpressions(ExpressionOptimizer.Optimize, false, builderContext);\r
358         }\r
359 \r
360         protected virtual SelectQuery BuildSqlQuery(ExpressionQuery expressionQuery, QueryContext queryContext)\r
361         {\r
362             var sql = SqlBuilder.BuildSelect(expressionQuery, queryContext);\r
363             var sqlQuery = new SelectQuery(queryContext.DataContext, sql, expressionQuery.Parameters, expressionQuery.RowObjectCreator, expressionQuery.Select.ExecuteMethodName);\r
364             return sqlQuery;\r
365         }\r
366 \r
367         private static IQueryCache queryCache;\r
368         protected IQueryCache QueryCache\r
369         {\r
370             get\r
371             {\r
372                 if (queryCache == null)\r
373                     queryCache = ObjectFactory.Get<IQueryCache>();\r
374                 return queryCache;\r
375             }\r
376         }\r
377 \r
378         protected virtual SelectQuery GetFromSelectCache(ExpressionChain expressions)\r
379         {\r
380             var cache = QueryCache;\r
381             return cache.GetFromSelectCache(expressions);\r
382         }\r
383 \r
384         protected virtual void SetInSelectCache(ExpressionChain expressions, SelectQuery sqlSelectQuery)\r
385         {\r
386             var cache = QueryCache;\r
387             cache.SetInSelectCache(expressions, sqlSelectQuery);\r
388         }\r
389 \r
390         protected virtual Delegate GetFromTableReaderCache(Type tableType, IList<string> columns)\r
391         {\r
392             var cache = QueryCache;\r
393             return cache.GetFromTableReaderCache(tableType, columns);\r
394         }\r
395 \r
396         protected virtual void SetInTableReaderCache(Type tableType, IList<string> columns, Delegate tableReader)\r
397         {\r
398             var cache = queryCache;\r
399             cache.SetInTableReaderCache(tableType, columns, tableReader);\r
400         }\r
401 \r
402         /// <summary>\r
403         /// Main entry point for the class. Builds or retrive from cache a SQL query corresponding to given Expressions\r
404         /// </summary>\r
405         /// <param name="expressions"></param>\r
406         /// <param name="queryContext"></param>\r
407         /// <returns></returns>\r
408         public SelectQuery GetSelectQuery(ExpressionChain expressions, QueryContext queryContext)\r
409         {\r
410             SelectQuery query = null;\r
411             if (queryContext.DataContext.QueryCacheEnabled)\r
412             {\r
413                 query = GetFromSelectCache(expressions);\r
414             }\r
415             if (query == null)\r
416             {\r
417                 Profiler.At("START: GetSelectQuery(), building Expression query");\r
418                 var expressionsQuery = BuildExpressionQuery(expressions, queryContext);\r
419                 Profiler.At("END: GetSelectQuery(), building Expression query");\r
420 \r
421                 Profiler.At("START: GetSelectQuery(), building Sql query");\r
422                 query = BuildSqlQuery(expressionsQuery, queryContext);\r
423                 Profiler.At("END: GetSelectQuery(), building Sql query");\r
424 \r
425                 if (queryContext.DataContext.QueryCacheEnabled)\r
426                 {\r
427                     SetInSelectCache(expressions, query);\r
428                 }\r
429             }\r
430             return query;\r
431         }\r
432 \r
433         /// <summary>\r
434         /// Returns a Delegate to create a row for a given IDataRecord\r
435         /// The Delegate is Func&lt;IDataRecord,MappingContext,"tableType">\r
436         /// </summary>\r
437         /// <param name="tableType">The table type (must be managed by DataContext)</param>\r
438         /// <param name="parameters"></param>\r
439         /// <param name="queryContext"></param>\r
440         /// <returns></returns>\r
441         public virtual Delegate GetTableReader(Type tableType, IList<string> parameters, QueryContext queryContext)\r
442         {\r
443             Delegate reader = null;\r
444             if (queryContext.DataContext.QueryCacheEnabled)\r
445             {\r
446                 reader = GetFromTableReaderCache(tableType, parameters);\r
447             }\r
448             if (reader == null)\r
449             {\r
450                 var lambda = ExpressionDispatcher.BuildTableReader(tableType, parameters,\r
451                                                                    new BuilderContext(queryContext));\r
452                 reader = lambda.Compile();\r
453                 if (queryContext.DataContext.QueryCacheEnabled)\r
454                 {\r
455                     SetInTableReaderCache(tableType, parameters, reader);\r
456                 }\r
457             }\r
458             return reader;\r
459         }\r
460 \r
461         private static readonly Regex parameterIdentifierEx = new Regex(@"\{(?<var>[\d.]+)\}", RegexOptions.Singleline | RegexOptions.ExplicitCapture | RegexOptions.Compiled);\r
462 \r
463         /// <summary>\r
464         /// Converts a direct SQL query to a safe query with named parameters\r
465         /// </summary>\r
466         /// <param name="sql">Raw SQL query</param>\r
467         /// <param name="queryContext"></param>\r
468         /// <returns></returns>\r
469         public virtual DirectQuery GetDirectQuery(string sql, QueryContext queryContext)\r
470         {\r
471             // TODO cache\r
472             var safeSql = queryContext.DataContext.Vendor.SqlProvider.GetSafeQuery(sql);\r
473             var parameters = new List<string>();\r
474             var parameterizedSql = parameterIdentifierEx.Replace(safeSql, delegate(Match e)\r
475             {\r
476                 var field = e.Groups[1].Value;\r
477                 var parameterIndex = int.Parse(field);\r
478                 while (parameters.Count <= parameterIndex)\r
479                     parameters.Add(string.Empty);\r
480                 var literalParameterName =\r
481                     queryContext.DataContext.Vendor.SqlProvider.GetParameterName(string.Format("p{0}", parameterIndex));\r
482                 parameters[parameterIndex] = literalParameterName;\r
483                 return literalParameterName;\r
484             });\r
485             return new DirectQuery(queryContext.DataContext, parameterizedSql, parameters);\r
486         }\r
487     }\r
488 }\r