diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java index ddee5aadbc5..1b9d798d1ee 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java @@ -167,7 +167,6 @@ public void getEnrollments(EventQueryParams params, Grid grid, int maxLimit) { ? buildEnrollmentQueryWithCte(params) : getAggregatedEnrollmentsSql(params, maxLimit); } - System.out.println(sql); // FIXME remove if (params.analyzeOnly()) { withExceptionHandling( () -> executionPlanStore.addExecutionPlan(params.getExplainOrderId(), sql)); @@ -593,7 +592,7 @@ private Condition buildCteConditions( private List buildItemConditions(QueryItem item, CteDefinition cteDef) { return item.getFilters().stream() .map(filter -> buildFilterCondition(filter, item, cteDef)) - .collect(Collectors.toList()); + .toList(); } /** diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/Condition.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/Condition.java index 95b5721d18b..0d287b8a5a0 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/Condition.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/Condition.java @@ -32,6 +32,7 @@ import java.util.Collection; import java.util.List; import java.util.Objects; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -46,6 +47,10 @@ public sealed interface Condition Condition.Raw, NotCondition, SimpleCondition { + + Pattern WHERE_AND_PATTERN = Pattern.compile("^(?i)(where|and)\\b.*"); + Pattern WHERE_AND_REPLACE_PATTERN = Pattern.compile("^(?i)(where|and)\\s+"); + /** * Converts the condition to its SQL string representation. * @@ -83,8 +88,8 @@ public String toSql() { // Remove only the first occurrence of WHERE or AND String cleaned = sql.trim(); - if (cleaned.toLowerCase().matches("^(where|and)\\b.*")) { - cleaned = cleaned.replaceFirst("(?i)^(where|and)\\s+", ""); + if (WHERE_AND_PATTERN.matcher(cleaned.toLowerCase()).matches()) { + cleaned = WHERE_AND_REPLACE_PATTERN.matcher(cleaned).replaceFirst(""); } return cleaned.trim(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/QuoteUtils.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/QuoteUtils.java new file mode 100644 index 00000000000..5495a6bc00e --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/QuoteUtils.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.util.sql; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class QuoteUtils { + + /** + * Removes surrounding quotes (double quotes " or backticks `) from a + * string. If the string is not quoted or the quotes do not match, the original string is returned + * unchanged. + * + *

Behavior: + * + *

+ * + *

Examples: + * + *

