Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"]
}
}
}
}
Comment on lines +177 to 199

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Gemini 1.5 Flash responseMimeType responseSchema structured JSON output support

💡 Result:

Short answer: Yes — the Gemini API supports structured JSON outputs via response_mime_type = "application/json" plus a response_json_schema/responseSchema, but Gemini 1.5 Flash has limited/spotty behavior and some JSON‑Schema keywords (and array constraints like maxItems) have caused failures; Google recommends using the newer Gemini 2.5 models for full, reliable JSON Schema structured-output support.

Sources:

  • Official Gemini structured-output docs (shows response_mime_type = application/json and response_json_schema, streaming support, and JSON‑Schema type limitations). [1]
  • Community/notes that responseMimeType + responseSchema is supported but model support varies (mentions Gemini 1.5 Pro / Flash and limitations). [2]
  • Forum report of gemini-1.5-flash failing when using maxItems (practical example of 1.5 limitations). [3]

If you want, I can: (a) draft an example generation request for gemini-1.5-flash (with a fallback strategy), or (b) show the same example targeting gemini-2.5-flash which is recommended for robust JSON Schema support. Which do you prefer?


🏁 Script executed:

grep -n "gemini" src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java | head -20

Repository: e4c5/antikythera-examples

Length of output: 141


Upgrade model to Gemini 2.5 for reliable JSON Schema support.

Gemini 1.5 Flash has spotty and limited support for structured JSON output via responseMimeType and responseSchema. Per official Google documentation, Gemini 2.5 models are recommended for robust JSON Schema support. Consider updating the model from gemini-1.5-flash (line 258) to gemini-2.5-flash to ensure reliable structured output handling.

🤖 Prompt for AI Agents
In src/main/java/sa/com/cloudsolutions/antikythera/examples/GeminiAIService.java
around lines 177 to 199 (and the model reference at line 258), the code uses the
gemini-1.5-flash model which has unreliable JSON Schema support; change the
model identifier to gemini-2.5-flash where the model is selected (line ~258),
ensure any model-specific config fields remain valid for 2.5, and run a quick
integration test to verify the responseMimeType/responseSchema produce stable
structured JSON outputs.

""", escapedSystemPrompt, escapedUserData);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -40,7 +44,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;
/**
Expand Down Expand Up @@ -68,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 {
flush();
current = new Stats(repo);
}
totalRepositoriesProcessed++;
}

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(),
current.repo,
current.queriesAnalyzed,
current.queryAnnotationsChanged,
current.methodSignaturesChanged,
current.methodCallsChanged,
current.dependentClassesModified,
current.liquibaseIndexesGenerated,
current.liquibaseIndexesDropped));
current = new Stats(repo);
totalRepositoriesProcessed++;
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());
}
out.close();
fw.close();
}

public static void updateQueriesAnalyzed(int queriesAnalyzed) {
Expand All @@ -118,6 +151,11 @@ public static void updateMethodCallsChanged(int i) {
total.methodCallsChanged += i;
}

public static void updateRepositoriesModified(int i) {
current.repositoriesModified += i;

Copilot AI Jan 3, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method lacks a null check for current before accessing it, which could cause a NullPointerException. Other similar methods like updateIndexesDropped have this protection. Add a null check: if (current != null) before line 155.

Suggested change
current.repositoriesModified += i;
if (current != null) {
current.repositoriesModified += i;
}

Copilot uses AI. Check for mistakes.
total.repositoriesModified += i;
}

public static void updateDependentClassesChanged(int i) {
current.dependentClassesModified += i;
total.dependentClassesModified += i;
Expand All @@ -130,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;
}

Expand All @@ -148,7 +188,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);
Comment thread
e4c5 marked this conversation as resolved.
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,31 @@ public QueryAnalysisResult analyzeQuery(RepositoryQuery repositoryQuery) {
}

ConversionResult conversionResult = repositoryQuery.getConversionResult();
Statement statementToAnalyze;
List<WhereCondition> whereConditions;
List<JoinCondition> 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<WhereCondition> whereConditions, ConversionResult conversionResult) {
Expand All @@ -68,6 +79,36 @@ private static void updateWhereConditions(List<WhereCondition> 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<JoinCondition> 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.).
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class QueryAnalysisResult {

private final RepositoryQuery query;
private final List<WhereCondition> whereConditions;
private List<JoinCondition> joinConditions;
private OptimizationIssue optimizationIssue;
private List<String> indexSuggestions;

Expand All @@ -27,6 +28,7 @@ public class QueryAnalysisResult {
public QueryAnalysisResult(RepositoryQuery query, List<WhereCondition> whereConditions) {
this.query = query;
this.whereConditions = new ArrayList<>(whereConditions != null ? whereConditions : Collections.emptyList());
this.joinConditions = Collections.emptyList();
this.indexSuggestions = List.of();
}

Expand Down Expand Up @@ -120,4 +122,12 @@ public void setIndexSuggestions(List<String> indexSuggestions) {
public void setOptimizationIssue(OptimizationIssue optimizationIssue) {
this.optimizationIssue = optimizationIssue;
}

public List<JoinCondition> getJoinConditions() {
return joinConditions != null ? joinConditions : Collections.emptyList();
}

public void setJoinConditions(List<JoinCondition> joinConditions) {
this.joinConditions = joinConditions != null ? new ArrayList<>(joinConditions) : Collections.emptyList();
}
}
Loading