Skip to content

Commit 29be78a

Browse files
aksOpsclaude
andcommitted
Fix CLI command crashes and H2 cache data loss
- Add no-arg constructors to FlowCommand, GraphCommand, FindCommand, and QueryCommand so Picocli can instantiate them without Spring DI - Fix H2 cache node table: replace single-column PRIMARY KEY(id) with auto-increment surrogate key to preserve duplicate node IDs from different files and within the same file - Deduplicate nodes in loadAllNodes() and getNodeCount() using GROUP BY/ DISTINCT for accurate stats display - Update error messages for graph/find/flow/query commands to point users to 'code-iq serve' or 'code-iq stats' instead of vague Neo4j reference Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ea6e213 commit 29be78a

5 files changed

Lines changed: 34 additions & 10 deletions

File tree

src/main/java/io/github/randomcodespace/iq/cache/AnalysisCache.java

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ CREATE TABLE IF NOT EXISTS files (
4747
);
4848
4949
CREATE TABLE IF NOT EXISTS nodes (
50-
id VARCHAR PRIMARY KEY,
50+
row_id BIGINT AUTO_INCREMENT PRIMARY KEY,
51+
id VARCHAR NOT NULL,
5152
content_hash VARCHAR NOT NULL,
5253
kind VARCHAR NOT NULL,
5354
data VARCHAR NOT NULL,
@@ -180,7 +181,7 @@ public void storeResults(String contentHash, String filePath, String language,
180181
stmt.execute();
181182
}
182183

183-
// Insert nodes
184+
// Insert nodes (no unique constraint on id — duplicates preserved for accurate cache replay)
184185
try (var stmt = conn.prepareStatement(
185186
"INSERT INTO nodes (id, content_hash, kind, data) VALUES (?, ?, ?, ?)")) {
186187
for (CodeNode node : nodes) {
@@ -321,7 +322,7 @@ public Map<String, Object> getStats() {
321322
Map<String, Object> stats = new LinkedHashMap<>();
322323
try {
323324
stats.put("cached_files", countTable("files"));
324-
stats.put("cached_nodes", countTable("nodes"));
325+
stats.put("cached_nodes", getNodeCount());
325326
stats.put("cached_edges", countTable("edges"));
326327
stats.put("total_runs", countTable("analysis_runs"));
327328
stats.put("db_path", dbPath.toString());
@@ -467,8 +468,10 @@ private long countTable(String table) throws SQLException {
467468
* Return the total number of cached nodes.
468469
*/
469470
public long getNodeCount() {
470-
try {
471-
return countTable("nodes");
471+
try (var stmt = conn.createStatement()) {
472+
ResultSet rs = stmt.executeQuery("SELECT COUNT(DISTINCT id) FROM nodes");
473+
rs.next();
474+
return rs.getLong(1);
472475
} catch (SQLException e) {
473476
log.debug("Failed to count nodes", e);
474477
return 0;
@@ -510,7 +513,8 @@ public void storeBatchResults(String batchId, String filePath, String language,
510513
*/
511514
public List<CodeNode> loadAllNodes() {
512515
List<CodeNode> nodes = new ArrayList<>();
513-
try (var stmt = conn.prepareStatement("SELECT data FROM nodes")) {
516+
// Use MIN(data) with GROUP BY id to deduplicate nodes that appear in multiple files
517+
try (var stmt = conn.prepareStatement("SELECT MIN(data) FROM nodes GROUP BY id")) {
514518
ResultSet rs = stmt.executeQuery();
515519
while (rs.next()) {
516520
nodes.add(deserializeNode(rs.getString(1)));

src/main/java/io/github/randomcodespace/iq/cli/FindCommand.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public class FindCommand implements Callable<Integer> {
3737

3838
private final GraphStore graphStore;
3939

40+
/** No-arg constructor for Picocli direct instantiation. */
41+
public FindCommand() {
42+
this.graphStore = null;
43+
}
44+
4045
@Autowired
4146
public FindCommand(Optional<GraphStore> graphStore) {
4247
this.graphStore = graphStore.orElse(null);
@@ -50,7 +55,7 @@ public FindCommand(Optional<GraphStore> graphStore) {
5055
@Override
5156
public Integer call() {
5257
if (graphStore == null) {
53-
CliOutput.error("Find queries require Neo4j. Run 'code-iq enrich' first, then 'code-iq serve'.");
58+
CliOutput.error("Find queries require the serve profile (Neo4j). Use 'code-iq serve' to start the server, or 'code-iq stats' for cache-based queries.");
5459
return 1;
5560
}
5661
NodeKind kind = resolveKind(what);

src/main/java/io/github/randomcodespace/iq/cli/FlowCommand.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public class FlowCommand implements Callable<Integer> {
4040

4141
private final FlowEngine flowEngine;
4242

43+
/** No-arg constructor for Picocli direct instantiation. */
44+
public FlowCommand() {
45+
this.flowEngine = null;
46+
}
47+
4348
@Autowired
4449
public FlowCommand(Optional<FlowEngine> flowEngine) {
4550
this.flowEngine = flowEngine.orElse(null);
@@ -53,7 +58,7 @@ public FlowCommand(Optional<FlowEngine> flowEngine) {
5358
@Override
5459
public Integer call() {
5560
if (flowEngine == null) {
56-
CliOutput.error("Flow diagrams require Neo4j. Run 'code-iq enrich' first, then 'code-iq serve'.");
61+
CliOutput.error("Flow diagrams require the serve profile (Neo4j). Use 'code-iq serve' to start the server, or 'code-iq stats' for cache-based queries.");
5762
return 1;
5863
}
5964
try {

src/main/java/io/github/randomcodespace/iq/cli/GraphCommand.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public class GraphCommand implements Callable<Integer> {
5252

5353
private final GraphStore graphStore;
5454

55+
/** No-arg constructor for Picocli direct instantiation. */
56+
public GraphCommand() {
57+
this.graphStore = null;
58+
}
59+
5560
@Autowired
5661
public GraphCommand(Optional<GraphStore> graphStore) {
5762
this.graphStore = graphStore.orElse(null);
@@ -65,7 +70,7 @@ public GraphCommand(Optional<GraphStore> graphStore) {
6570
@Override
6671
public Integer call() {
6772
if (graphStore == null) {
68-
CliOutput.error("Graph export requires Neo4j. Run 'code-iq enrich' first, then 'code-iq serve'.");
73+
CliOutput.error("Graph export requires the serve profile (Neo4j). Use 'code-iq serve' to start the server, or 'code-iq stats' for cache-based queries.");
6974
return 1;
7075
}
7176
List<CodeNode> nodes;

src/main/java/io/github/randomcodespace/iq/cli/QueryCommand.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ public class QueryCommand implements Callable<Integer> {
5050

5151
private final QueryService queryService;
5252

53+
/** No-arg constructor for Picocli direct instantiation. */
54+
public QueryCommand() {
55+
this.queryService = null;
56+
}
57+
5358
@Autowired
5459
public QueryCommand(Optional<QueryService> queryService) {
5560
this.queryService = queryService.orElse(null);
@@ -63,7 +68,7 @@ public QueryCommand(Optional<QueryService> queryService) {
6368
@Override
6469
public Integer call() {
6570
if (queryService == null) {
66-
CliOutput.error("Graph queries require Neo4j. Run 'code-iq enrich' first, then 'code-iq serve'.");
71+
CliOutput.error("Graph queries require the serve profile (Neo4j). Use 'code-iq serve' to start the server, or 'code-iq stats' for cache-based queries.");
6772
return 1;
6873
}
6974
if (consumersOf != null) {

0 commit comments

Comments
 (0)