Skip to content

Commit ecf954f

Browse files
geekypunkclaudevenkateshsakamuri-lab
authored
fix(brain): unwedge suggestion approval and refresh knowledge counts live (#77)
Follow-up to #74. That PR made the failure *visible* (bulk decide now returns `failures[]`, and the UI renders a partial/zero success as an error) but the approval itself still threw — its diagnosis, a `schema_documentation_source_check` missing `CODE_DERIVED`, did not apply to the reporting install, where the CHECK was already correct and the log said something else entirely. ## What was actually failing ``` WARN CodeScanService : bulk decide skipped e2011614-…: Query did not return a unique result: 2 results were returned WARN CodeScanService : bulk decide skipped f3689e0e-…: Query did not return a unique result: 2 results were returned ``` `schema_documentation` carried no unique constraint on `(connection_id, object_type, object_name, parent_object, source)` and `CodeSuggestionApplier.approve` took no row lock, so **one bulk approve submitted twice concurrently wrote 219 duplicate pairs** (pairs ~3s apart; in every group the older row is an orphan and the newer one holds the `applied_doc_id`). Every later approve landing on such a key threw out of the `Optional`-returning upsert finder, and bulk-decide swallowed it per item. The duplicate never self-heals, so **all 198 pending SCHEMA_DOC suggestions were permanently unapprovable**. ## Fix **1. Duplicate-tolerant upsert.** The three finders now return `List`, so the compiler forces every caller to handle N matches. `SchemaDocumentationDeduplicator` keeps the newest row, repoints any `applied_doc_id` off the rows it deletes (a loose reference, not an FK — a dangling value fails silently), and drops their RAG embeddings. Applied at all four call sites, including `SchemaDriftListener`, which would have thrown identically the first time one of 17 duplicated tables was dropped. **2. Data repair + constraint.** `V116__dedupe_schema_documentation.sql`, applied by `SchemaDocumentationDedupeInitializer` — there is no Flyway runtime here, so a SQL file alone would never run. Idempotent: it returns before touching a row once the index exists. `coalesce(parent_object,'')` in the key because Postgres treats NULLs as distinct. **3. The root cause.** `approve`/`reject` load the suggestion `FOR UPDATE`, so the concurrent double-submit blocks and the second caller sees `APPROVED`. ## Also fixed (reported after the first fix landed) - **Counts stale until reload.** An approval also writes `schema_documentation`, served by `brain/notes`, which backs the Write-notes tab and its coverage counts. The decide hooks invalidated only `codeScan` + `companyKnowledge`. `invalidateAfterDecision` now covers `brain` and `schemaContext` too. - **Newest notes sorted last.** `@PreUpdate` never fires on insert, so a new note has a null `updatedAt`; sorting on it alone with nulls last sent every brand-new note to the *bottom*. Now `COALESCE(updatedAt, createdAt)`, matching `CompanyKnowledgeEntryRepository`. - **Fresh approvals buried in the Approved view.** `listSuggestions` sorted every status by confidence. PENDING stays confidence-first (it is a work queue); decided statuses now sort by `decidedAt DESC NULLS LAST`. ## Two bugs in #74's own test tooling - Both scripts hardcoded `sudo -u postgres psql`, which does not exist on the Compose deployment `install.sh` produces — the verify command in #74's description failed before testing anything. Now resolved through `scripts/self-host/vaultdb.py`. - **`e2e-review-approvals.py` step 10 rewrote every real `CODE_DERIVED` row to `source='USER'` and never restored it.** Running it against a live install silently relabelled 339 approved docs, and post-V116 it would collide with the unique index. It now parks rows in a scratch table and restores them with a verified count, and its cleanup deletes the planted row only while nothing references it. ## Verification Run against the live self-host stack, not just unit tests. - Initializer on the affected install: `removed 219 duplicate rows, 219 orphaned embeddings`; 7068 → 6849 doc rows, 8190 → 7971 RAG rows, **0 duplicate groups, 0 dangling `applied_doc_id`**. Skipped cleanly on restart. - The two originally stuck suggestions: `{"requested": 2, "succeeded": 2, "failed": 0}`. - Ordering, approving the three *lowest*-confidence items so the two sorts disagree — all three land above older `conf=0.97` rows. - `brain/notes` 4607 → 4608 on a first-time doc (the count that never refreshed), and the just-approved `crm.customers` now sorts first. - 21 backend unit tests green (4 new on collapse, 2 new on note ordering). 184 related tests run; the 6 failures in `TrainingServiceBusinessTermTest` / `BrainInitStageExecutorTest` reproduce identically on pristine `main` and are unrelated. - **E2E suite: 30/30 pass**, including duplicate collapse, `applied_doc_id` repointing, the unique index rejecting a second row, and two genuinely concurrent approves writing exactly one row. ```bash python3 scripts/self-host/seed-review-suggestions.py <connectionId> --count 20 python3 scripts/self-host/e2e-review-approvals.py <connectionId> # ✓ All review-approval edge cases passed ``` Note: the suite consumes its own fixtures, so re-run the seed before each run. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018mG2xj9gWJ8WzfDP2fDePP Co-authored-by: Krishna Sasank Talasila <606482+geekypunk@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent f28cc95 commit ecf954f

20 files changed

Lines changed: 1028 additions & 83 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,7 @@ optd-sidecar/target/
104104
*.iml
105105
.local-admin-credentials
106106
.local-mcp-token
107+
108+
# Python bytecode from scripts/
109+
__pycache__/
110+
*.pyc

CLAUDE.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,16 @@ broken. Assert the *outcome*, never the attempt:
296296
- **Mocks hide SDK breaks.** `tests/tools/test_mcp_structured_content.py` uses a
297297
`_FakeCallToolResult` with a hardcoded `.isError`, so it kept passing precisely when
298298
the real SDK stopped matching. Pin the dependency; a fake cannot catch this.
299+
- **A self-host verification script must reach the DB the way the install does.**
300+
`seed-review-suggestions.py` / `e2e-review-approvals.py` hardcoded
301+
`sudo -u postgres psql`, which only exists on a bare-metal install — on the Compose
302+
deployment `install.sh` actually produces, the documented verify command died before
303+
testing anything. Both now resolve the path through `scripts/self-host/vaultdb.py`.
304+
- **A test that mutates shared state must restore it, and only what it created.** The
305+
same e2e suite parked every real `CODE_DERIVED` row by rewriting `source` to `USER`
306+
and never restored it, so a run against a live install silently relabelled the user's
307+
approved docs. It now copies rows to a scratch table and restores them, and its
308+
cleanup deletes the planted row only while nothing references it.
299309
- **Never claim a check you did not run.** `install.sh` reported "up to date" when it
300310
could not reach npm; it now says it could not check.
301311
- **`set -e` + `read` at EOF aborts silently.** Prompts in `install.sh` use
@@ -384,6 +394,53 @@ it against a real database — not a theoretical hardening pass.
384394
`POST /users/admin/reset` on every install that had run `setup-agent.sh`, since that
385395
mints an admin MCP token on each run.
386396

397+
- **An `Optional`-returning derived finder is an assertion that the key is unique.**
398+
Spring Data throws `IncorrectResultSizeDataAccessException` ("Query did not return a
399+
unique result: N results were returned") the moment it is not, and the row that broke
400+
it never repairs itself, so the failure is permanent rather than transient.
401+
`schema_documentation` had no unique constraint on
402+
`(connection_id, object_type, object_name, parent_object, source)` and
403+
`CodeSuggestionApplier.approve` took no row lock, so one bulk approve submitted twice
404+
concurrently wrote 219 duplicate pairs. Every later approve touching one of those keys
405+
threw, `CodeScanService.bulkDecide` swallowed it per item, and the Review queue
406+
reported "Approved 0 of 2" — with all 198 pending SCHEMA_DOC suggestions wedged.
407+
Three-part fix, and all three are load-bearing:
408+
1. `V116__dedupe_schema_documentation.sql` + `SchemaDocumentationDedupeInitializer`
409+
(no Flyway here, so the initializer is what actually applies it) collapse the
410+
duplicates and add `ux_schema_doc_target`, keyed on
411+
`coalesce(parent_object,'')` because Postgres treats NULLs as distinct.
412+
2. Those finders now return `List`, and `SchemaDocumentationDeduplicator.collapse`
413+
keeps the newest row, repoints any `applied_doc_id` off the rows it deletes, and
414+
drops their RAG embeddings. Do not restore an `Optional` variant — legacy installs
415+
still carry duplicates until the initializer runs.
416+
3. `approve`/`reject` load the suggestion via `findByIdForUpdate` (`PESSIMISTIC_WRITE`)
417+
so the concurrent double-submit that created the duplicates blocks instead of racing.
418+
- **`applied_doc_id` is a loose reference, not an FK.** Deleting a `schema_documentation`
419+
row it points at raises nothing and dangles silently — repoint before deleting.
420+
- **Approve *updates* the row an earlier scan wrote**, so a "freshly approved" doc row
421+
carries a historical `created_at`. A test that plants an "old" duplicate with a
422+
hardcoded past date can easily plant the *newer* of the two and assert nothing; anchor
423+
fixture timestamps to the real row's `created_at`.
424+
- **A write's blast radius decides what to invalidate, not the endpoint you called.**
425+
Approving a code-scan suggestion writes `code_knowledge_suggestion` *and*
426+
`schema_documentation` (served by `brain/notes`, which backs the Write-notes tab
427+
and its coverage counts) *and* `rag_documents` *and*, for KNOWLEDGE_ENTRY, a
428+
company knowledge entry. The decide hooks invalidated only `codeScan` +
429+
`companyKnowledge`, so every schema-doc-derived count stayed stale until the user
430+
reloaded the page. `invalidateAfterDecision` in `useCodeScan.js` is the single
431+
place that lists them; add to it when an approval starts writing something new.
432+
- **`@PreUpdate` does not fire on insert, so `updatedAt` is null on a brand-new row.**
433+
Sorting "newest first" on `updatedAt` alone with nulls last therefore sends every
434+
freshly created row to the *bottom* — which is why a just-approved note did not
435+
appear at the top of the Write-notes list. Sort on
436+
`COALESCE(updatedAt, createdAt)` (`BrainNoteService.touchedAt`,
437+
`CompanyKnowledgeEntryRepository.findByConnectionIdOrderByRecency`).
438+
- **Suggestion list order depends on the status being viewed.** PENDING is a work
439+
queue → `confidence DESC`. APPROVED/REJECTED are history → `decidedAt DESC NULLS
440+
LAST` so the decision you just made is at the top; confidence-sorting a decided
441+
list scattered fresh approvals among hundreds of older ones
442+
(`CodeScanService.sortFor`).
443+
387444
### Endpoint Authorization Rules
388445

389446
- **Authentication is not authorization.** `SecurityConfig` only asserts
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.dbaagent.config;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.context.annotation.DependsOn;
7+
import org.springframework.jdbc.core.JdbcTemplate;
8+
import org.springframework.transaction.support.TransactionTemplate;
9+
import org.springframework.transaction.PlatformTransactionManager;
10+
11+
import javax.sql.DataSource;
12+
13+
/**
14+
* Applies {@code V116__dedupe_schema_documentation.sql} at startup: collapses
15+
* duplicate {@code schema_documentation} rows and adds the unique index on the
16+
* logical key.
17+
*
18+
* <p>This repo has no Flyway runtime — {@code db/migration} is a hand-maintained
19+
* changelog and Hibernate {@code ddl-auto=update} never adds an index the entity
20+
* does not declare. Without this, self-host installs carrying duplicates from a
21+
* double-submitted bulk approve stay wedged: every SCHEMA_DOC approve throws
22+
* {@code Query did not return a unique result}. Mirrors
23+
* {@link SchemaDocumentationSourceCompatibilityInitializer}.
24+
*
25+
* <p>Idempotent and cheap on a clean install: the index exists, so it returns
26+
* before touching a row.
27+
*/
28+
@Configuration
29+
@Slf4j
30+
public class SchemaDocumentationDedupeInitializer {
31+
32+
private static final String TABLE = "schema_documentation";
33+
private static final String INDEX = "ux_schema_doc_target";
34+
35+
@Bean("schemaDocumentationDedupeBootstrap")
36+
@DependsOn("entityManagerFactory")
37+
public Object schemaDocumentationDedupeBootstrap(DataSource dataSource,
38+
PlatformTransactionManager txManager) {
39+
JdbcTemplate jdbc = new JdbcTemplate(dataSource);
40+
if (!tableExists(jdbc, TABLE)) {
41+
return new Object();
42+
}
43+
if (indexExists(jdbc, INDEX)) {
44+
return new Object();
45+
}
46+
47+
// One transaction: a half-applied dedupe (rows deleted, index missing)
48+
// would silently re-accumulate duplicates until the next boot.
49+
new TransactionTemplate(txManager).executeWithoutResult(status -> {
50+
int repointed = jdbc.update("""
51+
UPDATE code_knowledge_suggestion s
52+
SET applied_doc_id = l.keep_id
53+
FROM (%s) l
54+
WHERE s.applied_doc_id = l.id
55+
""".formatted(LOSERS));
56+
57+
int embeddings = jdbc.update(
58+
"DELETE FROM rag_documents WHERE id IN (SELECT id FROM (%s) l)".formatted(LOSERS));
59+
60+
int removed = jdbc.update(
61+
"DELETE FROM schema_documentation WHERE id IN (SELECT id FROM (%s) l)".formatted(LOSERS));
62+
63+
jdbc.execute("""
64+
CREATE UNIQUE INDEX IF NOT EXISTS %s
65+
ON %s (connection_id, object_type, object_name, coalesce(parent_object, ''), source)
66+
""".formatted(INDEX, TABLE));
67+
68+
if (removed > 0) {
69+
log.warn("Deduped {}: removed {} duplicate rows, {} orphaned embeddings, "
70+
+ "repointed {} applied_doc_id references",
71+
TABLE, removed, embeddings, repointed);
72+
}
73+
log.info("Ensured unique index {} on {}", INDEX, TABLE);
74+
});
75+
return new Object();
76+
}
77+
78+
/**
79+
* Every row but the newest within each logical key. Newest wins because it is
80+
* the row existing {@code applied_doc_id} references point at; {@code id}
81+
* breaks ties for rows written in the same clock tick. {@code coalesce} on
82+
* {@code parent_object} because Postgres treats NULLs as distinct, so TABLE
83+
* rows would otherwise never group together.
84+
*/
85+
private static final String LOSERS = """
86+
SELECT id, keep_id FROM (
87+
SELECT id,
88+
first_value(id) OVER w AS keep_id,
89+
row_number() OVER w AS rn
90+
FROM schema_documentation
91+
WINDOW w AS (
92+
PARTITION BY connection_id, object_type, object_name,
93+
coalesce(parent_object, ''), source
94+
ORDER BY created_at DESC NULLS LAST, id DESC
95+
)
96+
) ranked WHERE rn > 1
97+
""";
98+
99+
private boolean tableExists(JdbcTemplate jdbc, String tableName) {
100+
Integer count = jdbc.queryForObject("""
101+
SELECT COUNT(*)
102+
FROM information_schema.tables
103+
WHERE table_schema = 'public' AND table_name = ?
104+
""", Integer.class, tableName);
105+
return count != null && count > 0;
106+
}
107+
108+
private boolean indexExists(JdbcTemplate jdbc, String indexName) {
109+
Integer count = jdbc.queryForObject("""
110+
SELECT COUNT(*)
111+
FROM pg_indexes
112+
WHERE schemaname = 'public' AND indexname = ?
113+
""", Integer.class, indexName);
114+
return count != null && count > 0;
115+
}
116+
}

backend/src/main/java/com/dbaagent/repository/CodeKnowledgeSuggestionRepository.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,44 @@
33
import com.dbaagent.model.code.CodeKnowledgeSuggestion;
44
import org.springframework.data.domain.Page;
55
import org.springframework.data.domain.Pageable;
6+
import jakarta.persistence.LockModeType;
67
import org.springframework.data.jpa.repository.JpaRepository;
8+
import org.springframework.data.jpa.repository.Lock;
9+
import org.springframework.data.jpa.repository.Modifying;
10+
import org.springframework.data.jpa.repository.Query;
11+
import org.springframework.data.repository.query.Param;
712
import org.springframework.stereotype.Repository;
813

14+
import java.util.Collection;
915
import java.util.List;
16+
import java.util.Optional;
1017

1118
@Repository
1219
public interface CodeKnowledgeSuggestionRepository extends JpaRepository<CodeKnowledgeSuggestion, String> {
1320

21+
/**
22+
* Row-locking load used by approve/reject. Without it two concurrent bulk
23+
* decides both read the same suggestion as PENDING and both materialize a
24+
* {@code schema_documentation} row — the duplicate-row bug that
25+
* {@code V116__dedupe_schema_documentation.sql} had to clean up. The second
26+
* caller now blocks, then sees APPROVED and returns early.
27+
*/
28+
@Lock(LockModeType.PESSIMISTIC_WRITE)
29+
@Query("SELECT s FROM CodeKnowledgeSuggestion s WHERE s.id = :id")
30+
Optional<CodeKnowledgeSuggestion> findByIdForUpdate(@Param("id") String id);
31+
32+
/**
33+
* Repoints approvals at the surviving row when duplicate schema_documentation
34+
* rows are collapsed. {@code applied_doc_id} is a loose reference, not an FK,
35+
* so deleting a duplicate would otherwise leave a suggestion pointing at a row
36+
* that no longer exists — silently, since nothing enforces it.
37+
*/
38+
@Modifying(flushAutomatically = true)
39+
@Query("UPDATE CodeKnowledgeSuggestion s SET s.appliedDocId = :keepId "
40+
+ "WHERE s.appliedDocId IN :staleIds")
41+
int repointAppliedDocId(@Param("keepId") String keepId,
42+
@Param("staleIds") Collection<String> staleIds);
43+
1444
Page<CodeKnowledgeSuggestion> findByConnectionIdAndStatus(
1545
String connectionId,
1646
CodeKnowledgeSuggestion.Status status,

backend/src/main/java/com/dbaagent/repository/SchemaDocumentationRepository.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,15 @@ List<SchemaDocumentation> findByConnectionIdAndObjectType(
2121
SchemaDocumentation.DocumentationType objectType
2222
);
2323

24-
Optional<SchemaDocumentation> findByConnectionIdAndObjectTypeAndObjectName(
24+
/**
25+
* Returns a {@link List}, never an {@link Optional} — the logical key is not
26+
* unique in data written before {@code V116__dedupe_schema_documentation.sql}
27+
* added the constraint, and an {@code Optional} finder throws
28+
* {@code IncorrectResultSizeDataAccessException} on a legacy duplicate rather
29+
* than letting the caller repair it. Collapse matches with
30+
* {@link com.dbaagent.service.SchemaDocumentationDeduplicator}.
31+
*/
32+
List<SchemaDocumentation> findByConnectionIdAndObjectTypeAndObjectName(
2533
String connectionId,
2634
SchemaDocumentation.DocumentationType objectType,
2735
String objectName
@@ -51,12 +59,13 @@ AND TRIM(d.businessTerms) <> ''
5159
""")
5260
long countWithBusinessTerms(@Param("connectionId") String connectionId);
5361

54-
// Upsert support: find existing AI doc to update instead of creating duplicates
55-
Optional<SchemaDocumentation> findByConnectionIdAndObjectTypeAndObjectNameAndSource(
62+
// Upsert support: find existing doc to update instead of creating duplicates.
63+
// List-returning for the same reason as findByConnectionIdAndObjectTypeAndObjectName above.
64+
List<SchemaDocumentation> findByConnectionIdAndObjectTypeAndObjectNameAndSource(
5665
String connectionId, SchemaDocumentation.DocumentationType objectType,
5766
String objectName, DocumentationSource source);
5867

59-
Optional<SchemaDocumentation> findByConnectionIdAndObjectTypeAndObjectNameAndParentObjectAndSource(
68+
List<SchemaDocumentation> findByConnectionIdAndObjectTypeAndObjectNameAndParentObjectAndSource(
6069
String connectionId, SchemaDocumentation.DocumentationType objectType,
6170
String objectName, String parentObject, DocumentationSource source);
6271

backend/src/main/java/com/dbaagent/service/SchemaDescriptionService.java

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public class SchemaDescriptionService {
3434
private final ColumnProfileRepository columnProfileRepo;
3535
private final InferredTableRelationshipRepository inferredRelationshipRepository;
3636
private final TrainingService trainingService;
37+
private final SchemaDocumentationDeduplicator schemaDocDeduplicator;
3738
private final ConnectionService connectionService;
3839
private final DatabaseProviderRegistry providerRegistry;
3940
private final ObjectMapper objectMapper = new ObjectMapper();
@@ -71,6 +72,7 @@ public SchemaDescriptionService(
7172
ColumnProfileRepository columnProfileRepo,
7273
InferredTableRelationshipRepository inferredRelationshipRepository,
7374
TrainingService trainingService,
75+
SchemaDocumentationDeduplicator schemaDocDeduplicator,
7476
ConnectionService connectionService,
7577
DatabaseProviderRegistry providerRegistry,
7678
@Value("${brain.description.ai-concurrency:4}") int aiConcurrency) {
@@ -80,6 +82,7 @@ public SchemaDescriptionService(
8082
this.columnProfileRepo = columnProfileRepo;
8183
this.inferredRelationshipRepository = inferredRelationshipRepository;
8284
this.trainingService = trainingService;
85+
this.schemaDocDeduplicator = schemaDocDeduplicator;
8386
this.connectionService = connectionService;
8487
this.providerRegistry = providerRegistry;
8588
this.aiConcurrency = Math.max(1, aiConcurrency);
@@ -417,13 +420,14 @@ private int saveTableDescription(String connectionId, TableDescription desc,
417420
: desc.getTableName();
418421

419422
// Upsert table-level doc (find existing AI doc or create new)
420-
var existingTableDoc = schemaDocRepo
421-
.findByConnectionIdAndObjectTypeAndObjectNameAndSource(
423+
var existingTableDoc = schemaDocDeduplicator.collapse(
424+
schemaDocRepo.findByConnectionIdAndObjectTypeAndObjectNameAndSource(
422425
connectionId, DocumentationType.TABLE, objectName,
423-
DocumentationSource.AI_GENERATED);
426+
DocumentationSource.AI_GENERATED),
427+
objectName + " (TABLE, AI_GENERATED)");
424428
SchemaDocumentation tableDoc;
425-
if (existingTableDoc.isPresent()) {
426-
tableDoc = existingTableDoc.get();
429+
if (existingTableDoc != null) {
430+
tableDoc = existingTableDoc;
427431
tableDoc.setDescription(desc.getTableDescription());
428432
tableDoc.setBusinessTerms(desc.getBusinessTerms());
429433
tableDoc.setConfidence(desc.getConfidence());
@@ -445,13 +449,14 @@ private int saveTableDescription(String connectionId, TableDescription desc,
445449
// Upsert column-level docs
446450
for (var col : desc.getColumns()) {
447451
if (col.getDescription() == null || col.getDescription().isBlank()) continue;
448-
var existingColDoc = schemaDocRepo
449-
.findByConnectionIdAndObjectTypeAndObjectNameAndParentObjectAndSource(
452+
var existingColDoc = schemaDocDeduplicator.collapse(
453+
schemaDocRepo.findByConnectionIdAndObjectTypeAndObjectNameAndParentObjectAndSource(
450454
connectionId, DocumentationType.COLUMN, col.getName(),
451-
objectName, DocumentationSource.AI_GENERATED);
455+
objectName, DocumentationSource.AI_GENERATED),
456+
objectName + "." + col.getName() + " (COLUMN, AI_GENERATED)");
452457
SchemaDocumentation colDoc;
453-
if (existingColDoc.isPresent()) {
454-
colDoc = existingColDoc.get();
458+
if (existingColDoc != null) {
459+
colDoc = existingColDoc;
455460
colDoc.setDescription(col.getDescription());
456461
colDoc.setConfidence(col.getConfidence());
457462
} else {

0 commit comments

Comments
 (0)