Skip to content

fix(memory): normalize empty path to id in EnsureProject#154

Merged
wcatz merged 1 commit intomainfrom
fix/ensure-project-unique-constraint
Apr 23, 2026
Merged

fix(memory): normalize empty path to id in EnsureProject#154
wcatz merged 1 commit intomainfrom
fix/ensure-project-unique-constraint

Conversation

@wcatz
Copy link
Copy Markdown
Owner

@wcatz wcatz commented Apr 23, 2026

Summary

Fixes #153

ghost_memory_save was failing with UNIQUE constraint failed: projects.path when the MCP server called EnsureProject with path="". Since projects.path has a UNIQUE constraint, any two projects called this way would collide.

  • Normalize path="" to path=id at the top of EnsureProject — matches the invariant already on disk for name-as-ID projects (e.g. id=a7293a04b38a, path=a7293a04b38a)
  • Guard the ON CONFLICT DO UPDATE so a synthetic path=id fallback never clobbers a real filesystem path already stored

Test plan

  • TestEnsureProject_EmptyPath_NoUniqueConflict — two projects with path="", no conflict, idempotent repeat, path==id post-save
  • go test ./internal/memory/... passes

Summary by CodeRabbit

Release Notes

  • Bug Fixes
    • Fixed constraint violations when creating projects without specifying a custom path
    • Improved handling of repeated project creation attempts to prevent errors
    • Projects now reliably use their ID as the default path when no custom path is provided

…h MCP callers

MCP callers invoke EnsureProject with path="" because they have no filesystem
path. This caused a UNIQUE constraint violation (projects.path is UNIQUE) when
a second project was saved — both rows tried to claim path="".

Fix:
- Normalize path="" to path=id at the top of EnsureProject. Non-filesystem
  projects already use id-as-path on disk; this preserves that invariant and
  guarantees uniqueness (id is the PK).
- Change ON CONFLICT(id) DO UPDATE to keep the existing path when the incoming
  path equals the id (synthetic path), preventing an MCP call from silently
  clobbering a real filesystem path set by the orchestrator.

Adds a regression test: EnsureProject with empty path for two distinct project
IDs must succeed without error, and each project's path must equal its id.

Signed-off-by: wcatz <waynecataldo@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

EnsureProject now treats empty path inputs as path=id before collision logic. The upsert statement conditionally preserves existing projects.path when the incoming path matches the ID (name-as-ID case), preventing UNIQUE constraint violations on re-upserts while still updating path for other scenarios.

Changes

Cohort / File(s) Summary
EnsureProject Logic
internal/memory/store.go
Modified upsert to set path=id for empty path inputs and conditionally preserve existing path when incoming excluded.path equals excluded.id, while updating path in other cases. Updates updated_at on all conflicts.
EnsureProject Test Coverage
internal/memory/store_test.go
New test validates that creating multiple projects with empty path avoids UNIQUE constraint violations, ensures idempotency on repeated calls, and confirms resulting projects have Path equal to ID.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #138: Modifies EnsureProject in internal/memory/store.go to handle absolute-path duplicate detection and merge behavior, complementing this PR's name-as-ID collision logic.

Poem

🐰 A path that matches its name at last,
No UNIQUE constraint ghost from the past,
Empty becomes ID, conflicts now cease,
The upsert brings database peace! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: normalizing empty paths to the project ID in EnsureProject, which directly addresses the core fix.
Linked Issues check ✅ Passed The code changes fully address issue #153's requirements: empty path is normalized to ID at function start, ON CONFLICT logic conditionally preserves existing paths, and tests validate idempotency and proper deduplication.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the UNIQUE constraint issue: path normalization in EnsureProject and a targeted test for the empty path case.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ensure-project-unique-constraint

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
internal/memory/store_test.go (1)

1390-1426: Consider adding coverage for the "don't clobber existing filesystem path" invariant.

The PR description explicitly calls out: "guard the ON CONFLICT DO UPDATE so that a synthetic path==id fallback never overwrites an existing real filesystem path already stored." The current test exercises two empty-path projects and idempotent re-calls, but both re-call scenarios have existing.path == id already, so they don't actually hit the branch where the CASE must preserve a pre-existing real filesystem path against a synthetic path==id overwrite attempt.

A targeted test would directly exercise the core regression:

🧪 Suggested additional test
func TestEnsureProject_EmptyPath_DoesNotClobberFilesystemPath(t *testing.T) {
	s := testStore(t)
	ctx := context.Background()

	// Orchestrator registers the project with a real filesystem path first.
	const id = "a7293a04b38a"
	const fsPath = "/home/wayne/git/dingo"
	if err := s.EnsureProject(ctx, id, fsPath, "dingo"); err != nil {
		t.Fatalf("EnsureProject (fs): %v", err)
	}

	// MCP later calls with empty path for the same ID — must NOT overwrite fsPath.
	if err := s.EnsureProject(ctx, id, "", "dingo"); err != nil {
		t.Fatalf("EnsureProject (mcp): %v", err)
	}

	projects, err := s.ListProjects(ctx)
	if err != nil {
		t.Fatalf("ListProjects: %v", err)
	}
	for _, p := range projects {
		if p.ID == id && p.Path != fsPath {
			t.Errorf("expected path to remain %q, got %q", fsPath, p.Path)
		}
	}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/memory/store_test.go` around lines 1390 - 1426, Add a new unit test
that verifies EnsureProject does not overwrite an existing real filesystem path
when later called with an empty path: use testStore() and EnsureProject(ctx, id,
fsPath, ...) to create a project with a real filesystem path, then call
EnsureProject(ctx, id, "", ...) and assert via ListProjects() that the project's
Path remains the original fsPath; name the test
TestEnsureProject_EmptyPath_DoesNotClobberFilesystemPath and reference
EnsureProject, ListProjects, and testStore to locate the relevant store API.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/memory/store_test.go`:
- Around line 1390-1426: Add a new unit test that verifies EnsureProject does
not overwrite an existing real filesystem path when later called with an empty
path: use testStore() and EnsureProject(ctx, id, fsPath, ...) to create a
project with a real filesystem path, then call EnsureProject(ctx, id, "", ...)
and assert via ListProjects() that the project's Path remains the original
fsPath; name the test TestEnsureProject_EmptyPath_DoesNotClobberFilesystemPath
and reference EnsureProject, ListProjects, and testStore to locate the relevant
store API.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 0bfc72c9-99de-452f-8548-7cffaf686584

📥 Commits

Reviewing files that changed from the base of the PR and between a7e6385 and 3768e9e.

📒 Files selected for processing (2)
  • internal/memory/store.go
  • internal/memory/store_test.go

@wcatz wcatz merged commit 60803da into main Apr 23, 2026
5 checks passed
@wcatz wcatz deleted the fix/ensure-project-unique-constraint branch April 23, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ghost_memory_save fails with UNIQUE constraint on projects.path when project has non-filesystem path

1 participant