chore: initial git load of code space
This commit is contained in:
@@ -0,0 +1,671 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Arithmetic;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Comparisons;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Conditional.Logical;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Aggregate;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Functions.Conditional;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions.Literals;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.Core;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Generates markdown documentation for Expression trees.
|
||||
/// Creates human-readable documentation with expression structure, type information, and visual representations.
|
||||
/// </summary>
|
||||
public class ExpressionGenerator : IVisitor<string>
|
||||
{
|
||||
private int _indentLevel = 0;
|
||||
private readonly string _indentString = " ";
|
||||
|
||||
/// <summary>
|
||||
/// Generates markdown documentation from an Expression tree.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to document.</param>
|
||||
/// <param name="title">Optional title for the documentation.</param>
|
||||
/// <returns>A markdown formatted string documenting the expression.</returns>
|
||||
public string GenerateMarkdown(Expression expression, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("## Expression Structure");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("```");
|
||||
_indentLevel = 0;
|
||||
sb.AppendLine(expression.Accept(this));
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Expression Type");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"**Type:** `{expression.GetType().Name}`");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Mermaid Diagram");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(GenerateMermaidDiagram(expression));
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## Mathematical Expression");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(GenerateMathematicalExpression(expression));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mathematical expression using LaTeX notation for GitHub markdown.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to convert to mathematical notation.</param>
|
||||
/// <param name="inline">If true, generates inline math ($...$), otherwise block math ($$...$$).</param>
|
||||
/// <returns>A string containing the LaTeX mathematical expression.</returns>
|
||||
public static string GenerateMathematicalExpression(Expression expression, bool inline = false)
|
||||
{
|
||||
var latex = ConvertToLatex(expression);
|
||||
return inline ? $"${latex}$" : $"$$\n{latex}\n$$";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a mathematical expression from raw LaTeX in markdown format.
|
||||
/// </summary>
|
||||
/// <param name="latex">The LaTeX expression.</param>
|
||||
/// <param name="format">The format to use: "dollar" for $/$$ delimiters, "math" for ```math code fence.</param>
|
||||
/// <param name="inline">If true and format is "dollar", generates inline math ($...$), otherwise block math ($$...$$). Ignored for "math" format.</param>
|
||||
/// <returns>A string containing the formatted mathematical expression.</returns>
|
||||
public static string GenerateRawMathematicalExpression(string latex, string format = "dollar", bool inline = false)
|
||||
{
|
||||
return format.ToLower() switch
|
||||
{
|
||||
"math" => $"```math\n{latex}\n```",
|
||||
_ => inline ? $"${latex}$" : $"$$\n{latex}\n$$"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConvertToLatex(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
// Arithmetic expressions
|
||||
ArithmeticExpression arith => ConvertArithmeticToLatex(arith),
|
||||
|
||||
// Comparison expressions
|
||||
ComparisonOperatorExpression comp => ConvertComparisonToLatex(comp),
|
||||
|
||||
// Logical expressions
|
||||
AndExpression and => $"({ConvertToLatex(and.ExpressionA)} \\land {ConvertToLatex(and.ExpressionB)})",
|
||||
OrExpression or => $"({ConvertToLatex(or.ExpressionA)} \\lor {ConvertToLatex(or.ExpressionB)})",
|
||||
NotExpression not => $"\\neg({ConvertToLatex(not.ExpressionA)})",
|
||||
|
||||
// Literals
|
||||
NumberLiteralExpression num => num.Value.ToString() ?? "0",
|
||||
StringLiteralExpression str => $"\\text{{\"{EscapeLatex(str.Value)}\"}}",
|
||||
BooleanLiteralExpression b => b.Value ? "\\text{true}" : "\\text{false}",
|
||||
NullLiteralExpression => "\\text{NULL}",
|
||||
|
||||
// Column expressions
|
||||
ColumnExpression col => $"\\text{{{EscapeLatex(col.ColumnName)}}}",
|
||||
|
||||
// Parameter expressions
|
||||
ParameterExpression param => $"@{EscapeLatex(param.ParameterName)}",
|
||||
|
||||
// Case expressions (before FunctionExpression since it's a subclass)
|
||||
CaseExpression caseExpr => ConvertCaseToLatex(caseExpr),
|
||||
|
||||
// Functions
|
||||
FunctionExpression func => ConvertFunctionToLatex(func),
|
||||
|
||||
// Between expressions
|
||||
BetweenExpression between => $"{ConvertToLatex(between.Expression)} \\in [{ConvertToLatex(between.LowerBound)}, {ConvertToLatex(between.UpperBound)}]",
|
||||
|
||||
// IN expressions
|
||||
InExpression inExpr => $"{ConvertToLatex(inExpr.SearchExpression)} \\in \\{{{string.Join(", ", inExpr.ValuesToCompare.Select(ConvertToLatex))}\\}}",
|
||||
|
||||
// LIKE expressions
|
||||
LikeExpression like => $"{ConvertToLatex(like.Subject)} \\approx \\text{{\"{EscapeLatex(ConvertExpressionToString(like.Pattern))}\"}}",
|
||||
_ => $"\\text{{{EscapeLatex(expression.GetType().Name)}}}"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConvertArithmeticToLatex(ArithmeticExpression arith)
|
||||
{
|
||||
var left = ConvertToLatex(arith.ExpressionA);
|
||||
var right = ConvertToLatex(arith.ExpressionB);
|
||||
|
||||
var op = arith.ArithmeticOperator switch
|
||||
{
|
||||
"+" => "+",
|
||||
"-" => "-",
|
||||
"*" => "\\times",
|
||||
"/" => "\\div",
|
||||
"%" => "\\bmod",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
return $"({left} {op} {right})";
|
||||
}
|
||||
|
||||
private static string ConvertComparisonToLatex(ComparisonOperatorExpression comp)
|
||||
{
|
||||
var left = ConvertToLatex(comp.ExpressionA);
|
||||
var right = ConvertToLatex(comp.ExpressionB);
|
||||
|
||||
var op = comp.Operator switch
|
||||
{
|
||||
"=" => "=",
|
||||
"<>" => "\\neq",
|
||||
"!=" => "\\neq",
|
||||
">" => ">",
|
||||
">=" => "\\geq",
|
||||
"<" => "<",
|
||||
"<=" => "\\leq",
|
||||
_ => "?"
|
||||
};
|
||||
|
||||
return $"({left} {op} {right})";
|
||||
}
|
||||
|
||||
private static string ConvertFunctionToLatex(FunctionExpression func)
|
||||
{
|
||||
var args = string.Join(", ", func.Arguments.Select(ConvertToLatex));
|
||||
var funcName = EscapeLatex(func.FunctionName);
|
||||
|
||||
return func.FunctionName.ToUpper() switch
|
||||
{
|
||||
// Aggregate functions
|
||||
"COUNT" => $"\\text{{COUNT}}({args})",
|
||||
"SUM" => $"\\sum({args})",
|
||||
"AVG" => $"\\text{{AVG}}({args})",
|
||||
"MIN" => $"\\min({args})",
|
||||
"MAX" => $"\\max({args})",
|
||||
|
||||
// Math functions
|
||||
"ABS" => $"|{args}|",
|
||||
"SQRT" => $"\\sqrt{{{args}}}",
|
||||
"POWER" when func.Arguments.Length >= 2 =>
|
||||
$"{ConvertToLatex(func.Arguments[0])}^{{{ConvertToLatex(func.Arguments[1])}}}",
|
||||
"LOG" => $"\\log({args})",
|
||||
"EXP" => $"e^{{{args}}}",
|
||||
|
||||
// Default
|
||||
_ => $"\\text{{{funcName}}}({args})"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ConvertCaseToLatex(CaseExpression caseExpr)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("\\begin{cases}\n");
|
||||
|
||||
foreach (var (condition, result) in caseExpr.ConditionResultPairs)
|
||||
{
|
||||
sb.Append($" {ConvertToLatex(result)} & \\text{{if }} {ConvertToLatex(condition)} \\\\\n");
|
||||
}
|
||||
|
||||
if (caseExpr.ElseResultExpression is not null)
|
||||
{
|
||||
sb.Append($" {ConvertToLatex(caseExpr.ElseResultExpression)} & \\text{{otherwise}}\n");
|
||||
}
|
||||
|
||||
sb.Append("\\end{cases}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string ConvertExpressionToString(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
StringLiteralExpression str => str.Value,
|
||||
_ => expression.ToString() ?? ""
|
||||
};
|
||||
}
|
||||
|
||||
private static string EscapeLatex(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("{", "\\{")
|
||||
.Replace("}", "\\}")
|
||||
.Replace("_", "\\_")
|
||||
.Replace("$", "\\$")
|
||||
.Replace("%", "\\%")
|
||||
.Replace("&", "\\&")
|
||||
.Replace("#", "\\#");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid tree diagram from an Expression tree.
|
||||
/// </summary>
|
||||
private string GenerateMermaidDiagram(Expression expression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeCounter = 0;
|
||||
var nodeMap = new Dictionary<object, int>();
|
||||
GenerateMermaidNodes(expression, sb, nodeMap, ref nodeCounter);
|
||||
|
||||
sb.AppendLine("```");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private int GenerateMermaidNodes(Expression expression, StringBuilder sb, Dictionary<object, int> nodeMap, ref int nodeCounter)
|
||||
{
|
||||
var currentNode = nodeCounter++;
|
||||
nodeMap[expression] = currentNode;
|
||||
|
||||
var nodeLabel = GetNodeLabel(expression);
|
||||
var nodeShape = GetNodeShape(expression);
|
||||
|
||||
sb.AppendLine($" Node{currentNode}{nodeShape[0]}\"{EscapeMarkdown(nodeLabel)}\"{nodeShape[1]}");
|
||||
|
||||
// Process child expressions
|
||||
switch (expression)
|
||||
{
|
||||
case ComparisonOperatorExpression comp:
|
||||
var leftId = GenerateMermaidNodes(comp.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var rightId = GenerateMermaidNodes(comp.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{leftId}");
|
||||
sb.AppendLine($" Node{currentNode} --> Node{rightId}");
|
||||
break;
|
||||
|
||||
case AndExpression and:
|
||||
var andLeftId = GenerateMermaidNodes(and.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var andRightId = GenerateMermaidNodes(and.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} -->|Left| Node{andLeftId}");
|
||||
sb.AppendLine($" Node{currentNode} -->|Right| Node{andRightId}");
|
||||
break;
|
||||
|
||||
case OrExpression or:
|
||||
var orLeftId = GenerateMermaidNodes(or.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var orRightId = GenerateMermaidNodes(or.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} -->|Left| Node{orLeftId}");
|
||||
sb.AppendLine($" Node{currentNode} -->|Right| Node{orRightId}");
|
||||
break;
|
||||
|
||||
case NotExpression not:
|
||||
var notId = GenerateMermaidNodes(not.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{notId}");
|
||||
break;
|
||||
|
||||
case ArithmeticExpression arith:
|
||||
var arithmLeftId = GenerateMermaidNodes(arith.ExpressionA, sb, nodeMap, ref nodeCounter);
|
||||
var arithmRightId = GenerateMermaidNodes(arith.ExpressionB, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{arithmLeftId}");
|
||||
sb.AppendLine($" Node{currentNode} --> Node{arithmRightId}");
|
||||
break;
|
||||
|
||||
case FunctionExpression func:
|
||||
foreach (var arg in func.Arguments)
|
||||
{
|
||||
var argId = GenerateMermaidNodes(arg, sb, nodeMap, ref nodeCounter);
|
||||
sb.AppendLine($" Node{currentNode} --> Node{argId}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return currentNode;
|
||||
}
|
||||
|
||||
private static string GetNodeLabel(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
NumberLiteralExpression num => $"Number: {num.Value}",
|
||||
StringLiteralExpression str => $"String: {TruncateText(str.Value, 20)}",
|
||||
DateTimeLiteralExpression dt => $"DateTime: {dt.Value:yyyy-MM-dd}",
|
||||
BooleanLiteralExpression b => $"Boolean: {b.Value}",
|
||||
NullLiteralExpression => "NULL",
|
||||
ComparisonOperatorExpression comp => $"Comparison: {comp.Operator}",
|
||||
AndExpression => "AND",
|
||||
OrExpression => "OR",
|
||||
NotExpression => "NOT",
|
||||
ArithmeticExpression arith => $"Arithmetic: {arith.ArithmeticOperator}",
|
||||
FunctionExpression func => $"Function: {func.FunctionName}",
|
||||
ParameterExpression param => $"Parameter: @{param.ParameterName}",
|
||||
_ => expression.GetType().Name
|
||||
};
|
||||
}
|
||||
|
||||
private static string[] GetNodeShape(Expression expression)
|
||||
{
|
||||
return expression switch
|
||||
{
|
||||
NumberLiteralExpression or StringLiteralExpression or DateTimeLiteralExpression or BooleanLiteralExpression or NullLiteralExpression => new[] { "[", "]" },
|
||||
ComparisonOperatorExpression => new[] { "{", "}" },
|
||||
AndExpression or OrExpression or NotExpression => new[] { "{", "}" },
|
||||
FunctionExpression => new[] { "[[", "]]" },
|
||||
_ => new[] { "(", ")" }
|
||||
};
|
||||
}
|
||||
|
||||
private string Indent() => new string(' ', _indentLevel * _indentString.Length);
|
||||
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
.Replace("[", "[")
|
||||
.Replace("]", "]");
|
||||
}
|
||||
|
||||
private static string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
#region IVisitor Implementation
|
||||
|
||||
public string VisitTableSource(TableSource tableSource)
|
||||
{
|
||||
return $"{Indent()}TableSource: {tableSource.TableName}";
|
||||
}
|
||||
|
||||
public string VisitColumnExpression<TSource>(ColumnExpression<TSource> column) where TSource : SelectSource
|
||||
{
|
||||
return $"{Indent()}Column: {column.ColumnName}";
|
||||
}
|
||||
|
||||
public string VisitSelectClauseColumn(SelectClauseColumn selectClauseColumn)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}SelectClauseColumn:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(selectClauseColumn.Expression.Accept(this));
|
||||
if (!string.IsNullOrWhiteSpace(selectClauseColumn.Alias))
|
||||
{
|
||||
sb.AppendLine($"{Indent()}Alias: {selectClauseColumn.Alias}");
|
||||
}
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitParameterExpression(ParameterExpression parameterExpression)
|
||||
{
|
||||
return $"{Indent()}Parameter: @{parameterExpression.ParameterName}";
|
||||
}
|
||||
|
||||
public string VisitNumberLiteralExpression(NumberLiteralExpression numberLiteral)
|
||||
{
|
||||
return $"{Indent()}Number: {numberLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitStringLiteralExpression(StringLiteralExpression stringLiteral)
|
||||
{
|
||||
return $"{Indent()}String: '{stringLiteral.Value}'";
|
||||
}
|
||||
|
||||
public string VisitDateTimeLiteralExpression(DateTimeLiteralExpression dateTimeLiteral)
|
||||
{
|
||||
return $"{Indent()}DateTime: {dateTimeLiteral.Value:yyyy-MM-dd HH:mm:ss}";
|
||||
}
|
||||
|
||||
public string VisitNullLiteralExpression(NullLiteralExpression nullLiteral)
|
||||
{
|
||||
return $"{Indent()}NULL";
|
||||
}
|
||||
|
||||
public string VisitBooleanLiteralExpression(BooleanLiteralExpression booleanLiteral)
|
||||
{
|
||||
return $"{Indent()}Boolean: {booleanLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitParameterLiteralExpression(ParameterLiteralExpression parameterLiteral)
|
||||
{
|
||||
return $"{Indent()}Parameter: {parameterLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitSymbolLiteralExpression(SymbolLiteralExpression symbolLiteral)
|
||||
{
|
||||
return $"{Indent()}Symbol: {symbolLiteral.Value}";
|
||||
}
|
||||
|
||||
public string VisitComparisonExpression(ComparisonOperatorExpression comparison)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Comparison ({comparison.Operator}):");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Left:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(comparison.ExpressionA.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Right:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(comparison.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitAndExpression(AndExpression logical)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}AND:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(logical.ExpressionA.Accept(this));
|
||||
sb.AppendLine(logical.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitOrExpression(OrExpression logical)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}OR:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(logical.ExpressionA.Accept(this));
|
||||
sb.AppendLine(logical.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitNotExpression(NotExpression logical)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(logical.ExpressionA.Accept(this));
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitInExpression(InExpression inExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}IN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Search Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(inExpression.SearchExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Values:");
|
||||
_indentLevel++;
|
||||
foreach (var value in inExpression.ValuesToCompare)
|
||||
{
|
||||
sb.AppendLine(value.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitNotInExpression(NotInExpression inExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT IN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Search Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(inExpression.SearchExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Values:");
|
||||
_indentLevel++;
|
||||
foreach (var value in inExpression.ValuesToCompare)
|
||||
{
|
||||
sb.AppendLine(value.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitLikeExpression(LikeExpression likeExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}LIKE (Case {(likeExpression.CaseInsensitive ? "Insensitive" : "Sensitive")}):");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Subject:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(likeExpression.Subject.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Pattern:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(likeExpression.Pattern.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitNotLikeExpression(NotLikeExpression notLikeExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}NOT LIKE:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Subject:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(notLikeExpression.Subject.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Pattern:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(notLikeExpression.Pattern.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitBetweenExpression(BetweenExpression betweenExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}BETWEEN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Expression:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(betweenExpression.Expression.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Lower Bound:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(betweenExpression.LowerBound.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Upper Bound:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(betweenExpression.UpperBound.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitFunctionExpression(FunctionExpression function)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Function: {function.FunctionName}");
|
||||
if (function.Arguments.Any())
|
||||
{
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Arguments:");
|
||||
_indentLevel++;
|
||||
foreach (var arg in function.Arguments)
|
||||
{
|
||||
sb.AppendLine(arg.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitAggregateFunctionExpression(AggregateFunctionExpression aggregateFunction)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Aggregate Function: {aggregateFunction.FunctionName}");
|
||||
if (aggregateFunction.Arguments.Any())
|
||||
{
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Arguments:");
|
||||
_indentLevel++;
|
||||
foreach (var arg in aggregateFunction.Arguments)
|
||||
{
|
||||
sb.AppendLine(arg.Accept(this));
|
||||
}
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitCaseFunctionExpression(CaseExpression caseFunction)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}CASE:");
|
||||
_indentLevel++;
|
||||
foreach (var (condition, result) in caseFunction.ConditionResultPairs)
|
||||
{
|
||||
sb.AppendLine($"{Indent()}WHEN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(condition.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}THEN:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(result.Accept(this));
|
||||
_indentLevel--;
|
||||
}
|
||||
if (caseFunction.ElseResultExpression is not null)
|
||||
{
|
||||
sb.AppendLine($"{Indent()}ELSE:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(caseFunction.ElseResultExpression.Accept(this));
|
||||
_indentLevel--;
|
||||
}
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitArithmeticExpression(ArithmeticExpression arithmeticExpression)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"{Indent()}Arithmetic ({arithmeticExpression.ArithmeticOperator}):");
|
||||
_indentLevel++;
|
||||
sb.AppendLine($"{Indent()}Left:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(arithmeticExpression.ExpressionA.Accept(this));
|
||||
_indentLevel--;
|
||||
sb.AppendLine($"{Indent()}Right:");
|
||||
_indentLevel++;
|
||||
sb.AppendLine(arithmeticExpression.ExpressionB.Accept(this));
|
||||
_indentLevel--;
|
||||
_indentLevel--;
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public string VisitInputPropertyExpression(InputPropertyExpression inputPropertyExpression)
|
||||
{
|
||||
return $"{Indent()}InputProperty: {inputPropertyExpression.DataKeyLookup}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.Visitors.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Expressions;
|
||||
|
||||
/// <summary>
|
||||
/// Generates simplified markdown documentation for Expression trees focused on readability.
|
||||
/// </summary>
|
||||
public class SimpleExpressionGenerator
|
||||
{
|
||||
private readonly CommandVisitor _sqlVisitor = new();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a simple markdown document from an Expression.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to document.</param>
|
||||
/// <param name="title">Optional title for the documentation.</param>
|
||||
/// <returns>A markdown formatted string documenting the expression.</returns>
|
||||
public string GenerateMarkdown(Expression expression, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("## Expression");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("```sql");
|
||||
sb.AppendLine(expression.Accept(_sqlVisitor));
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Type Information");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"- **Expression Type:** `{expression.GetType().Name}`");
|
||||
sb.AppendLine($"- **Namespace:** `{expression.GetType().Namespace}`");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("## Description");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(GetExpressionDescription(expression));
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a comparison table for multiple expressions.
|
||||
/// </summary>
|
||||
/// <param name="expressions">Dictionary of expression names to expressions.</param>
|
||||
/// <param name="title">Optional title for the table.</param>
|
||||
/// <returns>A markdown formatted comparison table.</returns>
|
||||
public string GenerateComparisonTable(Dictionary<string, Expression> expressions, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("| Name | Expression | Type |");
|
||||
sb.AppendLine("|------|------------|------|");
|
||||
|
||||
foreach (var (name, expr) in expressions)
|
||||
{
|
||||
var sql = expr.Accept(_sqlVisitor).Replace("|", "\\|").Replace("\n", " ");
|
||||
var type = expr.GetType().Name;
|
||||
sb.AppendLine($"| {name} | `{sql}` | `{type}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a bulleted list of expressions.
|
||||
/// </summary>
|
||||
/// <param name="expressions">List of expressions to document.</param>
|
||||
/// <param name="title">Optional title for the list.</param>
|
||||
/// <returns>A markdown formatted bulleted list.</returns>
|
||||
public string GenerateBulletList(IEnumerable<Expression> expressions, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"## {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var expr in expressions)
|
||||
{
|
||||
var sql = expr.Accept(_sqlVisitor).Replace("\n", " ");
|
||||
sb.AppendLine($"- `{sql}` - *{expr.GetType().Name}*");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private string GetExpressionDescription(Expression expression)
|
||||
{
|
||||
var typeName = expression.GetType().Name;
|
||||
|
||||
return typeName switch
|
||||
{
|
||||
"AndExpression" => "A logical AND expression that combines two boolean expressions. Both expressions must evaluate to true for the result to be true.",
|
||||
"OrExpression" => "A logical OR expression that combines two boolean expressions. Either expression can evaluate to true for the result to be true.",
|
||||
"NotExpression" => "A logical NOT expression that negates a boolean expression.",
|
||||
"ComparisonOperatorExpression" => "A comparison expression that compares two values using an operator (=, <>, <, >, <=, >=).",
|
||||
"ArithmeticExpression" => "An arithmetic expression that performs mathematical operations (+, -, *, /) on numeric values.",
|
||||
"FunctionExpression" => "A SQL function call expression that invokes a database function with arguments.",
|
||||
"AggregateFunctionExpression" => "An aggregate function expression (SUM, COUNT, AVG, MIN, MAX) that operates on sets of values.",
|
||||
"CaseExpression" => "A CASE expression that provides conditional logic similar to if-then-else statements.",
|
||||
"InExpression" => "An IN expression that checks if a value exists in a set of values.",
|
||||
"BetweenExpression" => "A BETWEEN expression that checks if a value falls within a range.",
|
||||
"LikeExpression" => "A LIKE expression that performs pattern matching on strings using wildcards.",
|
||||
"NumberLiteralExpression" => "A numeric literal value.",
|
||||
"StringLiteralExpression" => "A string literal value enclosed in quotes.",
|
||||
"DateTimeLiteralExpression" => "A date/time literal value.",
|
||||
"BooleanLiteralExpression" => "A boolean literal value (true/false).",
|
||||
"NullLiteralExpression" => "A NULL literal value representing absence of data.",
|
||||
"ParameterExpression" => "A parameterized value placeholder that will be substituted at runtime.",
|
||||
"ColumnExpression" => "A reference to a database column from a table or view.",
|
||||
_ => $"A {typeName} expression."
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
using Strata.SqlTools.Breakdowns.LinqToSql;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from LINQ to SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the LINQ query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
private readonly SqlServer.QueryBreakdownGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownGenerator class.
|
||||
/// </summary>
|
||||
public QueryBreakdownGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.QueryBreakdownGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a LINQ to SQL QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
// Since LinqQueryBreakdown inherits from SqlServer.QueryBreakdown,
|
||||
// we can use the base generator which works with the shared properties
|
||||
return _baseGenerator.GenerateMermaidDiagram(queryBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing the LINQ method call chain.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid flowchart showing method calls.</returns>
|
||||
public string GenerateMethodChainDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart LR");
|
||||
sb.AppendLine();
|
||||
|
||||
if (queryBreakdown.MethodCallChain.Count == 0)
|
||||
{
|
||||
sb.AppendLine(" Start([IQueryable]) --> End([Result])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" Start([IQueryable])");
|
||||
|
||||
for (int i = 0; i < queryBreakdown.MethodCallChain.Count; i++)
|
||||
{
|
||||
var method = queryBreakdown.MethodCallChain[i];
|
||||
var nodeId = $"M{i}";
|
||||
var prevNodeId = i == 0 ? "Start" : $"M{i - 1}";
|
||||
|
||||
sb.AppendLine($" {nodeId}[\"{method}\"]");
|
||||
sb.AppendLine($" {prevNodeId} --> {nodeId}");
|
||||
}
|
||||
|
||||
var lastNodeId = $"M{queryBreakdown.MethodCallChain.Count - 1}";
|
||||
sb.AppendLine($" {lastNodeId} --> End([Result])");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a combined diagram showing both the query structure and method chain.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The LINQ QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing both diagrams.</returns>
|
||||
public string GenerateCombinedDiagram(LinqQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"## {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Method chain
|
||||
sb.AppendLine("### LINQ Method Chain");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateMethodChainDiagram(queryBreakdown));
|
||||
|
||||
// SQL Structure
|
||||
sb.AppendLine("### SQL Query Structure");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateMermaidDiagram(queryBreakdown));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces.QueryEngine;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.LinqToSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagrams for LINQ to SQL statements, including sequence diagrams
|
||||
/// for statement execution flow and entity-relationship diagrams.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
private readonly SqlServer.SqlStatementGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SqlStatementGenerator class.
|
||||
/// </summary>
|
||||
public SqlStatementGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.SqlStatementGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing LINQ to SQL statement execution flow.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The LINQ to SQL breakdown object.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid sequence diagram markdown.</returns>
|
||||
public string GenerateSequenceDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateSequenceDiagram(sqlBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid entity-relationship diagram from a SQL breakdown.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The SQL breakdown containing query information.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid ER diagram markdown.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
//Extract table names from breakdown - just use FROM clause for now
|
||||
var queryBreakdown = sqlBreakdown as IQueryBreakdown;
|
||||
var tableNames = new List<string>();
|
||||
|
||||
if (queryBreakdown != null && !string.IsNullOrWhiteSpace(queryBreakdown.FromClause?.ToString()))
|
||||
{
|
||||
tableNames.Add(queryBreakdown.FromClause.ToString());
|
||||
}
|
||||
|
||||
return _baseGenerator.GenerateEntityRelationshipDiagram(tableNames, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a diagram showing LINQ execution pipeline from a SQL breakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The query breakdown containing query information.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid diagram markdown.</returns>
|
||||
public string GenerateLinqPipelineDiagram(IQueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("sequenceDiagram");
|
||||
sb.AppendLine(" participant Client as Client Application");
|
||||
sb.AppendLine(" participant LINQ as LINQ Provider");
|
||||
sb.AppendLine(" participant ET as Expression Tree");
|
||||
sb.AppendLine(" participant SQL as SQL Generator");
|
||||
sb.AppendLine(" participant DB as Database");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" Client->>LINQ: LINQ Query");
|
||||
sb.AppendLine(" activate LINQ");
|
||||
|
||||
// Check if there's a WHERE clause
|
||||
var whereClause = queryBreakdown.WhereClause?.Clause;
|
||||
if (!string.IsNullOrWhiteSpace(whereClause))
|
||||
{
|
||||
sb.AppendLine(" LINQ->>ET: Where Predicate");
|
||||
sb.AppendLine(" activate ET");
|
||||
}
|
||||
|
||||
// Check if there's a custom SELECT
|
||||
var selectClause = queryBreakdown.SelectClause?.Clause;
|
||||
if (!string.IsNullOrWhiteSpace(selectClause) && selectClause.Trim() != "*")
|
||||
{
|
||||
sb.AppendLine(" LINQ->>ET: Select Projection");
|
||||
if (string.IsNullOrWhiteSpace(whereClause))
|
||||
{
|
||||
sb.AppendLine(" activate ET");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine(" ET->>SQL: Expression Tree");
|
||||
sb.AppendLine(" deactivate ET");
|
||||
sb.AppendLine(" SQL->>DB: Generate SQL");
|
||||
sb.AppendLine(" activate DB");
|
||||
sb.AppendLine(" DB-->>SQL: Result Set");
|
||||
sb.AppendLine(" deactivate DB");
|
||||
sb.AppendLine(" SQL-->>LINQ: Mapped Objects");
|
||||
sb.AppendLine(" LINQ-->>Client: IEnumerable Result");
|
||||
sb.AppendLine(" deactivate LINQ");
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
using QuerySummary = Strata.SqlTools.Breakdowns.SqlServer.QuerySummary;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Markdown documentation from QueryBreakdownCollection objects for PostgreSQL.
|
||||
/// Creates comprehensive reports including collection summaries, parameter analysis, and batch flow visualization
|
||||
/// with PostgreSQL-specific features.
|
||||
/// </summary>
|
||||
public static class QueryBreakdownCollectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a comprehensive collection report in Markdown format with PostgreSQL-specific information.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to document.</param>
|
||||
/// <param name="title">Optional title for the report.</param>
|
||||
/// <returns>A string containing the Markdown documentation.</returns>
|
||||
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Collection Summary
|
||||
sb.Append(GenerateCollectionSummary(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Parameter Analysis
|
||||
sb.Append(GenerateParameterAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Query Composition Report
|
||||
sb.Append(GenerateQueryCompositionReport(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary section for the collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
|
||||
/// <returns>Markdown summary section.</returns>
|
||||
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Collection Summary");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |");
|
||||
sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |");
|
||||
sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |");
|
||||
sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a parameter analysis report with PostgreSQL parameter syntax support.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown parameter analysis section.</returns>
|
||||
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var paramReport = collection.GetParameterUsageReport().ToList();
|
||||
|
||||
sb.AppendLine("## Parameter Analysis");
|
||||
sb.AppendLine();
|
||||
|
||||
if (paramReport.Count == 0)
|
||||
{
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No parameters are used in this collection.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Parameter | Type | Used In | Value |");
|
||||
sb.AppendLine("|-----------|------|---------|-------|");
|
||||
|
||||
foreach (var param in paramReport.OrderBy(p => p.ParameterName))
|
||||
{
|
||||
var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}";
|
||||
var value = param.Value?.ToString() ?? "NULL";
|
||||
// PostgreSQL supports both $n positional and :named parameters
|
||||
var paramSyntax = int.TryParse(param.ParameterName, out _)
|
||||
? $"${param.ParameterName}"
|
||||
: $":{param.ParameterName}";
|
||||
sb.AppendLine($"| {paramSyntax} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Parameter Dependency Diagram");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateParameterDependencyDiagram(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing parameter dependencies across queries.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
var queryBreakdowns = collection.QueryBreakdowns;
|
||||
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in queryBreakdowns)
|
||||
{
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = allParamNames.OrderBy(p => p).ToList();
|
||||
|
||||
// Create parameter nodes
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var paramNode = $"param{i}";
|
||||
var paramSyntax = int.TryParse(parameters[i], out _)
|
||||
? $"${parameters[i]}"
|
||||
: $":{parameters[i]}";
|
||||
sb.AppendLine($" {paramNode}[\"{paramSyntax}\"]");
|
||||
sb.AppendLine($" style {paramNode} fill:#e8f5e9");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// Create query nodes and connections
|
||||
for (int i = 0; i < queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = queryBreakdowns[i];
|
||||
var queryNode = $"query{i}";
|
||||
var queryType = DetermineQueryType(query);
|
||||
|
||||
sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]");
|
||||
sb.AppendLine($" style {queryNode} fill:#fff3e0");
|
||||
|
||||
// Collect all parameter names used by this query
|
||||
var queryParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
queryParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
queryParamNames.Add(paramName);
|
||||
}
|
||||
|
||||
// Connect parameters to this query
|
||||
foreach (var paramName in queryParamNames)
|
||||
{
|
||||
var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (paramIndex >= 0)
|
||||
{
|
||||
var paramNode = $"param{paramIndex}";
|
||||
sb.AppendLine($" {paramNode} --> {queryNode}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a detailed query composition report.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
|
||||
/// <returns>Markdown composition report section.</returns>
|
||||
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Query Composition Report");
|
||||
sb.AppendLine();
|
||||
|
||||
var summaries = collection.GetQuerySummaries().ToList();
|
||||
|
||||
for (int i = 0; i < summaries.Count; i++)
|
||||
{
|
||||
var summary = summaries[i];
|
||||
var query = collection.QueryBreakdowns[i];
|
||||
|
||||
AppendQueryCompositionTable(sb, i, summary);
|
||||
AppendQueryParameters(sb, query);
|
||||
AppendQueryCteSections(sb, summary, query);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the query composition table for a single query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QuerySummary summary)
|
||||
{
|
||||
sb.AppendLine($"### Query #{queryIndex}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Aspect | Present |");
|
||||
sb.AppendLine("|--------|---------|");
|
||||
sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |");
|
||||
sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |");
|
||||
sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |");
|
||||
sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |");
|
||||
sb.AppendLine($"| HAVING Clause | {FormatClausePresence(summary.HasHavingClause)} |");
|
||||
sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |");
|
||||
sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |");
|
||||
sb.AppendLine($"| Columns | {summary.ColumnCount} |");
|
||||
sb.AppendLine($"| Parameters | {summary.ParameterCount} |");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends parameter information for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query)
|
||||
{
|
||||
// Collect all unique parameters from both ParameterList and Parameters dictionary
|
||||
var allParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
allParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
if (allParams.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**Parameters Used:**");
|
||||
sb.AppendLine();
|
||||
foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = allParams[paramName];
|
||||
var paramSyntax = int.TryParse(paramName, out _)
|
||||
? $"${paramName}"
|
||||
: $":{paramName}";
|
||||
sb.AppendLine($"- `{paramSyntax}` = `{value?.ToString() ?? "NULL"}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends CTE section for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCteSections(StringBuilder sb, QuerySummary summary, QueryBreakdown query)
|
||||
{
|
||||
if (!summary.HasCTE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**CTEs Defined:**");
|
||||
sb.AppendLine();
|
||||
foreach (var cte in query.WithClauses)
|
||||
{
|
||||
sb.AppendLine($"- `{cte.TableName}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch execution flow diagram for PostgreSQL.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <param name="includeTransaction">Whether to show transaction wrapping.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeId = 0;
|
||||
|
||||
// Handle empty collection
|
||||
if (collection.QueryBreakdowns.Count == 0)
|
||||
{
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node0[\"BEGIN\"]");
|
||||
sb.AppendLine($" node0 --> node1[\"COMMIT\"]");
|
||||
sb.AppendLine($" node1 --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])");
|
||||
}
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Start node
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"BEGIN\"]");
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId - 1} --> node{nodeId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
}
|
||||
|
||||
// Query nodes
|
||||
for (int i = 0; i < collection.QueryBreakdowns.Count; i++)
|
||||
{
|
||||
if (i < collection.QueryBreakdowns.Count - 1)
|
||||
{
|
||||
// Not the last query - connect to next
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last query - connect to End (or COMMIT if transaction)
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId}[\"COMMIT\"]");
|
||||
sb.AppendLine($" node{nodeId} --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type name from a parameter value using PostgreSQL types.
|
||||
/// </summary>
|
||||
private static string GetParameterType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool => "BOOLEAN",
|
||||
byte or short => "SMALLINT",
|
||||
int => "INTEGER",
|
||||
long => "BIGINT",
|
||||
float => "REAL",
|
||||
double => "DOUBLE PRECISION",
|
||||
decimal => "NUMERIC",
|
||||
string => "TEXT",
|
||||
DateTime => "TIMESTAMP",
|
||||
_ => "UNKNOWN"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special Markdown characters.
|
||||
/// </summary>
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("|", "\\|")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats clause presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatClausePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Determines the query type from a QueryBreakdown.
|
||||
/// </summary>
|
||||
private static string DetermineQueryType(QueryBreakdown query)
|
||||
{
|
||||
var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause);
|
||||
if (hasSelect)
|
||||
{
|
||||
return "SELECT";
|
||||
}
|
||||
|
||||
var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause);
|
||||
return hasFrom ? "FROM" : "QUERY";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Strata.SqlTools.Breakdowns.PostgreSql;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from PostgreSQL SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
private readonly SqlServer.QueryBreakdownGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownGenerator class.
|
||||
/// </summary>
|
||||
public QueryBreakdownGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.QueryBreakdownGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a PostgreSQL QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The PostgreSQL QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(QueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
// Since PostgreSql.QueryBreakdown inherits from SqlServer.QueryBreakdown,
|
||||
// we can use the base generator which works with the shared properties
|
||||
return _baseGenerator.GenerateMermaidDiagram(queryBreakdown, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.PostgreSql;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagrams for PostgreSQL SQL statements, including sequence diagrams
|
||||
/// for statement execution flow and entity-relationship diagrams.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
private readonly SqlServer.SqlStatementGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SqlStatementGenerator class.
|
||||
/// </summary>
|
||||
public SqlStatementGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.SqlStatementGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing PostgreSQL SQL statement execution flow.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The PostgreSQL SQL breakdown object.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid sequence diagram markdown.</returns>
|
||||
public string GenerateSequenceDiagram(SqlBreakdownBase sqlBreakdown, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateSequenceDiagram(sqlBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid entity-relationship diagram from table names.
|
||||
/// </summary>
|
||||
/// <param name="tables">Collection of table names to include in the diagram.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid ER diagram markdown.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(IEnumerable<string> tables, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateEntityRelationshipDiagram(tables, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
# Strata.SqlTools.Markdown
|
||||
|
||||
Markdown documentation generation for SQL queries and expressions from Strata.SqlTools.
|
||||
|
||||
## Overview
|
||||
|
||||
This library provides tools to generate markdown documentation and Mermaid diagrams from SQL query breakdowns and expression trees. It's designed to help document SQL queries and their structure in a human-readable format.
|
||||
|
||||
## Features
|
||||
|
||||
### SqlServer Folder - Mermaid Diagram Generation
|
||||
|
||||
#### QueryBreakdownGenerator
|
||||
Generates Mermaid flowchart diagrams from SQL `QueryBreakdown` objects, visualizing:
|
||||
- WITH clauses (Common Table Expressions)
|
||||
- SELECT, FROM, WHERE clauses
|
||||
- GROUP BY, HAVING, ORDER BY clauses
|
||||
- Setup and Finish clauses
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
var query = QueryBreakdown.Parse(@"
|
||||
SELECT u.ID, u.Name, COUNT(o.OrderID) as OrderCount
|
||||
FROM Users u
|
||||
JOIN Orders o ON u.ID = o.UserID
|
||||
WHERE u.Active = 1
|
||||
GROUP BY u.ID, u.Name
|
||||
HAVING COUNT(o.OrderID) > 5
|
||||
ORDER BY OrderCount DESC
|
||||
");
|
||||
|
||||
var generator = new QueryBreakdownGenerator();
|
||||
string markdown = generator.GenerateMermaidDiagram(query, "User Orders Query");
|
||||
|
||||
// Output the markdown to a file or display
|
||||
Console.WriteLine(markdown);
|
||||
```
|
||||
|
||||
#### SqlStatementGenerator
|
||||
Generates Mermaid sequence diagrams showing SQL statement execution flow and entity-relationship diagrams.
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
var seqGenerator = new SqlStatementGenerator();
|
||||
string sequenceDiagram = seqGenerator.GenerateSequenceDiagram(sqlBreakdown, "Query Execution Flow");
|
||||
|
||||
// Generate ER diagram for tables
|
||||
var tables = new[] { "Users", "Orders", "OrderDetails" };
|
||||
string erDiagram = seqGenerator.GenerateEntityRelationshipDiagram(tables, "Database Schema");
|
||||
```
|
||||
|
||||
### Snowflake Folder - Snowflake SQL Support
|
||||
|
||||
The library fully supports Snowflake SQL syntax, including Snowflake-specific features like:
|
||||
- `:parameter` syntax (in addition to `@parameter`)
|
||||
- Double-quoted identifiers `"identifier"`
|
||||
- QUALIFY clauses for window functions
|
||||
- Type casting with `::` operator
|
||||
- JSON path notation with `:` accessor
|
||||
|
||||
#### QueryBreakdownGenerator (Snowflake)
|
||||
Generates Mermaid flowchart diagrams from Snowflake `QueryBreakdown` objects.
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
using Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
// Parse Snowflake SQL with :parameter syntax
|
||||
var query = QueryBreakdown.Parse(@"
|
||||
WITH ACTIVE_USERS AS (
|
||||
SELECT USER_ID, USER_NAME, EMAIL
|
||||
FROM USERS
|
||||
WHERE STATUS = :status AND REGION = :region
|
||||
)
|
||||
SELECT
|
||||
AU.USER_ID,
|
||||
AU.USER_NAME,
|
||||
COUNT(O.ORDER_ID) AS ORDER_COUNT,
|
||||
SUM(O.AMOUNT):: DECIMAL(10,2) AS TOTAL_AMOUNT
|
||||
FROM ACTIVE_USERS AU
|
||||
LEFT JOIN ORDERS O ON AU.USER_ID = O.USER_ID
|
||||
WHERE O.ORDER_DATE >= :startDate
|
||||
GROUP BY AU.USER_ID, AU.USER_NAME
|
||||
HAVING COUNT(O.ORDER_ID) > 0
|
||||
ORDER BY TOTAL_AMOUNT DESC
|
||||
", isMicrosoftSql: false);
|
||||
|
||||
var generator = new QueryBreakdownGenerator();
|
||||
string markdown = generator.GenerateMermaidDiagram(query, "Snowflake User Orders Analysis");
|
||||
|
||||
Console.WriteLine(markdown);
|
||||
```
|
||||
|
||||
#### SqlStatementGenerator (Snowflake)
|
||||
Generates sequence and ER diagrams for Snowflake SQL statements.
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
var seqGenerator = new SqlStatementGenerator();
|
||||
|
||||
// Generate sequence diagram for Snowflake query flow
|
||||
string sequenceDiagram = seqGenerator.GenerateSequenceDiagram(snowflakeQuery, "Snowflake Query Flow");
|
||||
|
||||
// Generate ER diagram for Snowflake tables (typically uppercase)
|
||||
var tables = new[] { "CUSTOMERS", "ORDERS", "ORDER_ITEMS", "PRODUCTS" };
|
||||
string erDiagram = seqGenerator.GenerateEntityRelationshipDiagram(tables, "Snowflake Schema");
|
||||
```
|
||||
|
||||
### Expressions Folder - Expression Documentation
|
||||
|
||||
#### ExpressionGenerator
|
||||
Generates comprehensive markdown documentation for SQL expression trees with:
|
||||
- Hierarchical structure visualization
|
||||
- Type information
|
||||
- Mermaid tree diagrams
|
||||
- Mathematical notation using LaTeX (GitHub compatible)
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
using Strata.SqlTools.SqlBreakdown.Expressions;
|
||||
using Strata.SqlTools.Markdown.Expressions;
|
||||
|
||||
// Build an expression
|
||||
Expression quantity = new ColumnExpression<TableSource>(tableSource, "Quantity");
|
||||
Expression unitPrice = new ColumnExpression<TableSource>(tableSource, "UnitPrice");
|
||||
Expression discount = new ColumnExpression<TableSource>(tableSource, "Discount");
|
||||
|
||||
var totalExpression = (quantity * unitPrice) * (1 - discount);
|
||||
|
||||
var generator = new ExpressionGenerator();
|
||||
string markdown = generator.GenerateMarkdown(totalExpression, "Order Line Total Calculation");
|
||||
|
||||
// Output includes:
|
||||
// - Expression structure tree
|
||||
// - Type information
|
||||
// - Mermaid diagram visualization
|
||||
// - Mathematical expression in LaTeX format
|
||||
Console.WriteLine(markdown);
|
||||
|
||||
// Or generate just the mathematical expression
|
||||
string mathExpr = generator.GenerateMathematicalExpression(totalExpression);
|
||||
// Produces: $$(Quantity \times UnitPrice) \times (1 - Discount)$$
|
||||
|
||||
// For inline math notation
|
||||
string inlineMath = generator.GenerateMathematicalExpression(totalExpression, inline: true);
|
||||
// Produces: $(Quantity \times UnitPrice) \times (1 - Discount)$
|
||||
|
||||
// For raw LaTeX expressions (e.g., mathematical formulas)
|
||||
var cauchySchwarz = @"\left( \sum_{k=1}^n a_k b_k \right)^2 \leq \left( \sum_{k=1}^n a_k^2 \right) \left( \sum_{k=1}^n b_k^2 \right)";
|
||||
string dollarFormat = generator.GenerateRawMathematicalExpression(cauchySchwarz);
|
||||
// Produces: $$
|
||||
// \left( \sum_{k=1}^n a_k b_k \right)^2 \leq ...
|
||||
// $$
|
||||
|
||||
string mathCodeFence = generator.GenerateRawMathematicalExpression(cauchySchwarz, format: "math");
|
||||
// Produces: ```math
|
||||
// \left( \sum_{k=1}^n a_k b_k \right)^2 \leq ...
|
||||
// ```
|
||||
```
|
||||
|
||||
**Mathematical Notation Features:**
|
||||
- Arithmetic operators: `+`, `-`, `×` (`\times`), `÷` (`\div`), `mod` (`\bmod`)
|
||||
- Comparison operators: `=`, `≠` (`\neq`), `<`, `>`, `≤` (`\leq`), `≥` (`\geq`)
|
||||
- Logical operators: `∧` (`\land`), `∨` (`\lor`), `¬` (`\neg`)
|
||||
- Functions: `SUM` (`\sum`), `MIN` (`\min`), `MAX` (`\max`), `|x|` (ABS), `√` (`\sqrt`), powers, etc.
|
||||
- Set operations: `∈` for BETWEEN and IN expressions
|
||||
- Case expressions using piecewise notation (`\begin{cases}`)
|
||||
|
||||
|
||||
#### SimpleExpressionGenerator
|
||||
Generates simplified, readable markdown documentation for expressions with:
|
||||
- SQL representation
|
||||
- Type information
|
||||
- Human-readable descriptions
|
||||
- Comparison tables for multiple expressions
|
||||
- Bulleted lists
|
||||
|
||||
**Example Usage:**
|
||||
```csharp
|
||||
var simpleGenerator = new SimpleExpressionGenerator();
|
||||
|
||||
// Generate simple markdown for a single expression
|
||||
string simpleMarkdown = simpleGenerator.GenerateMarkdown(expression, "Price Filter");
|
||||
|
||||
// Generate comparison table for multiple expressions
|
||||
var expressions = new Dictionary<string, Expression>
|
||||
{
|
||||
["Basic Filter"] = status == "Active",
|
||||
["Date Filter"] = orderDate > new DateTime(2024, 1, 1),
|
||||
["Complex Filter"] = (quantity > 10) & (price < 100)
|
||||
};
|
||||
|
||||
string comparisonTable = simpleGenerator.GenerateComparisonTable(expressions, "Filter Expressions");
|
||||
|
||||
// Generate bullet list
|
||||
var expressionList = new List<Expression> { expr1, expr2, expr3 };
|
||||
string bulletList = simpleGenerator.GenerateBulletList(expressionList, "Common Filters");
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
Add a reference to this project in your .csproj file:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.Markdown\Strata.SqlTools.Markdown.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Strata.SqlTools - Core SQL utilities library
|
||||
- Strata.SqlTools.SqlServer - SQL Server specific implementations
|
||||
- Strata.SqlTools.Snowflake - Snowflake specific implementations
|
||||
- .NET 9.0 or later
|
||||
|
||||
## Use Cases
|
||||
|
||||
1. **Documentation Generation**: Automatically generate documentation for complex SQL queries
|
||||
2. **Code Review**: Visualize query structure for easier code reviews
|
||||
3. **Learning Tool**: Help developers understand complex SQL queries through visual diagrams
|
||||
4. **Query Analysis**: Analyze query patterns and structures
|
||||
5. **API Documentation**: Document SQL expressions used in query builders
|
||||
|
||||
## Output Examples
|
||||
|
||||
### Mermaid Flowchart
|
||||
The `QueryBreakdownGenerator` produces flowcharts like:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Query Start]) --> Node1
|
||||
Node1["SELECT<br/>u.ID, u.Name, COUNT(o.OrderID)"]
|
||||
Node1 --> Node2
|
||||
Node2["FROM<br/>Users u JOIN Orders o"]
|
||||
Node2 --> Node3
|
||||
Node3{"WHERE<br/>u.Active = 1"}
|
||||
Node3 --> Node4
|
||||
Node4["GROUP BY<br/>u.ID, u.Name"]
|
||||
Node4 --> Node5
|
||||
Node5{"HAVING<br/>COUNT(o.OrderID) > 5"}
|
||||
Node5 --> Node6
|
||||
Node6["ORDER BY<br/>OrderCount DESC"]
|
||||
Node6 --> End([Query End])
|
||||
```
|
||||
|
||||
### Expression Documentation
|
||||
|
||||
#### Comprehensive Expression Markdown (ExpressionGenerator)
|
||||
|
||||
The `ExpressionGenerator` produces detailed documentation including structure, type info, diagrams, and mathematical notation:
|
||||
|
||||
```markdown
|
||||
# Order Line Total Calculation
|
||||
|
||||
## Expression Structure
|
||||
- **Type**: ArithmeticExpression
|
||||
- **Operator**: Multiply (*)
|
||||
- **Left Expression**: ArithmeticExpression (Quantity * UnitPrice)
|
||||
- **Right Expression**: ArithmeticExpression (1 - Discount)
|
||||
|
||||
## Mermaid Diagram
|
||||
```mermaid
|
||||
graph TD
|
||||
Root["* (Multiply)"]
|
||||
Root --> Left["* (Multiply)"]
|
||||
Root --> Right["- (Subtract)"]
|
||||
Left --> LeftLeft["Quantity (Column)"]
|
||||
Left --> LeftRight["UnitPrice (Column)"]
|
||||
Right --> RightLeft["1 (Constant)"]
|
||||
Right --> RightRight["Discount (Column)"]
|
||||
```
|
||||
|
||||
## Mathematical Expression
|
||||
$$(Quantity \times UnitPrice) \times (1 - Discount)$$
|
||||
```
|
||||
|
||||
#### Simple Expression Markdown (SimpleExpressionGenerator)
|
||||
|
||||
The `SimpleExpressionGenerator` produces concise, readable output:
|
||||
|
||||
**Single Expression:**
|
||||
```markdown
|
||||
# Price Filter
|
||||
|
||||
**Expression Type**: ComparisonExpression
|
||||
|
||||
**SQL Representation**:
|
||||
```sql
|
||||
UnitPrice < 100
|
||||
```
|
||||
|
||||
**Description**: Filters records where UnitPrice is less than 100
|
||||
```
|
||||
|
||||
**Comparison Table:**
|
||||
```markdown
|
||||
# Filter Expressions Comparison
|
||||
|
||||
| Name | Expression Type | SQL Representation |
|
||||
|------|----------------|-------------------|
|
||||
| Basic Filter | ComparisonExpression | `Status = 'Active'` |
|
||||
| Date Filter | ComparisonExpression | `OrderDate > '2024-01-01'` |
|
||||
| Complex Filter | LogicalExpression | `(Quantity > 10) AND (Price < 100)` |
|
||||
```
|
||||
|
||||
**Bullet List:**
|
||||
```markdown
|
||||
# Common Filters
|
||||
|
||||
- **Status = 'Active'** (ComparisonExpression)
|
||||
- **OrderDate > '2024-01-01'** (ComparisonExpression)
|
||||
- **(Quantity > 10) AND (Price < 100)** (LogicalExpression)
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please ensure all code follows the existing patterns and includes appropriate documentation.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright © Strata Decision Technology 2024-2026
|
||||
@@ -0,0 +1,459 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Markdown documentation from QueryBreakdownCollection objects for Snowflake.
|
||||
/// Creates comprehensive reports including collection summaries, parameter analysis, and batch flow visualization
|
||||
/// with Snowflake-specific features.
|
||||
/// </summary>
|
||||
public static class QueryBreakdownCollectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a comprehensive collection report in Markdown format with Snowflake-specific information.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to document.</param>
|
||||
/// <param name="title">Optional title for the report.</param>
|
||||
/// <returns>A string containing the Markdown documentation.</returns>
|
||||
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Collection Summary
|
||||
sb.Append(GenerateCollectionSummary(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Snowflake Features Analysis
|
||||
sb.Append(GenerateSnowflakeFeaturesAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Parameter Analysis
|
||||
sb.Append(GenerateParameterAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Query Composition Report
|
||||
sb.Append(GenerateQueryCompositionReport(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary section for the collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
|
||||
/// <returns>Markdown summary section.</returns>
|
||||
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Collection Summary");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |");
|
||||
sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |");
|
||||
sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |");
|
||||
sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Snowflake-specific features analysis section.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown Snowflake features section.</returns>
|
||||
public static string GenerateSnowflakeFeaturesAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Snowflake Features");
|
||||
sb.AppendLine();
|
||||
|
||||
var queriesWithStages = collection.WhereUseStageReference().ToList();
|
||||
var queriesWithSemiStructured = collection.WhereUseSemiStructuredData().ToList();
|
||||
|
||||
sb.AppendLine("| Feature | Used | Count |");
|
||||
sb.AppendLine("|---------|------|-------|");
|
||||
sb.AppendLine($"| Stage References | {FormatFeaturePresence(queriesWithStages.Count > 0)} | {queriesWithStages.Count} |");
|
||||
sb.AppendLine($"| Semi-Structured Data | {FormatFeaturePresence(queriesWithSemiStructured.Count > 0)} | {queriesWithSemiStructured.Count} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a parameter analysis report with Snowflake parameter syntax support.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown parameter analysis section.</returns>
|
||||
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var paramReport = collection.GetParameterUsageReport().ToList();
|
||||
|
||||
sb.AppendLine("## Parameter Analysis");
|
||||
sb.AppendLine();
|
||||
|
||||
if (paramReport.Count == 0)
|
||||
{
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No parameters are used in this collection.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Parameter | Type | Used In | Value |");
|
||||
sb.AppendLine("|-----------|------|---------|-------|");
|
||||
|
||||
foreach (var param in paramReport.OrderBy(p => p.ParameterName))
|
||||
{
|
||||
var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}";
|
||||
var value = param.Value?.ToString() ?? "NULL";
|
||||
// Snowflake supports both : and @ syntax for parameters
|
||||
sb.AppendLine($"| :{param.ParameterName} / @{param.ParameterName} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Parameter Dependency Diagram");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateParameterDependencyDiagram(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing parameter dependencies across queries.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
var queryBreakdowns = collection.QueryBreakdowns;
|
||||
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in queryBreakdowns)
|
||||
{
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = allParamNames.OrderBy(p => p).ToList();
|
||||
|
||||
// Create parameter nodes
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var paramNode = $"param{i}";
|
||||
sb.AppendLine($" {paramNode}[\":{parameters[i]}\"]");
|
||||
sb.AppendLine($" style {paramNode} fill:#e0f2f1");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// Create query nodes and connections
|
||||
for (int i = 0; i < queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = queryBreakdowns[i];
|
||||
var queryNode = $"query{i}";
|
||||
var queryType = DetermineQueryType(query);
|
||||
|
||||
sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]");
|
||||
sb.AppendLine($" style {queryNode} fill:#f1f8e9");
|
||||
|
||||
// Collect all parameter names used by this query
|
||||
var queryParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
queryParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
queryParamNames.Add(paramName);
|
||||
}
|
||||
|
||||
// Connect parameters to this query
|
||||
foreach (var paramName in queryParamNames)
|
||||
{
|
||||
var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (paramIndex >= 0)
|
||||
{
|
||||
var paramNode = $"param{paramIndex}";
|
||||
sb.AppendLine($" {paramNode} --> {queryNode}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a detailed query composition report with Snowflake-specific information.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
|
||||
/// <returns>Markdown composition report section.</returns>
|
||||
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Query Composition Report");
|
||||
sb.AppendLine();
|
||||
|
||||
var summaries = collection.GetQuerySummaries().ToList();
|
||||
var stageQueries = collection.WhereUseStageReference().ToList();
|
||||
var semiStructured = collection.WhereUseSemiStructuredData().ToList();
|
||||
|
||||
for (int i = 0; i < summaries.Count; i++)
|
||||
{
|
||||
var summary = summaries[i];
|
||||
var query = collection.QueryBreakdowns[i];
|
||||
|
||||
AppendQueryCompositionTable(sb, i, summary);
|
||||
AppendQueryParameters(sb, query);
|
||||
AppendQueryCteSections(sb, summary, query);
|
||||
AppendSnowflakeFeatures(sb, query, stageQueries, semiStructured);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the query composition table for a single query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, SnowflakeQueryAnalysis summary)
|
||||
{
|
||||
sb.AppendLine($"### Query #{queryIndex}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Aspect | Present |");
|
||||
sb.AppendLine("|--------|---------|");
|
||||
sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |");
|
||||
sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |");
|
||||
sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |");
|
||||
sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |");
|
||||
sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |");
|
||||
sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |");
|
||||
sb.AppendLine($"| Columns | {summary.ColumnCount} |");
|
||||
sb.AppendLine($"| Parameters | {summary.ParameterCount} |");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends parameter information for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query)
|
||||
{
|
||||
// Collect all unique parameters from both ParameterList and Parameters dictionary
|
||||
var allParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
allParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
if (allParams.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**Parameters Used:**");
|
||||
sb.AppendLine();
|
||||
foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = allParams[paramName];
|
||||
sb.AppendLine($"- `:{paramName}` = `{value?.ToString() ?? "NULL"}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends CTE section for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCteSections(StringBuilder sb, SnowflakeQueryAnalysis summary, QueryBreakdown query)
|
||||
{
|
||||
if (!summary.HasCTE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**CTEs Defined:**");
|
||||
sb.AppendLine();
|
||||
foreach (var cte in query.WithClauses)
|
||||
{
|
||||
sb.AppendLine($"- `{cte.TableName}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends Snowflake-specific feature information for a query.
|
||||
/// </summary>
|
||||
private static void AppendSnowflakeFeatures(StringBuilder sb, QueryBreakdown query, List<QueryBreakdown> stageQueries, List<QueryBreakdown> semiStructured)
|
||||
{
|
||||
if (stageQueries.Contains(query))
|
||||
{
|
||||
sb.AppendLine("**Snowflake Features:** Stage References");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (semiStructured.Contains(query))
|
||||
{
|
||||
sb.AppendLine("**Snowflake Features:** Semi-Structured Data");
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats clause presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatClausePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch execution flow diagram for Snowflake.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <param name="includeSessionSetup">Whether to show session setup statements.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeSessionSetup = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
// Handle empty collection
|
||||
if (collection.QueryBreakdowns.Count == 0)
|
||||
{
|
||||
if (includeSessionSetup)
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node0[\"Session Setup\"]");
|
||||
sb.AppendLine($" node0 --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])");
|
||||
}
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
int nodeId = 0;
|
||||
|
||||
// Start node
|
||||
if (includeSessionSetup)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Session Setup\"]");
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId - 1} --> node{nodeId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
}
|
||||
|
||||
// Query nodes
|
||||
for (int i = 0; i < collection.QueryBreakdowns.Count; i++)
|
||||
{
|
||||
if (i < collection.QueryBreakdowns.Count - 1)
|
||||
{
|
||||
// Not the last query - connect to next
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last query - connect to End
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type name from a parameter value.
|
||||
/// </summary>
|
||||
private static string GetParameterType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool => "BOOLEAN",
|
||||
byte or short or int or long => "NUMBER",
|
||||
float or double or decimal => "FLOAT",
|
||||
string => "VARCHAR",
|
||||
DateTime => "TIMESTAMP",
|
||||
_ => "VARIANT"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special Markdown characters.
|
||||
/// </summary>
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("|", "\\|")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats feature presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatFeaturePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Determines the query type from a QueryBreakdown.
|
||||
/// </summary>
|
||||
private static string DetermineQueryType(QueryBreakdown query)
|
||||
{
|
||||
var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause);
|
||||
if (hasSelect)
|
||||
{
|
||||
return "SELECT";
|
||||
}
|
||||
|
||||
var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause);
|
||||
return hasFrom ? "FROM" : "QUERY";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using Strata.SqlTools.Breakdowns.Snowflake;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from Snowflake SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
private readonly SqlServer.QueryBreakdownGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the QueryBreakdownGenerator class.
|
||||
/// </summary>
|
||||
public QueryBreakdownGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.QueryBreakdownGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a Snowflake QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The Snowflake QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(QueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
// Since Snowflake.QueryBreakdown inherits from SqlServer.QueryBreakdown,
|
||||
// we can use the base generator which works with the shared properties
|
||||
return _baseGenerator.GenerateMermaidDiagram(queryBreakdown, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Strata.SqlTools.SqlBreakdown.Classes;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.Snowflake;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagrams for Snowflake SQL statements, including sequence diagrams
|
||||
/// for statement execution flow and entity-relationship diagrams.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
private readonly SqlServer.SqlStatementGenerator _baseGenerator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the SqlStatementGenerator class.
|
||||
/// </summary>
|
||||
public SqlStatementGenerator()
|
||||
{
|
||||
_baseGenerator = new SqlServer.SqlStatementGenerator();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing Snowflake SQL statement execution flow.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The Snowflake SQL breakdown object.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid sequence diagram markdown.</returns>
|
||||
public string GenerateSequenceDiagram(SqlBreakdownBase sqlBreakdown, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateSequenceDiagram(sqlBreakdown, title);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid entity-relationship diagram from table names.
|
||||
/// </summary>
|
||||
/// <param name="tables">Collection of table names to include in the diagram.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid ER diagram markdown.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(IEnumerable<string> tables, string? title = null)
|
||||
{
|
||||
return _baseGenerator.GenerateEntityRelationshipDiagram(tables, title);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Markdown documentation from QueryBreakdownCollection objects.
|
||||
/// Creates comprehensive reports including collection summaries, parameter analysis, and batch flow visualization.
|
||||
/// </summary>
|
||||
public static class QueryBreakdownCollectionGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a comprehensive collection report in Markdown format.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to document.</param>
|
||||
/// <param name="title">Optional title for the report.</param>
|
||||
/// <returns>A string containing the Markdown documentation.</returns>
|
||||
public static string GenerateCollectionReport(QueryBreakdownCollection collection, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"# {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Collection Summary
|
||||
sb.Append(GenerateCollectionSummary(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Parameter Analysis
|
||||
sb.Append(GenerateParameterAnalysis(collection));
|
||||
sb.AppendLine();
|
||||
|
||||
// Query Composition Report
|
||||
sb.Append(GenerateQueryCompositionReport(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a summary section for the collection.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to summarize.</param>
|
||||
/// <returns>Markdown summary section.</returns>
|
||||
public static string GenerateCollectionSummary(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Collection Summary");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("| Metric | Value |");
|
||||
sb.AppendLine("|--------|-------|");
|
||||
sb.AppendLine($"| Total Queries | {collection.QueryBreakdowns.Count} |");
|
||||
sb.AppendLine($"| Total Parameters | {collection.GetAllUniqueParameters().Count()} |");
|
||||
sb.AppendLine($"| Total Columns Selected | {collection.GetTotalSelectedColumns()} |");
|
||||
sb.AppendLine($"| Unique Tables | {collection.GetUniqueTableReferences().Count()} |");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a parameter analysis report.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to analyze.</param>
|
||||
/// <returns>Markdown parameter analysis section.</returns>
|
||||
public static string GenerateParameterAnalysis(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var paramReport = collection.GetParameterUsageReport().ToList();
|
||||
|
||||
sb.AppendLine("## Parameter Analysis");
|
||||
sb.AppendLine();
|
||||
|
||||
if (paramReport.Count == 0)
|
||||
{
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("No parameters are used in this collection.");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
sb.AppendLine("### Parameters");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Parameter | Type | Used In | Value |");
|
||||
sb.AppendLine("|-----------|------|---------|-------|");
|
||||
|
||||
foreach (var param in paramReport.OrderBy(p => p.ParameterName))
|
||||
{
|
||||
var usageIndicator = param.IsUsedInAllQueries ? "✓ All" : $"{param.UsedInQueryCount}/{param.TotalQueries}";
|
||||
var value = param.Value?.ToString() ?? "NULL";
|
||||
|
||||
sb.AppendLine($"| @{param.ParameterName} | {GetParameterType(param.Value)} | {usageIndicator} | `{EscapeMarkdown(value)}` |");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("### Parameter Dependency Diagram");
|
||||
sb.AppendLine();
|
||||
sb.Append(GenerateParameterDependencyDiagram(collection));
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid diagram showing parameter dependencies across queries.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateParameterDependencyDiagram(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("graph TD");
|
||||
sb.AppendLine();
|
||||
|
||||
var queryBreakdowns = collection.QueryBreakdowns;
|
||||
|
||||
// Collect all unique parameter names from both ParameterList and Parameters dictionary
|
||||
var allParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var query in queryBreakdowns)
|
||||
{
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParamNames.Add(param.Name);
|
||||
}
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
allParamNames.Add(paramName);
|
||||
}
|
||||
}
|
||||
|
||||
var parameters = allParamNames.OrderBy(p => p).ToList();
|
||||
|
||||
// Create parameter nodes
|
||||
for (int i = 0; i < parameters.Count; i++)
|
||||
{
|
||||
var paramNode = $"param{i}";
|
||||
sb.AppendLine($" {paramNode}[\"@{parameters[i]}\"]");
|
||||
sb.AppendLine($" style {paramNode} fill:#e1f5ff");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
|
||||
// Create query nodes and connections
|
||||
for (int i = 0; i < queryBreakdowns.Count; i++)
|
||||
{
|
||||
var query = queryBreakdowns[i];
|
||||
var queryNode = $"query{i}";
|
||||
var queryType = DetermineQueryType(query);
|
||||
|
||||
sb.AppendLine($" {queryNode}[\"Query #{i}: {queryType}\"]");
|
||||
sb.AppendLine($" style {queryNode} fill:#f3e5f5");
|
||||
|
||||
// Collect all parameter names used by this query
|
||||
var queryParamNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
queryParamNames.Add(param.Name);
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var paramName in query.Parameters.Keys)
|
||||
{
|
||||
queryParamNames.Add(paramName);
|
||||
}
|
||||
|
||||
// Connect parameters to this query
|
||||
foreach (var paramName in queryParamNames)
|
||||
{
|
||||
var paramIndex = parameters.FindIndex(p => p.Equals(paramName, StringComparison.OrdinalIgnoreCase));
|
||||
if (paramIndex >= 0)
|
||||
{
|
||||
var paramNode = $"param{paramIndex}";
|
||||
sb.AppendLine($" {paramNode} --> {queryNode}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a detailed query composition report.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to report on.</param>
|
||||
/// <returns>Markdown composition report section.</returns>
|
||||
public static string GenerateQueryCompositionReport(QueryBreakdownCollection collection)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("## Query Composition Report");
|
||||
sb.AppendLine();
|
||||
|
||||
var summaries = collection.GetQuerySummaries().ToList();
|
||||
|
||||
for (int i = 0; i < summaries.Count; i++)
|
||||
{
|
||||
var summary = summaries[i];
|
||||
var query = collection.QueryBreakdowns[i];
|
||||
|
||||
AppendQueryCompositionTable(sb, i, summary);
|
||||
AppendQueryParameters(sb, query);
|
||||
AppendQueryCteSections(sb, summary, query);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the query composition table for a single query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCompositionTable(StringBuilder sb, int queryIndex, QuerySummary summary)
|
||||
{
|
||||
sb.AppendLine($"### Query #{queryIndex}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("| Aspect | Present |");
|
||||
sb.AppendLine("|--------|---------|");
|
||||
sb.AppendLine($"| SELECT Clause | {FormatClausePresence(summary.HasSelectClause)} |");
|
||||
sb.AppendLine($"| FROM Clause | {FormatClausePresence(summary.HasFromClause)} |");
|
||||
sb.AppendLine($"| WHERE Clause | {FormatClausePresence(summary.HasWhereClause)} |");
|
||||
sb.AppendLine($"| GROUP BY Clause | {FormatClausePresence(summary.HasGroupByClause)} |");
|
||||
sb.AppendLine($"| HAVING Clause | {FormatClausePresence(summary.HasHavingClause)} |");
|
||||
sb.AppendLine($"| ORDER BY Clause | {FormatClausePresence(summary.HasOrderByClause)} |");
|
||||
sb.AppendLine($"| CTE (WITH) | {FormatClausePresence(summary.HasCTE)} |");
|
||||
sb.AppendLine($"| Columns | {summary.ColumnCount} |");
|
||||
sb.AppendLine($"| Parameters | {summary.ParameterCount} |");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends parameter information for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryParameters(StringBuilder sb, QueryBreakdown query)
|
||||
{
|
||||
// Collect all unique parameters from both ParameterList and Parameters dictionary
|
||||
var allParams = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Add from ParameterList (parsed parameters)
|
||||
foreach (var param in query.ParameterList)
|
||||
{
|
||||
allParams[param.Name] = param.Value;
|
||||
}
|
||||
|
||||
// Add from Parameters dictionary (manually added parameters)
|
||||
foreach (var param in query.Parameters)
|
||||
{
|
||||
allParams[param.Key] = param.Value;
|
||||
}
|
||||
|
||||
if (allParams.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**Parameters Used:**");
|
||||
sb.AppendLine();
|
||||
foreach (var paramName in allParams.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var value = allParams[paramName];
|
||||
sb.AppendLine($"- `@{paramName}` = `{value?.ToString() ?? "NULL"}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends CTE section for a query.
|
||||
/// </summary>
|
||||
private static void AppendQueryCteSections(StringBuilder sb, QuerySummary summary, QueryBreakdown query)
|
||||
{
|
||||
if (!summary.HasCTE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
sb.AppendLine("**CTEs Defined:**");
|
||||
sb.AppendLine();
|
||||
foreach (var cte in query.WithClauses)
|
||||
{
|
||||
sb.AppendLine($"- `{cte.TableName}`");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a batch execution flow diagram.
|
||||
/// </summary>
|
||||
/// <param name="collection">The QueryBreakdownCollection to visualize.</param>
|
||||
/// <param name="includeTransaction">Whether to show transaction wrapping.</param>
|
||||
/// <returns>Mermaid diagram markdown.</returns>
|
||||
public static string GenerateBatchFlowDiagram(QueryBreakdownCollection collection, bool includeTransaction = false)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeId = 0;
|
||||
|
||||
// Handle empty collection
|
||||
if (collection.QueryBreakdowns.Count == 0)
|
||||
{
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node0[\"BEGIN TRANSACTION\"]");
|
||||
sb.AppendLine($" node0 --> node1[\"COMMIT TRANSACTION\"]");
|
||||
sb.AppendLine($" node1 --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> End([Batch Complete])");
|
||||
}
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
// Start node
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"BEGIN TRANSACTION\"]");
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId - 1} --> node{nodeId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" Start([Batch Start]) --> node{nodeId}");
|
||||
}
|
||||
|
||||
// Query nodes
|
||||
for (int i = 0; i < collection.QueryBreakdowns.Count; i++)
|
||||
{
|
||||
if (i < collection.QueryBreakdowns.Count - 1)
|
||||
{
|
||||
// Not the last query - connect to next
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Last query - connect to End (or COMMIT if transaction)
|
||||
if (includeTransaction)
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> node{nodeId + 1}");
|
||||
nodeId++;
|
||||
sb.AppendLine($" node{nodeId}[\"COMMIT TRANSACTION\"]");
|
||||
sb.AppendLine($" node{nodeId} --> End([Batch Complete])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($" node{nodeId}[\"Query {i}\"] --> End([Batch Complete])");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
sb.AppendLine();
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parameter type name from a parameter value.
|
||||
/// </summary>
|
||||
private static string GetParameterType(object? value)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
null => "NULL",
|
||||
bool => "BIT",
|
||||
byte => "TINYINT",
|
||||
short => "SMALLINT",
|
||||
int => "INT",
|
||||
long => "BIGINT",
|
||||
float => "REAL",
|
||||
double => "FLOAT",
|
||||
decimal => "DECIMAL",
|
||||
string => "NVARCHAR",
|
||||
DateTime => "DATETIME2",
|
||||
_ => "VARIANT"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special Markdown characters.
|
||||
/// </summary>
|
||||
private static string EscapeMarkdown(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\\", "\\\\")
|
||||
.Replace("|", "\\|")
|
||||
.Replace("\n", "\\n");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Formats clause presence as Yes/No with checkmark/cross.
|
||||
/// </summary>
|
||||
private static string FormatClausePresence(bool isPresent)
|
||||
=> isPresent ? "✓ Yes" : "✗ No";
|
||||
|
||||
/// <summary>
|
||||
/// Determines the query type from a QueryBreakdown.
|
||||
/// </summary>
|
||||
private static string DetermineQueryType(QueryBreakdown query)
|
||||
{
|
||||
var hasSelect = !string.IsNullOrWhiteSpace(query.SelectClause?.Clause);
|
||||
if (hasSelect)
|
||||
{
|
||||
return "SELECT";
|
||||
}
|
||||
|
||||
var hasFrom = !string.IsNullOrWhiteSpace(query.FromClause?.Clause);
|
||||
return hasFrom ? "FROM" : "QUERY";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.Breakdowns.SqlServer;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid diagram markdown from SQL QueryBreakdown objects.
|
||||
/// Creates flowchart visualizations showing the query structure and flow.
|
||||
/// </summary>
|
||||
public class QueryBreakdownGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from a QueryBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="queryBreakdown">The QueryBreakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(QueryBreakdown queryBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Add title if provided
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Start Mermaid flowchart
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
|
||||
int nodeId = 1;
|
||||
|
||||
// Start node
|
||||
sb.AppendLine($" Start([Query Start]) --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
|
||||
// WITH clause (CTE)
|
||||
if (queryBreakdown.IsUsingWithClause)
|
||||
{
|
||||
sb.AppendLine($" Node{nodeId}[\"WITH Clause<br/>Common Table Expressions\"]");
|
||||
foreach (var withClause in queryBreakdown.WithClauses)
|
||||
{
|
||||
sb.AppendLine($" Node{nodeId} --> CTE{nodeId}[\"{EscapeMermaidText(withClause.TableName)}\"]");
|
||||
nodeId++;
|
||||
}
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// SELECT clause
|
||||
if (!string.IsNullOrEmpty(queryBreakdown.SelectClause.Clause))
|
||||
{
|
||||
var selectText = TruncateText(queryBreakdown.SelectClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"SELECT<br/>{EscapeMermaidText(selectText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// FROM clause
|
||||
if (queryBreakdown.IsUsingFromClause && !string.IsNullOrWhiteSpace(queryBreakdown.FromClause?.Clause))
|
||||
{
|
||||
var fromText = TruncateText(queryBreakdown.FromClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"FROM<br/>{EscapeMermaidText(fromText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// WHERE clause
|
||||
if (queryBreakdown.IsUsingWhereClause && !string.IsNullOrWhiteSpace(queryBreakdown.WhereClause?.Clause))
|
||||
{
|
||||
var whereText = TruncateText(queryBreakdown.WhereClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}{{\"WHERE<br/>{EscapeMermaidText(whereText)}\"}}");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// GROUP BY clause
|
||||
if (queryBreakdown.IsUsingGroupByClause && !string.IsNullOrWhiteSpace(queryBreakdown.GroupByClause?.Clause))
|
||||
{
|
||||
var groupByText = TruncateText(queryBreakdown.GroupByClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"GROUP BY<br/>{EscapeMermaidText(groupByText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// HAVING clause
|
||||
if (queryBreakdown.IsUsingHavingClause && !string.IsNullOrWhiteSpace(queryBreakdown.HavingClause?.Clause))
|
||||
{
|
||||
var havingText = TruncateText(queryBreakdown.HavingClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}{{\"HAVING<br/>{EscapeMermaidText(havingText)}\"}}");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// ORDER BY clause
|
||||
if (queryBreakdown.IsUsingOrderByClause && !string.IsNullOrWhiteSpace(queryBreakdown.OrderByClause?.Clause))
|
||||
{
|
||||
var orderByText = TruncateText(queryBreakdown.OrderByClause.Clause, 50);
|
||||
sb.AppendLine($" Node{nodeId}[\"ORDER BY<br/>{EscapeMermaidText(orderByText)}\"]");
|
||||
sb.AppendLine($" Node{nodeId - 1} --> Node{nodeId}");
|
||||
sb.AppendLine();
|
||||
nodeId++;
|
||||
}
|
||||
|
||||
// End node
|
||||
sb.AppendLine($" Node{nodeId - 1} --> End([Query End])");
|
||||
|
||||
// End Mermaid diagram
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a Mermaid flowchart diagram from any SQL breakdown implementing ISqlBreakdown.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The SQL breakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown diagram.</returns>
|
||||
public string GenerateMermaidDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
// If it's a QueryBreakdown, use the specialized method
|
||||
if (sqlBreakdown is QueryBreakdown qb)
|
||||
{
|
||||
return GenerateMermaidDiagram(qb, title);
|
||||
}
|
||||
|
||||
// For other SQL breakdowns, generate a simple diagram
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("flowchart TD");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(" Start([SQL Statement Start])");
|
||||
|
||||
if (sqlBreakdown.IsUsingSetupClause)
|
||||
{
|
||||
sb.AppendLine(" Start --> Setup[\"Setup Clauses\"]");
|
||||
sb.AppendLine(" Setup --> Main[\"Main Statement\"]");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" Start --> Main[\"Main Statement\"]");
|
||||
}
|
||||
|
||||
if (sqlBreakdown.IsUsingFinishClause)
|
||||
{
|
||||
sb.AppendLine(" Main --> Finish[\"Finish Clauses\"]");
|
||||
sb.AppendLine(" Finish --> End([SQL Statement End])");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine(" Main --> End([SQL Statement End])");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes text for Mermaid diagram labels to prevent syntax errors.
|
||||
/// </summary>
|
||||
private string EscapeMermaidText(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
.Replace("[", "[")
|
||||
.Replace("]", "]")
|
||||
.Replace("{", "{")
|
||||
.Replace("}", "}")
|
||||
.Replace("(", "(")
|
||||
.Replace(")", ")")
|
||||
.Replace("<", "<")
|
||||
.Replace(">", ">");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length and adds ellipsis if needed.
|
||||
/// </summary>
|
||||
private string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
using System.Text;
|
||||
using Strata.SqlTools.SqlBreakdown.Interfaces;
|
||||
|
||||
namespace Strata.SqlTools.Markdown.SqlServer;
|
||||
|
||||
/// <summary>
|
||||
/// Generates Mermaid sequence diagrams from SQL statements to visualize statement execution flow.
|
||||
/// </summary>
|
||||
public class SqlStatementGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a Mermaid sequence diagram showing SQL statement execution.
|
||||
/// </summary>
|
||||
/// <param name="sqlBreakdown">The SQL breakdown to visualize.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown sequence diagram.</returns>
|
||||
public string GenerateSequenceDiagram(ISqlBreakdown sqlBreakdown, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("sequenceDiagram");
|
||||
sb.AppendLine(" participant App as Application");
|
||||
sb.AppendLine(" participant DB as Database");
|
||||
sb.AppendLine();
|
||||
|
||||
// Setup clauses
|
||||
if (sqlBreakdown.IsUsingSetupClause)
|
||||
{
|
||||
sb.AppendLine(" App->>DB: Execute Setup Clauses");
|
||||
foreach (var setupClause in sqlBreakdown.SetupClauses)
|
||||
{
|
||||
var setupText = TruncateText(setupClause, 40);
|
||||
sb.AppendLine($" activate DB");
|
||||
sb.AppendLine($" Note right of DB: {EscapeMermaidText(setupText)}");
|
||||
sb.AppendLine($" DB-->>App: Setup Complete");
|
||||
sb.AppendLine($" deactivate DB");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Main statement
|
||||
sb.AppendLine(" App->>DB: Execute Main Statement");
|
||||
sb.AppendLine(" activate DB");
|
||||
sb.AppendLine($" Note right of DB: Process SQL Statement");
|
||||
sb.AppendLine(" DB-->>App: Return Results");
|
||||
sb.AppendLine(" deactivate DB");
|
||||
sb.AppendLine();
|
||||
|
||||
// Finish clauses
|
||||
if (sqlBreakdown.IsUsingFinishClause)
|
||||
{
|
||||
sb.AppendLine(" App->>DB: Execute Finish Clauses");
|
||||
sb.AppendLine(" activate DB");
|
||||
sb.AppendLine($" Note right of DB: Cleanup Operations");
|
||||
sb.AppendLine(" DB-->>App: Cleanup Complete");
|
||||
sb.AppendLine(" deactivate DB");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates an entity-relationship diagram for tables referenced in the SQL statement.
|
||||
/// </summary>
|
||||
/// <param name="tableNames">List of table names referenced in the query.</param>
|
||||
/// <param name="title">Optional title for the diagram.</param>
|
||||
/// <returns>A string containing the Mermaid markdown ER diagram.</returns>
|
||||
public string GenerateEntityRelationshipDiagram(IEnumerable<string> tableNames, string? title = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
{
|
||||
sb.AppendLine($"### {title}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("```mermaid");
|
||||
sb.AppendLine("erDiagram");
|
||||
sb.AppendLine();
|
||||
|
||||
foreach (var tableName in tableNames)
|
||||
{
|
||||
var cleanName = CleanTableName(tableName);
|
||||
sb.AppendLine($" {cleanName} {{");
|
||||
sb.AppendLine($" string columns \"Referenced in query\"");
|
||||
sb.AppendLine($" }}");
|
||||
}
|
||||
|
||||
sb.AppendLine("```");
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes text for Mermaid diagram labels.
|
||||
/// </summary>
|
||||
private string EscapeMermaidText(string text)
|
||||
{
|
||||
return text
|
||||
.Replace("\"", """)
|
||||
.Replace("\n", " ")
|
||||
.Replace("\r", "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Truncates text to a maximum length.
|
||||
/// </summary>
|
||||
private string TruncateText(string text, int maxLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) || text.Length <= maxLength)
|
||||
{
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.Substring(0, maxLength) + "...";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleans table name for use in Mermaid diagrams.
|
||||
/// </summary>
|
||||
private string CleanTableName(string tableName)
|
||||
{
|
||||
return tableName
|
||||
.Replace("[", "")
|
||||
.Replace("]", "")
|
||||
.Replace(".", "_")
|
||||
.Replace(" ", "_");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<LangVersion>latest</LangVersion>
|
||||
|
||||
<!-- NuGet Package Metadata -->
|
||||
<PackageId>Strata.SqlTools.Markdown</PackageId>
|
||||
<Version>1.0.0</Version>
|
||||
<Authors>Strata Decision Technology</Authors>
|
||||
<Company>Strata Decision Technology</Company>
|
||||
<Product>Strata SQL Utilities - Markdown</Product>
|
||||
<Description>Markdown documentation generation for Strata.SqlTools, including Mermaid diagram generation for SQL queries and Expression trees.</Description>
|
||||
<PackageTags>sql;markdown;mermaid;documentation;query-visualization;expression-trees</PackageTags>
|
||||
<PackageProjectUrl>https://github.com/stratadecision/sql-builder</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/stratadecision/sql-builder</RepositoryUrl>
|
||||
<RepositoryType>git</RepositoryType>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<PackageReleaseNotes>Initial release with Mermaid diagram generation for SQL queries and markdown generation for expression trees.</PackageReleaseNotes>
|
||||
<Copyright>Copyright © Strata Decision Technology 2024-2026</Copyright>
|
||||
|
||||
<!-- Build Configuration -->
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<IncludeSymbols>true</IncludeSymbols>
|
||||
<SymbolPackageFormat>symbols.nupkg</SymbolPackageFormat>
|
||||
<EmbedUntrackedSources>true</EmbedUntrackedSources>
|
||||
<ContinuousIntegrationBuild Condition="'$(CI)' == 'true'">true</ContinuousIntegrationBuild>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlBreakdown\Strata.SqlTools.SqlBreakdown.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.SqlServer\Strata.SqlTools.SqlServer.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.Snowflake\Strata.SqlTools.Snowflake.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.PostgreSql\Strata.SqlTools.PostgreSql.csproj" />
|
||||
<ProjectReference Include="..\Strata.SqlTools.LinqToSql\Strata.SqlTools.LinqToSql.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user