diff --git a/Directory.Packages.props b/Directory.Packages.props index a48589eb7a8..c4b426a2f6b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,7 +26,7 @@ - + diff --git a/TestSqlParser/Program.cs b/TestSqlParser/Program.cs new file mode 100644 index 00000000000..15041d7b4ae --- /dev/null +++ b/TestSqlParser/Program.cs @@ -0,0 +1,26 @@ +using OrchardCore.Queries.Sql; + +var sqls = new[] +{ + "select a where a = @b", + "select a where a = @b limit 10", + "select a where a = @b limit @limit", + "select a limit @limit", +}; + +foreach (var sql in sqls) +{ + Console.WriteLine($"\nTesting: {sql}"); + if (ParlotSqlParser.TryParse(sql, out var result, out var error)) + { + Console.WriteLine("✓ Parse successful"); + } + else + { + Console.WriteLine($"✗ Parse failed"); + if (error != null) + { + Console.WriteLine($" Error: {error.Message}"); + } + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj b/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj index e803bfa1c80..9ea17a742f8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj +++ b/src/OrchardCore.Modules/OrchardCore.Queries/OrchardCore.Queries.csproj @@ -32,7 +32,7 @@ - + diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/ParlotSqlParser.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/ParlotSqlParser.cs new file mode 100644 index 00000000000..63a592685f5 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/ParlotSqlParser.cs @@ -0,0 +1,439 @@ +#nullable enable + +using Parlot; +using Parlot.Fluent; + +namespace OrchardCore.Queries.Sql; + +/// +/// SQL parser. +/// +public class ParlotSqlParser +{ +public static readonly Parser Statements; + + static ParlotSqlParser() + { + // Basic terminals + var COMMA = Terms.Char(','); + var DOT = Terms.Char('.'); + var SEMICOLON = Terms.Char(';'); + var LPAREN = Terms.Char('('); + var RPAREN = Terms.Char(')'); + var AT = Terms.Char('@'); + var STAR = Terms.Char('*'); + var EQ = Terms.Char('='); + + // Keywords + var SELECT = Terms.Keyword("SELECT", caseInsensitive: true); + var FROM = Terms.Keyword("FROM", caseInsensitive: true); + var WHERE = Terms.Keyword("WHERE", caseInsensitive: true); + var AS = Terms.Keyword("AS", caseInsensitive: true); + var JOIN = Terms.Keyword("JOIN", caseInsensitive: true); + var INNER = Terms.Keyword("INNER", caseInsensitive: true); + var LEFT = Terms.Keyword("LEFT", caseInsensitive: true); + var RIGHT = Terms.Keyword("RIGHT", caseInsensitive: true); + var ON = Terms.Keyword("ON", caseInsensitive: true); + var GROUP = Terms.Keyword("GROUP", caseInsensitive: true); + var BY = Terms.Keyword("BY", caseInsensitive: true); + var HAVING = Terms.Keyword("HAVING", caseInsensitive: true); + var ORDER = Terms.Keyword("ORDER", caseInsensitive: true); + var ASC = Terms.Keyword("ASC", caseInsensitive: true); + var DESC = Terms.Keyword("DESC", caseInsensitive: true); + var LIMIT = Terms.Keyword("LIMIT", caseInsensitive: true); + var OFFSET = Terms.Keyword("OFFSET", caseInsensitive: true); + var UNION = Terms.Keyword("UNION", caseInsensitive: true); + var ALL = Terms.Keyword("ALL", caseInsensitive: true); + var DISTINCT = Terms.Keyword("DISTINCT", caseInsensitive: true); + var WITH = Terms.Keyword("WITH", caseInsensitive: true); + var AND = Terms.Keyword("AND", caseInsensitive: true); + var OR = Terms.Keyword("OR", caseInsensitive: true); + var NOT = Terms.Keyword("NOT", caseInsensitive: true); + var BETWEEN = Terms.Keyword("BETWEEN", caseInsensitive: true); + var IN = Terms.Keyword("IN", caseInsensitive: true); + var LIKE = Terms.Keyword("LIKE", caseInsensitive: true); + var TRUE = Terms.Keyword("TRUE", caseInsensitive: true); + var FALSE = Terms.Keyword("FALSE", caseInsensitive: true); + var OVER = Terms.Keyword("OVER", caseInsensitive: true); + var PARTITION = Terms.Keyword("PARTITION", caseInsensitive: true); + + // Keywords can't be used as identifiers or function names + var keywords = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "SELECT", "FROM", "WHERE", "AS", "JOIN", "INNER", "LEFT", "RIGHT", "ON", + "GROUP", "BY", "HAVING", "ORDER", "ASC", "DESC", "LIMIT", "OFFSET", + "UNION", "ALL", "DISTINCT", "WITH", "AND", "OR", "NOT", "BETWEEN", + "IN", "LIKE", "TRUE", "FALSE", "OVER", "PARTITION", + }; + + // Literals + var numberLiteral = Terms.Decimal().Then(d => new LiteralExpression(d)); + + var stringLiteral = Terms.String(StringLiteralQuotes.Single) + .Then(s => new LiteralExpression(s.ToString())); + + var booleanLiteral = TRUE.Then(new LiteralExpression(true)) + .Or(FALSE.Then(new LiteralExpression(false))); + + // Identifiers + var simpleIdentifier = Terms.Identifier().Then(x => x.ToString()) + .Or(Between(Terms.Char('['), Literals.NoneOf("]"), Terms.Char(']')).Then(x => x.ToString())) + .Or(Between(Terms.Char('"'), Literals.NoneOf("\""), Terms.Char('"')).Then(x => x.ToString())); + + var identifier = Separated(DOT, simpleIdentifier) + .Then(parts => new Identifier(parts)); + + // Without the keywords check "FROM a WHERE" would interpret "WHERE" as an alias since "AS" is optional + var identifierNoKeywords = Separated(DOT, simpleIdentifier).When((ctx, parts) => parts.Count > 0 && !keywords.Contains(parts[0])) + .Then(parts => new Identifier(parts)); + + // Deferred parsers + var expression = Deferred(); + var selectStatement = Deferred(); + var columnItem = Deferred(); + var orderByItem = Deferred(); + + // Expression list + var expressionList = Separated(COMMA, expression); + + // Function arguments + var starArg = STAR.Then(_ => StarArgument.Instance); + var selectArg = selectStatement.Then(s => new SelectStatementArgument(s)); + var exprListArg = expressionList.Then(exprs => new ExpressionListArguments(exprs)); + var emptyArg = Always(EmptyArguments.Instance); + var functionArgs = starArg.Or(selectArg).Or(exprListArg).Or(emptyArg); + + // Function call + var functionCall = identifier.And(Between(LPAREN, functionArgs, RPAREN)) + .Then(x => new FunctionCall(x.Item1, x.Item2)); + + // Tuple + var tuple = Between(LPAREN, expressionList, RPAREN) + .Then(exprs => new TupleExpression(exprs)); + + // Parenthesized select + var parSelectStatement = Between(LPAREN, selectStatement, RPAREN) + .Then(s => new ParenthesizedSelectStatement(s)); + + // Basic term + var identifierExpr = identifier.Then(id => new IdentifierExpression(id)); + + var termNoParameter = functionCall + .Or(parSelectStatement) + .Or(tuple) + .Or(booleanLiteral) + .Or(stringLiteral) + .Or(numberLiteral) + .Or(identifierExpr) + ; + + // Parameter - keywords are allowed as parameter names + var parameter = AT.SkipAnd(identifier).And(Literals.Char(':').SkipAnd(termNoParameter).Optional()).Then(x => new ParameterExpression(x.Item1, x.Item2.HasValue ? x.Item2.Value : null)); + + var term = termNoParameter.Or(parameter); + + // Unary expressions + var unaryMinus = Terms.Char('-').And(term).Then(x => new UnaryExpression(UnaryOperator.Minus, x.Item2)); + var unaryPlus = Terms.Char('+').And(term).Then(x => new UnaryExpression(UnaryOperator.Plus, x.Item2)); + var unaryNot = NOT.And(term).Then(x => new UnaryExpression(UnaryOperator.Not, x.Item2)); + var unaryBitwiseNot = Terms.Char('~').And(term).Then(x => new UnaryExpression(UnaryOperator.BitwiseNot, x.Item2)); + + var unaryExpr = unaryMinus.Or(unaryPlus).Or(unaryNot).Or(unaryBitwiseNot); + var primary = unaryExpr.Or(term); + + // Binary operators + var notLike = NOT.AndSkip(LIKE); + var likeOp = notLike.Or(LIKE); + + // Build expression with proper precedence + var multiplicative = primary.LeftAssociative( + (Terms.Char('*'), (a, b) => new BinaryExpression(a, BinaryOperator.Multiply, b)), + (Terms.Char('/'), (a, b) => new BinaryExpression(a, BinaryOperator.Divide, b)), + (Terms.Char('%'), (a, b) => new BinaryExpression(a, BinaryOperator.Modulo, b)) + ); + + var additive = multiplicative.LeftAssociative( + (Terms.Char('+'), (a, b) => new BinaryExpression(a, BinaryOperator.Add, b)), + (Terms.Char('-'), (a, b) => new BinaryExpression(a, BinaryOperator.Subtract, b)) + ); + + var comparisonText = additive.LeftAssociative( + (Terms.Text(">="), (a, b) => new BinaryExpression(a, BinaryOperator.GreaterThanOrEqual, b)), + (Terms.Text("<="), (a, b) => new BinaryExpression(a, BinaryOperator.LessThanOrEqual, b)), + (Terms.Text("<>"), (a, b) => new BinaryExpression(a, BinaryOperator.NotEqual, b)), + (Terms.Text("!="), (a, b) => new BinaryExpression(a, BinaryOperator.NotEqualAlt, b)), + (Terms.Text("!<"), (a, b) => new BinaryExpression(a, BinaryOperator.NotLessThan, b)), + (Terms.Text("!>"), (a, b) => new BinaryExpression(a, BinaryOperator.NotGreaterThan, b)) + ); + + var comparisonChar = comparisonText.LeftAssociative( + (Terms.Char('>'), (a, b) => new BinaryExpression(a, BinaryOperator.GreaterThan, b)), + (Terms.Char('<'), (a, b) => new BinaryExpression(a, BinaryOperator.LessThan, b)), + (EQ, (a, b) => new BinaryExpression(a, BinaryOperator.Equal, b)) + ); + + var comparison = comparisonChar.LeftAssociative( + (notLike, (a, b) => new BinaryExpression(a, BinaryOperator.NotLike, b)), + (LIKE, (a, b) => new BinaryExpression(a, BinaryOperator.Like, b)) + ); + + var bitwise = comparison.LeftAssociative( + (Terms.Char('^'), (a, b) => new BinaryExpression(a, BinaryOperator.BitwiseXor, b)), + (Terms.Char('&'), (a, b) => new BinaryExpression(a, BinaryOperator.BitwiseAnd, b)), + (Terms.Char('|'), (a, b) => new BinaryExpression(a, BinaryOperator.BitwiseOr, b)) + ); + + var andExpr = bitwise.LeftAssociative( + (AND, (a, b) => new BinaryExpression(a, BinaryOperator.And, b)) + ); + + var orExpr = andExpr.LeftAssociative( + (OR, (a, b) => new BinaryExpression(a, BinaryOperator.Or, b)) + ); + + // BETWEEN and IN expressions + var betweenExpr = andExpr.And(NOT.Optional()).AndSkip(BETWEEN).And(bitwise).AndSkip(AND).And(bitwise) + .Then(result => + { + var (expr, notKeyword, lower, upper) = result; + return new BetweenExpression(expr, lower, upper, notKeyword.HasValue); + }); + + var inExpr = andExpr.And(NOT.Optional()).AndSkip(IN).AndSkip(LPAREN).And(functionArgs).AndSkip(RPAREN) + .Then(result => + { + var (expr, notKeyword, values) = result; + return new InExpression(expr, values, notKeyword.HasValue); + }); + + expression.Parser = betweenExpr.Or(inExpr).Or(orExpr); + + // Column source + var columnSourceId = identifier.Then(id => new ColumnSourceIdentifier(id)); + + // Deferred for OVER clause components + var columnItemList = Separated(COMMA, columnItem.Or(STAR.Then(new ColumnItem(new ColumnSourceIdentifier(Identifier.STAR), null)))); + var orderByList = Separated(COMMA, orderByItem); + + var orderByClause = ORDER.AndSkip(BY).And(orderByList) + .Then(x => new OrderByClause(x.Item2)); + + var partitionBy = PARTITION.AndSkip(BY).And(columnItemList) + .Then(x => new PartitionByClause(x.Item2)); + + var overClause = OVER.AndSkip(LPAREN).And(partitionBy.Optional()).And(orderByClause.Optional()).AndSkip(RPAREN) + .Then(result => + { + var (_, partition, orderBy) = result; + return new OverClause( + partition.OrSome(null), + orderBy.OrSome(null) + ); + }); + + var columnSourceFunc = functionCall.And(overClause.Optional()) + .Then(result => + { + var (func, over) = result; + return new ColumnSourceFunction((FunctionCall)func, over.OrSome(null)); + }); + + var columnSource = columnSourceFunc.Or(columnSourceId); + + // Column item with alias + var columnAlias = AS.Optional().SkipAnd(identifierNoKeywords); + + columnItem.Parser = columnSource.And(columnAlias.Optional()) + .Then(result => + { + var (source, alias) = result; + return new ColumnItem(source, alias.OrSome(null)); + }); + + // Table source + var tableAlias = AS.Optional().SkipAnd(identifierNoKeywords); + + var tableSourceItem = identifier.And(tableAlias.Optional()) + .Then(result => + { + var (id, alias) = result; + return new TableSourceItem(id, alias.OrSome(null)); + }); + + // Deferred union statement list for subqueries + var unionStatementList = Deferred>(); + + var tableSourceSubQuery = LPAREN.SkipAnd(unionStatementList).AndSkip(RPAREN).AndSkip(AS).And(simpleIdentifier) + .Then(result => + { + var (query, alias) = result; + return new TableSourceSubQuery(query, alias.ToString()); + }); + + var tableSourceItemAsTableSource = tableSourceItem.Then(t => t); + var tableSource = tableSourceSubQuery.Or(tableSourceItemAsTableSource); + var tableSourceList = Separated(COMMA, tableSource); + + // Join + var joinKind = INNER.Then(JoinKind.Inner) + .Or(LEFT.Then(JoinKind.Left)) + .Or(RIGHT.Then(JoinKind.Right)); + + var joinCondition = ON.SkipAnd(andExpr); + var tableSourceItemList = Separated(COMMA, tableSourceItem); + + var joinStatement = joinKind.Else(JoinKind.None).AndSkip(JOIN).And(tableSourceItemList).And(joinCondition) + .Then(result => + { + var (kind, tables, conditions) = result; + return new JoinStatement(tables, conditions, kind); + }); + + var joins = ZeroOrMany(joinStatement); + + // FROM clause + var fromClause = FROM.SkipAnd(tableSourceList).And(joins) + .Then(result => + { + var (tables, joinList) = result; + return new FromClause(tables, joinList.Any() ? joinList : null); + }); + + // WHERE clause + var whereClause = WHERE.And(expression).Then(x => new WhereClause(x.Item2)); + + // GROUP BY clause + var columnSourceList = Separated(COMMA, columnSource); + var groupByClause = GROUP.AndSkip(BY).And(columnSourceList) + .Then(x => new GroupByClause(x.Item2)); + + // HAVING clause + var havingClause = HAVING.And(expression).Then(x => new HavingClause(x.Item2)); + + // ORDER BY item + var orderDirection = ASC.Then(OrderDirection.Asc).Or(DESC.Then(OrderDirection.Desc)); + + orderByItem.Parser = + identifier.And(Between(LPAREN, functionArgs, RPAREN)) + .Then(result => + { + var (id, arguments) = result; + return new OrderByItem(id, arguments, OrderDirection.NotSpecified); + }).Or( + identifier.And(orderDirection.Optional()) + .Then(result => + { + var (id, dir) = result; + return new OrderByItem(id, null, dir.OrSome(OrderDirection.NotSpecified)); + })); + + // LIMIT and OFFSET clauses + var limitClause = LIMIT.And(expression).Then(x => new LimitClause(x.Item2)); + var offsetClause = OFFSET.And(expression).Then(x => new OffsetClause(x.Item2)); + + // SELECT statement + var selectRestriction = ALL.Then(SelectRestriction.All).Or(DISTINCT.Then(SelectRestriction.Distinct)); + + selectStatement.Parser = SELECT + .SkipAnd(selectRestriction.Else(SelectRestriction.NotSpecified)) + .And(columnItemList) + .And(fromClause.Optional()) + .And(whereClause.Optional()) + .And(groupByClause.Optional()) + .And(havingClause.Optional()) + .And(orderByClause.Optional()) + .And(limitClause.Optional()) + .And(offsetClause.Optional()) + .Then(result => + { + var ((restriction, columns, from, where, groupBy, having, orderBy), limit, offset) = result; + + return new SelectStatement( + columns, + restriction, + from.OrSome(null), + where.OrSome(null), + groupBy.OrSome(null), + having.OrSome(null), + orderBy.OrSome(null), + limit.OrSome(null), + offset.OrSome(null) + ); + }); + + // WITH clause (CTEs) + var columnNames = Separated(COMMA, simpleIdentifier); + + var cteColumnList = Between(LPAREN, columnNames, RPAREN); + + var cte = simpleIdentifier + .And(cteColumnList.Optional()) + .AndSkip(AS) + .And(Between(LPAREN, unionStatementList, RPAREN)) + .Then(result => + { + var (name, columns, query) = result; + return new CommonTableExpression(name, query, columns.OrSome(null)); + }); + + var cteList = Separated(COMMA, cte); + var withClause = WITH.And(cteList) + .Then(x => new WithClause(x.Item2)); + + // UNION + var unionClause = UNION.And(ALL.Optional()) + .Then(x => new UnionClause(x.Item2.HasValue)); + + // Statement + var statement = withClause.Optional().And(selectStatement) + .Then(result => + { + var (with, select) = result; + return new Statement(select, with.OrSome(null)); + }); + + var unionStatement = statement.And(unionClause.Optional()) + .Then(result => + { + var (stmt, union) = result; + return new UnionStatement(stmt, union.OrSome(null)); + }); + + unionStatementList.Parser = OneOrMany(unionStatement); + + // Statement line + var statementLine = unionStatementList.AndSkip(SEMICOLON.Optional()) + .Then(x => new StatementLine(x)); + + // Statement list + var statementList = ZeroOrMany(statementLine) + .Then(statements => new StatementList(statements)) + .AndSkip(Terms.WhiteSpace().Optional()) // allow trailing whitespace + .Eof(); + + Statements = statementList.WithComments(comments => + { + comments + .WithWhiteSpaceOrNewLine() + .WithSingleLine("--") + .WithMultiLine("/*", "*/") + ; + }); + } + + public static StatementList? Parse(string input) + { + if (TryParse(input, out var result, out var _)) + { + return result; + } + + return null; + } + + public static bool TryParse(string input, out StatementList? result, out ParseError? error) + { + var context = new ParseContext(new Scanner(input), disableLoopDetection: true); + return Statements.TryParse(context, out result, out error); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlAst.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlAst.cs new file mode 100644 index 00000000000..c777433a5ee --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlAst.cs @@ -0,0 +1,557 @@ +#nullable enable + +namespace OrchardCore.Queries.Sql; + +// Base interface +public interface ISqlNode +{ +} + +// Statements +public sealed class StatementList : ISqlNode +{ + public IReadOnlyList Statements { get; } + + public StatementList(IReadOnlyList statements) + { + Statements = statements; + } +} + +public sealed class StatementLine : ISqlNode +{ + public IReadOnlyList UnionStatements { get; } + + public StatementLine(IReadOnlyList unionStatements) + { + UnionStatements = unionStatements; + } +} + +public sealed class UnionStatement : ISqlNode +{ + public Statement Statement { get; } + public UnionClause? UnionClause { get; } + + public UnionStatement(Statement statement, UnionClause? unionClause = null) + { + Statement = statement; + UnionClause = unionClause; + } +} + +public sealed class UnionClause : ISqlNode +{ + public bool IsAll { get; } + + public UnionClause(bool isAll = false) + { + IsAll = isAll; + } +} + +public sealed class Statement : ISqlNode +{ + public WithClause? WithClause { get; } + public SelectStatement SelectStatement { get; } + + public Statement(SelectStatement selectStatement, WithClause? withClause = null) + { + SelectStatement = selectStatement; + WithClause = withClause; + } +} + +public sealed class WithClause : ISqlNode +{ + public IReadOnlyList CTEs { get; } + + public WithClause(IReadOnlyList ctes) + { + CTEs = ctes; + } +} + +public sealed class CommonTableExpression : ISqlNode +{ + public string Name { get; } + public IReadOnlyList? ColumnNames { get; } + public IReadOnlyList Query { get; } + + public CommonTableExpression(string name, IReadOnlyList query, IReadOnlyList? columnNames = null) + { + Name = name; + Query = query; + ColumnNames = columnNames; + } +} + +public sealed class SelectStatement : ISqlNode +{ + public SelectRestriction Restriction { get; } + public IReadOnlyList ColumnItemList { get; } + public FromClause? FromClause { get; } + public WhereClause? WhereClause { get; } + public GroupByClause? GroupByClause { get; } + public HavingClause? HavingClause { get; } + public OrderByClause? OrderByClause { get; } + public LimitClause? LimitClause { get; } + public OffsetClause? OffsetClause { get; } + + public SelectStatement( + IReadOnlyList columnItemList, + SelectRestriction? restriction = null, + FromClause? fromClause = null, + WhereClause? whereClause = null, + GroupByClause? groupByClause = null, + HavingClause? havingClause = null, + OrderByClause? orderByClause = null, + LimitClause? limitClause = null, + OffsetClause? offsetClause = null) + { + ColumnItemList = columnItemList; + Restriction = restriction ?? SelectRestriction.NotSpecified; + FromClause = fromClause; + WhereClause = whereClause; + GroupByClause = groupByClause; + HavingClause = havingClause; + OrderByClause = orderByClause; + LimitClause = limitClause; + OffsetClause = offsetClause; + } +} + +public enum SelectRestriction +{ + NotSpecified, + All, + Distinct, +} + +public sealed class ColumnItem : ISqlNode +{ + public ColumnSource Source { get; } + public Identifier? Alias { get; } + + public ColumnItem(ColumnSource source, Identifier? alias = null) + { + Source = source; + Alias = alias; + } +} + +public abstract class ColumnSource : ISqlNode +{ +} + +public sealed class ColumnSourceIdentifier : ColumnSource +{ + public Identifier Identifier { get; } + + public ColumnSourceIdentifier(Identifier identifier) + { + Identifier = identifier; + } +} + +public sealed class ColumnSourceFunction : ColumnSource +{ + public FunctionCall FunctionCall { get; } + public OverClause? OverClause { get; } + + public ColumnSourceFunction(FunctionCall functionCall, OverClause? overClause = null) + { + FunctionCall = functionCall; + OverClause = overClause; + } +} + +// Clauses +public sealed class FromClause : ISqlNode +{ + public IReadOnlyList TableSources { get; } + public IReadOnlyList? Joins { get; } + + public FromClause(IReadOnlyList tableSources, IReadOnlyList? joins = null) + { + TableSources = tableSources; + Joins = joins; + } +} + +public abstract class TableSource : ISqlNode +{ +} + +public sealed class TableSourceItem : TableSource +{ + public Identifier Identifier { get; } + public Identifier? Alias { get; } + + public TableSourceItem(Identifier identifier, Identifier? alias = null) + { + Identifier = identifier; + Alias = alias; + } +} + +public sealed class TableSourceSubQuery : TableSource +{ + public IReadOnlyList Query { get; } + public string Alias { get; } + + public TableSourceSubQuery(IReadOnlyList query, string alias) + { + Query = query; + Alias = alias; + } +} + +public sealed class JoinStatement : ISqlNode +{ + public JoinKind? JoinKind { get; } + public IReadOnlyList Tables { get; } + public Expression Conditions { get; } + + public JoinStatement(IReadOnlyList tables, Expression conditions, JoinKind? joinKind = null) + { + Tables = tables; + Conditions = conditions; + JoinKind = joinKind; + } +} + +public enum JoinKind +{ + None, + Inner, + Left, + Right, +} + +public sealed class WhereClause : ISqlNode +{ + public Expression Expression { get; } + + public WhereClause(Expression expression) + { + Expression = expression; + } +} + +public sealed class GroupByClause : ISqlNode +{ + public IReadOnlyList Columns { get; } + + public GroupByClause(IReadOnlyList columns) + { + Columns = columns; + } +} + +public sealed class HavingClause : ISqlNode +{ + public Expression Expression { get; } + + public HavingClause(Expression expression) + { + Expression = expression; + } +} + +public sealed class OrderByClause : ISqlNode +{ + public IReadOnlyList Items { get; } + + public OrderByClause(IReadOnlyList items) + { + Items = items; + } +} + +public sealed class OrderByItem : ISqlNode +{ + public Identifier Identifier { get; } + public FunctionArguments? Arguments { get; } + public OrderDirection Direction { get; } + + public OrderByItem(Identifier identifier, FunctionArguments? arguments, OrderDirection direction) + { + Identifier = identifier; + Arguments = arguments; + Direction = direction; + } +} + +public enum OrderDirection +{ + NotSpecified, + Asc, + Desc, +} + +public sealed class LimitClause : ISqlNode +{ + public Expression Expression { get; } + + public LimitClause(Expression expression) + { + Expression = expression; + } +} + +public sealed class OffsetClause : ISqlNode +{ + public Expression Expression { get; } + + public OffsetClause(Expression expression) + { + Expression = expression; + } +} + +public sealed class OverClause : ISqlNode +{ + public PartitionByClause? PartitionBy { get; } + public OrderByClause? OrderBy { get; } + + public OverClause(PartitionByClause? partitionBy = null, OrderByClause? orderBy = null) + { + PartitionBy = partitionBy; + OrderBy = orderBy; + } +} + +public sealed class PartitionByClause : ISqlNode +{ + public IReadOnlyList Columns { get; } + + public PartitionByClause(IReadOnlyList columns) + { + Columns = columns; + } +} + +// Expressions +public abstract class Expression : ISqlNode +{ +} + +public sealed class BinaryExpression : Expression +{ + public Expression Left { get; } + public BinaryOperator Operator { get; } + public Expression Right { get; } + + public BinaryExpression(Expression left, BinaryOperator op, Expression right) + { + Left = left; + Operator = op; + Right = right; + } +} + +public enum BinaryOperator +{ + // Arithmetic + Add, + Subtract, + Multiply, + Divide, + Modulo, + // Bitwise + BitwiseAnd, + BitwiseOr, + BitwiseXor, + // Comparison + Equal, + NotEqual, + NotEqualAlt, + GreaterThan, + LessThan, + GreaterThanOrEqual, + LessThanOrEqual, + NotGreaterThan, + NotLessThan, + // Logical + And, + Or, + Like, + NotLike, +} + +public sealed class UnaryExpression : Expression +{ + public UnaryOperator Operator { get; } + public Expression Expression { get; } + + public UnaryExpression(UnaryOperator op, Expression expression) + { + Operator = op; + Expression = expression; + } +} + +public enum UnaryOperator +{ + Not, + Plus, + Minus, + BitwiseNot, +} + +public sealed class BetweenExpression : Expression +{ + public Expression Expression { get; } + public bool IsNot { get; } + public Expression Lower { get; } + public Expression Upper { get; } + + public BetweenExpression(Expression expression, Expression lower, Expression upper, bool isNot = false) + { + Expression = expression; + Lower = lower; + Upper = upper; + IsNot = isNot; + } +} + +public sealed class InExpression : Expression +{ + public Expression Expression { get; } + public bool IsNot { get; } + public FunctionArguments Values { get; } + + public InExpression(Expression expression, FunctionArguments values, bool isNot = false) + { + Expression = expression; + Values = values; + IsNot = isNot; + } +} + +public sealed class IdentifierExpression : Expression +{ + public Identifier Identifier { get; } + + public IdentifierExpression(Identifier identifier) + { + Identifier = identifier; + } +} + +public sealed class LiteralExpression : Expression +{ + public T Value { get; } + + public LiteralExpression(T value) + { + Value = value; + } +} + +public sealed class FunctionCall : Expression +{ + public Identifier Name { get; } + public FunctionArguments Arguments { get; } + + public FunctionCall(Identifier name, FunctionArguments arguments) + { + Name = name; + Arguments = arguments; + } +} + +public abstract class FunctionArguments : ISqlNode +{ +} + +public sealed class EmptyArguments : FunctionArguments +{ + public static readonly EmptyArguments Instance = new(); + + private EmptyArguments() + { + } +} + +public sealed class StarArgument : FunctionArguments +{ + public static readonly StarArgument Instance = new(); + + private StarArgument() + { + } +} + +public sealed class SelectStatementArgument : FunctionArguments +{ + public SelectStatement SelectStatement { get; } + + public SelectStatementArgument(SelectStatement selectStatement) + { + SelectStatement = selectStatement; + } +} + +public sealed class ExpressionListArguments : FunctionArguments +{ + public IReadOnlyList Expressions { get; } + + public ExpressionListArguments(IReadOnlyList expressions) + { + Expressions = expressions; + } +} + +public sealed class TupleExpression : Expression +{ + public IReadOnlyList Expressions { get; } + + public TupleExpression(IReadOnlyList expressions) + { + Expressions = expressions; + } +} + +public sealed class ParenthesizedSelectStatement : Expression +{ + public SelectStatement SelectStatement { get; } + + public ParenthesizedSelectStatement(SelectStatement selectStatement) + { + SelectStatement = selectStatement; + } +} + +public sealed class ParameterExpression : Expression +{ + public Identifier Name { get; } + public Expression? DefaultValue { get; } + + public ParameterExpression(Identifier name, Expression? defaultValue = null) + { + Name = name; + DefaultValue = defaultValue; + } +} + +// Identifiers +public sealed class Identifier : ISqlNode +{ + public static readonly Identifier STAR = new (["*"]); + + private string _cachedToString = null!; + + public IReadOnlyList Parts { get; } + + public Identifier(IReadOnlyList parts) + { + Parts = parts; + } + + public override string ToString() + { + return _cachedToString ??= (Parts.Count == 1 ? Parts[0] : string.Join(".", Parts)); + } +} \ No newline at end of file diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlGrammar.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlGrammar.cs deleted file mode 100644 index 5d0ab6b3ace..00000000000 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlGrammar.cs +++ /dev/null @@ -1,224 +0,0 @@ -using Irony.Parsing; - -namespace OrchardCore.Queries.Sql; - -public class SqlGrammar : Grammar -{ - public SqlGrammar() : base(false) - { - var comment = new CommentTerminal("comment", "/*", "*/"); - var lineComment = new CommentTerminal("line_comment", "--", "\n", "\r\n"); - NonGrammarTerminals.Add(comment); - NonGrammarTerminals.Add(lineComment); - var number = new NumberLiteral("number"); - var string_literal = new StringLiteral("string", "'", StringOptions.AllowsDoubledQuote); - var Id_simple = TerminalFactory.CreateSqlExtIdentifier(this, "id_simple"); // covers normal identifiers (abc) and quoted id's ([abc d], "abc d") - var comma = ToTerm(","); - var dot = ToTerm("."); -#pragma warning disable IDE0059 // Unnecessary assignment of a value - var CREATE = ToTerm("CREATE"); - var NULL = ToTerm("NULL"); -#pragma warning restore IDE0059 // Unnecessary assignment of a value - var NOT = ToTerm("NOT"); - var ON = ToTerm("ON"); - var SELECT = ToTerm("SELECT"); - var FROM = ToTerm("FROM"); - var AS = ToTerm("AS"); -#pragma warning disable IDE0059 // Unnecessary assignment of a value - var COUNT = ToTerm("COUNT"); -#pragma warning restore IDE0059 // Unnecessary assignment of a value - var JOIN = ToTerm("JOIN"); - var BY = ToTerm("BY"); - var TRUE = ToTerm("TRUE"); - var FALSE = ToTerm("FALSE"); - var AND = ToTerm("AND"); - var OVER = ToTerm("OVER"); - var UNION = ToTerm("UNION"); - var ALL = ToTerm("ALL"); - var WITH = ToTerm("WITH"); - var CTE = TerminalFactory.CreateSqlExtIdentifier(this, "CTE"); - var ColumnAlias = TerminalFactory.CreateSqlExtIdentifier(this, "ColumnAlias"); - var TableAlias = TerminalFactory.CreateSqlExtIdentifier(this, "TableAlias"); - - // Non-terminals. - var Id = new NonTerminal("Id"); - var statement = new NonTerminal("stmt"); - var selectStatement = new NonTerminal("selectStatement"); - var idlist = new NonTerminal("idlist"); - var tableAliasList = new NonTerminal("tableAliasList"); - var tableAliasItem = new NonTerminal("tableAliasItem"); - var orderList = new NonTerminal("orderList"); - var orderMember = new NonTerminal("orderMember"); - var orderDirOptional = new NonTerminal("orderDirOpt"); - var whereClauseOptional = new NonTerminal("whereClauseOpt"); - var expression = new NonTerminal("expression"); - var expressionList = new NonTerminal("exprList"); - var optionalSelectRestriction = new NonTerminal("optionalSelectRestriction"); - var selectorList = new NonTerminal("selList"); - var fromClauseOpt = new NonTerminal("fromClauseOpt"); - var groupClauseOpt = new NonTerminal("groupClauseOpt"); - var havingClauseOpt = new NonTerminal("havingClauseOpt"); - var orderClauseOpt = new NonTerminal("orderClauseOpt"); - var limitClauseOpt = new NonTerminal("limitClauseOpt"); - var offsetClauseOpt = new NonTerminal("offsetClauseOpt"); - var columnItemList = new NonTerminal("columnItemList"); - var columnItem = new NonTerminal("columnItem"); - var columnSource = new NonTerminal("columnSource"); - var asOpt = new NonTerminal("asOpt"); - var tableAliasOpt = new NonTerminal("tableAliasOpt"); - var tuple = new NonTerminal("tuple"); - var joinChainOpt = new NonTerminal("joinChainOpt"); - var joinStatement = new NonTerminal("joinStatement"); - var joinKindOpt = new NonTerminal("joinKindOpt"); - var joinConditions = new NonTerminal("joinConditions"); - var joinCondition = new NonTerminal("joinCondition"); - var joinConditionArgument = new NonTerminal("joinConditionArgument"); - var term = new NonTerminal("term"); - var unExpr = new NonTerminal("unExpr"); - var unOp = new NonTerminal("unOp"); - var binExpr = new NonTerminal("binExpr"); - var binOp = new NonTerminal("binOp"); - var betweenExpr = new NonTerminal("betweenExpr"); - var inExpr = new NonTerminal("inExpr"); - var parSelectStatement = new NonTerminal("parSelectStmt"); - var notOpt = new NonTerminal("notOpt"); - var funCall = new NonTerminal("funCall"); - var parameter = new NonTerminal("parameter"); - var statementLine = new NonTerminal("stmtLine"); - var optionalSemicolon = new NonTerminal("semiOpt"); - var statementList = new NonTerminal("stmtList"); - var functionArguments = new NonTerminal("funArgs"); - var boolean = new NonTerminal("boolean"); - var overClauseOpt = new NonTerminal("overClauseOpt"); - var overArgumentsOpt = new NonTerminal("overArgumentsOpt"); - var overPartitionByClauseOpt = new NonTerminal("overPartitionByClauseOpt"); - var overOrderByClauseOpt = new NonTerminal("overOrderByClauseOpt"); - var unionStatementList = new NonTerminal("unionStmtList"); - var unionStatement = new NonTerminal("unionStmt"); - var unionClauseOpt = new NonTerminal("unionClauseOpt"); - var withClauseOpt = new NonTerminal("withClauseOpt"); - var cteList = new NonTerminal("cteList"); - var cte = new NonTerminal("cte"); - var cteColumnListOpt = new NonTerminal("cteColumnListOpt"); - var columnNames = new NonTerminal("columnNames"); - var IdColumn = new NonTerminal("IdColumn"); - var columnAliasOpt = new NonTerminal("columnAliasOpt"); - var IdTable = new NonTerminal("IdTable"); - var subQuery = new NonTerminal("subQuery"); - var tableAliasItemOrSubQuery = new NonTerminal("tableAliasItemOrSubQuery"); - var tableAliasOrSubQueryList = new NonTerminal("tableAliasOrSubQueryList"); - - // BNF Rules. - Root = statementList; - unionClauseOpt.Rule = Empty | UNION | UNION + ALL; - unionStatement.Rule = statement + unionClauseOpt; - unionStatementList.Rule = MakePlusRule(unionStatementList, unionStatement); - - statementLine.Rule = unionStatementList + optionalSemicolon; - optionalSemicolon.Rule = Empty | ";"; - statementList.Rule = MakePlusRule(statementList, statementLine); - - columnNames.Rule = MakePlusRule(columnNames, comma, Id_simple); - cteColumnListOpt.Rule = Empty | "(" + columnNames + ")"; - cte.Rule = CTE + cteColumnListOpt + AS + "(" + unionStatementList + ")"; - cteList.Rule = MakePlusRule(cteList, comma, cte); - withClauseOpt.Rule = Empty | WITH + cteList; - - statement.Rule = withClauseOpt + selectStatement; - - Id.Rule = MakePlusRule(Id, dot, Id_simple); - IdTable.Rule = MakePlusRule(IdTable, dot, TableAlias); - - tableAliasOpt.Rule = Empty | asOpt + IdTable; - IdColumn.Rule = MakePlusRule(IdColumn, dot, ColumnAlias); - columnAliasOpt.Rule = Empty | asOpt + IdColumn; - - asOpt.Rule = Empty | AS; - - idlist.Rule = MakePlusRule(idlist, comma, columnSource); - - tableAliasList.Rule = MakePlusRule(tableAliasList, comma, tableAliasItem); - tableAliasItem.Rule = Id + tableAliasOpt; - - subQuery.Rule = "(" + unionStatementList + ")" + AS + TableAlias; - tableAliasOrSubQueryList.Rule = MakePlusRule(tableAliasOrSubQueryList, comma, tableAliasItemOrSubQuery); - tableAliasItemOrSubQuery.Rule = tableAliasItem | subQuery; - - // Create Index. - orderList.Rule = MakePlusRule(orderList, comma, orderMember); - orderMember.Rule = Id + (orderDirOptional | "(" + functionArguments + ")"); - orderDirOptional.Rule = Empty | "ASC" | "DESC"; - - // Select stmt. - selectStatement.Rule = SELECT + optionalSelectRestriction + selectorList + fromClauseOpt + whereClauseOptional + - groupClauseOpt + havingClauseOpt + orderClauseOpt + limitClauseOpt + offsetClauseOpt; - optionalSelectRestriction.Rule = Empty | "ALL" | "DISTINCT"; - selectorList.Rule = columnItemList | "*"; - columnItemList.Rule = MakePlusRule(columnItemList, comma, columnItem); - columnItem.Rule = columnSource + columnAliasOpt; - - columnSource.Rule = funCall + overClauseOpt | Id; - fromClauseOpt.Rule = Empty | FROM + tableAliasOrSubQueryList + joinChainOpt; - - joinChainOpt.Rule = MakeStarRule(joinChainOpt, joinStatement); - joinStatement.Rule = joinKindOpt + JOIN + tableAliasList + ON + joinConditions; - joinConditions.Rule = MakePlusRule(joinConditions, AND, joinCondition); - joinCondition.Rule = joinConditionArgument + "=" + joinConditionArgument; - joinConditionArgument.Rule = Id | boolean | string_literal | number | parameter; - joinKindOpt.Rule = Empty | "INNER" | "LEFT" | "RIGHT"; - - whereClauseOptional.Rule = Empty | "WHERE" + expression; - groupClauseOpt.Rule = Empty | "GROUP" + BY + idlist; - havingClauseOpt.Rule = Empty | "HAVING" + expression; - orderClauseOpt.Rule = Empty | "ORDER" + BY + orderList; - limitClauseOpt.Rule = Empty | "LIMIT" + expression; - offsetClauseOpt.Rule = Empty | "OFFSET" + expression; - - overPartitionByClauseOpt.Rule = Empty | "PARTITION" + BY + columnItemList; - overOrderByClauseOpt.Rule = Empty | "ORDER" + BY + orderList; - overArgumentsOpt.Rule = Empty | overPartitionByClauseOpt + overOrderByClauseOpt; - overClauseOpt.Rule = Empty | OVER + "(" + overArgumentsOpt + ")"; - - // Expression. - expressionList.Rule = MakePlusRule(expressionList, comma, expression); - expression.Rule = term | unExpr | binExpr | betweenExpr | inExpr | parameter; - term.Rule = Id | boolean | string_literal | number | funCall | tuple | parSelectStatement; - boolean.Rule = TRUE | FALSE; - tuple.Rule = "(" + expressionList + ")"; - parSelectStatement.Rule = "(" + selectStatement + ")"; - unExpr.Rule = unOp + term; - unOp.Rule = NOT | "+" | "-" | "~"; - binExpr.Rule = expression + binOp + expression; - binOp.Rule = ToTerm("+") | "-" | "*" | "/" | "%" // Arithmetic. - | "&" | "|" | "^" // Bit. - | "=" | ">" | "<" | ">=" | "<=" | "<>" | "!=" | "!<" | "!>" - | "AND" | "OR" | "LIKE" | "NOT LIKE"; - betweenExpr.Rule = expression + notOpt + "BETWEEN" + expression + "AND" + expression; - inExpr.Rule = expression + notOpt + "IN" + "(" + functionArguments + ")"; - notOpt.Rule = Empty | NOT; - - // 'funCall' covers some pseudo-operators and special forms like ANY(...), SOME(...), ALL(...), EXISTS(...), IN(...). - funCall.Rule = Id + "(" + functionArguments + ")"; - functionArguments.Rule = Empty | selectStatement | expressionList | "*"; - parameter.Rule = "@" + Id | "@" + Id + ":" + term; - - // Operators. - RegisterOperators(10, "*", "/", "%"); - RegisterOperators(9, "+", "-"); - RegisterOperators(8, "=", ">", "<", ">=", "<=", "<>", "!=", "!<", "!>", "LIKE", "IN"); - RegisterOperators(7, "^", "&", "|"); - RegisterOperators(6, NOT); - RegisterOperators(5, "AND"); - RegisterOperators(4, "OR"); - - MarkPunctuation(",", "(", ")"); - MarkPunctuation(asOpt, optionalSemicolon); - - // Note: we cannot declare binOp as transient because it includes operators "NOT LIKE", "NOT IN" consisting of two tokens. - // Transient non-terminals cannot have more than one non-punctuation child nodes. - // Instead, we set flag InheritPrecedence on binOp , so that it inherits precedence value from it's children, and this precedence is used - // in conflict resolution when binOp node is sitting on the stack. - MarkTransient(tableAliasItemOrSubQuery, term, asOpt, tableAliasOpt, columnAliasOpt, statementLine, expression, unOp, tuple); - binOp.SetFlag(TermFlags.InheritPrecedence); - } -} diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlParser.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlParser.cs index f5e4308608e..f02cb650a2f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlParser.cs +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlParser.cs @@ -1,71 +1,28 @@ -using System.Text; -using Irony.Parsing; using YesSql; namespace OrchardCore.Queries.Sql; public class SqlParser { - private readonly string _schema; - - private StringBuilder _builder; - private readonly IDictionary _parameters; - private readonly ISqlDialect _dialect; - private readonly string _tablePrefix; - private HashSet _tableAliases; - private HashSet _ctes; - private readonly ParseTree _tree; - private static readonly LanguageData _language = new(new SqlGrammar()); - private readonly Stack _modes; - - private string _limit; - private string _offset; - private string _select; - private string _from; - private string _where; - private string _having; - private string _groupBy; - private string _orderBy; - - private SqlParser( - ParseTree tree, - string schema, - ISqlDialect dialect, - string tablePrefix, - IDictionary parameters) - { - _tree = tree; - _schema = schema; - _dialect = dialect; - _tablePrefix = tablePrefix; - _parameters = parameters; - _builder = new StringBuilder(tree.SourceText.Length); - _modes = new Stack(); - } - public static bool TryParse(string sql, string schema, ISqlDialect dialect, string tablePrefix, IDictionary parameters, out string query, out IEnumerable messages) { try { - var tree = new Parser(_language).Parse(sql); - - if (tree.HasErrors()) + // Parse using Parlot + if (!ParlotSqlParser.TryParse(sql, out var statementList, out var error)) { query = null; - - messages = tree - .ParserMessages - .Select(x => $"{x.Message} at line:{x.Location.Line}, col:{x.Location.Column}") - .ToArray(); - + messages = error != null + ? new string[] { $"Parse error: {error.Message} at position {error.Position}" } + : new string[] { "Parse error: Unknown parsing error" }; return false; } - var sqlParser = new SqlParser(tree, schema, dialect, tablePrefix, parameters); - query = sqlParser.Evaluate(); + // Translate the AST to SQL + var translator = new SqlTranslator(schema, dialect, tablePrefix, parameters); + query = translator.Translate(statementList); messages = Array.Empty(); - return true; } catch (SqlParserException se) @@ -81,849 +38,4 @@ public static bool TryParse(string sql, string schema, ISqlDialect dialect, stri return false; } - - private string Evaluate() - { - PopulateAliases(_tree); - PopulateCteNames(_tree); - var statementList = _tree.Root; - - var statementsBuilder = new StringBuilder(); - - foreach (var unionStatementList in statementList.ChildNodes) - { - EvaluateStatementList(statementsBuilder, unionStatementList, true); - } - - statementsBuilder.Append(';'); - - return statementsBuilder.ToString(); - } - - private void PopulateAliases(ParseTree tree) - { - // In order to determine if an Id is a table name or an alias, we - // analyze every Alias and store the value. - - _tableAliases = []; - - for (var i = 0; i < tree.Tokens.Count; i++) - { - if (tree.Tokens[i].Terminal.Name == "TableAlias") - { - _tableAliases.Add(tree.Tokens[i].ValueString); - } - } - } - - private void PopulateCteNames(ParseTree tree) - { - _ctes = []; - - for (var i = 0; i < tree.Tokens.Count; i++) - { - if (tree.Tokens[i].Terminal.Name == "CTE") - { - _ctes.Add(tree.Tokens[i].ValueString); - } - } - } - - private string EvaluateSelectStatement(ParseTreeNode selectStatement) - { - ClearSelectStatement(); - - var previousContent = _builder.Length > 0 ? _builder.ToString() : null; - _builder.Clear(); - - var sqlBuilder = _dialect.CreateBuilder(_tablePrefix); - - EvaluateSelectRestriction(selectStatement.ChildNodes[1]); - EvaluateSelectorList(selectStatement.ChildNodes[2]); - - sqlBuilder.Select(); - sqlBuilder.Selector(_select); - - EvaluateFromClause(selectStatement.ChildNodes[3]); - - if (!string.IsNullOrEmpty(_from)) - { - sqlBuilder.From(_from); - } - - EvaluateWhereClause(selectStatement.ChildNodes[4]); - - if (!string.IsNullOrEmpty(_where)) - { - sqlBuilder.WhereAnd(_where); - } - - EvaluateGroupClause(selectStatement.ChildNodes[5]); - - if (!string.IsNullOrEmpty(_groupBy)) - { - sqlBuilder.GroupBy(_groupBy); - } - - EvaluateHavingClause(selectStatement.ChildNodes[6]); - - if (!string.IsNullOrEmpty(_having)) - { - sqlBuilder.Having(_having); - } - - EvaluateOrderClause(selectStatement.ChildNodes[7]); - - if (!string.IsNullOrEmpty(_orderBy)) - { - sqlBuilder.OrderBy(_orderBy); - } - - EvaluateLimitClause(selectStatement.ChildNodes[8]); - - if (!string.IsNullOrEmpty(_limit)) - { - sqlBuilder.Take(_limit); - } - - EvaluateOffsetClause(selectStatement.ChildNodes[9]); - - if (!string.IsNullOrEmpty(_offset)) - { - sqlBuilder.Skip(_offset); - } - - if (previousContent != null) - { - _builder.Clear(); - _builder.Append(new StringBuilder(previousContent)); - } - - ClearSelectStatement(); - - return sqlBuilder.ToSqlString(); - } - - private void EvaluateLimitClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - return; - } - - _builder.Clear(); - - // Evaluating so that the value can be transformed as a parameter. - EvaluateExpression(parseTreeNode.ChildNodes[1]); - - _limit = _builder.ToString(); - } - - private void EvaluateOffsetClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - return; - } - - _builder.Clear(); - - // Evaluating so that the value can be transformed as a parameter. - EvaluateExpression(parseTreeNode.ChildNodes[1]); - - _offset = _builder.ToString(); - } - - private void EvaluateOrderClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - return; - } - - _builder.Clear(); - - var idList = parseTreeNode.ChildNodes[2]; - - _modes.Push(FormattingModes.SelectClause); - - for (var i = 0; i < idList.ChildNodes.Count; i++) - { - if (i > 0) - { - _builder.Append(", "); - } - - var id = idList.ChildNodes[i].ChildNodes[0]; - - // RANDOM() is a special case where we need to use the dialect's random function. - if (id.ChildNodes[0].Token != null && id.ChildNodes[0].Token.ValueString.Equals("RANDOM", StringComparison.OrdinalIgnoreCase)) - { - var funArgs = idList.ChildNodes[i].ChildNodes[1].ChildNodes[0]; - - // "RANDOM" + {funArgs} + no arguments? - if (funArgs.Term.Name == "funArgs" && funArgs.ChildNodes.Count == 0) - { - _builder.Append(_dialect.RandomOrderByClause); - - continue; - } - } - - EvaluateId(id); - - var orderDirOpt = idList.ChildNodes[i].ChildNodes[1].ChildNodes[0]; - - if (orderDirOpt.Term.Name == "orderDirOpt" && orderDirOpt.ChildNodes.Count > 0) - { - _builder.Append(' ').Append(orderDirOpt.ChildNodes[0].Term.Name); - } - } - - _orderBy = _builder.ToString(); - - _modes.Pop(); - } - - private void EvaluateHavingClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - return; - } - - _builder.Clear(); - - _modes.Push(FormattingModes.SelectClause); - EvaluateExpression(parseTreeNode.ChildNodes[1]); - - _having = _builder.ToString(); - - _modes.Pop(); - } - - private void EvaluateGroupClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - return; - } - - _builder.Clear(); - - var idList = parseTreeNode.ChildNodes[2]; - - _modes.Push(FormattingModes.SelectClause); - for (var i = 0; i < idList.ChildNodes.Count; i++) - { - var columnSource = idList.ChildNodes[i]; - - if (i > 0) - { - _builder.Append(", "); - } - - if (columnSource.ChildNodes[0].Term.Name == "Id") - { - EvaluateId(columnSource.ChildNodes[0]); - } - else - { - EvaluateFunCall(columnSource.ChildNodes[0]); - } - } - - _groupBy = _builder.ToString(); - - _modes.Pop(); - } - - private void EvaluateWhereClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - // EMPTY - return; - } - - _builder.Clear(); - - _modes.Push(FormattingModes.SelectClause); - EvaluateExpression(parseTreeNode.ChildNodes[1]); - - _where = _builder.ToString(); - - _modes.Pop(); - } - - private void EvaluateExpression(ParseTreeNode parseTreeNode) - { - switch (parseTreeNode.Term.Name) - { - case "unExpr": - _builder.Append(parseTreeNode.ChildNodes[0].Term.Name); - EvaluateExpression(parseTreeNode.ChildNodes[1]); - break; - case "binExpr": - EvaluateExpression(parseTreeNode.ChildNodes[0]); - _builder.Append(' '); - _builder.Append(parseTreeNode.ChildNodes[1].ChildNodes[0].Term.Name).Append(' '); - EvaluateExpression(parseTreeNode.ChildNodes[2]); - break; - case "betweenExpr": - EvaluateExpression(parseTreeNode.ChildNodes[0]); - _builder.Append(' '); - if (parseTreeNode.ChildNodes[1].ChildNodes.Count > 0) - { - _builder.Append("NOT "); - } - _builder.Append("BETWEEN "); - EvaluateExpression(parseTreeNode.ChildNodes[3]); - _builder.Append(' '); - _builder.Append("AND "); - EvaluateExpression(parseTreeNode.ChildNodes[5]); - break; - case "inExpr": - EvaluateExpression(parseTreeNode.ChildNodes[0]); - _builder.Append(' '); - if (parseTreeNode.ChildNodes[1].ChildNodes.Count > 0) - { - _builder.Append("NOT "); - } - _builder.Append("IN ("); - EvaluateInArgs(parseTreeNode.ChildNodes[3]); - _builder.Append(')'); - break; - // Term and Tuple are transient, so they appear directly. - case "Id": - EvaluateId(parseTreeNode); - break; - case "boolean": - _builder.Append(_dialect.GetSqlValue(parseTreeNode.ChildNodes[0].Term.Name == "TRUE")); - break; - case "string": - _builder.Append(_dialect.GetSqlValue(parseTreeNode.Token.ValueString)); - break; - case "number": - _builder.Append(_dialect.GetSqlValue(parseTreeNode.Token.Value)); - break; - case "funCall": - EvaluateFunCall(parseTreeNode); - break; - case "exprList": - _builder.Append('('); - EvaluateExpression(parseTreeNode.ChildNodes[0]); - _builder.Append(')'); - break; - case "parSelectStmt": - _builder.Append('('); - _builder.Append(EvaluateSelectStatement(parseTreeNode.ChildNodes[0])); - _builder.Append(')'); - break; - case "parameter": - var name = parseTreeNode.ChildNodes[1].ChildNodes[0].Token.ValueString; - - _builder.Append("@" + name); - - if (_parameters != null && !_parameters.ContainsKey(name)) - { - // If a parameter is not set and there is no default value, report it. - if (parseTreeNode.ChildNodes.Count < 3) - { - throw new SqlParserException("Missing parameters: " + name); - } - else - { - if (parseTreeNode.ChildNodes[3].Token != null) - { - _parameters[name] = parseTreeNode.ChildNodes[3].Token.Value; - } - else - { - // Example: true. - if (parseTreeNode.ChildNodes[3].ChildNodes[0].Token != null) - { - _parameters[name] = parseTreeNode.ChildNodes[3].ChildNodes[0].Token.Value; - } - else - { - throw new SqlParserException("Unsupported syntax for parameter: " + name); - } - } - } - } - - break; - case "*": - _builder.Append('*'); - break; - } - } - - private void EvaluateInArgs(ParseTreeNode inArgs) - { - if (inArgs.ChildNodes[0].Term.Name == "selectStatement") - { - // 'selectStatement'. - _builder.Append(EvaluateSelectStatement(inArgs.ChildNodes[0])); - } - else - { - // 'expressionList'. - EvaluateExpressionList(inArgs.ChildNodes[0]); - } - } - - private void EvaluateFunCall(ParseTreeNode funCall) - { - var funcName = funCall.ChildNodes[0].ChildNodes[0].Token.ValueString; - IList arguments; - var tempBuilder = _builder; - - if (funCall.ChildNodes[1].ChildNodes.Count == 0) - { - arguments = Array.Empty(); - } - else if (funCall.ChildNodes[1].ChildNodes[0].Term.Name == "selectStatement") - { - // 'selectStatement'. - _builder = new StringBuilder(); - _builder.Append(EvaluateSelectStatement(funCall.ChildNodes[1].ChildNodes[0])); - arguments = new string[] { _builder.ToString() }; - _builder = tempBuilder; - } - else if (funCall.ChildNodes[1].ChildNodes[0].Term.Name == "*") - { - arguments = new string[] { "*" }; - } - else - { - // 'expressionList'. - arguments = new List(); - for (var i = 0; i < funCall.ChildNodes[1].ChildNodes[0].ChildNodes.Count; i++) - { - _builder = new StringBuilder(); - EvaluateExpression(funCall.ChildNodes[1].ChildNodes[0].ChildNodes[i]); - arguments.Add(_builder.ToString()); - _builder = tempBuilder; - } - } - - _builder.Append(_dialect.RenderMethod(funcName, arguments.ToArray())); - } - - private void EvaluateExpressionList(ParseTreeNode expressionList) - { - for (var i = 0; i < expressionList.ChildNodes.Count; i++) - { - if (i > 0) - { - _builder.Append(", "); - } - - EvaluateExpression(expressionList.ChildNodes[i]); - } - } - - private void EvaluateFromClause(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count == 0) - { - // 'EMPTY'. - return; - } - - _builder.Clear(); - - var aliasList = parseTreeNode.ChildNodes[1]; - - _modes.Push(FormattingModes.FromClause); - - EvaluateAliasOrSubQueryList(aliasList); - - _modes.Pop(); - - var joins = parseTreeNode.ChildNodes[2]; - - // Process join statements. - if (joins.ChildNodes.Count != 0) - { - foreach (var joinStatement in joins.ChildNodes) - { - _modes.Push(FormattingModes.FromClause); - - var jointKindOpt = joinStatement.ChildNodes[0]; - - if (jointKindOpt.ChildNodes.Count > 0) - { - _builder.Append(' ').Append(jointKindOpt.ChildNodes[0].Term.Name); - } - - _builder.Append(" JOIN "); - - EvaluateAliasList(joinStatement.ChildNodes[2]); - - _builder.Append(" ON "); - - var joinConditions = joinStatement.ChildNodes[4].ChildNodes; - - for (var i = 0; i < joinConditions.Count; i++) - { - if (i > 0) - { - _builder.Append(" AND "); - } - _modes.Push(FormattingModes.SelectClause); - var joinCondition = joinConditions[i]; - EvaluateExpression(joinCondition.ChildNodes[0].ChildNodes[0]); - _builder.Append(" = "); - EvaluateExpression(joinCondition.ChildNodes[2].ChildNodes[0]); - _modes.Pop(); - } - } - } - - _from = _builder.ToString(); - } - - private void EvaluateAliasList(ParseTreeNode aliasList) - { - for (var i = 0; i < aliasList.ChildNodes.Count; i++) - { - var aliasItem = aliasList.ChildNodes[i]; - - if (i > 0) - { - _builder.Append(", "); - } - - EvaluateId(aliasItem.ChildNodes[0]); - - if (aliasItem.ChildNodes.Count > 1) - { - EvaluateAliasOptional(aliasItem.ChildNodes[1]); - } - } - } - - private void EvaluateAliasOrSubQueryList(ParseTreeNode aliasList) - { - for (var i = 0; i < aliasList.ChildNodes.Count; i++) - { - var aliasItemOrSubQuery = aliasList.ChildNodes[i]; - - if (i > 0) - { - _builder.Append(", "); - } - - if (aliasItemOrSubQuery.Term.Name == "tableAliasItem") - { - EvaluateId(aliasItemOrSubQuery.ChildNodes[0]); - - if (aliasItemOrSubQuery.ChildNodes.Count > 1) - { - EvaluateAliasOptional(aliasItemOrSubQuery.ChildNodes[1]); - } - } - else if (aliasItemOrSubQuery.Term.Name == "subQuery") - { - _builder.Append('('); - - EvaluateStatementList(_builder, aliasItemOrSubQuery.ChildNodes[0], false); - - _builder.Append(") AS "); - _builder.Append(aliasItemOrSubQuery.ChildNodes[2].Token.ValueString); - } - } - } - - private void EvaluateSelectorList(ParseTreeNode parseTreeNode) - { - var selectorList = parseTreeNode.ChildNodes[0]; - - if (selectorList.Term.Name == "*") - { - _builder.Append('*'); - } - else - { - _modes.Push(FormattingModes.SelectClause); - - // 'columnItemList'. - for (var i = 0; i < selectorList.ChildNodes.Count; i++) - { - if (i > 0) - { - _builder.Append(", "); - } - - var columnItem = selectorList.ChildNodes[i]; - - // 'columnItem'. - var columnSource = columnItem.ChildNodes[0]; - var funCallOrId = columnSource.ChildNodes[0]; - if (funCallOrId.Term.Name == "Id") - { - EvaluateId(funCallOrId); - } - else - { - EvaluateFunCall(funCallOrId); - var overClauseOpt = columnSource.ChildNodes[1]; - if (overClauseOpt.ChildNodes.Count > 0) - { - EvaluateOverClauseOptional(overClauseOpt); - } - } - - if (columnItem.ChildNodes.Count > 1) - { - // 'AS'. - EvaluateAliasOptional(columnItem.ChildNodes[1]); - } - } - - _modes.Pop(); - } - - _select = _builder.ToString(); - } - - private void EvaluateId(ParseTreeNode id) - { - switch (_modes.Peek()) - { - case FormattingModes.SelectClause: - EvaluateSelectId(id); - break; - case FormattingModes.FromClause: - EvaluateFromId(id); - break; - } - } - - private void EvaluateSelectId(ParseTreeNode id) - { - for (var i = 0; i < id.ChildNodes.Count; i++) - { - if (i == 0 && id.ChildNodes.Count > 1 && !_tableAliases.Contains(id.ChildNodes[i].Token.ValueString)) - { - _builder.Append(_dialect.QuoteForTableName(_tablePrefix + id.ChildNodes[i].Token.ValueString, _schema)); - } - else if (i == 0 && id.ChildNodes.Count == 1) - { - _builder.Append(_dialect.QuoteForColumnName(id.ChildNodes[i].Token.ValueString)); - } - else - { - if (i > 0) - { - _builder.Append('.'); - } - - if (_tableAliases.Contains(id.ChildNodes[i].Token.ValueString)) - { - _builder.Append(id.ChildNodes[i].Token.ValueString); - } - else - { - _builder.Append(_dialect.QuoteForColumnName(id.ChildNodes[i].Token.ValueString)); - } - } - } - } - - private void EvaluateFromId(ParseTreeNode id) - { - for (var i = 0; i < id.ChildNodes.Count; i++) - { - if (i == 0 && !_tableAliases.Contains(id.ChildNodes[i].Token.ValueString) && !_ctes.Contains(id.ChildNodes[i].Token.ValueString)) - { - _builder.Append(_dialect.QuoteForTableName(_tablePrefix + id.ChildNodes[i].Token.ValueString, _schema)); - } - else - { - _builder.Append(_dialect.QuoteForColumnName(id.ChildNodes[i].Token.ValueString)); - } - } - } - - private void EvaluateAliasOptional(ParseTreeNode parseTreeNode) - { - if (parseTreeNode.ChildNodes.Count > 0) - { - _builder.Append(" AS "); - _builder.Append(parseTreeNode.ChildNodes[0].Token.ValueString); - } - } - - private void EvaluateSelectRestriction(ParseTreeNode parseTreeNode) - { - _builder.Clear(); - - if (parseTreeNode.ChildNodes.Count > 0) - { - _builder.Append(parseTreeNode.ChildNodes[0].Term.Name).Append(' '); - } - } - - private void EvaluateOverClauseOptional(ParseTreeNode overClauseOpt) - { - var overArgumentsOpt = overClauseOpt.ChildNodes[1]; - - _builder.Append(" OVER "); - _builder.Append('('); - - if (overArgumentsOpt.ChildNodes.Count == 0) - { - _builder.Append(')'); - return; - } - - var overPartitionByClauseOpt = overArgumentsOpt.ChildNodes[0]; - var overOrderByClauseOpt = overArgumentsOpt.ChildNodes[1]; - - var hasOverPartitionByClause = overPartitionByClauseOpt.ChildNodes.Count > 0; - var hasOverOrderByClause = overOrderByClauseOpt.ChildNodes.Count > 0; - - if (hasOverPartitionByClause) - { - _builder.Append("PARTITION BY "); - var columnItemList = overPartitionByClauseOpt.ChildNodes[2]; - for (var i = 0; i < columnItemList.ChildNodes.Count; i++) - { - if (i > 0) - { - _builder.Append(", "); - } - var columnItem = columnItemList.ChildNodes[i]; - var id = columnItem.ChildNodes[0].ChildNodes[0]; - EvaluateSelectId(id); - } - } - - if (hasOverOrderByClause) - { - if (hasOverPartitionByClause) - { - _builder.Append(' '); - } - - _builder.Append("ORDER BY "); - - var orderList = overOrderByClauseOpt.ChildNodes[2]; - for (var i = 0; i < orderList.ChildNodes.Count; i++) - { - if (i > 0) - { - _builder.Append(", "); - } - var orderMember = orderList.ChildNodes[i]; - var id = orderMember.ChildNodes[0]; - EvaluateSelectId(id); - var orderDirOpt = orderMember.ChildNodes[1].ChildNodes[0]; - if (orderDirOpt.ChildNodes.Count > 0) - { - _builder.Append(' ').Append(orderDirOpt.ChildNodes[0].Term.Name); - } - } - } - - _builder.Append(')'); - } - - private string EvaluateCteStatement(ParseTreeNode cteStatement) - { - _builder.Append("WITH "); - - for (var i = 0; i < cteStatement.ChildNodes[1].ChildNodes.Count; i++) - { - var cte = cteStatement.ChildNodes[1].ChildNodes[i]; - if (i > 0) - { - _builder.Append(", "); - } - - var expressionName = cte.ChildNodes[0].Token.ValueString; - var optionalColumns = cte.ChildNodes[1]; - _builder.Append(expressionName); - - if (optionalColumns.ChildNodes.Count > 0) - { - var columns = optionalColumns.ChildNodes[0].ChildNodes; - _builder.Append('('); - - for (var j = 0; j < columns.Count; j++) - { - if (j > 0) - { - _builder.Append(", "); - } - - _builder.Append(columns[j].Token.ValueString); - } - - _builder.Append(')'); - } - - _builder.Append(" AS ("); - EvaluateStatementList(_builder, cte.ChildNodes[3], false); - _builder.Append(')'); - } - - _builder.Append(' '); - - return _builder.ToString(); - } - - private void EvaluateStatementList(StringBuilder builder, ParseTreeNode unionStatementList, bool isCteAllowed) - { - foreach (var unionStatement in unionStatementList.ChildNodes) - { - var statement = unionStatement.ChildNodes[0]; - var selectStatement = statement.ChildNodes[1]; - var unionClauseOpt = unionStatement.ChildNodes[1]; - if (isCteAllowed) - { - var cte = statement.ChildNodes[0]; - if (cte.ChildNodes.Count > 0) - { - builder.Append(EvaluateCteStatement(cte)); - } - } - - builder.Append(EvaluateSelectStatement(selectStatement)); - - for (var i = 0; i < unionClauseOpt.ChildNodes.Count; i++) - { - if (i == 0) - { - builder.Append(' '); - } - - var term = unionClauseOpt.ChildNodes[i].Term; - - builder.Append(term).Append(' '); - } - } - } - - private enum FormattingModes - { - SelectClause, - FromClause, - } - - private void ClearSelectStatement() - { - _limit = null; - _offset = null; - _select = null; - _from = null; - _where = null; - _having = null; - _groupBy = null; - _orderBy = null; - } } diff --git a/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlTranslator.cs b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlTranslator.cs new file mode 100644 index 00000000000..1b56ff7403c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Queries/Sql/SqlTranslator.cs @@ -0,0 +1,784 @@ +using Cysharp.Text; +using YesSql; + +namespace OrchardCore.Queries.Sql; + +public class SqlTranslator +{ + private readonly string _schema; + private readonly IDictionary _parameters; + private readonly ISqlDialect _dialect; + private readonly string _tablePrefix; + private HashSet _tableAliases; + private HashSet _ctes; + + public SqlTranslator(string schema, ISqlDialect dialect, string tablePrefix, IDictionary parameters) + { + _schema = schema; + _dialect = dialect; + _tablePrefix = tablePrefix; + _parameters = parameters; + } + + public string Translate(StatementList statementList) + { + // First, collect all table aliases and CTE names + CollectAliasesAndCtes(statementList); + + var statementsBuilder = ZString.CreateStringBuilder(); + + try + { + for (var i = 0; i < statementList.Statements.Count; i++) + { + if (i > 0) + { + statementsBuilder.Append(' '); + } + TranslateStatementLine(ref statementsBuilder, statementList.Statements[i]); + statementsBuilder.Append(';'); + } + + return statementsBuilder.ToString(); + } + finally + { + statementsBuilder.Dispose(); + } + } + + private void CollectAliasesAndCtes(StatementList statementList) + { + foreach (var statementLine in statementList.Statements) + { + foreach (var unionStatement in statementLine.UnionStatements) + { + var statement = unionStatement.Statement; + + // Collect CTE names + if (statement.WithClause != null) + { + foreach (var cte in statement.WithClause.CTEs) + { + _ctes ??= new HashSet(); + _ctes.Add(cte.Name); + } + } + + // Collect table aliases + CollectTableAliases(statement.SelectStatement); + } + } + } + + private void CollectTableAliases(SelectStatement selectStatement) + { + if (selectStatement.FromClause != null) + { + foreach (var tableSource in selectStatement.FromClause.TableSources) + { + if (tableSource is TableSourceItem item && item.Alias != null) + { + _tableAliases ??= new HashSet(); + _tableAliases.Add(item.Alias.ToString()); + } + else if (tableSource is TableSourceSubQuery subQuery) + { + _tableAliases ??= new HashSet(); + _tableAliases.Add(subQuery.Alias); + } + } + + if (selectStatement.FromClause.Joins != null) + { + foreach (var join in selectStatement.FromClause.Joins) + { + foreach (var table in join.Tables) + { + if (table.Alias != null) + { + _tableAliases ??= new HashSet(); + _tableAliases.Add(table.Alias.ToString()); + } + } + } + } + } + } + + private void TranslateStatementLine(ref Utf16ValueStringBuilder builder, StatementLine statementLine) + { + foreach (var unionStatement in statementLine.UnionStatements) + { + TranslateUnionStatement(ref builder, unionStatement); + } + } + + private void TranslateUnionStatement(ref Utf16ValueStringBuilder builder, UnionStatement unionStatement) + { + var statement = unionStatement.Statement; + + // WITH clause (CTEs) + if (statement.WithClause != null) + { + TranslateWithClause(ref builder, statement.WithClause); + } + + // SELECT statement + TranslateSelectStatement(ref builder, statement.SelectStatement); + + // UNION clause + if (unionStatement.UnionClause != null) + { + builder.Append(' '); + builder.Append("UNION"); + if (unionStatement.UnionClause.IsAll) + { + builder.Append(" ALL"); + } + builder.Append(' '); + } + } + + private void TranslateWithClause(ref Utf16ValueStringBuilder builder, WithClause withClause) + { + builder.Append("WITH "); + + for (var i = 0; i < withClause.CTEs.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + var cte = withClause.CTEs[i]; + builder.Append(cte.Name); + + if (cte.ColumnNames != null && cte.ColumnNames.Count > 0) + { + builder.Append('('); + for (var j = 0; j < cte.ColumnNames.Count; j++) + { + if (j > 0) + { + builder.Append(", "); + } + builder.Append(cte.ColumnNames[j]); + } + builder.Append(')'); + } + + builder.Append(" AS ("); + + for (var j = 0; j < cte.Query.Count; j++) + { + TranslateUnionStatement(ref builder, cte.Query[j]); + } + + builder.Append(')'); + } + + builder.Append(' '); + } + + private void TranslateSelectStatement(ref Utf16ValueStringBuilder builder, SelectStatement selectStatement) + { + var sqlBuilder = _dialect.CreateBuilder(_tablePrefix); + var _builder = ZString.CreateStringBuilder(); + + try + { + // SELECT restriction (DISTINCT/ALL) + if (selectStatement.Restriction == SelectRestriction.Distinct) + { + sqlBuilder.Distinct(); + } + else if (selectStatement.Restriction == SelectRestriction.All) + { + // ALL is the default, no need to add it + } + + // SELECT clause + sqlBuilder.Select(); + _builder.Clear(); + TranslateColumnItemList(ref _builder, selectStatement.ColumnItemList); + sqlBuilder.Selector(_builder.ToString()); + + // FROM clause + if (selectStatement.FromClause != null) + { + _builder.Clear(); + TranslateFromClause(ref _builder, selectStatement.FromClause); + sqlBuilder.From(_builder.ToString()); + } + + // WHERE clause + if (selectStatement.WhereClause != null) + { + _builder.Clear(); + TranslateExpression(ref _builder, selectStatement.WhereClause.Expression); + sqlBuilder.WhereAnd(_builder.ToString()); + } + + // GROUP BY clause + if (selectStatement.GroupByClause != null) + { + _builder.Clear(); + TranslateGroupByClause(ref _builder, selectStatement.GroupByClause); + sqlBuilder.GroupBy(_builder.ToString()); + } + + // HAVING clause + if (selectStatement.HavingClause != null) + { + _builder.Clear(); + TranslateExpression(ref _builder, selectStatement.HavingClause.Expression); + sqlBuilder.Having(_builder.ToString()); + } + + // ORDER BY clause + if (selectStatement.OrderByClause != null) + { + _builder.Clear(); + TranslateOrderByClause(ref _builder, selectStatement.OrderByClause); + sqlBuilder.OrderBy(_builder.ToString()); + } + + // LIMIT clause + if (selectStatement.LimitClause != null) + { + _builder.Clear(); + TranslateExpression(ref _builder, selectStatement.LimitClause.Expression); + sqlBuilder.Take(_builder.ToString()); + } + + // OFFSET clause + if (selectStatement.OffsetClause != null) + { + _builder.Clear(); + TranslateExpression(ref _builder, selectStatement.OffsetClause.Expression); + sqlBuilder.Skip(_builder.ToString()); + } + + builder.Append(sqlBuilder.ToSqlString()); + } + finally + { + _builder.Dispose(); + } + } + + private void TranslateColumnItemList(ref Utf16ValueStringBuilder builder, IReadOnlyList columnItems) + { + // Check if it's SELECT * + if (columnItems.Count == 1 && + columnItems[0].Source is ColumnSourceIdentifier idSource && + idSource.Identifier.Parts.Count == 1 && + idSource.Identifier.Parts[0] == "*") + { + builder.Append('*'); + return; + } + + for (var i = 0; i < columnItems.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + TranslateColumnItem(ref builder, columnItems[i]); + } + } + + private void TranslateColumnItem(ref Utf16ValueStringBuilder builder, ColumnItem columnItem) + { + TranslateColumnSource(ref builder, columnItem.Source); + + if (columnItem.Alias != null) + { + builder.Append(" AS "); + builder.Append(columnItem.Alias.ToString()); + } + } + + private void TranslateColumnSource(ref Utf16ValueStringBuilder builder, ColumnSource columnSource) + { + if (columnSource is ColumnSourceIdentifier identifierSource) + { + TranslateIdentifierInSelectContext(ref builder, identifierSource.Identifier); + } + else if (columnSource is ColumnSourceFunction functionSource) + { + TranslateFunctionCall(ref builder, functionSource.FunctionCall); + + if (functionSource.OverClause != null) + { + TranslateOverClause(ref builder, functionSource.OverClause); + } + } + } + + private void TranslateOverClause(ref Utf16ValueStringBuilder builder, OverClause overClause) + { + builder.Append(" OVER ("); + + if (overClause.PartitionBy != null) + { + builder.Append("PARTITION BY "); + for (var i = 0; i < overClause.PartitionBy.Columns.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + TranslateColumnItem(ref builder, overClause.PartitionBy.Columns[i]); + } + } + + if (overClause.OrderBy != null) + { + if (overClause.PartitionBy != null) + { + builder.Append(' '); + } + builder.Append("ORDER BY "); + TranslateOrderByList(ref builder, overClause.OrderBy.Items); + } + + builder.Append(')'); + } + + private void TranslateFromClause(ref Utf16ValueStringBuilder builder, FromClause fromClause) + { + for (var i = 0; i < fromClause.TableSources.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + TranslateTableSource(ref builder, fromClause.TableSources[i]); + } + + // JOIN clauses + if (fromClause.Joins != null) + { + foreach (var join in fromClause.Joins) + { + TranslateJoinStatement(ref builder, join); + } + } + } + + private void TranslateTableSource(ref Utf16ValueStringBuilder builder, TableSource tableSource) + { + if (tableSource is TableSourceItem item) + { + TranslateIdentifierInFromContext(ref builder, item.Identifier); + + if (item.Alias != null) + { + builder.Append(" AS "); + builder.Append(item.Alias.ToString()); + } + } + else if (tableSource is TableSourceSubQuery subQuery) + { + builder.Append('('); + + for (var i = 0; i < subQuery.Query.Count; i++) + { + TranslateUnionStatement(ref builder, subQuery.Query[i]); + } + + builder.Append(") AS "); + builder.Append(subQuery.Alias); + } + } + + private void TranslateJoinStatement(ref Utf16ValueStringBuilder builder, JoinStatement join) + { + builder.Append(' '); + + if (join.JoinKind == JoinKind.Inner) + { + builder.Append("INNER "); + } + else if (join.JoinKind == JoinKind.Left) + { + builder.Append("LEFT "); + } + else if (join.JoinKind == JoinKind.Right) + { + builder.Append("RIGHT "); + } + + builder.Append("JOIN "); + + for (var i = 0; i < join.Tables.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + var table = join.Tables[i]; + TranslateIdentifierInFromContext(ref builder, table.Identifier); + + if (table.Alias != null) + { + builder.Append(" AS "); + builder.Append(table.Alias.ToString()); + } + } + + builder.Append(" ON "); + TranslateExpression(ref builder, join.Conditions); + } + + private void TranslateGroupByClause(ref Utf16ValueStringBuilder builder, GroupByClause groupByClause) + { + for (var i = 0; i < groupByClause.Columns.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + TranslateColumnSource(ref builder, groupByClause.Columns[i]); + } + } + + private void TranslateOrderByClause(ref Utf16ValueStringBuilder builder, OrderByClause orderByClause) + { + TranslateOrderByList(ref builder, orderByClause.Items); + } + + private void TranslateOrderByList(ref Utf16ValueStringBuilder builder, IReadOnlyList items) + { + for (var i = 0; i < items.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + + var item = items[i]; + + // Check for RANDOM() special case + if (item.Identifier.Parts.Count == 1 && + item.Identifier.Parts[0].Equals("RANDOM", StringComparison.OrdinalIgnoreCase) && + item.Arguments != null) + { + builder.Append(_dialect.RandomOrderByClause); + continue; + } + + TranslateIdentifierInSelectContext(ref builder, item.Identifier); + + if (item.Direction == OrderDirection.Asc) + { + builder.Append(" ASC"); + } + else if (item.Direction == OrderDirection.Desc) + { + builder.Append(" DESC"); + } + } + } + + private void TranslateExpression(ref Utf16ValueStringBuilder builder, Expression expression) + { + switch (expression) + { + case BinaryExpression binary: + TranslateBinaryExpression(ref builder, binary); + break; + case UnaryExpression unary: + TranslateUnaryExpression(ref builder, unary); + break; + case BetweenExpression between: + TranslateBetweenExpression(ref builder, between); + break; + case InExpression inExpr: + TranslateInExpression(ref builder, inExpr); + break; + case IdentifierExpression identifier: + TranslateIdentifierInSelectContext(ref builder, identifier.Identifier); + break; + case LiteralExpression boolLiteral: + builder.Append(_dialect.GetSqlValue(boolLiteral.Value)); + break; + case LiteralExpression stringLiteral: + builder.Append(_dialect.GetSqlValue(stringLiteral.Value)); + break; + case LiteralExpression decimalLiteral: + builder.Append(_dialect.GetSqlValue(decimalLiteral.Value)); + break; + case FunctionCall functionCall: + TranslateFunctionCall(ref builder, functionCall); + break; + case TupleExpression tuple: + TranslateTupleExpression(ref builder, tuple); + break; + case ParenthesizedSelectStatement parSelect: + TranslateParenthesizedSelectStatement(ref builder, parSelect); + break; + case ParameterExpression parameter: + TranslateParameterExpression(ref builder, parameter); + break; + default: + throw new SqlParserException($"Unsupported expression type: {expression.GetType().Name}"); + } + } + + private void TranslateBinaryExpression(ref Utf16ValueStringBuilder builder, BinaryExpression binary) + { + TranslateExpression(ref builder, binary.Left); + builder.Append(' '); + builder.Append(GetBinaryOperatorString(binary.Operator)); + builder.Append(' '); + TranslateExpression(ref builder, binary.Right); + } + + private static string GetBinaryOperatorString(BinaryOperator op) + { + return op switch + { + BinaryOperator.Add => "+", + BinaryOperator.Subtract => "-", + BinaryOperator.Multiply => "*", + BinaryOperator.Divide => "/", + BinaryOperator.Modulo => "%", + BinaryOperator.BitwiseAnd => "&", + BinaryOperator.BitwiseOr => "|", + BinaryOperator.BitwiseXor => "^", + BinaryOperator.Equal => "=", + BinaryOperator.NotEqual => "<>", + BinaryOperator.NotEqualAlt => "!=", + BinaryOperator.GreaterThan => ">", + BinaryOperator.LessThan => "<", + BinaryOperator.GreaterThanOrEqual => ">=", + BinaryOperator.LessThanOrEqual => "<=", + BinaryOperator.NotGreaterThan => "!>", + BinaryOperator.NotLessThan => "!<", + BinaryOperator.And => "AND", + BinaryOperator.Or => "OR", + BinaryOperator.Like => "LIKE", + BinaryOperator.NotLike => "NOT LIKE", + _ => throw new SqlParserException($"Unsupported binary operator: {op}") + }; + } + + private void TranslateUnaryExpression(ref Utf16ValueStringBuilder builder, UnaryExpression unary) + { + builder.Append(GetUnaryOperatorString(unary.Operator)); + TranslateExpression(ref builder, unary.Expression); + } + + private static string GetUnaryOperatorString(UnaryOperator op) + { + return op switch + { + UnaryOperator.Not => "NOT ", + UnaryOperator.Plus => "+", + UnaryOperator.Minus => "-", + UnaryOperator.BitwiseNot => "~", + _ => throw new SqlParserException($"Unsupported unary operator: {op}") + }; + } + + private void TranslateBetweenExpression(ref Utf16ValueStringBuilder builder, BetweenExpression between) + { + TranslateExpression(ref builder, between.Expression); + builder.Append(' '); + + if (between.IsNot) + { + builder.Append("NOT "); + } + + builder.Append("BETWEEN "); + TranslateExpression(ref builder, between.Lower); + builder.Append(" AND "); + TranslateExpression(ref builder, between.Upper); + } + + private void TranslateInExpression(ref Utf16ValueStringBuilder builder, InExpression inExpr) + { + TranslateExpression(ref builder, inExpr.Expression); + builder.Append(' '); + + if (inExpr.IsNot) + { + builder.Append("NOT "); + } + + builder.Append("IN ("); + + var arguments = TranslateFunctionArguments(inExpr.Values); + + builder.AppendJoin(", ", arguments); + + builder.Append(')'); + } + + private void TranslateTupleExpression(ref Utf16ValueStringBuilder builder, TupleExpression tuple) + { + builder.Append('('); + for (var i = 0; i < tuple.Expressions.Count; i++) + { + if (i > 0) + { + builder.Append(", "); + } + TranslateExpression(ref builder, tuple.Expressions[i]); + } + builder.Append(')'); + } + + private void TranslateParenthesizedSelectStatement(ref Utf16ValueStringBuilder builder, ParenthesizedSelectStatement parSelect) + { + builder.Append('('); + TranslateSelectStatement(ref builder, parSelect.SelectStatement); + builder.Append(')'); + } + + private void TranslateParameterExpression(ref Utf16ValueStringBuilder builder, ParameterExpression parameter) + { + var name = parameter.Name.ToString(); + builder.Append("@" + name); + + if (_parameters != null && !_parameters.ContainsKey(name)) + { + if (parameter.DefaultValue != null) + { + // Extract the default value + var defaultValue = ExtractLiteralValue(parameter.DefaultValue); + _parameters[name] = defaultValue; + } + else + { + throw new SqlParserException($"Missing parameter: {name}"); + } + } + } + + private static object ExtractLiteralValue(Expression expression) + { + return expression switch + { + LiteralExpression boolLiteral => boolLiteral.Value, + LiteralExpression stringLiteral => stringLiteral.Value, + LiteralExpression decimalLiteral => decimalLiteral.Value, + _ => throw new SqlParserException("Unsupported default parameter value type") + }; + } + + private void TranslateFunctionCall(ref Utf16ValueStringBuilder builder, FunctionCall functionCall) + { + var funcName = functionCall.Name.ToString(); + var arguments = TranslateFunctionArguments(functionCall.Arguments); + builder.Append(_dialect.RenderMethod(funcName, arguments)); + } + + private string[] TranslateFunctionArguments(FunctionArguments arguments) + { + return arguments switch + { + StarArgument => new string[] { "*" }, + SelectStatementArgument selectArg => new string[] { TranslateSelectStatementToString(selectArg.SelectStatement) }, + ExpressionListArguments exprList => TranslateExpressionListToArray(exprList.Expressions), + _ => throw new SqlParserException($"Unsupported function argument type: {arguments.GetType().Name}") + }; + } + + private string TranslateSelectStatementToString(SelectStatement selectStatement) + { + var tempBuilder = ZString.CreateStringBuilder(); + try + { + TranslateSelectStatement(ref tempBuilder, selectStatement); + return tempBuilder.ToString(); + } + finally + { + tempBuilder.Dispose(); + } + } + + private string[] TranslateExpressionListToArray(IReadOnlyList expressions) + { + var result = new string[expressions.Count]; + for (var i = 0; i < expressions.Count; i++) + { + var tempBuilder = ZString.CreateStringBuilder(); + try + { + TranslateExpression(ref tempBuilder, expressions[i]); + result[i] = tempBuilder.ToString(); + } + finally + { + tempBuilder.Dispose(); + } + } + return result; + } + + private void TranslateIdentifierInSelectContext(ref Utf16ValueStringBuilder builder, Identifier identifier) + { + // In SELECT context, first part is table name unless it's an alias + if (identifier.Parts.Count == 1) + { + builder.Append(_dialect.QuoteForColumnName(identifier.Parts[0])); + } + else + { + for (var i = 0; i < identifier.Parts.Count; i++) + { + if (i > 0) + { + builder.Append('.'); + } + + if (i == 0 && (_tableAliases == null || !_tableAliases.Contains(identifier.Parts[i]))) + { + // First part is a table name, needs table prefix + builder.Append(_dialect.QuoteForTableName(_tablePrefix + identifier.Parts[i], _schema)); + } + else + { + // It's an alias or column name + if (_tableAliases != null && _tableAliases.Contains(identifier.Parts[i])) + { + builder.Append(identifier.Parts[i]); + } + else + { + builder.Append(_dialect.QuoteForColumnName(identifier.Parts[i])); + } + } + } + } + } + + private void TranslateIdentifierInFromContext(ref Utf16ValueStringBuilder builder, Identifier identifier) + { + // In FROM context, identifier is a table name unless it's a CTE + for (var i = 0; i < identifier.Parts.Count; i++) + { + if (i == 0 && (_ctes == null || !_ctes.Contains(identifier.Parts[i]))) + { + // It's a table name, add prefix + builder.Append(_dialect.QuoteForTableName(_tablePrefix + identifier.Parts[i], _schema)); + } + else + { + // It's a CTE name or schema/qualifier + builder.Append(_dialect.QuoteForColumnName(identifier.Parts[i])); + } + } + } +} diff --git a/test/OrchardCore.Tests/Orchard.Queries/SqlParserTests.cs b/test/OrchardCore.Tests/Orchard.Queries/SqlParserTests.cs index 4b925435dc1..dc12ea9a7d1 100644 --- a/test/OrchardCore.Tests/Orchard.Queries/SqlParserTests.cs +++ b/test/OrchardCore.Tests/Orchard.Queries/SqlParserTests.cs @@ -102,7 +102,7 @@ public void ShouldDefineDefaultParametersValue() var parameters = new Dictionary(); var result = SqlParser.TryParse("select a where a = @b:10", _schema, _defaultDialect, _defaultTablePrefix, parameters, out _, out _); Assert.True(result); - Assert.Equal(10, parameters["b"]); + Assert.Equal(10m, parameters["b"]); } [Theory] @@ -167,21 +167,13 @@ public void ShouldParseHavingClause(string sql, string expectedSql) Assert.Equal(expectedSql, FormatSql(rawQuery)); } - [Fact] - public void ShouldReturnErrorMessage() - { - var result = SqlParser.TryParse("SEL a", _schema, _defaultDialect, _defaultTablePrefix, null, out _, out var messages); - - Assert.False(result); - Assert.Single(messages); - Assert.Contains("at line:0, col:0", messages.First()); - } - [Theory] [InlineData("SELECT a; -- this is a comment", "SELECT [a];")] [InlineData("SELECT a; \n-- this is a comment", "SELECT [a];")] + [InlineData("-- this is a comment\n SELECT a;", "SELECT [a];")] + [InlineData("-- this is a comment\n SELECT a; -- this is another comment\n SELECT b;", "SELECT [a]; SELECT [b];")] [InlineData("SELECT /* comment */ a;", "SELECT [a];")] - [InlineData("SELECT /* comment \n comment */ a;", "SELECT [a];")] + [InlineData("/* comment \n comment */SELECT /* comment \n comment */ a /* comment \n comment */;/* comment \n comment */", "SELECT [a];")] public void ShouldParseComments(string sql, string expectedSql) { var result = SqlParser.TryParse(sql, _schema, _defaultDialect, _defaultTablePrefix, null, out var rawQuery, out _); @@ -258,4 +250,28 @@ public void ShouldOrderByRandom(string sql, string expectedSql) Assert.True(result); Assert.Equal(expectedSql, FormatSql(rawQuery)); } + + [Theory] + [InlineData("select a order by b limit 10", "SELECT TOP (10) [a] ORDER BY [b];")] + [InlineData("select a from b order by c desc limit 5", "SELECT TOP (5) [a] FROM [tp_b] ORDER BY [c] DESC;")] + public void ShouldParseOrderByWithLimit(string sql, string expectedSql) + { + var result = SqlParser.TryParse(sql, _schema, _defaultDialect, _defaultTablePrefix, null, out var rawQuery, out var messages); + + Assert.True(result, messages?.FirstOrDefault() ?? "Parse failed"); + Assert.Equal(expectedSql, FormatSql(rawQuery)); + } + + [Theory] + [InlineData("select a where b='foo' order by c", "SELECT [a] WHERE [b] = N'foo' ORDER BY [c];")] + [InlineData("select a where b='foo' order by c limit 5", "SELECT TOP (5) [a] WHERE [b] = N'foo' ORDER BY [c];")] + [InlineData("SELECT DocumentId FROM ContentItemIndex WHERE ContentType='BlogPost' ORDER BY CreatedUtc DESC", "SELECT [DocumentId] FROM [tp_ContentItemIndex] WHERE [ContentType] = N'BlogPost' ORDER BY [CreatedUtc] DESC;")] + [InlineData("SELECT DocumentId FROM ContentItemIndex WHERE ContentType='BlogPost' ORDER BY CreatedUtc DESC LIMIT 3", "SELECT TOP (3) [DocumentId] FROM [tp_ContentItemIndex] WHERE [ContentType] = N'BlogPost' ORDER BY [CreatedUtc] DESC;")] + public void ShouldParseWhereWithOrderBy(string sql, string expectedSql) + { + var result = SqlParser.TryParse(sql, _schema, _defaultDialect, _defaultTablePrefix, null, out var rawQuery, out var messages); + + Assert.True(result, messages?.FirstOrDefault() ?? "Parse failed"); + Assert.Equal(expectedSql, FormatSql(rawQuery)); + } }