Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
0da58f0
Query optimization tracker created
e4c5 Feb 2, 2026
4a94ccb
Target class setting added to restrict to a single class and also a s…
e4c5 Feb 2, 2026
3d411d7
Repo parsing errors targetted
e4c5 Feb 2, 2026
a926dc0
handle different query types differently
e4c5 Feb 3, 2026
c53d40a
Checkpointing
e4c5 Feb 3, 2026
43621fe
Removed the skip option in the configuration
e4c5 Feb 3, 2026
4ade4e0
More logging to figure out why method name changes are slow
e4c5 Feb 5, 2026
8607c98
SOme fields were getting duplicated in the display
e4c5 Feb 5, 2026
39fa26d
Updating service classes in batch after a repository signature changes
e4c5 Feb 5, 2026
6b7ebdc
Where clause display fix
e4c5 Feb 5, 2026
65ce104
Optimization to increase the speed of changing the callers of methods…
e4c5 Feb 6, 2026
49b07b4
Additional logs to get to the bottom of them refactoring getting stuck
e4c5 Feb 7, 2026
a29983d
A duplicate block was eliminated
e4c5 Feb 8, 2026
8d771a9
Remove redundant index checks and optimize index cleanup logic
e4c5 Feb 8, 2026
b9c8044
Pom version bumps
e4c5 Feb 8, 2026
c681cb9
Finally a speed up
e4c5 Feb 8, 2026
70565cb
Clean up
e4c5 Feb 8, 2026
909a165
Sonar linting
e4c5 Feb 8, 2026
d7a9e5b
Jackson needed to be explicitly added
e4c5 Feb 8, 2026
58ab4ca
Cleaning up
e4c5 Feb 8, 2026
2da70b3
New tests
e4c5 Feb 8, 2026
34a6573
Cleaner code
e4c5 Feb 9, 2026
767f6a6
Introduced a skip_class setting
e4c5 Feb 9, 2026
42079e4
Clean up
e4c5 Feb 9, 2026
78eef2a
Improvements suggested by code ant
e4c5 Feb 9, 2026
3fac774
Testing improvements
e4c5 Feb 9, 2026
0043118
Improvements suggested by code ant
e4c5 Feb 9, 2026
1ef14d7
Changes suggested by code rabbit
e4c5 Feb 9, 2026
f04582c
Overhaul the Checkpoint manager
e4c5 Feb 9, 2026
30d32f0
Sonar issues
e4c5 Feb 9, 2026
f7a1346
That method was unreadable
e4c5 Feb 9, 2026
fdd3bc0
Do not rename if there is already a method by that name
e4c5 Feb 9, 2026
0c9f84f
parameter reording fix
e4c5 Feb 9, 2026
1d05815
Linting
e4c5 Feb 9, 2026
b452cce
One more round of fixes to the method renaming
e4c5 Feb 9, 2026
0ef8f35
Will no longer refactor methods with a long list of parameters if the…
e4c5 Feb 10, 2026
eeaa4d0
Clean code
e4c5 Feb 10, 2026
d1642bd
A gap in mockit when calls
e4c5 Feb 10, 2026
0e05e0e
Merge remote-tracking branch 'origin/repeatable' into repeatable
e4c5 Feb 10, 2026
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
2 changes: 1 addition & 1 deletion docs/query-optimization-review-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ This prompt is designed to be used with AI coding agents (like Antigravity, GitH
---

## 🚀 AI Review Prompt
Carry out a review of the diff between the current branch and the main branch, focusing on the optimizations made by the `QueryOptimizer` tool.
Carry out a review of the diff of the current, focusing on the optimizations made by the `QueryOptimizer` tool.

Copilot AI Feb 8, 2026

Copy link

Choose a reason for hiding this comment

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

The sentence is grammatically incomplete (“diff of the current”); clarify what “current” refers to (e.g., “current branch” / “current PR”) and what it’s diffed against (e.g., “main”) so the prompt is unambiguous.

Suggested change
Carry out a review of the diff of the current, focusing on the optimizations made by the `QueryOptimizer` tool.
Carry out a review of the current Git diff, focusing on the optimizations made by the `QueryOptimizer` tool.

Copilot uses AI. Check for mistakes.

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 | 🟡 Minor

Incomplete sentence — missing noun after "current".

The prompt reads "Carry out a review of the diff of the current, focusing on…" which is grammatically incomplete. It likely should be "of the current branch" or "of the current changes".

✏️ Suggested fix
-Carry out a review of the diff of the current, focusing on the optimizations made by the `QueryOptimizer` tool.
+Carry out a review of the diff of the current branch, focusing on the optimizations made by the `QueryOptimizer` tool.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Carry out a review of the diff of the current, focusing on the optimizations made by the `QueryOptimizer` tool.
Carry out a review of the diff of the current branch, focusing on the optimizations made by the `QueryOptimizer` tool.
🤖 Prompt for AI Agents
In `@docs/query-optimization-review-prompt.md` at line 8, The sentence "Carry out
a review of the diff of the current, focusing on the optimizations made by the
`QueryOptimizer` tool." is grammatically incomplete; replace "current" with a
clear noun like "branch", "changes", or "diff" so the prompt reads e.g. "Carry
out a review of the diff of the current branch, focusing on the optimizations
made by the `QueryOptimizer` tool." Update the prompt text in
docs/query-optimization-review-prompt.md accordingly, keeping the referenced
`QueryOptimizer` token intact.


**Context:**
I have executed an automated `QueryOptimizer` on my Spring Boot / JPA project. The tool has performed two optimizations:
Expand Down
17 changes: 11 additions & 6 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<slf4j.version>2.0.13</slf4j.version>
<javaparser.version>3.26.2</javaparser.version>
<maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
<antikythera.version>0.1.3.2</antikythera.version>
</properties>

<repositories>
Expand All @@ -28,15 +29,14 @@
<dependency>
<groupId>sa.com.cloudsolutions</groupId>
<artifactId>antikythera</artifactId>
<version>0.1.3.1</version>
<version>${antikythera.version}</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Explicit dependencies for IDE resolution -->
<dependency>
<groupId>org.slf4j</groupId>
Expand Down Expand Up @@ -65,7 +65,7 @@
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.11.0</version>
<scope>test</scope>
<!-- Evaluator in antikythera requires Mockito at runtime for field setup -->
</dependency>
<dependency>
<groupId>com.github.jsqlparser</groupId>
Expand All @@ -89,7 +89,7 @@
<dependency>
<groupId>com.raditha</groupId>
<artifactId>hql-parser</artifactId>
<version>0.0.15</version>
<version>0.0.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down Expand Up @@ -136,12 +136,17 @@
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.17.0</version>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.17.2</version>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package sa.com.cloudsolutions.antikythera.examples;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.UUID;

/**
* Manages checkpoint/resume functionality for QueryOptimizer.
* Saves state to a JSON file after each successful repository analysis,
* allowing the optimizer to resume from where it left off if interrupted.
*
* <p>The checkpoint file is automatically deleted on successful completion
* of a full run.</p>
*
* <p>Usage:</p>
* <pre>
* CheckpointManager checkpoint = new CheckpointManager();
* checkpoint.load(); // Load existing checkpoint if present
*
* // Before processing each repository:
* if (checkpoint.isProcessed(repoName)) {
* // Skip - already processed in previous run
* continue;
* }
*
* // After successful processing:
* checkpoint.markProcessed(repoName);
* checkpoint.save();
*
* // On successful completion of all repositories:
* checkpoint.clear();
* </pre>
*/
public class CheckpointManager {
private static final Logger logger = LoggerFactory.getLogger(CheckpointManager.class);
private static final String DEFAULT_CHECKPOINT_FILE = ".query-optimizer-checkpoint.json";

private final File checkpointFile;
private final ObjectMapper objectMapper;
private Checkpoint checkpoint;
private boolean loaded = false;

/**
* Creates a CheckpointManager using the default checkpoint file location.
*/
public CheckpointManager() {
this(new File(DEFAULT_CHECKPOINT_FILE));
}

/**
* Creates a CheckpointManager with a custom checkpoint file location.
*
* @param checkpointFile the file to use for checkpoint storage
*/
public CheckpointManager(File checkpointFile) {
this.checkpointFile = checkpointFile;
this.objectMapper = new ObjectMapper();
this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
this.checkpoint = new Checkpoint();
}

/**
* Loads an existing checkpoint from disk if present.
* If no checkpoint exists or loading fails, starts with a fresh checkpoint.
* This method is idempotent - calling it multiple times returns the same result.
*
* @return true if a checkpoint was loaded, false if starting fresh
*/
public boolean load() {
// Return cached result if already loaded
if (loaded) {
return checkpoint.processedRepositories != null && !checkpoint.processedRepositories.isEmpty();
}

if (!checkpointFile.exists()) {
logger.debug("No checkpoint file found, starting fresh");
checkpoint = new Checkpoint();
checkpoint.sessionId = UUID.randomUUID().toString();
checkpoint.startTime = Instant.now().toString();
loaded = true;
return false;
}

try {
checkpoint = objectMapper.readValue(checkpointFile, Checkpoint.class);
logger.info("Loaded checkpoint from {} - {} repositories already processed",
checkpointFile.getName(), checkpoint.processedRepositories.size());
loaded = true;
Comment on lines +94 to +98

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 | 🟡 Minor

Potential NPE if deserialized checkpoint contains "processedRepositories": null.

When Jackson deserializes a JSON file with an explicit null for processedRepositories, the field initializer (new HashSet<>()) is overwritten with null. Line 97 then calls .size() on it, causing a NullPointerException. The same applies to save() (line 119), isProcessed() (line 147), and markProcessed() (line 156).

Consider adding a null-guard after deserialization, similar to the null-checks already present in getSuggestedNewIndexes() and getModifiedFiles().

🛡️ Proposed fix — normalize nulls after deserialization
         try {
             checkpoint = objectMapper.readValue(checkpointFile, Checkpoint.class);
+            if (checkpoint.processedRepositories == null) {
+                checkpoint.processedRepositories = new HashSet<>();
+            }
             logger.info("Loaded checkpoint from {} - {} repositories already processed",
                     checkpointFile.getName(), checkpoint.processedRepositories.size());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
checkpoint = objectMapper.readValue(checkpointFile, Checkpoint.class);
logger.info("Loaded checkpoint from {} - {} repositories already processed",
checkpointFile.getName(), checkpoint.processedRepositories.size());
loaded = true;
try {
checkpoint = objectMapper.readValue(checkpointFile, Checkpoint.class);
if (checkpoint.processedRepositories == null) {
checkpoint.processedRepositories = new HashSet<>();
}
logger.info("Loaded checkpoint from {} - {} repositories already processed",
checkpointFile.getName(), checkpoint.processedRepositories.size());
loaded = true;
🤖 Prompt for AI Agents
In
`@src/main/java/sa/com/cloudsolutions/antikythera/examples/CheckpointManager.java`
around lines 94 - 98, After deserializing the Checkpoint in CheckpointManager
(the objectMapper.readValue call), guard against a null processedRepositories by
initializing checkpoint.processedRepositories to an empty HashSet if it is null;
do the same normalization wherever Checkpoint instances are accepted (so save(),
isProcessed(), markProcessed()) or add a private normalize(Checkpoint) helper
used after readValue—mirror the existing null-guards used in
getSuggestedNewIndexes() and getModifiedFiles() to ensure processedRepositories
is never null before calling .size(), contains(), add(), or when saving.

return true;
} catch (IOException e) {
logger.warn("Failed to load checkpoint file: {}. Starting fresh.", e.getMessage());
checkpoint = new Checkpoint();
checkpoint.sessionId = UUID.randomUUID().toString();
checkpoint.startTime = Instant.now().toString();
loaded = true;
return false;
}
}

/**
* Saves the current checkpoint state to disk.
* Call this after each successful repository analysis.
*/
public void save() {
checkpoint.lastUpdate = Instant.now().toString();
try {
objectMapper.writeValue(checkpointFile, checkpoint);
logger.debug("Checkpoint saved: {} repositories processed",
checkpoint.processedRepositories.size());
} catch (IOException e) {
logger.error("Failed to save checkpoint: {}", e.getMessage());
}
}
Comment on lines +114 to +123

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 | 🟡 Minor

save() silently swallows IOException — callers assume checkpoint was persisted.

analyzeRepository in QueryOptimizer calls checkpointManager.save() after each repository to ensure resume capability. If save() fails silently, the caller proceeds believing the checkpoint is durable, but the next interruption will replay already-processed work (or worse, lose modified-file tracking).

Consider either propagating the exception or returning a boolean so callers can react.

🤖 Prompt for AI Agents
In
`@src/main/java/sa/com/cloudsolutions/antikythera/examples/CheckpointManager.java`
around lines 114 - 123, The save() method in CheckpointManager currently
swallows IOException which makes callers like QueryOptimizer.analyzeRepository
assume the checkpoint was persisted; change save() to propagate failures or
return status so callers can react: either (a) rethrow the IOException (or a
checked/unchecked wrapper) from CheckpointManager.save() so callers (e.g.,
QueryOptimizer.analyzeRepository) can handle retry/abort, or (b) change save()
to return boolean (true on success, false on failure) and update callers to
check the result and handle failures (retry, abort, or log+stop) accordingly;
locate the method named save() in class CheckpointManager and update all call
sites (e.g., QueryOptimizer.analyzeRepository) to follow the chosen contract.


/**
* Clears the checkpoint file. Call this on successful completion of a full run.
*/
public void clear() {
if (checkpointFile.exists()) {
if (checkpointFile.delete()) {
logger.info("Checkpoint cleared - run completed successfully");
} else {
logger.warn("Failed to delete checkpoint file: {}", checkpointFile.getName());
}
}
checkpoint = new Checkpoint();
loaded = false;
}

/**
* Checks if a repository has already been processed in this session.
*
* @param fullyQualifiedName the fully qualified repository class name
* @return true if already processed
*/
public boolean isProcessed(String fullyQualifiedName) {
return checkpoint.processedRepositories.contains(fullyQualifiedName);
}

/**
* Marks a repository as successfully processed.
*
* @param fullyQualifiedName the fully qualified repository class name
*/
public void markProcessed(String fullyQualifiedName) {
checkpoint.processedRepositories.add(fullyQualifiedName);
}

/**
* Gets the set of already processed repositories.
*
* @return unmodifiable set of processed repository names
*/
public Set<String> getProcessedRepositories() {
return Set.copyOf(checkpoint.processedRepositories);
}

/**
* Gets the number of repositories processed so far.
*
* @return count of processed repositories
*/
public int getProcessedCount() {
return checkpoint.processedRepositories.size();
}

/**
* Stores accumulated index suggestions for resume capability.
*
* @param singleColumnIndexes single-column index suggestions (table|column format)
* @param multiColumnIndexes multi-column index suggestions (table|col1,col2 format)
*/
public void setIndexSuggestions(LinkedHashSet<String> singleColumnIndexes,
LinkedHashSet<String> multiColumnIndexes) {
checkpoint.suggestedNewIndexes = new LinkedHashSet<>(singleColumnIndexes);
checkpoint.suggestedMultiColumnIndexes = new LinkedHashSet<>(multiColumnIndexes);
}

/**
* Gets the accumulated single-column index suggestions.
*
* @return set of index suggestions in table|column format
*/
public LinkedHashSet<String> getSuggestedNewIndexes() {
return checkpoint.suggestedNewIndexes != null
? new LinkedHashSet<>(checkpoint.suggestedNewIndexes)
: new LinkedHashSet<>();
}

/**
* Gets the accumulated multi-column index suggestions.
*
* @return set of index suggestions in table|col1,col2 format
*/
public LinkedHashSet<String> getSuggestedMultiColumnIndexes() {
return checkpoint.suggestedMultiColumnIndexes != null
? new LinkedHashSet<>(checkpoint.suggestedMultiColumnIndexes)
: new LinkedHashSet<>();
}

/**
* Stores the set of modified files for resume capability.
*
* @param modifiedFiles set of fully qualified class names that were modified
*/
public void setModifiedFiles(Set<String> modifiedFiles) {
checkpoint.modifiedFiles = new HashSet<>(modifiedFiles);
}

/**
* Gets the set of files that were modified in this session.
*
* @return set of modified file class names
*/
public Set<String> getModifiedFiles() {
return checkpoint.modifiedFiles != null
? new HashSet<>(checkpoint.modifiedFiles)
: new HashSet<>();
}

/**
* Checks if there's an existing checkpoint to resume from.
*
* @return true if a checkpoint file exists
*/
public boolean hasCheckpoint() {
return checkpointFile.exists();
}

/**
* Gets the session ID for the current checkpoint.
*
* @return the session UUID string
*/
public String getSessionId() {
return checkpoint.sessionId;
}

/**
* Gets the checkpoint file location.
*
* @return the checkpoint file
*/
public File getCheckpointFile() {
return checkpointFile;
}

/**
* Data class representing the checkpoint state.
* Uses Jackson for JSON serialization.
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Checkpoint {
private String sessionId;
private String startTime;
private String lastUpdate;
Comment on lines +264 to +266

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.

Suggestion: The checkpoint metadata fields were changed from public to private without adding getters/setters or Jackson annotations, so with the default ObjectMapper configuration these fields will no longer be serialized/deserialized, causing sessionId, startTime, and lastUpdate to be lost across runs and breaking the intended checkpoint tracking behavior. [logic error]

Severity Level: Major ⚠️
- ⚠️ Checkpoint JSON omits sessionId/startTime/lastUpdate metadata fields.
- ⚠️ Session ID changes when resuming from existing checkpoint file.
- ⚠️ Docs JSON example no longer matches runtime checkpoint output.
- ⚠️ Harder to correlate optimization runs across interruptions and resumes.
Suggested change
private String sessionId;
private String startTime;
private String lastUpdate;
public String sessionId;
public String startTime;
public String lastUpdate;
Steps of Reproduction ✅
1. Run the unit test setup in
`src/test/java/sa/com/cloudsolutions/antikythera/examples/CheckpointManagerTest.java:23-27`,
which creates a `CheckpointManager` pointing at a temp file and calls `manager.load()`
(CheckpointManager.load at
`src/main/java/sa/com/cloudsolutions/antikythera/examples/CheckpointManager.java:79-108`).

2. In the same JVM, mark some repositories and call `manager.save()` as in
`CheckpointManagerTest.testMarkProcessedAndSave` at lines 45-55; this invokes
`CheckpointManager.save()` (`CheckpointManager.java:114-123`), which writes the checkpoint
JSON to disk.

3. Inspect the written JSON file (the `checkpointFile` used in the test). Because
`Checkpoint.Checkpoint` declares `sessionId`, `startTime`, and `lastUpdate` as private
fields at `CheckpointManager.java:264-266` and `ObjectMapper` is created with default
visibility at `CheckpointManager.java:65-69` (no custom field visibility configuration or
Jackson annotations), the JSON will contain only `processedRepositories`,
`suggestedNewIndexes`, `suggestedMultiColumnIndexes`, and `modifiedFiles`, but no
`sessionId`, `startTime`, or `lastUpdate` keys.

4. Create a new `CheckpointManager` instance with the same file path, as done in
`CheckpointManagerTest.testResumeFromCheckpoint` at lines 67-73, and call
`newManager.load()` (`CheckpointManager.java:79-107`), then call
`newManager.getSessionId()` (`CheckpointManager.java:241-247`); the returned UUID comes
solely from the `Checkpoint` default constructor (`CheckpointManager.java:272-276`) and is
not restored from the JSON file, demonstrating that session metadata is not persisted
across runs.

5. Compare this behavior with the documentation example JSON in
`docs/spring_boot_2.3_to_2.4_migration.md:1632-1635`, which shows `startTime` as part of
the checkpoint data; the runtime JSON produced by the current code no longer includes
these documented metadata fields, confirming the loss of persisted session information.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/main/java/sa/com/cloudsolutions/antikythera/examples/CheckpointManager.java
**Line:** 264:266
**Comment:**
	*Logic Error: The checkpoint metadata fields were changed from public to private without adding getters/setters or Jackson annotations, so with the default ObjectMapper configuration these fields will no longer be serialized/deserialized, causing `sessionId`, `startTime`, and `lastUpdate` to be lost across runs and breaking the intended checkpoint tracking behavior.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

public Set<String> processedRepositories = new HashSet<>();
public Set<String> suggestedNewIndexes = new LinkedHashSet<>();
public Set<String> suggestedMultiColumnIndexes = new LinkedHashSet<>();
public Set<String> modifiedFiles = new HashSet<>();

// Default constructor for Jackson
public Checkpoint() {
this.sessionId = UUID.randomUUID().toString();
this.startTime = Instant.now().toString();
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading