aggregate-analyzer.vala 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  1. using Invercargill.DataStructures;
  2. using Invercargill.Expressions;
  3. namespace InvercargillSql.Orm.Projections {
  4. /**
  5. * Analysis result containing information about aggregate functions found in an expression.
  6. *
  7. * Returned by AggregateAnalyzer.analyze(), this class provides details about
  8. * which aggregate functions were detected and their locations in the expression.
  9. *
  10. * Example:
  11. * {{{
  12. * var analysis = analyzer.analyze("COUNT(o.id) + SUM(o.total)");
  13. * // analysis.contains_aggregate == true
  14. * // analysis.aggregate_functions_found contains "COUNT", "SUM"
  15. * }}}
  16. */
  17. public class AggregateAnalysis : Object {
  18. /**
  19. * Indicates whether the expression contains any aggregate functions.
  20. */
  21. public bool contains_aggregate { get; construct; }
  22. /**
  23. * The list of aggregate function names found in the expression.
  24. *
  25. * This may contain duplicates if the same function appears multiple times.
  26. * The names are in their original case as they appear in the expression.
  27. */
  28. public Vector<string> aggregate_functions_found { get; construct; }
  29. /**
  30. * The original expression that was analyzed.
  31. */
  32. public string original_expression { get; construct; }
  33. /**
  34. * Creates a new AggregateAnalysis.
  35. *
  36. * @param contains_aggregate Whether aggregates were found
  37. * @param aggregate_functions_found List of aggregate function names found
  38. * @param original_expression The expression that was analyzed
  39. */
  40. public AggregateAnalysis(
  41. bool contains_aggregate,
  42. Vector<string> aggregate_functions_found,
  43. string original_expression
  44. ) {
  45. Object(
  46. contains_aggregate: contains_aggregate,
  47. aggregate_functions_found: aggregate_functions_found,
  48. original_expression: original_expression
  49. );
  50. }
  51. }
  52. /**
  53. * Result of splitting an expression into aggregate and non-aggregate parts.
  54. *
  55. * When a compound expression contains both aggregate and non-aggregate conditions,
  56. * this class holds the separated parts for use in WHERE and HAVING clauses.
  57. *
  58. * Example:
  59. * {{{
  60. * // Input: "user_id > 100 && order_count >= 5"
  61. * // Where order_count is an aggregate like COUNT(o.id)
  62. * split.non_aggregate_part == "user_id > 100" // Goes to WHERE
  63. * split.aggregate_part == "order_count >= 5" // Goes to HAVING
  64. * }}}
  65. */
  66. public class SplitExpression : Object {
  67. /**
  68. * The non-aggregate part of the expression.
  69. *
  70. * This part should be applied to the WHERE clause.
  71. * May be null if the entire expression contains aggregates.
  72. */
  73. public string? non_aggregate_part { get; construct; }
  74. /**
  75. * The aggregate part of the expression.
  76. *
  77. * This part should be applied to the HAVING clause.
  78. * May be null if the expression contains no aggregates.
  79. */
  80. public string? aggregate_part { get; construct; }
  81. /**
  82. * Indicates if the expression combines aggregate and non-aggregate parts with OR.
  83. *
  84. * When true, a subquery wrapper is required because SQL cannot mix
  85. * WHERE and HAVING conditions with OR logic.
  86. *
  87. * Example requiring subquery:
  88. * {{{
  89. * // "user_id > 100 || COUNT(o.id) >= 5"
  90. * // Cannot be split into separate WHERE and HAVING
  91. * split.needs_subquery == true
  92. * }}}
  93. */
  94. public bool needs_subquery { get; construct; }
  95. /**
  96. * Creates a new SplitExpression.
  97. *
  98. * @param non_aggregate_part The WHERE clause part (or null)
  99. * @param aggregate_part The HAVING clause part (or null)
  100. * @param needs_subquery True if subquery wrapper is needed
  101. */
  102. public SplitExpression(
  103. string? non_aggregate_part,
  104. string? aggregate_part,
  105. bool needs_subquery
  106. ) {
  107. Object(
  108. non_aggregate_part: non_aggregate_part,
  109. aggregate_part: aggregate_part,
  110. needs_subquery: needs_subquery
  111. );
  112. }
  113. }
  114. /**
  115. * Analyzes expressions to detect aggregate functions for WHERE/HAVING split.
  116. *
  117. * SQL requires aggregate conditions to be in the HAVING clause, while
  118. * non-aggregate conditions go in the WHERE clause. This analyzer detects
  119. * aggregate functions and helps split compound expressions appropriately.
  120. *
  121. * Aggregate functions detected:
  122. * - COUNT - Counts rows or non-null values
  123. * - SUM - Sums numeric values
  124. * - AVG - Calculates average
  125. * - MIN - Finds minimum value
  126. * - MAX - Finds maximum value
  127. * - GROUP_CONCAT - Concatenates strings (SQLite specific)
  128. *
  129. * Example usage:
  130. * {{{
  131. * var analyzer = new AggregateAnalyzer();
  132. *
  133. * // Check for aggregates
  134. * if (analyzer.contains_aggregate("COUNT(o.id) > 5")) {
  135. * // Use in HAVING clause
  136. * }
  137. *
  138. * // Split mixed expression
  139. * var split = analyzer.split_expression("user_id > 100 && COUNT(o.id) >= 5");
  140. * // split.non_aggregate_part == "user_id > 100"
  141. * // split.aggregate_part == "COUNT(o.id) >= 5"
  142. * }}}
  143. */
  144. public class AggregateAnalyzer : Object {
  145. /**
  146. * Set of aggregate function names that trigger HAVING clause usage.
  147. *
  148. * These are stored in uppercase for case-insensitive matching.
  149. */
  150. private static HashSet<string>? _aggregate_functions = null;
  151. /**
  152. * Gets the set of aggregate function names.
  153. *
  154. * Lazily initializes the set on first access.
  155. *
  156. * @return The set of aggregate function names in uppercase
  157. */
  158. private static HashSet<string> get_aggregate_functions() {
  159. if (_aggregate_functions == null) {
  160. _aggregate_functions = new HashSet<string>();
  161. _aggregate_functions.add("COUNT");
  162. _aggregate_functions.add("SUM");
  163. _aggregate_functions.add("AVG");
  164. _aggregate_functions.add("MIN");
  165. _aggregate_functions.add("MAX");
  166. _aggregate_functions.add("GROUP_CONCAT");
  167. }
  168. return _aggregate_functions;
  169. }
  170. /**
  171. * Analyzes an expression string and returns information about aggregates.
  172. *
  173. * This method parses the expression and identifies any aggregate functions.
  174. * It returns an AggregateAnalysis object with details about what was found.
  175. *
  176. * @param expression The Invercargill expression string to analyze
  177. * @return An AggregateAnalysis with detection results
  178. */
  179. public AggregateAnalysis analyze(string expression) {
  180. var found_functions = new Vector<string>();
  181. bool contains = scan_for_aggregates(expression, found_functions);
  182. return new AggregateAnalysis(contains, found_functions, expression);
  183. }
  184. /**
  185. * Checks if an expression contains any aggregate functions.
  186. *
  187. * This is a convenience method for quickly checking if HAVING clause
  188. * handling is needed.
  189. *
  190. * @param expression The Invercargill expression string to check
  191. * @return True if the expression contains aggregate functions
  192. */
  193. public bool contains_aggregate(string expression) {
  194. return scan_for_aggregates(expression, null);
  195. }
  196. /**
  197. * Splits a compound expression into aggregate and non-aggregate parts.
  198. *
  199. * This method analyzes an expression and separates conditions that
  200. * should go in WHERE vs HAVING clauses. It handles AND-connected
  201. * conditions properly but flags OR-combined mixed conditions as
  202. * requiring a subquery wrapper.
  203. *
  204. * Example:
  205. * {{{
  206. * // AND-connected (can split):
  207. * // Input: "user_id > 100 && COUNT(o.id) >= 5"
  208. * // Result:
  209. * // non_aggregate_part = "user_id > 100"
  210. * // aggregate_part = "COUNT(o.id) >= 5"
  211. * // needs_subquery = false
  212. *
  213. * // OR-connected mixed (needs subquery):
  214. * // Input: "user_id > 100 || COUNT(o.id) >= 5"
  215. * // Result:
  216. * // non_aggregate_part = null
  217. * // aggregate_part = null
  218. * // needs_subquery = true
  219. * }}}
  220. *
  221. * @param expression The compound expression to split
  222. * @return A SplitExpression with the separated parts
  223. */
  224. public SplitExpression split_expression(string expression) {
  225. var trimmed = expression.strip();
  226. if (trimmed.length == 0) {
  227. return new SplitExpression(null, null, false);
  228. }
  229. // Parse as expression tree if possible
  230. try {
  231. var parsed = ExpressionParser.parse(trimmed);
  232. return split_expression_tree(parsed);
  233. } catch (Error e) {
  234. // Fall back to simple scanning if parsing fails
  235. return split_expression_simple(trimmed);
  236. }
  237. }
  238. /**
  239. * Scans an expression string for aggregate function calls.
  240. *
  241. * This method uses simple pattern matching to find function calls
  242. * and check if they are aggregate functions.
  243. *
  244. * @param expression The expression to scan
  245. * @param found_functions Optional vector to collect found function names
  246. * @return True if any aggregate functions were found
  247. */
  248. private bool scan_for_aggregates(string expression, Vector<string>? found_functions) {
  249. var aggregates = get_aggregate_functions();
  250. bool found = false;
  251. // Scan for function calls pattern: WORD(
  252. // We need to find word boundaries and check against known aggregates
  253. int i = 0;
  254. int len = expression.length;
  255. while (i < len) {
  256. // Skip whitespace
  257. while (i < len && expression[i].isspace()) {
  258. i++;
  259. }
  260. if (i >= len) break;
  261. // Check if we're at the start of an identifier
  262. if (expression[i].isalpha() || expression[i] == '_') {
  263. int start = i;
  264. // Read the identifier
  265. while (i < len && (expression[i].isalnum() || expression[i] == '_')) {
  266. i++;
  267. }
  268. string identifier = expression.substring(start, i - start).up();
  269. // Skip whitespace before potential parenthesis
  270. int j = i;
  271. while (j < len && expression[j].isspace()) {
  272. j++;
  273. }
  274. // Check if this is a function call
  275. if (j < len && expression[j] == '(') {
  276. if (aggregates.contains(identifier)) {
  277. found = true;
  278. if (found_functions != null) {
  279. found_functions.add(identifier);
  280. }
  281. }
  282. }
  283. i = j;
  284. } else {
  285. i++;
  286. }
  287. }
  288. return found;
  289. }
  290. /**
  291. * Splits a parsed expression tree into aggregate and non-aggregate parts.
  292. *
  293. * This method handles the expression tree structure properly, correctly
  294. * identifying AND vs OR combinations and nested expressions.
  295. *
  296. * @param expr The parsed expression tree
  297. * @return A SplitExpression with the separated parts
  298. */
  299. private SplitExpression split_expression_tree(Expression expr) {
  300. // Check if it's a binary expression (potential AND/OR)
  301. if (expr is BinaryExpression) {
  302. var binary = (BinaryExpression) expr;
  303. // Handle AND - can split
  304. if (binary.op == BinaryOperator.AND) {
  305. var left_split = split_expression_tree(binary.left);
  306. var right_split = split_expression_tree(binary.right);
  307. // Combine results
  308. return combine_and_splits(left_split, right_split);
  309. }
  310. // Handle OR - check if mixed
  311. if (binary.op == BinaryOperator.OR) {
  312. bool left_has_agg = expression_contains_aggregate(binary.left);
  313. bool right_has_agg = expression_contains_aggregate(binary.right);
  314. // If both sides are same type, no subquery needed
  315. if (left_has_agg == right_has_agg) {
  316. if (left_has_agg) {
  317. // Both aggregate - whole thing goes to HAVING
  318. return new SplitExpression(null, expr_to_string(expr), false);
  319. } else {
  320. // Both non-aggregate - whole thing goes to WHERE
  321. return new SplitExpression(expr_to_string(expr), null, false);
  322. }
  323. }
  324. // Mixed OR - needs subquery
  325. return new SplitExpression(null, null, true);
  326. }
  327. }
  328. // For non-binary or other operators, check if it contains aggregates
  329. bool has_aggregate = expression_contains_aggregate(expr);
  330. string expr_str = expr_to_string(expr);
  331. if (has_aggregate) {
  332. return new SplitExpression(null, expr_str, false);
  333. } else {
  334. return new SplitExpression(expr_str, null, false);
  335. }
  336. }
  337. /**
  338. * Combines two SplitExpressions from AND-connected expressions.
  339. *
  340. * @param left The split from the left side of AND
  341. * @param right The split from the right side of AND
  342. * @return A combined SplitExpression
  343. */
  344. private SplitExpression combine_and_splits(SplitExpression left, SplitExpression right) {
  345. // If either needs subquery, propagate that
  346. if (left.needs_subquery || right.needs_subquery) {
  347. return new SplitExpression(null, null, true);
  348. }
  349. // Combine non-aggregate parts
  350. string? non_agg = null;
  351. if (left.non_aggregate_part != null && right.non_aggregate_part != null) {
  352. non_agg = @"($(left.non_aggregate_part) AND $(right.non_aggregate_part))";
  353. } else if (left.non_aggregate_part != null) {
  354. non_agg = left.non_aggregate_part;
  355. } else if (right.non_aggregate_part != null) {
  356. non_agg = right.non_aggregate_part;
  357. }
  358. // Combine aggregate parts
  359. string? agg = null;
  360. if (left.aggregate_part != null && right.aggregate_part != null) {
  361. agg = @"($(left.aggregate_part) AND $(right.aggregate_part))";
  362. } else if (left.aggregate_part != null) {
  363. agg = left.aggregate_part;
  364. } else if (right.aggregate_part != null) {
  365. agg = right.aggregate_part;
  366. }
  367. return new SplitExpression(non_agg, agg, false);
  368. }
  369. /**
  370. * Checks if an expression tree contains aggregate functions.
  371. *
  372. * @param expr The expression tree to check
  373. * @return True if aggregates are found
  374. */
  375. private bool expression_contains_aggregate(Expression expr) {
  376. var visitor = new AggregateDetectionVisitor();
  377. expr.accept(visitor);
  378. return visitor.found_aggregate;
  379. }
  380. /**
  381. * Converts an expression tree back to a string representation.
  382. *
  383. * @param expr The expression tree to convert
  384. * @return The string representation
  385. */
  386. private string expr_to_string(Expression expr) {
  387. var visitor = new ExpressionStringVisitor();
  388. expr.accept(visitor);
  389. return visitor.get_string();
  390. }
  391. /**
  392. * Fallback method for splitting expressions when parsing fails.
  393. *
  394. * Uses simple scanning to detect aggregates and cannot properly
  395. * handle complex nested expressions.
  396. *
  397. * @param expression The expression to split
  398. * @return A SplitExpression based on simple scanning
  399. */
  400. private SplitExpression split_expression_simple(string expression) {
  401. bool has_aggregate = contains_aggregate(expression);
  402. if (has_aggregate) {
  403. // Check for OR that might mix aggregate and non-aggregate
  404. // This is a simplified check - doesn't handle nested parentheses
  405. bool has_or = contains_mixed_or(expression);
  406. if (has_or) {
  407. return new SplitExpression(null, null, true);
  408. }
  409. return new SplitExpression(null, expression, false);
  410. } else {
  411. return new SplitExpression(expression, null, false);
  412. }
  413. }
  414. /**
  415. * Checks if an expression contains OR that might mix aggregate and non-aggregate.
  416. *
  417. * This is a simplified check used as a fallback when expression parsing fails.
  418. *
  419. * @param expression The expression to check
  420. * @return True if potentially mixed OR is found
  421. */
  422. private bool contains_mixed_or(string expression) {
  423. // Look for OR keyword at the top level (not inside parentheses)
  424. int paren_depth = 0;
  425. int i = 0;
  426. int len = expression.length;
  427. while (i < len) {
  428. char c = expression[i];
  429. if (c == '(') {
  430. paren_depth++;
  431. } else if (c == ')') {
  432. paren_depth--;
  433. } else if (paren_depth == 0) {
  434. // Check for OR keyword
  435. if (i + 2 < len) {
  436. string substr = expression.substring(i, 2).up();
  437. if (substr == "OR" || (i + 3 < len && expression.substring(i, 3).up() == "OR ")) {
  438. // Found OR at top level - check if both sides have different aggregate status
  439. string left = expression.substring(0, i).strip();
  440. string right = expression.substring(i + 2).strip();
  441. // Remove leading "OR" if present
  442. if (right.has_prefix("OR") || right.has_prefix("or")) {
  443. right = right.substring(2).strip();
  444. }
  445. bool left_has = contains_aggregate(left);
  446. bool right_has = contains_aggregate(right);
  447. if (left_has != right_has) {
  448. return true;
  449. }
  450. }
  451. }
  452. }
  453. i++;
  454. }
  455. return false;
  456. }
  457. }
  458. /**
  459. * Expression visitor that detects aggregate function calls.
  460. *
  461. * This visitor traverses an expression tree and sets found_aggregate to true
  462. * if any aggregate functions (COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT) are found.
  463. */
  464. internal class AggregateDetectionVisitor : Object, ExpressionVisitor {
  465. /**
  466. * Indicates whether an aggregate function was found during traversal.
  467. */
  468. public bool found_aggregate { get; private set; }
  469. private static HashSet<string> _aggregate_functions = null;
  470. /**
  471. * Creates a new AggregateDetectionVisitor.
  472. */
  473. public AggregateDetectionVisitor() {
  474. found_aggregate = false;
  475. }
  476. /**
  477. * Gets the set of aggregate function names.
  478. */
  479. private static HashSet<string> get_aggregate_functions() {
  480. if (_aggregate_functions == null) {
  481. _aggregate_functions = new HashSet<string>();
  482. _aggregate_functions.add("COUNT");
  483. _aggregate_functions.add("SUM");
  484. _aggregate_functions.add("AVG");
  485. _aggregate_functions.add("MIN");
  486. _aggregate_functions.add("MAX");
  487. _aggregate_functions.add("GROUP_CONCAT");
  488. }
  489. return _aggregate_functions;
  490. }
  491. public void visit_binary(BinaryExpression expr) {
  492. if (!found_aggregate) {
  493. expr.left.accept(this);
  494. }
  495. if (!found_aggregate) {
  496. expr.right.accept(this);
  497. }
  498. }
  499. public void visit_property(PropertyExpression expr) {
  500. // Properties don't contain aggregates
  501. }
  502. public void visit_literal(LiteralExpression expr) {
  503. // Literals don't contain aggregates
  504. }
  505. public void visit_unary(UnaryExpression expr) {
  506. if (!found_aggregate) {
  507. // The operand will be visited by UnaryExpression.accept()
  508. }
  509. }
  510. public void visit_ternary(TernaryExpression expr) {
  511. if (!found_aggregate) {
  512. expr.condition.accept(this);
  513. }
  514. if (!found_aggregate) {
  515. expr.true_expression.accept(this);
  516. }
  517. if (!found_aggregate) {
  518. expr.false_expression.accept(this);
  519. }
  520. }
  521. public void visit_lambda(LambdaExpression expr) {
  522. if (!found_aggregate) {
  523. expr.body.accept(this);
  524. }
  525. }
  526. public void visit_bracketed(BracketedExpression expr) {
  527. if (!found_aggregate) {
  528. expr.inner.accept(this);
  529. }
  530. }
  531. public void visit_variable(VariableExpression expr) {
  532. // Variables don't contain aggregates
  533. }
  534. public void visit_function_call(FunctionCallExpression expr) {
  535. // Method calls don't contain aggregates (they're not SQL functions)
  536. // But their arguments might
  537. if (!found_aggregate && expr.arguments != null) {
  538. foreach (var arg in expr.arguments) {
  539. if (found_aggregate) break;
  540. arg.accept(this);
  541. }
  542. }
  543. }
  544. public void visit_global_function_call(GlobalFunctionCallExpression expr) {
  545. var aggregates = get_aggregate_functions();
  546. string func_name = expr.function_name.up();
  547. if (aggregates.contains(func_name)) {
  548. found_aggregate = true;
  549. }
  550. // Also check arguments for nested aggregates
  551. if (!found_aggregate && expr.arguments != null) {
  552. foreach (var arg in expr.arguments) {
  553. if (found_aggregate) break;
  554. arg.accept(this);
  555. }
  556. }
  557. }
  558. public void visit_lot_literal(LotLiteralExpression expr) {
  559. // Collection literals don't contain aggregates
  560. }
  561. }
  562. /**
  563. * Expression visitor that converts an expression tree back to a string.
  564. *
  565. * This is used to reconstruct expression strings after splitting.
  566. */
  567. internal class ExpressionStringVisitor : Object, ExpressionVisitor {
  568. private StringBuilder _builder;
  569. private string? _pending_property_name = null; // Property name waiting for target variable
  570. /**
  571. * Creates a new ExpressionStringVisitor.
  572. */
  573. public ExpressionStringVisitor() {
  574. _builder = new StringBuilder();
  575. _pending_property_name = null;
  576. }
  577. /**
  578. * Gets the string representation of the visited expression.
  579. *
  580. * @return The expression as a string
  581. */
  582. public string get_string() {
  583. return _builder.str;
  584. }
  585. public void visit_binary(BinaryExpression expr) {
  586. _builder.append("(");
  587. expr.left.accept(this);
  588. _builder.append(get_operator_string(expr.op));
  589. expr.right.accept(this);
  590. _builder.append(")");
  591. }
  592. private string get_operator_string(BinaryOperator op) {
  593. switch (op) {
  594. case BinaryOperator.EQUAL: return " == ";
  595. case BinaryOperator.NOT_EQUAL: return " != ";
  596. case BinaryOperator.GREATER_THAN: return " > ";
  597. case BinaryOperator.GREATER_EQUAL: return " >= ";
  598. case BinaryOperator.LESS_THAN: return " < ";
  599. case BinaryOperator.LESS_EQUAL: return " <= ";
  600. case BinaryOperator.AND: return " && ";
  601. case BinaryOperator.OR: return " || ";
  602. case BinaryOperator.ADD: return " + ";
  603. case BinaryOperator.SUBTRACT: return " - ";
  604. case BinaryOperator.MULTIPLY: return " * ";
  605. case BinaryOperator.DIVIDE: return " / ";
  606. case BinaryOperator.MODULO: return " % ";
  607. default: return " ? ";
  608. }
  609. }
  610. public void visit_property(PropertyExpression expr) {
  611. // The library's PropertyExpression.accept() calls:
  612. // 1. visit_property() - our method here
  613. // 2. target.accept() - which calls visit_variable()
  614. // So visit_variable is called AFTER visit_property.
  615. // We save the property name and output it when visit_variable is called.
  616. _pending_property_name = expr.property_name;
  617. }
  618. public void visit_literal(LiteralExpression expr) {
  619. var value = expr.value;
  620. if (value.assignable_to_type(typeof(string))) {
  621. string? s = null;
  622. if (value.try_get_as<string>(out s) && s != null) {
  623. _builder.append("\"");
  624. _builder.append(s);
  625. _builder.append("\"");
  626. }
  627. } else if (value.assignable_to_type(typeof(int64))) {
  628. int64? i = null;
  629. if (value.try_get_as<int64?>(out i) && i != null) {
  630. _builder.append(i.to_string());
  631. }
  632. } else if (value.assignable_to_type(typeof(double))) {
  633. double? d = null;
  634. if (value.try_get_as<double?>(out d) && d != null) {
  635. _builder.append(d.to_string());
  636. }
  637. } else if (value.assignable_to_type(typeof(bool))) {
  638. bool? b = null;
  639. if (value.try_get_as<bool>(out b) && b != null) {
  640. _builder.append(b ? "true" : "false");
  641. }
  642. } else if (value.is_null()) {
  643. _builder.append("null");
  644. } else {
  645. _builder.append(value.to_string());
  646. }
  647. }
  648. public void visit_unary(UnaryExpression expr) {
  649. if (expr.operator == UnaryOperator.NOT) {
  650. _builder.append("!");
  651. } else if (expr.operator == UnaryOperator.NEGATE) {
  652. _builder.append("-");
  653. }
  654. }
  655. public void visit_ternary(TernaryExpression expr) {
  656. expr.condition.accept(this);
  657. _builder.append(" ? ");
  658. expr.true_expression.accept(this);
  659. _builder.append(" : ");
  660. expr.false_expression.accept(this);
  661. }
  662. public void visit_lambda(LambdaExpression expr) {
  663. _builder.append(expr.parameter_name);
  664. _builder.append(" => ");
  665. expr.body.accept(this);
  666. }
  667. public void visit_bracketed(BracketedExpression expr) {
  668. _builder.append("(");
  669. expr.inner.accept(this);
  670. _builder.append(")");
  671. }
  672. public void visit_variable(VariableExpression expr) {
  673. // If there's a pending property name, this variable is the target of a property expression
  674. // Output: variable.property_name
  675. if (_pending_property_name != null) {
  676. _builder.append(expr.variable_name);
  677. _builder.append(".");
  678. _builder.append(_pending_property_name);
  679. _pending_property_name = null; // Clear it
  680. } else {
  681. _builder.append(expr.variable_name);
  682. }
  683. }
  684. public void visit_function_call(FunctionCallExpression expr) {
  685. expr.target.accept(this);
  686. _builder.append(".");
  687. _builder.append(expr.function_name);
  688. _builder.append("(");
  689. if (expr.arguments != null) {
  690. bool first = true;
  691. foreach (var arg in expr.arguments) {
  692. if (!first) _builder.append(", ");
  693. arg.accept(this);
  694. first = false;
  695. }
  696. }
  697. _builder.append(")");
  698. }
  699. public void visit_global_function_call(GlobalFunctionCallExpression expr) {
  700. _builder.append(expr.function_name);
  701. _builder.append("(");
  702. if (expr.arguments != null) {
  703. bool first = true;
  704. foreach (var arg in expr.arguments) {
  705. if (!first) _builder.append(", ");
  706. arg.accept(this);
  707. first = false;
  708. }
  709. }
  710. _builder.append(")");
  711. }
  712. public void visit_lot_literal(LotLiteralExpression expr) {
  713. _builder.append("[");
  714. bool first = true;
  715. foreach (var element in expr.elements) {
  716. if (!first) _builder.append(", ");
  717. element.accept(this);
  718. first = false;
  719. }
  720. _builder.append("]");
  721. }
  722. }
  723. }