|
|
@@ -58,41 +58,48 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
|
|
|
/**
|
|
|
* Result of splitting an expression into aggregate and non-aggregate parts.
|
|
|
- *
|
|
|
+ *
|
|
|
* When a compound expression contains both aggregate and non-aggregate conditions,
|
|
|
* this class holds the separated parts for use in WHERE and HAVING clauses.
|
|
|
- *
|
|
|
+ *
|
|
|
+ * The parts are stored as Expression objects to preserve all type information,
|
|
|
+ * including parameter values in ParameterExpression nodes.
|
|
|
+ *
|
|
|
* Example:
|
|
|
* {{{
|
|
|
* // Input: "user_id > 100 && order_count >= 5"
|
|
|
* // Where order_count is an aggregate like COUNT(o.id)
|
|
|
- * split.non_aggregate_part == "user_id > 100" // Goes to WHERE
|
|
|
- * split.aggregate_part == "order_count >= 5" // Goes to HAVING
|
|
|
+ * split.non_aggregate_expression // Expression for "user_id > 100" - goes to WHERE
|
|
|
+ * split.aggregate_expression // Expression for "order_count >= 5" - goes to HAVING
|
|
|
* }}}
|
|
|
*/
|
|
|
public class SplitExpression : Object {
|
|
|
/**
|
|
|
* The non-aggregate part of the expression.
|
|
|
- *
|
|
|
+ *
|
|
|
* This part should be applied to the WHERE clause.
|
|
|
* May be null if the entire expression contains aggregates.
|
|
|
+ *
|
|
|
+ * Stored as Expression to preserve parameter values and type information.
|
|
|
*/
|
|
|
- public string? non_aggregate_part { get; construct; }
|
|
|
+ public Expression? non_aggregate_expression { get; construct; }
|
|
|
|
|
|
/**
|
|
|
* The aggregate part of the expression.
|
|
|
- *
|
|
|
+ *
|
|
|
* This part should be applied to the HAVING clause.
|
|
|
* May be null if the expression contains no aggregates.
|
|
|
+ *
|
|
|
+ * Stored as Expression to preserve parameter values and type information.
|
|
|
*/
|
|
|
- public string? aggregate_part { get; construct; }
|
|
|
+ public Expression? aggregate_expression { get; construct; }
|
|
|
|
|
|
/**
|
|
|
* Indicates if the expression combines aggregate and non-aggregate parts with OR.
|
|
|
- *
|
|
|
+ *
|
|
|
* When true, a subquery wrapper is required because SQL cannot mix
|
|
|
* WHERE and HAVING conditions with OR logic.
|
|
|
- *
|
|
|
+ *
|
|
|
* Example requiring subquery:
|
|
|
* {{{
|
|
|
* // "user_id > 100 || COUNT(o.id) >= 5"
|
|
|
@@ -104,19 +111,19 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
|
|
|
/**
|
|
|
* Creates a new SplitExpression.
|
|
|
- *
|
|
|
- * @param non_aggregate_part The WHERE clause part (or null)
|
|
|
- * @param aggregate_part The HAVING clause part (or null)
|
|
|
+ *
|
|
|
+ * @param non_aggregate_expression The WHERE clause Expression (or null)
|
|
|
+ * @param aggregate_expression The HAVING clause Expression (or null)
|
|
|
* @param needs_subquery True if subquery wrapper is needed
|
|
|
*/
|
|
|
public SplitExpression(
|
|
|
- string? non_aggregate_part,
|
|
|
- string? aggregate_part,
|
|
|
+ Expression? non_aggregate_expression,
|
|
|
+ Expression? aggregate_expression,
|
|
|
bool needs_subquery
|
|
|
) {
|
|
|
Object(
|
|
|
- non_aggregate_part: non_aggregate_part,
|
|
|
- aggregate_part: aggregate_part,
|
|
|
+ non_aggregate_expression: non_aggregate_expression,
|
|
|
+ aggregate_expression: aggregate_expression,
|
|
|
needs_subquery: needs_subquery
|
|
|
);
|
|
|
}
|
|
|
@@ -251,12 +258,15 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
|
|
|
/**
|
|
|
* Splits a parsed expression tree into aggregate and non-aggregate parts.
|
|
|
- *
|
|
|
+ *
|
|
|
* This method handles the expression tree structure properly, correctly
|
|
|
* identifying AND vs OR combinations and nested expressions.
|
|
|
- *
|
|
|
+ *
|
|
|
+ * All parts are preserved as Expression objects to maintain type information
|
|
|
+ * and parameter values.
|
|
|
+ *
|
|
|
* @param expr The parsed expression tree
|
|
|
- * @return A SplitExpression with the separated parts
|
|
|
+ * @return A SplitExpression with the separated parts as Expression objects
|
|
|
*/
|
|
|
private SplitExpression split_expression_tree(Expression expr) {
|
|
|
// Check if it's a binary expression (potential AND/OR)
|
|
|
@@ -269,7 +279,7 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
var right_split = split_expression_tree(binary.right);
|
|
|
|
|
|
// Combine results
|
|
|
- return combine_and_splits(left_split, right_split);
|
|
|
+ return combine_and_splits(left_split, right_split, binary);
|
|
|
}
|
|
|
|
|
|
// Handle OR - check if mixed
|
|
|
@@ -281,10 +291,10 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
if (left_has_agg == right_has_agg) {
|
|
|
if (left_has_agg) {
|
|
|
// Both aggregate - whole thing goes to HAVING
|
|
|
- return new SplitExpression(null, expr_to_string(expr), false);
|
|
|
+ return new SplitExpression(null, expr, false);
|
|
|
} else {
|
|
|
// Both non-aggregate - whole thing goes to WHERE
|
|
|
- return new SplitExpression(expr_to_string(expr), null, false);
|
|
|
+ return new SplitExpression(expr, null, false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -295,46 +305,59 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
|
|
|
// For non-binary or other operators, check if it contains aggregates
|
|
|
bool has_aggregate = expression_contains_aggregate(expr);
|
|
|
- string expr_str = expr_to_string(expr);
|
|
|
|
|
|
if (has_aggregate) {
|
|
|
- return new SplitExpression(null, expr_str, false);
|
|
|
+ return new SplitExpression(null, expr, false);
|
|
|
} else {
|
|
|
- return new SplitExpression(expr_str, null, false);
|
|
|
+ return new SplitExpression(expr, null, false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Combines two SplitExpressions from AND-connected expressions.
|
|
|
- *
|
|
|
+ *
|
|
|
+ * When both sides have expressions of the same type (aggregate or non-aggregate),
|
|
|
+ * they are combined into a new BinaryExpression with AND operator.
|
|
|
+ *
|
|
|
* @param left The split from the left side of AND
|
|
|
* @param right The split from the right side of AND
|
|
|
+ * @param original_binary The original BinaryExpression (used to preserve structure when needed)
|
|
|
* @return A combined SplitExpression
|
|
|
*/
|
|
|
- private SplitExpression combine_and_splits(SplitExpression left, SplitExpression right) {
|
|
|
+ private SplitExpression combine_and_splits(SplitExpression left, SplitExpression right, BinaryExpression original_binary) {
|
|
|
// If either needs subquery, propagate that
|
|
|
if (left.needs_subquery || right.needs_subquery) {
|
|
|
return new SplitExpression(null, null, true);
|
|
|
}
|
|
|
|
|
|
// Combine non-aggregate parts
|
|
|
- string? non_agg = null;
|
|
|
- if (left.non_aggregate_part != null && right.non_aggregate_part != null) {
|
|
|
- non_agg = @"($(left.non_aggregate_part) AND $(right.non_aggregate_part))";
|
|
|
- } else if (left.non_aggregate_part != null) {
|
|
|
- non_agg = left.non_aggregate_part;
|
|
|
- } else if (right.non_aggregate_part != null) {
|
|
|
- non_agg = right.non_aggregate_part;
|
|
|
+ Expression? non_agg = null;
|
|
|
+ if (left.non_aggregate_expression != null && right.non_aggregate_expression != null) {
|
|
|
+ // Both sides have non-aggregate parts - combine with AND
|
|
|
+ non_agg = new BinaryExpression(
|
|
|
+ left.non_aggregate_expression,
|
|
|
+ right.non_aggregate_expression,
|
|
|
+ BinaryOperator.AND
|
|
|
+ );
|
|
|
+ } else if (left.non_aggregate_expression != null) {
|
|
|
+ non_agg = left.non_aggregate_expression;
|
|
|
+ } else if (right.non_aggregate_expression != null) {
|
|
|
+ non_agg = right.non_aggregate_expression;
|
|
|
}
|
|
|
|
|
|
// Combine aggregate parts
|
|
|
- string? agg = null;
|
|
|
- if (left.aggregate_part != null && right.aggregate_part != null) {
|
|
|
- agg = @"($(left.aggregate_part) AND $(right.aggregate_part))";
|
|
|
- } else if (left.aggregate_part != null) {
|
|
|
- agg = left.aggregate_part;
|
|
|
- } else if (right.aggregate_part != null) {
|
|
|
- agg = right.aggregate_part;
|
|
|
+ Expression? agg = null;
|
|
|
+ if (left.aggregate_expression != null && right.aggregate_expression != null) {
|
|
|
+ // Both sides have aggregate parts - combine with AND
|
|
|
+ agg = new BinaryExpression(
|
|
|
+ left.aggregate_expression,
|
|
|
+ right.aggregate_expression,
|
|
|
+ BinaryOperator.AND
|
|
|
+ );
|
|
|
+ } else if (left.aggregate_expression != null) {
|
|
|
+ agg = left.aggregate_expression;
|
|
|
+ } else if (right.aggregate_expression != null) {
|
|
|
+ agg = right.aggregate_expression;
|
|
|
}
|
|
|
|
|
|
return new SplitExpression(non_agg, agg, false);
|
|
|
@@ -342,7 +365,7 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
|
|
|
/**
|
|
|
* Checks if an expression tree contains aggregate functions.
|
|
|
- *
|
|
|
+ *
|
|
|
* @param expr The expression tree to check
|
|
|
* @return True if aggregates are found
|
|
|
*/
|
|
|
@@ -351,18 +374,6 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
expr.accept(visitor);
|
|
|
return visitor.found_aggregate;
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * Converts an expression tree back to a string representation.
|
|
|
- *
|
|
|
- * @param expr The expression tree to convert
|
|
|
- * @return The string representation
|
|
|
- */
|
|
|
- private string expr_to_string(Expression expr) {
|
|
|
- var visitor = new ExpressionStringVisitor();
|
|
|
- expr.accept(visitor);
|
|
|
- return visitor.get_string();
|
|
|
- }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -595,181 +606,4 @@ namespace InvercargillSql.Orm.Projections {
|
|
|
// Collection literals don't contain aggregates
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- /**
|
|
|
- * Expression visitor that converts an expression tree back to a string.
|
|
|
- *
|
|
|
- * This is used to reconstruct expression strings after splitting.
|
|
|
- */
|
|
|
- internal class ExpressionStringVisitor : Object, ExpressionVisitor {
|
|
|
-
|
|
|
- private StringBuilder _builder;
|
|
|
- private string? _pending_property_name = null; // Property name waiting for target variable
|
|
|
-
|
|
|
- /**
|
|
|
- * Creates a new ExpressionStringVisitor.
|
|
|
- */
|
|
|
- public ExpressionStringVisitor() {
|
|
|
- _builder = new StringBuilder();
|
|
|
- _pending_property_name = null;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * Gets the string representation of the visited expression.
|
|
|
- *
|
|
|
- * @return The expression as a string
|
|
|
- */
|
|
|
- public string get_string() {
|
|
|
- return _builder.str;
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_binary(BinaryExpression expr) {
|
|
|
- _builder.append("(");
|
|
|
- expr.left.accept(this);
|
|
|
- _builder.append(get_operator_string(expr.op));
|
|
|
- expr.right.accept(this);
|
|
|
- _builder.append(")");
|
|
|
- }
|
|
|
-
|
|
|
- private string get_operator_string(BinaryOperator op) {
|
|
|
- switch (op) {
|
|
|
- case BinaryOperator.EQUAL: return " == ";
|
|
|
- case BinaryOperator.NOT_EQUAL: return " != ";
|
|
|
- case BinaryOperator.GREATER_THAN: return " > ";
|
|
|
- case BinaryOperator.GREATER_EQUAL: return " >= ";
|
|
|
- case BinaryOperator.LESS_THAN: return " < ";
|
|
|
- case BinaryOperator.LESS_EQUAL: return " <= ";
|
|
|
- case BinaryOperator.AND: return " && ";
|
|
|
- case BinaryOperator.OR: return " || ";
|
|
|
- case BinaryOperator.ADD: return " + ";
|
|
|
- case BinaryOperator.SUBTRACT: return " - ";
|
|
|
- case BinaryOperator.MULTIPLY: return " * ";
|
|
|
- case BinaryOperator.DIVIDE: return " / ";
|
|
|
- case BinaryOperator.MODULO: return " % ";
|
|
|
- default: return " ? ";
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_property(PropertyExpression expr) {
|
|
|
- // The library's PropertyExpression.accept() calls:
|
|
|
- // 1. visit_property() - our method here
|
|
|
- // 2. target.accept() - which calls visit_variable()
|
|
|
- // So visit_variable is called AFTER visit_property.
|
|
|
- // We save the property name and output it when visit_variable is called.
|
|
|
- _pending_property_name = expr.property_name;
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_literal(LiteralExpression expr) {
|
|
|
- var value = expr.value;
|
|
|
- if (value.assignable_to_type(typeof(string))) {
|
|
|
- string? s = null;
|
|
|
- if (value.try_get_as<string>(out s) && s != null) {
|
|
|
- _builder.append("\"");
|
|
|
- _builder.append(s);
|
|
|
- _builder.append("\"");
|
|
|
- }
|
|
|
- } else if (value.assignable_to_type(typeof(int64))) {
|
|
|
- int64? i = null;
|
|
|
- if (value.try_get_as<int64?>(out i) && i != null) {
|
|
|
- _builder.append(i.to_string());
|
|
|
- }
|
|
|
- } else if (value.assignable_to_type(typeof(double))) {
|
|
|
- double? d = null;
|
|
|
- if (value.try_get_as<double?>(out d) && d != null) {
|
|
|
- _builder.append(d.to_string());
|
|
|
- }
|
|
|
- } else if (value.assignable_to_type(typeof(bool))) {
|
|
|
- bool? b = null;
|
|
|
- if (value.try_get_as<bool>(out b) && b != null) {
|
|
|
- _builder.append(b ? "true" : "false");
|
|
|
- }
|
|
|
- } else if (value.is_null()) {
|
|
|
- _builder.append("null");
|
|
|
- } else {
|
|
|
- _builder.append(value.to_string());
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_unary(UnaryExpression expr) {
|
|
|
- if (expr.operator == UnaryOperator.NOT) {
|
|
|
- _builder.append("!");
|
|
|
- } else if (expr.operator == UnaryOperator.NEGATE) {
|
|
|
- _builder.append("-");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_ternary(TernaryExpression expr) {
|
|
|
- expr.condition.accept(this);
|
|
|
- _builder.append(" ? ");
|
|
|
- expr.true_expression.accept(this);
|
|
|
- _builder.append(" : ");
|
|
|
- expr.false_expression.accept(this);
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_lambda(LambdaExpression expr) {
|
|
|
- _builder.append(expr.parameter_name);
|
|
|
- _builder.append(" => ");
|
|
|
- expr.body.accept(this);
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_bracketed(BracketedExpression expr) {
|
|
|
- _builder.append("(");
|
|
|
- expr.inner.accept(this);
|
|
|
- _builder.append(")");
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_variable(VariableExpression expr) {
|
|
|
- // If there's a pending property name, this variable is the target of a property expression
|
|
|
- // Output: variable.property_name
|
|
|
- if (_pending_property_name != null) {
|
|
|
- _builder.append(expr.variable_name);
|
|
|
- _builder.append(".");
|
|
|
- _builder.append(_pending_property_name);
|
|
|
- _pending_property_name = null; // Clear it
|
|
|
- } else {
|
|
|
- _builder.append(expr.variable_name);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_function_call(FunctionCallExpression expr) {
|
|
|
- expr.target.accept(this);
|
|
|
- _builder.append(".");
|
|
|
- _builder.append(expr.function_name);
|
|
|
- _builder.append("(");
|
|
|
- if (expr.arguments != null) {
|
|
|
- bool first = true;
|
|
|
- foreach (var arg in expr.arguments) {
|
|
|
- if (!first) _builder.append(", ");
|
|
|
- arg.accept(this);
|
|
|
- first = false;
|
|
|
- }
|
|
|
- }
|
|
|
- _builder.append(")");
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_global_function_call(GlobalFunctionCallExpression expr) {
|
|
|
- _builder.append(expr.function_name);
|
|
|
- _builder.append("(");
|
|
|
- if (expr.arguments != null) {
|
|
|
- bool first = true;
|
|
|
- foreach (var arg in expr.arguments) {
|
|
|
- if (!first) _builder.append(", ");
|
|
|
- arg.accept(this);
|
|
|
- first = false;
|
|
|
- }
|
|
|
- }
|
|
|
- _builder.append(")");
|
|
|
- }
|
|
|
-
|
|
|
- public void visit_lot_literal(LotLiteralExpression expr) {
|
|
|
- _builder.append("[");
|
|
|
- bool first = true;
|
|
|
- foreach (var element in expr.elements) {
|
|
|
- if (!first) _builder.append(", ");
|
|
|
- element.accept(this);
|
|
|
- first = false;
|
|
|
- }
|
|
|
- _builder.append("]");
|
|
|
- }
|
|
|
- }
|
|
|
}
|