// // // // // $Revision$ // using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Text; namespace ICSharpCode.Profiler.Controller.Data.Linq { /// /// The SingleCall class is never instanciated; it only used to represent database rows /// inside expression trees. /// abstract class SingleCall { // Warning CS0649: Field is never assigned to, and will always have its default value 0 #pragma warning disable 649 public int ID; public int ParentID; public int DataSetID; #pragma warning restore 649 public static readonly FieldInfo IDField = typeof(SingleCall).GetField("ID"); public static readonly FieldInfo ParentIDField = typeof(SingleCall).GetField("ParentID"); public static readonly FieldInfo DataSetIdField = typeof(SingleCall).GetField("DataSetID"); } /// /// Base class for nodes in the Query AST. /// abstract class QueryNode : Expression { public enum SqlStatementKind { Select, SelectWhere, SelectGroupBy, SelectGroupByHaving, SelectOrderBy, SelectLimit } public readonly QueryNode Target; protected QueryNode(QueryNode target) { this.Target = target; } protected override ExpressionType NodeTypeImpl() { return ExpressionType.Extension; } protected override Type TypeImpl() { return typeof(IQueryable); } protected abstract override Expression VisitChildren(Func visitor); /// /// SQL construction documentation see SQLiteQueryProvider documentation. /// public abstract SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context); /// /// Wraps the current SQL statement into an inner select, allowing to continue with "WHERE" queries /// even after ORDER BY or LIMIT. /// protected static void WrapSqlIntoNestedStatement(StringBuilder b, SqlQueryContext context) { CallTreeNodeSqlNameSet oldNames = context.CurrentNameSet; CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context, false); b.Insert(0, "SELECT " + SqlAs(oldNames.ID, newNames.ID) + ", " + SqlAs(oldNames.NameID, newNames.NameID) + ", " + SqlAs(oldNames.CpuCyclesSpent, newNames.CpuCyclesSpent) + ", " + SqlAs(oldNames.CallCount, newNames.CallCount) + ", " + SqlAs(oldNames.HasChildren, newNames.HasChildren) + ", " + SqlAs(oldNames.ActiveCallCount, newNames.ActiveCallCount) + Environment.NewLine + "FROM (" + Environment.NewLine); b.AppendLine(")"); context.CurrentNameSet = newNames; } /// /// Helper function that builds the text 'expression AS newName' /// protected static string SqlAs(string expression, string newName) { if (expression == newName) return newName; else return expression + " AS " + newName; } public IQueryable Execute(SQLiteQueryProvider provider, QueryExecutionOptions options) { StringBuilder b = new StringBuilder(); BuildSql(b, new SqlQueryContext(provider)); if (options.HasLoggers) options.WriteLogLine(b.ToString()); Stopwatch w = Stopwatch.StartNew(); IList result = provider.RunSQLNodeList(b.ToString()); w.Stop(); if (options.HasLoggers) { options.WriteLogLine("Query returned " + result.Count + " rows in " + w.Elapsed); } return result.AsQueryable(); } } /// /// Query AST node representing the whole 'FunctionData' table. /// This is the source of all queries. /// Produces SELECT .. FROM .. in SQL. /// sealed class AllCalls : QueryNode { public static readonly AllCalls Instance = new AllCalls(); private AllCalls() : base(null) { } protected override Expression VisitChildren(Func visitor) { return this; } public override string ToString() { return "AllCalls"; } public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context) { CallTreeNodeSqlNameSet newNames = context.CurrentNameSet = new CallTreeNodeSqlNameSet(context, true); b.AppendLine("SELECT " + SqlAs("id", newNames.ID) + ", " + SqlAs("nameid", newNames.NameID) + ", " + SqlAs("timespent", newNames.CpuCyclesSpent) + ", " // TODO : change to CpuCyclesSpent + SqlAs("callcount", newNames.CallCount) + ", " + SqlAs("(id != endid)", newNames.HasChildren) + ", " + SqlAs("((datasetid = " + context.StartDataSetID + ") AND isActiveAtStart)", newNames.ActiveCallCount)); b.AppendLine("FROM FunctionData"); return SqlStatementKind.Select; } } /// /// Query node that represents the 'group by nameid and merge' operation. Produces SELECT ... GROUP BY .. in SQL. /// sealed class MergeByName : QueryNode { public MergeByName(QueryNode target) : base(target) { Debug.Assert(target != null); } protected override Expression VisitChildren(Func visitor) { QueryNode newTarget = (QueryNode)visitor(Target); if (newTarget == Target) return this; else return new MergeByName(newTarget); } public override string ToString() { return Target + ".MergeByName()"; } public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context) { Target.BuildSql(b, context); CallTreeNodeSqlNameSet oldNames = context.CurrentNameSet; CallTreeNodeSqlNameSet newNames = new CallTreeNodeSqlNameSet(context, false); b.Insert(0, "SELECT " + SqlAs("GROUP_CONCAT(" + oldNames.ID + ")", newNames.ID) + ", " + SqlAs(oldNames.NameID, newNames.NameID) + ", " + SqlAs("SUM(" + oldNames.CpuCyclesSpent + ")", newNames.CpuCyclesSpent) + ", " + SqlAs("SUM(" + oldNames.CallCount + ")", newNames.CallCount) + ", " + SqlAs("MAX(" + oldNames.HasChildren + ")", newNames.HasChildren) + ", " + SqlAs("SUM(" + oldNames.ActiveCallCount + ")", newNames.ActiveCallCount) + Environment.NewLine + "FROM (" + Environment.NewLine); b.AppendLine(") GROUP BY " + oldNames.NameID); context.CurrentNameSet = newNames; return SqlStatementKind.SelectGroupBy; } } /// /// Query node that filters the input using conditions. Produces WHERE or HAVING in SQL. /// sealed class Filter : QueryNode { /// /// List of conditions. The operator between these is AND. /// public readonly ReadOnlyCollection Conditions; public Filter(QueryNode target, params LambdaExpression[] conditions) : base(target) { Debug.Assert(target != null); foreach (LambdaExpression l in conditions) { Debug.Assert(l.Parameters.Count == 1); } this.Conditions = Array.AsReadOnly(conditions); } protected override Expression VisitChildren(Func visitor) { QueryNode newTarget = (QueryNode)visitor(Target); LambdaExpression[] newConditions = new LambdaExpression[Conditions.Count]; bool unchanged = (newTarget == Target); for (int i = 0; i < newConditions.Length; i++) { newConditions[i] = (LambdaExpression)visitor(Conditions[i]); unchanged &= newConditions[i] == Conditions[i]; } if (unchanged) return this; else return new Filter(newTarget, newConditions); } public override string ToString() { StringBuilder b = new StringBuilder(); b.Append(Target.ToString()); b.Append(".Filter("); for (int i = 0; i < Conditions.Count; i++) { if (i > 0) b.Append(" && "); b.Append(Conditions[i].ToString()); } b.Append(')'); return b.ToString(); } public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context) { SqlStatementKind kind = Target.BuildSql(b, context); SqlStatementKind resultKind; switch (kind) { case SqlStatementKind.Select: b.Append(" WHERE "); resultKind = SqlStatementKind.SelectWhere; break; case SqlStatementKind.SelectGroupBy: b.Append(" HAVING "); resultKind = SqlStatementKind.SelectGroupByHaving; break; default: WrapSqlIntoNestedStatement(b, context); b.Append(" WHERE "); resultKind = SqlStatementKind.SelectWhere; break; } for (int i = 0; i < Conditions.Count; i++) { if (i > 0) b.Append(" AND "); BuildSqlForCondition(b, context, Conditions[i]); } b.AppendLine(); return resultKind; } static void BuildSqlForCondition(StringBuilder b, SqlQueryContext context, LambdaExpression condition) { Debug.Assert(condition.Parameters.Count == 1); StringWriter w = new StringWriter(CultureInfo.InvariantCulture); ExpressionSqlWriter writer = new ExpressionSqlWriter(w, context.CurrentNameSet, condition.Parameters[0]); writer.Write(condition.Body); b.Append(w.ToString()); } } /// /// Query node that limits the amount of data selected. Produces LIMIT in SQL. /// sealed class Limit : QueryNode { public int Start { get; private set; } public int Length { get; private set; } public Limit(QueryNode target, int start, int length) : base(target) { this.Start = start; this.Length = length; } protected override Expression VisitChildren(Func visitor) { QueryNode newTarget = (QueryNode)visitor(Target); if (newTarget == Target) return this; else return new Limit(newTarget, Start, Length); } public override string ToString() { return Target + ".Limit(" + Start + "," + Length + ")"; } public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context) { SqlStatementKind kind = Target.BuildSql(b, context); if (kind == SqlStatementKind.SelectLimit) WrapSqlIntoNestedStatement(b, context); b.Append(" LIMIT " + Length); b.AppendLine(" OFFSET " + Start); return SqlStatementKind.SelectLimit; } } struct SortArgument { LambdaExpression arg; bool desc; public SortArgument(LambdaExpression arg, bool desc) { this.arg = arg; this.desc = desc; } public bool Descending { get { return desc; } } public LambdaExpression Argument { get { return arg; } } } sealed class Sort : QueryNode { ReadOnlyCollection arguments; public Sort(QueryNode target, LambdaExpression argument, bool desc) : this(target, new[] { new SortArgument(argument, desc) }) { } Sort(QueryNode target, IList args) : base(target) { this.arguments = new ReadOnlyCollection(args); } public override SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context) { SqlStatementKind kind = Target.BuildSql(b, context); if (kind == SqlStatementKind.SelectOrderBy) WrapSqlIntoNestedStatement(b, context); b.Append(" ORDER BY "); for (int i = 0; i < arguments.Count; i++) { StringWriter w = new StringWriter(); ExpressionSqlWriter writer = new ExpressionSqlWriter(w, context.CurrentNameSet, arguments[i].Argument.Parameters[0]); writer.Write(arguments[i].Argument.Body); if (i == 0) b.Append(w.ToString()); else b.Append(", " + w.ToString()); if (arguments[i].Descending) b.Append(" DESC"); else b.Append(" ASC"); } return SqlStatementKind.SelectOrderBy; } protected override Expression VisitChildren(Func visitor) { QueryNode newTarget = (QueryNode)visitor(Target); if (newTarget == Target) return this; else return new Sort(newTarget, arguments); } public override string ToString() { StringBuilder builder = new StringBuilder(); foreach (var item in arguments) builder.Append(item.Argument + " " + (item.Descending ? "DESC" : "ASC") + ","); return Target + ".Sort( {" + builder.ToString() + "} )"; } } }