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));
+ }
}