From 29918fa2c098b6ab1df6e0cc943d055efa86ea56 Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Fri, 5 Dec 2025 21:47:14 +0530 Subject: [PATCH 1/7] Sending the json header --- .../antikythera/examples/GeminiAIService.java | 31 +++++++++++++++++-- .../antikythera/examples/QueryOptimizer.java | 6 ++-- .../query-optimization-system-prompt.md | 3 +- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java index 095d0f50..1cd06ec6 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java @@ -153,6 +153,7 @@ String buildRequestPayload(QueryBatch batch) { * Builds the Gemini API request with proper message structure. * Uses Gemini's contents format with separate parts for system and user * content. + * Enables JSON Mode via responseMimeType to guarantee valid JSON output. */ String buildGeminiApiRequest(String userQueryData) { // Escape strings for JSON @@ -173,7 +174,28 @@ String buildGeminiApiRequest(String userQueryData) { { "text": "%s" } ] } - ] + ], + "generationConfig": { + "responseMimeType": "application/json", + "responseSchema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "originalMethod": { + "type": "string" + }, + "optimizedCodeElement": { + "type": "string" + }, + "notes": { + "type": "string" + } + }, + "required": ["originalMethod", "optimizedCodeElement", "notes"] + } + } + } } """, escapedSystemPrompt, escapedUserData); } @@ -491,7 +513,12 @@ OptimizedQueryResult extractRecommendedColumnOrder(String optimizedCodeElement, CompilationUnit cu = new CompilationUnit(); cu.addType(cdecl); - CompilationUnit tmp = StaticJavaParser.parse(String.format("interface Dummy{ %s }", optimizedCodeElement)); + // Jackson converts JSON \n to actual newlines, but StaticJavaParser needs them + // escaped + // Convert actual newlines back to \\n for parsing + String escapedForParsing = optimizedCodeElement.replace("\n", "\\n"); + + CompilationUnit tmp = StaticJavaParser.parse(String.format("interface Dummy{ %s }", escapedForParsing)); for (ImportDeclaration importDecl : originalCompilationUnit.getImports()) { cu.addImport(importDecl); } diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java index d4e8f16a..e9bba2f8 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java @@ -58,9 +58,9 @@ public QueryOptimizer(File liquibaseXmlPath) throws Exception { @Override void analyzeRepository(TypeWrapper typeWrapper) throws IOException, ReflectiveOperationException, InterruptedException { - if (!typeWrapper.getFullyQualifiedName().equals("com.csi.bm.invoice.repository.InvoiceItemRepository")) { - return; - } +// if (typeWrapper.getFullyQualifiedName().endsWith("ProcedureApprovalRepository")) { +// return; +// } super.analyzeRepository(typeWrapper); OptimizationStatsLogger.updateQueriesAnalyzed(results.size()); diff --git a/src/main/resources/ai-prompts/query-optimization-system-prompt.md b/src/main/resources/ai-prompts/query-optimization-system-prompt.md index 3faed825..4a4f3a7f 100644 --- a/src/main/resources/ai-prompts/query-optimization-system-prompt.md +++ b/src/main/resources/ai-prompts/query-optimization-system-prompt.md @@ -94,8 +94,7 @@ Analyze each input query and provide the optimized code element. Return your entire response as a single, structured JSON array ([]). Do not add any introductory text, conversation, or closing remarks. -The array must contain one JSON object per input query, in the exact same order as the input. Try to break long queries -to be multi line/ +The array must contain one JSON object per input query, in the exact same order as the input. For long queries, use \n to break them into multiple lines within the JSON string values. ` [{ From e9373563426e8a6f33777e9d147cccbabbc31e96 Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Fri, 5 Dec 2025 22:11:56 +0530 Subject: [PATCH 2/7] Query formatting --- .../examples/OptimizationStatsLogger.java | 15 ++++-- .../antikythera/examples/QueryOptimizer.java | 54 +++++++++++++++---- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java index a4c99d13..bc1d7d2e 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java @@ -40,7 +40,11 @@ public static class Stats { */ private int methodCallsChanged = 0; /** - * The number of classes changed so far! + * The number of repository files that were actually modified. + */ + private int repositoriesModified = 0; + /** + * The number of dependent classes changed (not including repositories). */ private int dependentClassesModified = 0; /** @@ -92,8 +96,8 @@ public static void initialize(String repo) throws IOException { current.liquibaseIndexesGenerated, current.liquibaseIndexesDropped)); current = new Stats(repo); - totalRepositoriesProcessed++; } + totalRepositoriesProcessed++; out.close(); fw.close(); } @@ -118,6 +122,11 @@ public static void updateMethodCallsChanged(int i) { total.methodCallsChanged += i; } + public static void updateRepositoriesModified(int i) { + current.repositoriesModified += i; + total.repositoriesModified += i; + } + public static void updateDependentClassesChanged(int i) { current.dependentClassesModified += i; total.dependentClassesModified += i; @@ -148,7 +157,7 @@ public static void printSummary(PrintStream out) { out.println("šŸ“ CODE MODIFICATION SUMMARY"); out.println("=".repeat(80)); out.printf("Repositories processed: %d%n", totalRepositoriesProcessed); - out.printf("Dependent Files modified: %d%n", total.dependentClassesModified); + out.printf("Repository files modified: %d%n", total.repositoriesModified); out.printf("@Query annotations changed: %d%n", total.queryAnnotationsChanged); out.printf("Method signatures changed: %d%n", total.methodSignaturesChanged); out.printf("Method calls updated: %d%n", total.methodCallsChanged); diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java index e9bba2f8..03f1683b 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java @@ -14,6 +14,8 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter; import com.github.javaparser.ast.NodeList; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sa.com.cloudsolutions.antikythera.configuration.Settings; @@ -58,9 +60,11 @@ public QueryOptimizer(File liquibaseXmlPath) throws Exception { @Override void analyzeRepository(TypeWrapper typeWrapper) throws IOException, ReflectiveOperationException, InterruptedException { -// if (typeWrapper.getFullyQualifiedName().endsWith("ProcedureApprovalRepository")) { -// return; -// } + // if + // (typeWrapper.getFullyQualifiedName().endsWith("ProcedureApprovalRepository")) + // { + // return; + // } super.analyzeRepository(typeWrapper); OptimizationStatsLogger.updateQueriesAnalyzed(results.size()); @@ -77,6 +81,8 @@ void analyzeRepository(TypeWrapper typeWrapper) this.repositoryParser.getCompilationUnit()); if (!fileWasWritten) { repositoryFileModified = false; + } else { + OptimizationStatsLogger.updateRepositoriesModified(1); } } } @@ -127,26 +133,54 @@ void actOnAnalysisResult(QueryAnalysisResult result, List u } } + /** + * Formats a SQL query string with proper line wrapping and indentation. + * Uses JSqlParser to parse and pretty-print the query. + * + * @param queryValue the raw SQL/HQL query string + * @return formatted query string, or original if parsing fails + */ + private String formatSqlQuery(String queryValue) { + if (queryValue == null || queryValue.trim().isEmpty()) { + return queryValue; + } + + try { + // Try to parse as SQL statement + Statement statement = CCJSqlParserUtil.parse(queryValue); + // JSqlParser's toString() produces formatted output + return statement.toString(); + } catch (Exception e) { + // If parsing fails (e.g., HQL-specific syntax), return original + logger.debug("Could not parse query as SQL, using original formatting: {}", e.getMessage()); + return queryValue; + } + } + /** * Updates the annotation value with proper text block support. - * If the query contains literal \n characters or actual newlines, it uses + * If the query contains literal \\n characters or actual newlines, it uses * TextBlockLiteralExpr. * Otherwise, it uses StringLiteralExpr. + * The query is formatted using JSqlParser for proper line wrapping. * * @param method the method declaration containing the annotation * @param newStringValue the new query value */ private void updateAnnotationValueWithTextBlockSupport(MethodDeclaration method, String newStringValue) { - // Check if the string contains literal \n or actual newlines - boolean isMultiline = newStringValue != null && - (newStringValue.contains("\\n") || newStringValue.contains("\n")); + // Format the SQL query first + String formattedValue = formatSqlQuery(newStringValue); + + // Check if the string contains literal \\n or actual newlines + boolean isMultiline = formattedValue != null && + (formattedValue.contains("\\n") || formattedValue.contains("\n")); if (isMultiline) { - // Convert literal \n to actual newlines - String processedValue = " " + newStringValue.replace("\\n", "\n "); + // Convert literal \\n to actual newlines and add proper indentation + String processedValue = " " + formattedValue.replace("\\n", "\n "); updateAnnotationValue(method, "Query", processedValue, true); } else { - updateAnnotationValue(method, "Query", newStringValue, false); + updateAnnotationValue(method, "Query", formattedValue, false); } } From 6ccec5076af20000f791708eb48166cca742ae8f Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Thu, 11 Dec 2025 11:44:42 +0530 Subject: [PATCH 3/7] logger improvements --- .../sa/com/cloudsolutions/antikythera/examples/Logger.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/Logger.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/Logger.java index d298013c..1f292092 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/Logger.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/Logger.java @@ -65,7 +65,8 @@ private static void processClass(String classname, CompilationUnit cu) throws Fi loggerFields.clear(); FieldVisitor visitor = new FieldVisitor(); - if (decl.getAnnotationByName("Slf4j").isPresent()) { + if (decl.getAnnotationByName("Slf4j").isPresent() || + decl.getAnnotationByName("Log4j2").isPresent()) { loggerFields.add("log"); } // Always visit fields to find any explicitly declared loggers From 947bb3035103efa07360910c4ea4aee2bc377c16 Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Thu, 11 Dec 2025 12:02:03 +0530 Subject: [PATCH 4/7] QueryOptimizer optimization --- .../antikythera/examples/GeminiAIService.java | 7 +--- .../antikythera/examples/QueryOptimizer.java | 40 +++---------------- .../query-optimization-system-prompt.md | 6 ++- 3 files changed, 10 insertions(+), 43 deletions(-) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java index 1cd06ec6..4384164e 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java @@ -513,12 +513,7 @@ OptimizedQueryResult extractRecommendedColumnOrder(String optimizedCodeElement, CompilationUnit cu = new CompilationUnit(); cu.addType(cdecl); - // Jackson converts JSON \n to actual newlines, but StaticJavaParser needs them - // escaped - // Convert actual newlines back to \\n for parsing - String escapedForParsing = optimizedCodeElement.replace("\n", "\\n"); - - CompilationUnit tmp = StaticJavaParser.parse(String.format("interface Dummy{ %s }", escapedForParsing)); + CompilationUnit tmp = StaticJavaParser.parse(String.format("interface Dummy{ %s }", optimizedCodeElement)); for (ImportDeclaration importDecl : originalCompilationUnit.getImports()) { cu.addImport(importDecl); } diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java index 03f1683b..19f3dcd6 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java @@ -14,8 +14,6 @@ import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter; import com.github.javaparser.ast.NodeList; -import net.sf.jsqlparser.parser.CCJSqlParserUtil; -import net.sf.jsqlparser.statement.Statement; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sa.com.cloudsolutions.antikythera.configuration.Settings; @@ -133,54 +131,26 @@ void actOnAnalysisResult(QueryAnalysisResult result, List u } } - /** - * Formats a SQL query string with proper line wrapping and indentation. - * Uses JSqlParser to parse and pretty-print the query. - * - * @param queryValue the raw SQL/HQL query string - * @return formatted query string, or original if parsing fails - */ - private String formatSqlQuery(String queryValue) { - if (queryValue == null || queryValue.trim().isEmpty()) { - return queryValue; - } - - try { - // Try to parse as SQL statement - Statement statement = CCJSqlParserUtil.parse(queryValue); - // JSqlParser's toString() produces formatted output - return statement.toString(); - } catch (Exception e) { - // If parsing fails (e.g., HQL-specific syntax), return original - logger.debug("Could not parse query as SQL, using original formatting: {}", e.getMessage()); - return queryValue; - } - } - /** * Updates the annotation value with proper text block support. * If the query contains literal \\n characters or actual newlines, it uses * TextBlockLiteralExpr. * Otherwise, it uses StringLiteralExpr. - * The query is formatted using JSqlParser for proper line wrapping. * * @param method the method declaration containing the annotation * @param newStringValue the new query value */ private void updateAnnotationValueWithTextBlockSupport(MethodDeclaration method, String newStringValue) { - // Format the SQL query first - String formattedValue = formatSqlQuery(newStringValue); - // Check if the string contains literal \\n or actual newlines - boolean isMultiline = formattedValue != null && - (formattedValue.contains("\\n") || formattedValue.contains("\n")); + boolean isMultiline = newStringValue != null && + (newStringValue.contains("\\\\n") || newStringValue.contains("\n")); if (isMultiline) { - // Convert literal \\n to actual newlines and add proper indentation - String processedValue = " " + formattedValue.replace("\\n", "\n "); + // Convert literal \\n to actual newlines + String processedValue = " " + newStringValue.replace("\\\\n", "\n "); updateAnnotationValue(method, "Query", processedValue, true); } else { - updateAnnotationValue(method, "Query", formattedValue, false); + updateAnnotationValue(method, "Query", newStringValue, false); } } diff --git a/src/main/resources/ai-prompts/query-optimization-system-prompt.md b/src/main/resources/ai-prompts/query-optimization-system-prompt.md index 4a4f3a7f..941ca147 100644 --- a/src/main/resources/ai-prompts/query-optimization-system-prompt.md +++ b/src/main/resources/ai-prompts/query-optimization-system-prompt.md @@ -94,12 +94,14 @@ Analyze each input query and provide the optimized code element. Return your entire response as a single, structured JSON array ([]). Do not add any introductory text, conversation, or closing remarks. -The array must contain one JSON object per input query, in the exact same order as the input. For long queries, use \n to break them into multiple lines within the JSON string values. +The array must contain one JSON object per input query, in the exact same order as the input. + +**CRITICAL:** All SQL queries in the `optimizedCodeElement` field must be formatted as **single-line strings**. Do NOT use `\n` or newline characters to break SQL queries into multiple lines. Keep all SQL on a single line with spaces between clauses. ` [{ "originalMethod": "[The original methodName from the input]", -"optimizedCodeElement": "[The FULL optimized method signature OR the FULL @Query annotation (including the method signature)]", +"optimizedCodeElement": "[The FULL optimized method signature OR the FULL @Query annotation (including the method signature) - ALL SQL MUST BE ON A SINGLE LINE]", "notes": "[Choose ONE: 'Reordered method signature and parameters for optimal derived query performance.' OR 'Reordered predicates in the HQL/JPQL WHERE clause for optimal performance.' OR 'Reordered predicates in the NATIVE_SQL WHERE clause for optimal performance.' OR 'N/A - Query is already optimized for cardinality.' OR 'N/A - Query is single-column.' OR 'N/A - All predicates have identical cardinality; no optimization possible.']" }]` From b73e1e2d9847cba0cd0febb692f8e9b943e8500f Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Thu, 11 Dec 2025 13:25:55 +0530 Subject: [PATCH 5/7] Add indexes for the joins --- .../examples/QueryAnalysisEngine.java | 49 ++++++- .../examples/QueryAnalysisResult.java | 10 ++ .../examples/QueryOptimizationChecker.java | 120 ++++++++++++++++-- .../antikythera/examples/QueryOptimizer.java | 25 +--- 4 files changed, 169 insertions(+), 35 deletions(-) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisEngine.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisEngine.java index a92a1c4f..918d64bf 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisEngine.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisEngine.java @@ -34,20 +34,31 @@ public QueryAnalysisResult analyzeQuery(RepositoryQuery repositoryQuery) { } ConversionResult conversionResult = repositoryQuery.getConversionResult(); + Statement statementToAnalyze; List whereConditions; + List joinConditions; + if (conversionResult != null && conversionResult.getNativeSql() != null) { try { - Statement st = CCJSqlParserUtil.parse(conversionResult.getNativeSql()); - whereConditions = QueryOptimizationExtractor.extractWhereConditions(st); + statementToAnalyze = CCJSqlParserUtil.parse(conversionResult.getNativeSql()); + whereConditions = QueryOptimizationExtractor.extractWhereConditions(statementToAnalyze); + joinConditions = QueryOptimizationExtractor.extractJoinConditions(statementToAnalyze); } catch (JSQLParserException jsqe) { throw new AntikytheraException(jsqe); } } else { - whereConditions = QueryOptimizationExtractor.extractWhereConditions(repositoryQuery.getStatement()); + statementToAnalyze = repositoryQuery.getStatement(); + whereConditions = QueryOptimizationExtractor.extractWhereConditions(statementToAnalyze); + joinConditions = QueryOptimizationExtractor.extractJoinConditions(statementToAnalyze); } + updateWhereConditions(whereConditions, conversionResult); - return new QueryAnalysisResult(repositoryQuery, whereConditions); + updateJoinConditions(joinConditions, conversionResult); + + QueryAnalysisResult result = new QueryAnalysisResult(repositoryQuery, whereConditions); + result.setJoinConditions(joinConditions); + return result; } private static void updateWhereConditions(List whereConditions, ConversionResult conversionResult) { @@ -68,6 +79,36 @@ private static void updateWhereConditions(List whereConditions, } } + /** + * Updates JOIN conditions by resolving entity aliases to actual table names. + * Focuses on right-side table/column resolution as these are critical for JOIN performance. + * + * @param joinConditions the list of JOIN conditions to update + * @param conversionResult the conversion result containing entity metadata + */ + private static void updateJoinConditions(List joinConditions, ConversionResult conversionResult) { + for (JoinCondition condition : joinConditions) { + if (conversionResult != null) { + // Resolve right-side table name (the probe table - critical for JOIN performance) + if (condition.getRightTable() != null) { + String entity = conversionResult.getMetaData().getEntityForAlias(condition.getRightTable()); + if (entity != null) { + String tableName = EntityMappingResolver.getTableNameForEntity(entity); + condition.setRightTable(tableName); + } + } + // Resolve left-side table name for completeness + if (condition.getLeftTable() != null) { + String entity = conversionResult.getMetaData().getEntityForAlias(condition.getLeftTable()); + if (entity != null) { + String tableName = EntityMappingResolver.getTableNameForEntity(entity); + condition.setLeftTable(tableName); + } + } + } + } + } + /** * Handles analysis of derived query methods (findBy*, countBy*, etc.). */ diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisResult.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisResult.java index 3c146a42..9fa669ac 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisResult.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryAnalysisResult.java @@ -15,6 +15,7 @@ public class QueryAnalysisResult { private final RepositoryQuery query; private final List whereConditions; + private List joinConditions; private OptimizationIssue optimizationIssue; private List indexSuggestions; @@ -27,6 +28,7 @@ public class QueryAnalysisResult { public QueryAnalysisResult(RepositoryQuery query, List whereConditions) { this.query = query; this.whereConditions = new ArrayList<>(whereConditions != null ? whereConditions : Collections.emptyList()); + this.joinConditions = Collections.emptyList(); this.indexSuggestions = List.of(); } @@ -120,4 +122,12 @@ public void setIndexSuggestions(List indexSuggestions) { public void setOptimizationIssue(OptimizationIssue optimizationIssue) { this.optimizationIssue = optimizationIssue; } + + public List getJoinConditions() { + return joinConditions != null ? joinConditions : Collections.emptyList(); + } + + public void setJoinConditions(List joinConditions) { + this.joinConditions = joinConditions != null ? new ArrayList<>(joinConditions) : Collections.emptyList(); + } } diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java index d5b3de85..588b52f9 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java @@ -281,6 +281,8 @@ QueryAnalysisResult createResultWithIndexAnalysis(OptimizationIssue llmRecommend } } + analyzeJoinRightSide(engineResult, requiredIndexes); + // Create enhanced optimization issue with index analysis OptimizationIssue enhancedRecommendation = new OptimizationIssue( llmRecommendation.query(), @@ -296,6 +298,29 @@ QueryAnalysisResult createResultWithIndexAnalysis(OptimizationIssue llmRecommend return result; } + private void analyzeJoinRightSide(QueryAnalysisResult engineResult, List requiredIndexes) { + // Analyze right-side JOIN columns for missing indexes (critical for JOIN performance) + for (JoinCondition joinCondition : engineResult.getJoinConditions()) { + String rightTable = joinCondition.getRightTable(); + String rightColumn = joinCondition.getRightColumn(); + + if (rightTable != null && rightColumn != null) { + CardinalityLevel cardinality = CardinalityAnalyzer.analyzeColumnCardinality(rightTable, rightColumn); + + // Only suggest indexes for non-low cardinality columns + if (cardinality != CardinalityLevel.LOW && !hasOptimalIndexForColumn(rightTable, rightColumn)) { + String indexRecommendation = String.format("%s.%s (JOIN)", rightTable, rightColumn); + + // Avoid duplication - check if already suggested from WHERE clause + String baseRecommendation = String.format("%s.%s", rightTable, rightColumn); + if (!requiredIndexes.contains(baseRecommendation)) { + requiredIndexes.add(indexRecommendation); + } + } + } + } + } + /** * Checks if there's an optimal index for the given column using the Indexes * class. @@ -393,17 +418,7 @@ void reportOptimizationIssues(QueryAnalysisResult result) { System.out.println(formatOptimizationIssueEnhanced(issue, result)); if (!result.getIndexSuggestions().isEmpty()) { - System.out.println(" šŸ“‹ Required Indexes:"); - for (String indexRecommendation : result.getIndexSuggestions()) { - String[] parts = indexRecommendation.split("\\.", 2); - if (parts.length == 2) { - String table = parts[0]; - String column = parts[1]; - boolean hasExistingIndex = CardinalityAnalyzer.hasIndexWithLeadingColumn(table, column); - String status = hasExistingIndex ? "āœ“ EXISTS" : "⚠ MISSING"; - System.out.printf(" • %s.%s [%s]\n", table, column, status); - } - } + reportOptimizationIssuesHelper(result); } // Collect any index creation suggestions for consolidation at the end @@ -412,6 +427,59 @@ void reportOptimizationIssues(QueryAnalysisResult result) { System.out.println(); // Add blank line for readability } + private static void reportOptimizationIssuesHelper(QueryAnalysisResult result) { + // Separate WHERE and JOIN index suggestions + List whereIndexes = new ArrayList<>(); + List joinIndexes = new ArrayList<>(); + + for (String indexRecommendation : result.getIndexSuggestions()) { + if (indexRecommendation.contains("(JOIN)")) { + joinIndexes.add(indexRecommendation); + } else { + whereIndexes.add(indexRecommendation); + } + } + + // Report WHERE clause indexes + if (!whereIndexes.isEmpty()) { + reportWhereClauseIndexes(whereIndexes); + } + + // Report JOIN indexes with critical marker + if (!joinIndexes.isEmpty()) { + reportJoinColumnIndexes(joinIndexes); + } + } + + private static void reportJoinColumnIndexes(List joinIndexes) { + System.out.println(" šŸ”“ Critical JOIN Indexes (right-side probe table):"); + for (String indexRecommendation : joinIndexes) { + String cleanRecommendation = indexRecommendation.replace(" (JOIN)", ""); + String[] parts = cleanRecommendation.split("\\.", 2); + if (parts.length == 2) { + String table = parts[0]; + String column = parts[1]; + boolean hasExistingIndex = CardinalityAnalyzer.hasIndexWithLeadingColumn(table, column); + String status = hasExistingIndex ? "āœ“ EXISTS" : "⚠ MISSING"; + System.out.printf(" • %s.%s [%s] - JOIN performance critical\n", table, column, status); + } + } + } + + private static void reportWhereClauseIndexes(List whereIndexes) { + System.out.println(" šŸ“‹ Required Indexes (WHERE clause):"); + for (String indexRecommendation : whereIndexes) { + String[] parts = indexRecommendation.split("\\.", 2); + if (parts.length == 2) { + String table = parts[0]; + String column = parts[1]; + boolean hasExistingIndex = CardinalityAnalyzer.hasIndexWithLeadingColumn(table, column); + String status = hasExistingIndex ? "āœ“ EXISTS" : "⚠ MISSING"; + System.out.printf(" • %s.%s [%s]\n", table, column, status); + } + } + } + /** * Formats an optimization issue with enhanced display including cardinality * information, @@ -605,6 +673,7 @@ void collectIndexSuggestions(QueryAnalysisResult result) { // multiple tables Map> columnsByTable = new HashMap<>(); + // Process WHERE conditions for (WhereCondition condition : result.getWhereConditions()) { if (condition.cardinality() != CardinalityLevel.LOW) { String tableName = condition.tableName() == null ? result.getQuery().getPrimaryTable() @@ -613,6 +682,12 @@ void collectIndexSuggestions(QueryAnalysisResult result) { } } + groupJoinColumnsByTable(result, columnsByTable); + + generatedRequiredIndexesList(columnsByTable); + } + + private void generatedRequiredIndexesList(Map> columnsByTable) { // Process each table's columns separately to create table-specific indexes for (Map.Entry> entry : columnsByTable.entrySet()) { String table = entry.getKey(); @@ -643,6 +718,29 @@ void collectIndexSuggestions(QueryAnalysisResult result) { } } + private static void groupJoinColumnsByTable(QueryAnalysisResult result, Map> columnsByTable) { + // Process right-side JOIN columns (critical for JOIN performance) + for (JoinCondition joinCondition : result.getJoinConditions()) { + String rightTable = joinCondition.getRightTable(); + String rightColumn = joinCondition.getRightColumn(); + + if (rightTable != null && rightColumn != null) { + CardinalityLevel cardinality = CardinalityAnalyzer.analyzeColumnCardinality(rightTable, rightColumn); + + // Only add non-low cardinality columns that don't already have indexes + if (cardinality != CardinalityLevel.LOW && + !CardinalityAnalyzer.hasIndexWithLeadingColumn(rightTable, rightColumn)) { + + // Add to columnsByTable for index generation, avoiding duplicates + List tableColumns = columnsByTable.computeIfAbsent(rightTable, k -> new ArrayList<>()); + if (!tableColumns.contains(rightColumn)) { + tableColumns.add(rightColumn); + } + } + } + } + } + String indent(String s, int spaces) { String pad = " ".repeat(Math.max(0, spaces)); return java.util.Arrays.stream(s.split("\n")) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java index 19f3dcd6..154591dd 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java @@ -29,9 +29,7 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -58,34 +56,22 @@ public QueryOptimizer(File liquibaseXmlPath) throws Exception { @Override void analyzeRepository(TypeWrapper typeWrapper) throws IOException, ReflectiveOperationException, InterruptedException { - // if - // (typeWrapper.getFullyQualifiedName().endsWith("ProcedureApprovalRepository")) - // { - // return; - // } super.analyzeRepository(typeWrapper); OptimizationStatsLogger.updateQueriesAnalyzed(results.size()); - List updates = new ArrayList<>(); repositoryFileModified = false; - for (QueryAnalysisResult result : results) { - actOnAnalysisResult(result, updates); + actOnAnalysisResult(result); } - if (repositoryFileModified) { - boolean fileWasWritten = writeFile(typeWrapper.getFullyQualifiedName(), - this.repositoryParser.getCompilationUnit()); - if (!fileWasWritten) { - repositoryFileModified = false; - } else { - OptimizationStatsLogger.updateRepositoriesModified(1); - } + if (repositoryFileModified && writeFile(typeWrapper.getFullyQualifiedName(), + this.repositoryParser.getCompilationUnit())) { + OptimizationStatsLogger.updateRepositoriesModified(1); } } - void actOnAnalysisResult(QueryAnalysisResult result, List updates) { + void actOnAnalysisResult(QueryAnalysisResult result) { OptimizationIssue issue = result.getOptimizationIssue(); if (issue != null) { RepositoryQuery optimizedQuery = issue.optimizedQuery(); @@ -126,7 +112,6 @@ void actOnAnalysisResult(QueryAnalysisResult result, List u if (methodNameChanged) { OptimizationStatsLogger.updateMethodSignaturesChanged(1); } - updates.add(result); } } } From c8f73f7c85e9c4bc6711e9df6b5a1c9675c5635c Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Fri, 2 Jan 2026 10:19:26 +0530 Subject: [PATCH 6/7] This file was in the wrong place --- .../com/raditha/samples/SampleJUnit4Test.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/test/java/com/raditha/samples/SampleJUnit4Test.java diff --git a/src/test/java/com/raditha/samples/SampleJUnit4Test.java b/src/test/java/com/raditha/samples/SampleJUnit4Test.java deleted file mode 100644 index 6fde8da7..00000000 --- a/src/test/java/com/raditha/samples/SampleJUnit4Test.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.test.sample; - -import org.junit.Before; -import org.junit.After; -import org.junit.Test; -import org.junit.Ignore; -import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -@RunWith(MockitoJUnitRunner.class) -public class SampleJUnit4Test { - - @Before - public void setUp() { - System.out.println("Setup"); - } - - @After - public void tearDown() { - System.out.println("Teardown"); - } - - @Test - public void testWithMessage() { - assertEquals("Values should be equal", 5, 2 + 3); - assertTrue("Should be true", 1 < 2); - } - - @Test(expected = IllegalArgumentException.class) - public void testExpectedException() { - throw new IllegalArgumentException("Expected"); - } - - @Test(timeout = 1000) - public void testWithTimeout() { - // Should complete quickly - } - - @Ignore("Not ready yet") - @Test - public void testIgnored() { - // Ignored test - } -} From 28a17346114014a718682cdf64d590a3a546ad6e Mon Sep 17 00:00:00 2001 From: Raditha Dissanayake Date: Fri, 2 Jan 2026 12:30:14 +0530 Subject: [PATCH 7/7] Stats logging improvements --- .../examples/OptimizationStatsLogger.java | 77 +++++++++++++------ .../examples/QueryOptimizationChecker.java | 11 ++- .../antikythera/examples/QueryOptimizer.java | 4 +- 3 files changed, 63 insertions(+), 29 deletions(-) diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java index bc1d7d2e..0c4b13dc 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/OptimizationStatsLogger.java @@ -1,5 +1,8 @@ package sa.com.cloudsolutions.antikythera.examples; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.FileWriter; import java.io.IOException; import java.io.PrintStream; @@ -14,8 +17,9 @@ * the changes made to repository classes. */ public class OptimizationStatsLogger { + private static final Logger logger = LoggerFactory.getLogger(OptimizationStatsLogger.class); private static final String CSV_FILENAME = "query-optimization-stats.csv"; - private static final String CSV_HEADER = "Date,Repositories,Queries,Annotations Changed,Signatures Changed,Calls Updated,Dependency Classes,Indexes Created, Indexes Dropped"; + private static final String CSV_HEADER = "Date,Repository Name,Queries,Annotations Changed,Signatures Changed,Calls Updated,Dependency Classes,Indexes Created,Indexes Dropped"; private static Stats current = null; private static Stats total; @@ -72,34 +76,59 @@ public static Stats getCurrent() { return current; } - public static void initialize(String repo) throws IOException { - Path csvPath = Paths.get(CSV_FILENAME); - FileWriter fw = new FileWriter(CSV_FILENAME, true); - PrintWriter out = new PrintWriter(fw); - - if (!csvPath.toFile().exists()) { - out.println(CSV_HEADER); - } - + public static void initialize(String repo) { if (current == null) { current = new Stats(repo); total = new Stats(""); } else { - out.println(String.format("%s,%s,%d,%d,%d,%d,%d,%d,%d", - java.time.LocalDate.now(), - current.repo, - current.queriesAnalyzed, - current.queryAnnotationsChanged, - current.methodSignaturesChanged, - current.methodCallsChanged, - current.dependentClassesModified, - current.liquibaseIndexesGenerated, - current.liquibaseIndexesDropped)); + flush(); current = new Stats(repo); } totalRepositoriesProcessed++; - out.close(); - fw.close(); + } + + public static void flush() { + if (current == null) { + return; + } + logStats(current); + // Reset current stats to zero for any post-analysis global updates, + // but preserve the repo name to avoid NullPointerException if used. + current.queriesAnalyzed = 0; + current.queryAnnotationsChanged = 0; + current.methodSignaturesChanged = 0; + current.methodCallsChanged = 0; + current.repositoriesModified = 0; + current.dependentClassesModified = 0; + current.liquibaseIndexesGenerated = 0; + current.liquibaseIndexesDropped = 0; + } + + + private static void logStats(Stats stats) { + Path csvPath = Paths.get(CSV_FILENAME); + boolean exists = csvPath.toFile().exists(); + + try (FileWriter fw = new FileWriter(CSV_FILENAME, true); + PrintWriter out = new PrintWriter(fw)) { + + if (!exists) { + out.println(CSV_HEADER); + } + + out.println(String.format("%s,%s,%d,%d,%d,%d,%d,%d,%d", + java.time.LocalDate.now(), + stats.repo, + stats.queriesAnalyzed, + stats.queryAnnotationsChanged, + stats.methodSignaturesChanged, + stats.methodCallsChanged, + stats.dependentClassesModified, + stats.liquibaseIndexesGenerated, + stats.liquibaseIndexesDropped)); + } catch (IOException e) { + logger.error("Failed to write to CSV: {}", e.getMessage()); + } } public static void updateQueriesAnalyzed(int queriesAnalyzed) { @@ -139,7 +168,9 @@ public static int updateIndexesGenerated(int i) { } public static void updateIndexesDropped(int i) { - current.liquibaseIndexesDropped += i; + if (current != null) { + current.liquibaseIndexesDropped += i; + } total.liquibaseIndexesDropped += i; } diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java index 588b52f9..6fc9f20d 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizationChecker.java @@ -107,6 +107,7 @@ public void analyze() throws IOException, ReflectiveOperationException, Interrup } } } + OptimizationStatsLogger.flush(); System.out.printf("\nāœ… Successfully analyzed %d out of %d repositories%n", repositoriesProcessed, i); } @@ -705,14 +706,18 @@ private void generatedRequiredIndexesList(Map> columnsByTab if (filteredColumns.size() > 1) { // Create multi-column index for this specific table String key = (table + "|" + String.join(",", filteredColumns)).toLowerCase(); - suggestedMultiColumnIndexes.add(key); + if (suggestedMultiColumnIndexes.add(key)) { + OptimizationStatsLogger.updateIndexesGenerated(1); + } } else if (filteredColumns.size() == 1) { // Only one column needs indexing - create single-column index String column = filteredColumns.get(0); boolean hasExisting = CardinalityAnalyzer.hasIndexWithLeadingColumn(table, column); if (!hasExisting) { String key = (table + "|" + column).toLowerCase(); - suggestedNewIndexes.add(key); + if (suggestedNewIndexes.add(key)) { + OptimizationStatsLogger.updateIndexesGenerated(1); + } } } } @@ -810,7 +815,7 @@ List generateLiquibaseChangesets() { result.add(indentXml(changeSet, 4)); } } - int totalIndexCreateRecommendations = OptimizationStatsLogger.updateIndexesGenerated(result.size()); + int totalIndexCreateRecommendations = OptimizationStatsLogger.getTotalIndexesGenerated(); // Add create index summary comment (or note no create recommendations) if (totalIndexCreateRecommendations > 0) { diff --git a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java index 154591dd..e6925a60 100644 --- a/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java +++ b/src/main/java/sa/com/cloudsolutions/antikythera/examples/QueryOptimizer.java @@ -448,15 +448,13 @@ public static void main(String[] args) throws Exception { CardinalityAnalyzer.configureUserDefinedCardinality(lowOverride, highOverride); QueryOptimizer checker = new QueryOptimizer(getLiquibasePath()); - OptimizationStatsLogger.initialize(""); - checker.analyze(); // Generate Liquibase file with suggested changes and include in master checker.generateLiquibaseChangesFile(); - OptimizationStatsLogger.printSummary(System.out); OptimizationStatsLogger.updateDependentClassesChanged(modifiedFiles.size()); + OptimizationStatsLogger.printSummary(System.out); updateFiles(); if (!quietMode) {