You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
12 KiB
417 lines
12 KiB
// <file> |
|
// <copyright see="prj:///doc/copyright.txt"/> |
|
// <license see="prj:///doc/license.txt"/> |
|
// <owner name="Daniel Grunwald"/> |
|
// <version>$Revision$</version> |
|
// </file> |
|
|
|
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 |
|
{ |
|
/// <summary> |
|
/// The SingleCall class is never instanciated; it only used to represent database rows |
|
/// inside expression trees. |
|
/// </summary> |
|
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"); |
|
} |
|
|
|
/// <summary> |
|
/// Base class for nodes in the Query AST. |
|
/// </summary> |
|
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<CallTreeNode>); |
|
} |
|
|
|
protected abstract override Expression VisitChildren(Func<Expression, Expression> visitor); |
|
|
|
/// <summary> |
|
/// SQL construction documentation see SQLiteQueryProvider documentation. |
|
/// </summary> |
|
public abstract SqlStatementKind BuildSql(StringBuilder b, SqlQueryContext context); |
|
|
|
/// <summary> |
|
/// Wraps the current SQL statement into an inner select, allowing to continue with "WHERE" queries |
|
/// even after ORDER BY or LIMIT. |
|
/// </summary> |
|
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; |
|
} |
|
|
|
/// <summary> |
|
/// Helper function that builds the text 'expression AS newName' |
|
/// </summary> |
|
protected static string SqlAs(string expression, string newName) |
|
{ |
|
if (expression == newName) |
|
return newName; |
|
else |
|
return expression + " AS " + newName; |
|
} |
|
|
|
public IQueryable<CallTreeNode> 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<CallTreeNode> result = provider.RunSQLNodeList(b.ToString()); |
|
w.Stop(); |
|
if (options.HasLoggers) { |
|
options.WriteLogLine("Query returned " + result.Count + " rows in " + w.Elapsed); |
|
} |
|
return result.AsQueryable(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Query AST node representing the whole 'FunctionData' table. |
|
/// This is the source of all queries. |
|
/// Produces SELECT .. FROM .. in SQL. |
|
/// </summary> |
|
sealed class AllCalls : QueryNode |
|
{ |
|
public static readonly AllCalls Instance = new AllCalls(); |
|
|
|
private AllCalls() : base(null) |
|
{ |
|
} |
|
|
|
protected override Expression VisitChildren(Func<Expression, Expression> 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; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Query node that represents the 'group by nameid and merge' operation. Produces SELECT ... GROUP BY .. in SQL. |
|
/// </summary> |
|
sealed class MergeByName : QueryNode |
|
{ |
|
public MergeByName(QueryNode target) |
|
: base(target) |
|
{ |
|
Debug.Assert(target != null); |
|
} |
|
|
|
protected override Expression VisitChildren(Func<Expression, Expression> 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; |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Query node that filters the input using conditions. Produces WHERE or HAVING in SQL. |
|
/// </summary> |
|
sealed class Filter : QueryNode |
|
{ |
|
/// <summary> |
|
/// List of conditions. The operator between these is AND. |
|
/// </summary> |
|
public readonly ReadOnlyCollection<LambdaExpression> 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<Expression, Expression> 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()); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Query node that limits the amount of data selected. Produces LIMIT in SQL. |
|
/// </summary> |
|
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<Expression, Expression> 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<SortArgument> arguments; |
|
|
|
public Sort(QueryNode target, LambdaExpression argument, bool desc) |
|
: this(target, new[] { new SortArgument(argument, desc) }) |
|
{ |
|
} |
|
|
|
Sort(QueryNode target, IList<SortArgument> args) |
|
: base(target) |
|
{ |
|
this.arguments = new ReadOnlyCollection<SortArgument>(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<Expression, Expression> 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() + "} )"; |
|
} |
|
} |
|
}
|
|
|