Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e376c1d
Surreal files per perspective wip 1
lucksus Jan 27, 2026
35275ce
Avoid duplicate links w/ unique index and handle lock and write errors
lucksus Jan 27, 2026
6d90f2f
Fix new remove_link on surreal service
lucksus Jan 28, 2026
053c5dc
Rename update_surreal_cache() to persist_link_diff()
lucksus Jan 28, 2026
19abd3b
Temporary perspective data migration from rusqlite to surreal
lucksus Jan 28, 2026
48d1ef2
fmt
lucksus Jan 28, 2026
818d756
fix: address CodeRabbit issues #2, #3, #7
data-coasys Jan 28, 2026
6aecb22
fix: preserve original link status instead of hardcoding Local (issue…
data-coasys Jan 28, 2026
8c65f6c
fix: propagate SurrealDB write failures to prevent desync (issue #1)
data-coasys Jan 28, 2026
a9e43af
fix: honor full unique constraint in SurrealDB lookups (issue #6)
data-coasys Jan 28, 2026
fa3c173
fix: prevent TOCTOU race in initialize_from_db (issue #5)
data-coasys Jan 28, 2026
c79a924
fix: clone link.status to avoid partial move
data-coasys Jan 28, 2026
640b6e6
fix: borrow links in migration loop to avoid partial move
data-coasys Jan 28, 2026
5b76446
chore: run cargo fmt and add PR fixes summary
data-coasys Jan 28, 2026
cbc752d
refactor: remove redundant variable reassignments in get_link
data-coasys Jan 29, 2026
8292044
fix: address CodeRabbit feedback on PR #652
data-coasys Jan 29, 2026
fa4382f
reset bootstrapSeed.json
lucksus Jan 29, 2026
ee1143d
Handle fallback sync read failure gracefully.
lucksus Jan 29, 2026
9e8e5b4
Don’t proceed when migration fails.
lucksus Jan 29, 2026
e57b8f8
Fix inconsistent error handling: .expect() vs. map_err()?
lucksus Jan 29, 2026
ce4c6c0
fix: handle SurrealDB service creation failure in initialize_from_db
data-coasys Jan 30, 2026
6b34334
Merge branch 'dev' into surreal-files
lucksus Feb 11, 2026
0da45d7
Merge branch 'dev' into surreal-files
lucksus Feb 12, 2026
3931d7c
don't fail silently if links from DB can't be parsed
lucksus Feb 12, 2026
2e0aef8
don't panic on DB write failures but log error
lucksus Feb 12, 2026
942e2d4
Merge branch 'dev' into surreal-files
lucksus Feb 12, 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
70 changes: 70 additions & 0 deletions CODERABBIT_FIXES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# CodeRabbit PR #652 Fix Tasks

## Critical Issues

### 1. perspective_instance.rs (lines 2425-2457)
**Issue**: `persist_link_diff` and `retry_surreal_op` silently swallow errors, risking desync.
**Fix**: Make both return `Result` and propagate failures to callers.

```rust
// Change retry_surreal_op signature to return Result
async fn retry_surreal_op<F, Fut>(
op: F,
uuid: &str,
op_name: &str,
) -> Result<(), anyhow::Error>

// Change persist_link_diff signature to return Result
pub(crate) async fn persist_link_diff(
&self,
diff: &DecoratedPerspectiveDiff,
) -> Result<(), AnyError>

// Update callsites:
// - Functions returning Result: change `.await;` to `.await?;`
// - diff_from_link_language: use `.await.expect("SurrealDB link persistence failed");`
```

### 2. MIGRATION_REMOVAL_GUIDE.md (line 112-113)
**Issue**: Incomplete sentence in heading.
**Fix**: Change "Can optionally be cleaned up:" to "The following can optionally be cleaned up:"

### 3. migration.rs (lines 97-119)
**Issue**: Marks perspective as migrated even when some links failed, risking data loss.
**Fix**: Only mark as migrated when `error_count == 0`, otherwise return error.

### 4. mod.rs (lines 441-457)
**Issue**: Hardcodes `LinkStatus::Local` instead of preserving original status.
**Fix**: Read `link.status` and use it (fallback to `LinkStatus::Local` if None).

### 5. mod.rs (lines 56-66)
**Issue**: TOCTOU race in `initialize_from_db` - multiple tasks can create duplicate SurrealDB services.
**Fix**: Use atomic entry-or-init pattern with write lock to prevent duplicates.

### 6. perspective_instance.rs (lines 878-891)
**Issue**: SurrealDB lookup only uses (source, target, predicate) instead of full unique constraint.
**Fix**: Include author and timestamp in lookups to honor all 5 fields of link_unique_idx.

### 7. surreal_service/mod.rs (lines 532-576)
**Issue**: Overly broad error handling treats any "index" error as benign.
**Fix**: Only check for "unique", "duplicate", "already exists" - remove "index" check.

## Nitpicks

### 8. mod.rs (lines 80-93)
**Note**: Migration failures logged at error level but user won't see in UI.
Consider adding visible notification mechanism for production.

### 9. surreal_service/mod.rs (lines 756-904)
**Note**: DRY the repeated SurrealLink deserialization pattern across getters.
Consider extracting helper function.

---

## Execution Plan

1. Fix critical issues 1-7 first
2. Address nitpicks 8-9 if time permits
3. Run cargo fmt
4. Commit and push to `surreal-files` branch
5. Wait for CodeRabbit re-analysis
36 changes: 36 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

57 changes: 57 additions & 0 deletions PR_652_FIXES_SUMMARY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# PR #652 CodeRabbit Fixes - Summary

## All 7 Actionable Issues Fixed ✅

### Issue #1: Error Propagation (perspective_instance.rs lines 2425-2457)
**Problem**: `persist_link_diff` and `retry_surreal_op` silently swallowed errors
**Fix**:
- Changed both functions to return `Result<(), AnyError>`
- Updated all callsites to propagate errors using `?` or `.expect()`
- Functions returning `()` use `.expect()` to fail-fast
- Functions returning `Result` use `?` to propagate

### Issue #2: Documentation (MIGRATION_REMOVAL_GUIDE.md line 112-113)
**Problem**: Incomplete sentence in heading
**Fix**: Changed "Can optionally be cleaned up:" to "The following can optionally be cleaned up:"

### Issue #3: Migration Safety (migration.rs lines 97-119)
**Problem**: Marked perspective as migrated even when errors occurred
**Fix**: Only mark as migrated when `error_count == 0`, otherwise return error

### Issue #4: Link Status Preservation (mod.rs lines 441-457)
**Problem**: Hardcoded `LinkStatus::Local` instead of preserving original
**Fix**: Read `link.status` and use it (fallback to `LinkStatus::Local` if None)

### Issue #5: TOCTOU Race (mod.rs lines 56-66)
**Problem**: Race condition in `initialize_from_db` allowing duplicate SurrealDB services
**Fix**: Added atomic check-and-insert after async initialization completes

### Issue #6: Unique Constraint (perspective_instance.rs lines 878-891)
**Problem**: SurrealDB lookups only used 3 fields instead of full unique constraint (5 fields)
**Fix**:
- Updated `get_link` to accept optional `author` and `timestamp`
- When provided, queries using all 5 fields: source, target, predicate, author, timestamp
- All callsites updated to pass author and timestamp from LinkExpression

### Issue #7: Error Handling (surreal_service/mod.rs lines 532-576)
**Problem**: Overly broad error check treated any "index" error as benign
**Fix**: Removed "index" check, only checks for "unique", "duplicate", and "already exists"

## Additional Compilation Fixes
- Fixed partial move error in `import_perspective` (clone link.status)
- Fixed partial move error in migration loop (borrow &links instead of consuming)

## Commits
1. `818d756e` - Issues #2, #3, #7 + docstrings
2. `6aecb225` - Issue #4 (preserve link status)
3. `8c65f6c7` - Issue #1 (error propagation)
4. `a9e43aff` - Issue #6 (unique constraint)
5. `fa3c1734` - Issue #5 (TOCTOU race)
6. `c79a9244` - Fix partial move (link.status)
7. `640b6e65` - Fix partial move (migration loop)

## Status
- ✅ All CodeRabbit actionable comments addressed
- ✅ Additional docstrings added for coverage
- 🔄 Building rust-executor to verify compilation
- ⏳ Next: cargo fmt, run full test suite
2 changes: 1 addition & 1 deletion rust-executor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ anyhow = "1.0.95"
portpicker = "0.1.1"
deno_error = "0.5.6"
thiserror = "2.0.12"
surrealdb = { version = "2.4", default-features = false, features = ["kv-mem", "scripting"] }
surrealdb = { version = "2.4", default-features = false, features = ["kv-rocksdb", "kv-mem", "scripting"] }
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

🧩 Analysis chain

🌐 Web query:

SurrealDB 2.4 kv-rocksdb feature RocksDB build requirements

💡 Result:

For SurrealDB 2.4.x, enabling the RocksDB storage engine is done via the Cargo feature kv-rocksdb (e.g. cargo build --features surrealdb/kv-rocksdb). [1]

What you need installed to build with kv-rocksdb

Because this feature compiles native RocksDB bindings, you generally need:

  • A C/C++ build toolchain (compiler + linker; RocksDB is C++). RocksDB requires a C++11-capable compiler (e.g., GCC/Clang). [5]
  • Clang/LLVM (libclang) available for bindgen (the RocksDB Rust bindings require this). [3][4]
    • On macOS/Linux you may need to set LIBCLANG_PATH / LLVM_CONFIG_PATH if autodetection fails (common when compiling librocksdb-sys). [6]
  • (Platform packaging hint) Some distributions/ports express this as needing llvm-config at build time and linking against librocksdb (and often zstd). [2]

Optional / common native deps

  • RocksDB can be built with optional compression libraries such as snappy, lz4, zstd, zlib, bzip2. Whether they’re needed depends on how the Rust RocksDB wrapper is configured/features enabled, but they’re common native deps in RocksDB builds. [5]

Notes

  • If you enable RocksDB LTO in the RocksDB wrapper, it specifically requires building with clang and matching LLVM versions/flags. [3]

Sources: SurrealDB blog/docs and crate docs for enabling kv-rocksdb and bindgen/libclang requirements, plus RocksDB install notes and packaging metadata. [1][2][3][4][5][6]


🌐 Web query:

SurrealDB cargo features kv-rocksdb kv-mem scripting compatibility 2.4

💡 Result:

For SurrealDB Rust crates in the 2.4.x line, the Cargo feature flags you mentioned are present and compatible:

  • kv-mem: in-memory storage engine (and it’s the default for surrealdb-core 2.4.1). (docs.rs)
  • kv-rocksdb: RocksDB-backed storage engine. (Enabling embedded engines requires turning on the corresponding kv-* feature; RocksDB also brings native build deps.) (docs.rs)
  • scripting: enables SurrealDB’s scripting support (wired to a js dependency in surrealdb-core). (docs.rs)

In the top-level surrealdb crate, these features map through to surrealdb-core (e.g., scripting = ["surrealdb-core/scripting"], kv-rocksdb = ["surrealdb-core/kv-rocksdb", ...]). (docs.rs)

Example Cargo.toml (2.4.x) enabling all three:

[dependencies]
surrealdb = { version = "2.4", default-features = false, features = ["kv-mem", "kv-rocksdb", "scripting"] }

Citations:


Ensure CI has C/C++ compiler and LLVM/clang for building with kv-rocksdb.

The feature combination is supported in SurrealDB 2.4, but enabling kv-rocksdb requires native build dependencies: a C++11-capable C/C++ compiler and LLVM/clang (for bindgen to compile RocksDB bindings). Verify that CI runners and local build environments have these toolchains available; you may also need to set LIBCLANG_PATH or LLVM_CONFIG_PATH if autodetection fails.

🤖 Prompt for AI Agents
In `@rust-executor/Cargo.toml` at line 137, The Cargo.toml dependency enables
SurrealDB with the kv-rocksdb feature which requires a C/C++11-capable compiler
and LLVM/clang for bindgen; update CI and developer docs to ensure those
toolchains are installed on runners (or switch CI to use the kv-mem feature),
and add guidance to set LIBCLANG_PATH or LLVM_CONFIG_PATH when autodetection
fails; reference the surrealdb dependency line and the kv-rocksdb feature in
Cargo.toml and ensure CI job images/install steps include a C/C++ compiler and
LLVM toolchain so cargo build/bindgen succeed.


[dev-dependencies]
maplit = "1.0.2"
Expand Down
113 changes: 113 additions & 0 deletions rust-executor/MIGRATION_REMOVAL_GUIDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Migration Removal Guide

This document explains how to remove the one-time Rusqlite-to-SurrealDB migration code after all users have migrated.

## When to Remove

Remove this migration code in a future version (e.g., version 0.13.0 or later) after confirming:
- All active users have upgraded to version 0.11.2 or later
- Sufficient time has passed for users to run the migration (recommend at least 2-3 releases)
- No support requests related to missing links after migration

## Removal Steps

### 1. Delete the Migration Module
```bash
rm src/perspectives/migration.rs
```

### 2. Update src/perspectives/mod.rs

Remove the migration module declaration:
```rust
// Remove this line:
pub mod migration; // TODO: Remove this module after all users have migrated to SurrealDB
```

Remove migration calls from `initialize_from_db()`:
```rust
// Remove these lines (around line 74-78):
// Migrate links from Rusqlite to SurrealDB (one-time migration)
// TODO: Remove this migration call after all users have migrated
if let Err(e) = migration::migrate_links_from_rusqlite_to_surrealdb(&handle_clone.uuid, &surreal_service).await {
log::error!("Failed to migrate links for perspective {}: {}", handle_clone.uuid, e);
}
```

Remove migration calls from `add_perspective()`:
```rust
// Remove these lines (around line 119-123):
// Migrate links from Rusqlite to SurrealDB (one-time migration)
// TODO: Remove this migration call after all users have migrated
if let Err(e) = migration::migrate_links_from_rusqlite_to_surrealdb(&handle.uuid, &surreal_service).await {
log::error!("Failed to migrate links for perspective {}: {}", handle.uuid, e);
}
```

### 3. (Optional) Clean Up Database Migration Support

If you want to completely remove migration infrastructure from src/db.rs:

Remove the migration tracking table from `new()`:
```rust
// Remove these lines (around line 158-165):
conn.execute(
"CREATE TABLE IF NOT EXISTS perspective_link_migration (
id INTEGER PRIMARY KEY,
perspective_uuid TEXT NOT NULL UNIQUE,
migrated_at TEXT NOT NULL
)",
[],
)?;
```

Remove migration methods:
```rust
// Remove these methods:
- pub fn is_perspective_migrated(&self, perspective_uuid: &str) -> Ad4mDbResult<bool>
- pub fn mark_perspective_as_migrated(&self, perspective_uuid: &str) -> Ad4mDbResult<()>
- pub fn delete_all_links_for_perspective(&self, perspective_uuid: &str) -> Ad4mDbResult<usize>
```

**Note:** Keeping these methods is harmless and may be useful for future migrations, so removal is optional.

### 4. Verify Removal

After removing the migration code:

1. **Build the project:**
```bash
cargo build --lib
```

2. **Run tests:**
```bash
cargo test --lib
```

3. **Verify no references remain:**
```bash
grep -r "migrate_links_from_rusqlite_to_surrealdb" rust-executor/src/
grep -r "migration::" rust-executor/src/perspectives/mod.rs
```
Both should return no results.

## Migration Summary

### What the Migration Did
- Moved all link data from the centralized Rusqlite database to per-perspective SurrealDB databases
- Preserved all link metadata (author, timestamp, proof, status)
- Ensured idempotent operation (safe to run multiple times)
- Cleaned up old link data from Rusqlite after successful migration
- Tracked migration status per perspective to prevent repeated migrations

### Files Affected by Migration

**Will be removed:**
- `src/perspectives/migration.rs` - The migration logic and tests

**Will be updated:**
- `src/perspectives/mod.rs` - Remove module declaration and migration calls

**The following can optionally be cleaned up:**
- `src/db.rs` - Migration tracking methods (optional to remove)
57 changes: 57 additions & 0 deletions rust-executor/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,15 @@ impl Ad4mDb {
[],
)?;

conn.execute(
"CREATE TABLE IF NOT EXISTS perspective_link_migration (
id INTEGER PRIMARY KEY,
perspective_uuid TEXT NOT NULL UNIQUE,
migrated_at TEXT NOT NULL
)",
[],
)?;

conn.execute(
"CREATE TABLE IF NOT EXISTS outbox (
id INTEGER PRIMARY KEY,
Expand Down Expand Up @@ -1470,6 +1479,54 @@ impl Ad4mDb {
Ok(links?)
}

/// Check if a perspective's links have been migrated from Rusqlite to SurrealDB
///
/// # Arguments
/// * `perspective_uuid` - UUID of the perspective to check
///
/// # Returns
/// * `Ok(true)` if the perspective has been migrated
/// * `Ok(false)` if the perspective has not been migrated yet
pub fn is_perspective_migrated(&self, perspective_uuid: &str) -> Ad4mDbResult<bool> {
let mut stmt = self.conn.prepare(
"SELECT COUNT(*) FROM perspective_link_migration WHERE perspective_uuid = ?1",
)?;
let count: i64 = stmt.query_row(params![perspective_uuid], |row| row.get(0))?;
Ok(count > 0)
}

/// Mark a perspective as having been migrated from Rusqlite to SurrealDB
///
/// This function is idempotent - calling it multiple times for the same perspective is safe.
///
/// # Arguments
/// * `perspective_uuid` - UUID of the perspective to mark as migrated
pub fn mark_perspective_as_migrated(&self, perspective_uuid: &str) -> Ad4mDbResult<()> {
let timestamp = chrono::Utc::now().to_rfc3339();
self.conn.execute(
"INSERT OR IGNORE INTO perspective_link_migration (perspective_uuid, migrated_at) VALUES (?1, ?2)",
params![perspective_uuid, timestamp],
)?;
Ok(())
}

/// Delete all links for a perspective from Rusqlite storage
///
/// This should only be called after successfully migrating links to SurrealDB.
///
/// # Arguments
/// * `perspective_uuid` - UUID of the perspective whose links should be deleted
///
/// # Returns
/// The number of links that were deleted
pub fn delete_all_links_for_perspective(&self, perspective_uuid: &str) -> Ad4mDbResult<usize> {
let count = self.conn.execute(
"DELETE FROM link WHERE perspective = ?1",
params![perspective_uuid],
)?;
Ok(count)
}

pub fn add_pending_diff(
&self,
perspective_uuid: &str,
Expand Down
4 changes: 4 additions & 0 deletions rust-executor/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ pub async fn run(mut config: Ad4mConfig) -> JoinHandle<()> {
info!("js_core initialized.");

LanguageController::init_global_instance(js_core_handle.clone());

// Set app data path for perspectives module (needed for file-based SurrealDB)
perspectives::set_app_data_path(config.app_data_path.clone().unwrap());

perspectives::initialize_from_db();

let app_dir = config
Expand Down
Loading