+ * + * @param quoted The string from which to remove quotes. Can be null or empty. + * @return The string without surrounding quotes, or the original string if it is not quoted. + * Returns an empty string if the input is null or empty. + */ + static String unquote(String quoted) { + // Handle null or empty + if (quoted == null || quoted.isEmpty()) { + return ""; + } + + // Check minimum length (needs at least 2 chars for quotes) + if (quoted.length() < 2) { + return quoted; + } + + char firstChar = quoted.charAt(0); + char lastChar = quoted.charAt(quoted.length() - 1); + + // Check if quotes match + if ((firstChar == '"' && lastChar == '"') || (firstChar == '`' && lastChar == '`')) { + return quoted.substring(1, quoted.length() - 1); + } + + return quoted; + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SelectBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SelectBuilder.java index eab082a3d1b..63afadd82ca 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SelectBuilder.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SelectBuilder.java @@ -27,9 +27,12 @@ */ package org.hisp.dhis.analytics.util.sql; +import static org.hisp.dhis.analytics.util.sql.QuoteUtils.unquote; + import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.regex.Pattern; import java.util.stream.Collectors; /** @@ -68,6 +71,7 @@ public class SelectBuilder { private final List orderByClauses = new ArrayList<>(); private Integer limit; private Integer offset; + private static final Pattern ORDER_BY_PATTERN = Pattern.compile("^(?i)order\\s+by\\s+"); /** * Represents a column in the SELECT clause of a SQL query. Handles column expressions with @@ -428,7 +432,7 @@ public SelectBuilder orderBy(String rawSortClause) { } // Remove "order by" prefix if present - String cleaned = rawSortClause.trim().replaceFirst("(?i)^order\\s+by\\s+", ""); + String cleaned = ORDER_BY_PATTERN.matcher(rawSortClause.trim()).replaceFirst(""); // Split by commas, but not commas within CASE statements List parts = splitPreservingCaseStatements(cleaned); @@ -728,26 +732,4 @@ private String sanitizeFromClause(String input) { return sanitized; } - - private static String unquote(String quoted) { - // Handle null or empty - if (quoted == null || quoted.isEmpty()) { - return ""; - } - - // Check minimum length (needs at least 2 chars for quotes) - if (quoted.length() < 2) { - return quoted; - } - - char firstChar = quoted.charAt(0); - char lastChar = quoted.charAt(quoted.length() - 1); - - // Check if quotes match - if ((firstChar == '"' && lastChar == '"') || (firstChar == '`' && lastChar == '`')) { - return quoted.substring(1, quoted.length() - 1); - } - - return quoted; - } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlAliasReplacer.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlAliasReplacer.java index ff00a111778..6bad170e632 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlAliasReplacer.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlAliasReplacer.java @@ -97,14 +97,13 @@ public static String replaceTableAliases(String whereClause, List column expr.accept(visitor); return expr.toString(); } catch (Exception e) { - throw new RuntimeException("Error parsing SQL where clause: " + e.getMessage(), e); + throw new IllegalArgumentException("Error parsing SQL where clause: " + e.getMessage(), e); } } private static class ColumnReplacementVisitor extends ExpressionVisitorAdapter { private final Set columns; private static final Table PLACEHOLDER_TABLE = new Table("%s"); - private static final Table OUTER_REFERENCE_TABLE = new Table("%z"); // New constant private boolean inSubQuery = false; @@ -120,7 +119,6 @@ public ColumnReplacementVisitor(List columns) { public void visit(Column column) { String columnName = column.getColumnName(); String rawColumnName = stripQuotes(columnName); - Table currentTable = column.getTable(); if (columns.contains(rawColumnName.toLowerCase())) { String quoteType = getQuoteType(columnName); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlColumnParser.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlColumnParser.java index b96b9f78b69..87b822324b1 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlColumnParser.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlColumnParser.java @@ -27,6 +27,8 @@ */ package org.hisp.dhis.analytics.util.sql; +import static org.hisp.dhis.analytics.util.sql.QuoteUtils.unquote; + import lombok.experimental.UtilityClass; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.parser.CCJSqlParserUtil; @@ -60,30 +62,7 @@ public static String removeTableAlias(String columnReference) { // Extract the column name return unquote(column.getColumnName()); } catch (Exception e) { - throw new RuntimeException("Error parsing SQL: " + e.getMessage(), e); - } - } - - // FIXME - this method is duplicated in SqlWhereClauseExtractor - private static String unquote(String quoted) { - // Handle null or empty - if (quoted == null || quoted.isEmpty()) { - return ""; - } - - // Check minimum length (needs at least 2 chars for quotes) - if (quoted.length() < 2) { - return quoted; + throw new IllegalArgumentException("Error parsing SQL: " + e.getMessage(), e); } - - char firstChar = quoted.charAt(0); - char lastChar = quoted.charAt(quoted.length() - 1); - - // Check if quotes match - if ((firstChar == '"' && lastChar == '"') || (firstChar == '`' && lastChar == '`')) { - return quoted.substring(1, quoted.length() - 1); - } - - return quoted; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoiner.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoiner.java index af51d0cb117..82932e372cc 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoiner.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoiner.java @@ -27,36 +27,75 @@ */ package org.hisp.dhis.analytics.util.sql; +import java.util.Arrays; +import java.util.stream.Collectors; +import lombok.experimental.UtilityClass; + +/** + * A utility class for joining multiple SQL conditions into a single WHERE clause. This class is + * designed to handle conditions that may or may not include the "WHERE" keyword and ensures proper + * formatting of the final SQL WHERE clause. + */ +@UtilityClass public class SqlConditionJoiner { + /** + * Joins multiple SQL conditions into a single WHERE clause, separated by the AND + * operator. Conditions that are null, empty, or consist solely of whitespace are ignored. + * + *

Behavior: + * + *

    + *
  • Returns an empty string if no valid conditions are provided (null, empty, or all + * conditions are blank). + *
  • Removes leading "WHERE" or " where" from individual conditions before joining. + *
  • Joins valid conditions with the AND operator. + *
  • Adds a "WHERE" prefix to the final result if at least one valid condition is present. + *
+ * + *

Examples: + * + *

    + *
  • joinSqlConditions("column1 = 1", "column2 = 2") returns + * "where column1 = 1 and column2 = 2" + *
  • joinSqlConditions("where column1 = 1", "where column2 = 2") returns + * "where column1 = 1 and column2 = 2" + *
  • joinSqlConditions("", null, "column1 = 1") returns "where column1 = 1" + * + *
  • joinSqlConditions() returns "" + *
  • joinSqlConditions(null, "", " ") returns "" + *
+ * + * @param conditions The SQL conditions to join. Can be null, empty, or contain null/blank + * strings. + * @return A single WHERE clause combining the valid conditions with AND operators. + * Returns an empty string if no valid conditions are provided. + */ public static String joinSqlConditions(String... conditions) { + if (conditions == null || conditions.length == 0) { return ""; } - StringBuilder result = new StringBuilder("where "); - boolean firstCondition = true; - - for (String condition : conditions) { - if (condition == null || condition.trim().isEmpty()) { - continue; - } - - // Remove leading "where" or " where" and trim - String cleanedCondition = condition.trim(); - if (cleanedCondition.toLowerCase().startsWith("where")) { - cleanedCondition = cleanedCondition.substring(5).trim(); - } - - if (!cleanedCondition.isEmpty()) { - if (!firstCondition) { - result.append(" and "); - } - result.append(cleanedCondition); - firstCondition = false; - } - } + // Filter out null, empty, or blank conditions + String joinedConditions = + Arrays.stream(conditions) + .filter(condition -> condition != null && !condition.trim().isEmpty()) + .map( + condition -> { + // Remove leading "where" or " where" and trim + String cleanedCondition = condition.trim(); + if (cleanedCondition.toLowerCase().startsWith("where")) { + cleanedCondition = cleanedCondition.substring(5).trim(); + } + return cleanedCondition; + }) + .filter( + cleanedCondition -> + !cleanedCondition.isEmpty()) // Filter out empty strings after cleaning + .collect(Collectors.joining(" and ")); - return result.toString(); + // Return the result with "where" prefix if there are valid conditions + return joinedConditions.isEmpty() ? "" : "where " + joinedConditions; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlFormatter.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlFormatter.java index fbc595c7400..be00f20796e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlFormatter.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlFormatter.java @@ -28,8 +28,13 @@ package org.hisp.dhis.analytics.util.sql; import java.util.Set; +import java.util.regex.Pattern; +import lombok.experimental.UtilityClass; +@UtilityClass public class SqlFormatter { + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + private static final Set MAIN_CLAUSES = Set.of( "with", @@ -121,7 +126,7 @@ public static String lowercase(String sql) { } // Replace all whitespace sequences (including newlines) with a single space - result = result.replaceAll("\\s+", " "); + result = WHITESPACE_PATTERN.matcher(result).replaceAll(" "); return result.trim(); } @@ -132,9 +137,7 @@ private static String formatParentheses(String sql) { boolean inString = false; char[] chars = sql.toCharArray(); - for (int i = 0; i < chars.length; i++) { - char c = chars[i]; - + for (char c : chars) { // Handle string literals if (c == '\'') { inString = !inString; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlWhereClauseExtractor.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlWhereClauseExtractor.java index 73c27a92b69..c2bdc281874 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlWhereClauseExtractor.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/sql/SqlWhereClauseExtractor.java @@ -31,6 +31,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import lombok.experimental.UtilityClass; import net.sf.jsqlparser.expression.BinaryExpression; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Function; @@ -44,9 +45,57 @@ import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.Select; +/** + * A utility class for extracting column names from the WHERE clause of a SQL query. This class uses + * the JSqlParser library to parse SQL statements and recursively traverses the WHERE clause to + * identify and extract column names. + * + *

Supported SQL Features: + * + *

    + *
  • Simple equality conditions: column1 = 'value' + *
  • Multiple conditions with AND and OR: + * column1 = 'value' AND column2 = 10 + *
  • IN conditions: column1 IN (1, 2, 3) + *
  • Nested parentheses: (column1 = 'value' AND (column2 = 10)) + *
  • LIKE operator: column1 LIKE '%test%' + *
  • BETWEEN operator: column1 BETWEEN 1 AND 10 + *
  • IS NULL conditions: column1 IS NULL + *
  • Function calls: UPPER(column1) = 'TEST' + *
  • Subqueries in IN conditions: column1 IN (SELECT id FROM other_table) + * + *
  • Special characters in column names: "Special!@#$%^&*()" = 'value' + *
  • Case-sensitive column names: COLUMN1 = 'value' and column1 = 'value' + * are treated as distinct + *
  • Duplicate column references: Duplicates are removed from the result + *
  • Complex mixed conditions: UPPER(column1) LIKE '%TEST%' AND (column2 BETWEEN 1 AND 10) + * + *
  • Nested function calls: UPPER(TRIM(column1)) = 'TEST' + *
  • Functions with multiple parameters: CONCAT(column1, column2) = 'TEST' + *
  • Column references in BETWEEN: column1 BETWEEN column2 AND column3 + *
+ */ +@UtilityClass public class SqlWhereClauseExtractor { - // GIUSEPPE/MAIKEL: can we use a different approach to avoid using jsqlparser? + /** + * Extracts the column names used in the WHERE clause of the provided SQL query. + * + *

Behavior: + * + *

    + *
  • Returns an empty list if the SQL query does not contain a WHERE clause. + *
  • Throws IllegalArgumentException if the SQL query is null, empty, or invalid. + *
  • Handles nested parentheses, function calls, and complex conditions. + *
  • Removes duplicate column names from the result. + *
  • Preserves case sensitivity in column names. + *
+ * + * @param sql The SQL query string from which to extract column names. + * @return A list of column names found in the WHERE clause. The list is empty if no WHERE clause + * is present or if no columns are found. + * @throws RuntimeException If the SQL query is null, empty, or cannot be parsed. + */ public static List extractWhereColumns(String sql) { List columns = new ArrayList<>(); try { @@ -66,11 +115,28 @@ public static List extractWhereColumns(String sql) { } } } catch (Exception e) { - throw new RuntimeException("Error parsing SQL: " + e.getMessage(), e); + throw new IllegalArgumentException("Error parsing SQL: " + e.getMessage(), e); } return columns; } + /** + * Recursively extracts column names from a given SQL expression and adds them to the provided + * set. This method handles various types of expressions, including: + * + *
    + *
  • Simple column references: column1 = 'value' + *
  • Binary expressions: column1 = column2 + *
  • IN conditions: column1 IN (1, 2, 3) + *
  • Parentheses: (column1 = 'value') + *
  • IS NULL conditions: column1 IS NULL + *
  • Function calls: UPPER(column1) + *
  • BETWEEN conditions: column1 BETWEEN 1 AND 10 + *
+ * + * @param expression The SQL expression to process. + * @param columns A set to store the extracted column names. + */ private static void extractColumnsFromExpression(Expression expression, Set columns) { if (expression instanceof net.sf.jsqlparser.schema.Column column) { // Add the column name without table alias to the set diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoinerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoinerTest.java new file mode 100644 index 00000000000..e45113b973e --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/sql/SqlConditionJoinerTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.util.sql; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SqlConditionJoinerTest { + @Test + @DisplayName("Join multiple valid conditions") + void testJoinSqlConditions_multipleValidConditions() { + String result = + SqlConditionJoiner.joinSqlConditions("column1 = 1", "column2 = 2", "column3 = 3"); + assertEquals("where column1 = 1 and column2 = 2 and column3 = 3", result); + } + + @Test + @DisplayName("Join conditions with leading 'WHERE' keyword") + void testJoinSqlConditions_conditionsWithLeadingWhere() { + String result = SqlConditionJoiner.joinSqlConditions("WHERE column1 = 1", "where column2 = 2"); + assertEquals("where column1 = 1 and column2 = 2", result); + } + + @Test + @DisplayName("Join conditions with mixed valid and invalid inputs") + void testJoinSqlConditions_mixedValidAndInvalidConditions() { + String result = + SqlConditionJoiner.joinSqlConditions("", null, "column1 = 1", " ", "column2 = 2"); + assertEquals("where column1 = 1 and column2 = 2", result); + } + + @Test + @DisplayName("Join single valid condition") + void testJoinSqlConditions_singleValidCondition() { + String result = SqlConditionJoiner.joinSqlConditions("column1 = 1"); + assertEquals("where column1 = 1", result); + } + + @Test + @DisplayName("Join no conditions (empty input)") + void testJoinSqlConditions_noConditions() { + String result = SqlConditionJoiner.joinSqlConditions(); + assertEquals("", result); + } + + @Test + @DisplayName("Join null conditions") + void testJoinSqlConditions_nullConditions() { + String result = SqlConditionJoiner.joinSqlConditions((String[]) null); + assertEquals("", result); + } + + @Test + @DisplayName("Join all null or blank conditions") + void testJoinSqlConditions_allNullOrBlankConditions() { + String result = SqlConditionJoiner.joinSqlConditions(null, "", " "); + assertEquals("", result); + } + + @Test + @DisplayName("Join conditions with extra whitespace") + void testJoinSqlConditions_conditionsWithExtraWhitespace() { + String result = SqlConditionJoiner.joinSqlConditions(" column1 = 1 ", " column2 = 2 "); + assertEquals("where column1 = 1 and column2 = 2", result); + } + + @Test + @DisplayName("Join conditions with mixed case 'WHERE' keyword") + void testJoinSqlConditions_mixedCaseWhereKeyword() { + String result = SqlConditionJoiner.joinSqlConditions("WHERE column1 = 1", "wHeRe column2 = 2"); + assertEquals("where column1 = 1 and column2 = 2", result); + } + + @Test + @DisplayName("Join conditions with complex expressions") + void testJoinSqlConditions_complexExpressions() { + String result = + SqlConditionJoiner.joinSqlConditions( + "column1 = 1 AND column2 = 2", "column3 BETWEEN 1 AND 10", "column4 IS NULL"); + assertEquals( + "where column1 = 1 AND column2 = 2 and column3 BETWEEN 1 AND 10 and column4 IS NULL", + result); + } + + @Test + @DisplayName("Join conditions with special characters") + void testJoinSqlConditions_specialCharacters() { + String result = + SqlConditionJoiner.joinSqlConditions("\"column!@#\" = 'value'", "`column$%^` = 123"); + assertEquals("where \"column!@#\" = 'value' and `column$%^` = 123", result); + } + + @Test + @DisplayName("Join conditions with function calls") + void testJoinSqlConditions_functionCalls() { + String result = + SqlConditionJoiner.joinSqlConditions("UPPER(column1) = 'TEST'", "LOWER(column2) = 'test'"); + assertEquals("where UPPER(column1) = 'TEST' and LOWER(column2) = 'test'", result); + } +}