Skip to content

Add shared action foundation#30

Merged
TerminallyLazy merged 13 commits into
mainfrom
codex/shared-action-foundation
Jul 9, 2026
Merged

Add shared action foundation#30
TerminallyLazy merged 13 commits into
mainfrom
codex/shared-action-foundation

Conversation

@TerminallyLazy

@TerminallyLazy TerminallyLazy commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

  • add shared action request/report contracts for durable CLI and TUI operations
  • wire existing CLI and TUI flows through those actions without changing user-facing behavior
  • split private SQLite helpers behind the existing SQLiteMemoryStore facade
  • document final certification evidence and preserve TUI /remember global scope behavior

Verification

  • cargo fmt --check
  • cargo test --locked
  • cargo clippy --locked --all-targets
  • git diff --check
  • sh scripts/certify-tree-ring.sh

Certification Evidence

  • release binary: 6,137,088 bytes
  • project install: 6,064 KB
  • global install: 6,020 KB
  • CLI import: 10,000 memories in 5s (~2,000/s)
  • 10k smoke: 2,146.6 inserts/sec, recall avg 3.729 ms, recall max 6.539 ms
  • 30k smoke: 711.5 inserts/sec, recall avg 7.978 ms, recall max 14.444 ms
  • Agent Zero plugin smoke skipped because TREE_RING_AGENT_ZERO_ROOT was not set

Review Notes

  • task-by-task SDD reviews approved
  • final branch re-review approved after restoring TUI /remember scope to global and adding regression coverage

Summary by CodeRabbit

  • New Features

    • Added CLI and TUI support for shared memory actions, including remember, recall, export/import, audit, lifecycle, integrations, and sync flows.
    • Export and import now support JSONL handling with dry-run options and JSON output for scripting.
    • Added easier recall output and integration scanning in the UI.
  • Bug Fixes

    • Improved dry-run behavior so read-only checks no longer create database files.
    • Better handling when the memory store is missing by falling back to safe, empty audits and maintenance runs.
  • Chores

    • Updated documentation and release status notes.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces a shared "actions" layer in the CLI crate encapsulating remember, recall, export/import, audit, lifecycle (consolidate/maintain), adapter (DOX/Revolve sync), and integration scan operations. CLI and TUI now delegate to these actions. The SQLite crate splits internals into schema, search, write, and lifecycle submodules. Documentation and plan/spec files are added.

Changes

Shared action foundation

Layer / File(s) Summary
Actions module scaffolding and remember/recall actions
crates/tree-ring-memory-cli/src/actions/mod.rs, .../actions/remember.rs, .../actions/recall.rs
Adds ActionResult alias, module exports, and remember/recall request/report types and functions with unit tests.
Export/import and audit actions
.../actions/export_import.rs, .../actions/audit.rs
Adds export_jsonl, import_jsonl, import_json_payload, and audit_store with request/report types and tests.
Lifecycle, adapters and integration scan actions
.../actions/lifecycle.rs, .../actions/adapters.rs, .../actions/integrations.rs
Adds consolidate, consolidate_dry_run_from_path, maintain, sync_dox, sync_revolve, and scan action functions with tests.
Scriptable output printers
.../commands/scriptable.rs, .../commands/mod.rs
Adds print_recall_report, print_export_report, print_import_report for JSON/text output.
CLI main.rs wiring
.../src/main.rs
Refactors command branches (integrations, import, audit, consolidate, maintain, DOX/Revolve sync, remember, recall, export/import) to call action functions; adds regression tests.
TUI app wiring
.../src/tui/app.rs
Routes /remember, export confirmation, and integrations scan through shared actions; adds tests.
SQLite schema and search modules
crates/tree-ring-memory-sqlite/src/schema.rs, .../search.rs
Adds connection-opening/URI helpers and row-mapping/collection/filter helpers.
SQLite write and lifecycle modules
.../write.rs, .../lifecycle.rs
Adds transaction write/delete/redact/retry helpers and FTS rebuild/count/consolidation-row helpers.
lib.rs delegation
crates/tree-ring-memory-sqlite/src/lib.rs
Delegates SQLiteMemoryStore methods to new submodules; adds a facade preservation test.
Documentation and plans
docs/architecture/rust-core-status.md, docs/superpowers/plans/*, docs/superpowers/specs/*, .gitignore
Updates status doc, adds implementation plan/design spec, ignores .worktrees/.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant main
  participant actions
  participant SQLiteMemoryStore
  User->>main: run command
  main->>actions: build *ActionRequest
  actions->>SQLiteMemoryStore: read/write as needed
  SQLiteMemoryStore-->>actions: report
  actions-->>main: *ActionReport
  main->>main: print via commands::scriptable
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.65% 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 matches the main change: introducing a shared action layer/foundation for CLI and TUI operations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 codex/shared-action-foundation

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.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request implements a shared action foundation for Tree Ring Memory, refactoring both the CLI and TUI to consume common action request and report contracts (such as remember, recall, export/import, audit, lifecycle, and adapters). Additionally, it simplifies the SQLite storage layer by splitting private helper functions into dedicated submodules (schema, write, search, and lifecycle) while preserving the public SQLiteMemoryStore facade. I have no feedback to provide as there are no review comments and the changes are well-structured and thoroughly tested.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (2)
crates/tree-ring-memory-sqlite/src/lifecycle.rs (1)

29-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated memory_fts INSERT SQL across lifecycle.rs and write.rs.

This statement mirrors the one in write.rs's put_with_statements/put_in_transaction. Consider extracting a shared constant or small helper for the FTS insert columns/statement to avoid schema drift between the two split modules.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tree-ring-memory-sqlite/src/lifecycle.rs` around lines 29 - 31, The
`memory_fts` INSERT SQL is duplicated between `lifecycle.rs` and `write.rs`,
which risks the two statements drifting apart. Extract the shared FTS insert
statement or at least the common columns into a reusable constant/helper and
have both `lifecycle.rs` and `put_with_statements`/`put_in_transaction` in
`write.rs` use that single source of truth.
crates/tree-ring-memory-cli/src/actions/export_import.rs (1)

65-92: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Dry-run validation logic duplicates SQLiteMemoryStore::import_jsonl's dry-run branch.

The dry-run path here re-implements decode_jsonl + normalize_import_events + manual ImportReport construction, which is functionally identical to the dry-run branch inside SQLiteMemoryStore::import_jsonl (see upstream contract snippet: builds the same ImportReport shape and returns early on dry_run). If the upstream dry-run logic ever changes (e.g. additional validation), this action wrapper will silently drift out of sync, causing CLI/TUI dry-run previews to diverge from the real import outcome.

Consider extracting a shared "plan import" helper (in tree-ring-memory-core or tree-ring-memory-sqlite) that both the store's dry-run branch and this store-less action wrapper delegate to.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tree-ring-memory-cli/src/actions/export_import.rs` around lines 65 -
92, The dry-run branch in import_jsonl is duplicating
SQLiteMemoryStore::import_jsonl’s dry-run logic, so the CLI preview can drift
from the real import behavior. Refactor the shared decode/normalize/report
building into a common helper (for example a plan_import-style function in the
core/sqlite layer) and have both import_jsonl in export_import.rs and
SQLiteMemoryStore::import_jsonl call it. Keep the helper responsible for
decode_jsonl, normalize_import_events, and constructing the ImportReport so both
paths stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/tree-ring-memory-cli/src/actions/export_import.rs`:
- Around line 135-159: The dry-run test in
import_action_dry_run_validates_without_writing does not exercise the actual
write path because import_jsonl is called with None, so target is never used.
Update the test to pass the created SQLiteMemoryStore into import_jsonl through
the shared import action path, then verify that the store remains empty after a
dry run. Keep the assertions tied to import_jsonl and ImportActionRequest so the
test validates the real no-write behavior rather than an untouched target.

In `@crates/tree-ring-memory-cli/src/actions/lifecycle.rs`:
- Around line 75-89: The maintain path in maintainer::maintain should not always
open SQLiteMemoryStore in read-only mode when store is None. Update the logic to
choose the connection mode based on request.dry_run: keep open_read_only for dry
runs, but open a writable store for apply mode before calling
store.maintain(&request). Make sure the behavior is handled in maintain and any
helper used by SQLiteMemoryStore so the None-store fallback cannot attempt
writes on a read-only connection.

In `@crates/tree-ring-memory-sqlite/src/schema.rs`:
- Around line 17-33: The URI built in open_read_only_connection is not
normalized correctly for Windows paths, so drive-letter paths and UNC paths can
be misinterpreted by SQLite. Update the path-to-URI handling in
open_read_only_connection, using normalize_sqlite_uri_path and sqlite_uri_path,
so drive-letter paths are prefixed with a leading slash and UNC paths are
handled separately before constructing the file: URI. Keep the existing
Connection::open_with_flags flow and adjust only the URI assembly logic to
preserve correct read-only behavior on Windows.

In `@crates/tree-ring-memory-sqlite/src/search.rs`:
- Around line 24-40: push_in_filter currently emits invalid SQL when values is
empty, so make the function defensive even if callers forget to guard. Update
push_in_filter in search.rs to check values.is_empty() up front and return
without modifying sql or parameters; otherwise keep the existing IN clause and
parameter population behavior unchanged.

---

Nitpick comments:
In `@crates/tree-ring-memory-cli/src/actions/export_import.rs`:
- Around line 65-92: The dry-run branch in import_jsonl is duplicating
SQLiteMemoryStore::import_jsonl’s dry-run logic, so the CLI preview can drift
from the real import behavior. Refactor the shared decode/normalize/report
building into a common helper (for example a plan_import-style function in the
core/sqlite layer) and have both import_jsonl in export_import.rs and
SQLiteMemoryStore::import_jsonl call it. Keep the helper responsible for
decode_jsonl, normalize_import_events, and constructing the ImportReport so both
paths stay aligned.

In `@crates/tree-ring-memory-sqlite/src/lifecycle.rs`:
- Around line 29-31: The `memory_fts` INSERT SQL is duplicated between
`lifecycle.rs` and `write.rs`, which risks the two statements drifting apart.
Extract the shared FTS insert statement or at least the common columns into a
reusable constant/helper and have both `lifecycle.rs` and
`put_with_statements`/`put_in_transaction` in `write.rs` use that single source
of truth.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b545cfb-996b-4ad5-a7bb-9f24736051e5

📥 Commits

Reviewing files that changed from the base of the PR and between 7d00800 and 6e12500.

📒 Files selected for processing (21)
  • .gitignore
  • crates/tree-ring-memory-cli/src/actions/adapters.rs
  • crates/tree-ring-memory-cli/src/actions/audit.rs
  • crates/tree-ring-memory-cli/src/actions/export_import.rs
  • crates/tree-ring-memory-cli/src/actions/integrations.rs
  • crates/tree-ring-memory-cli/src/actions/lifecycle.rs
  • crates/tree-ring-memory-cli/src/actions/mod.rs
  • crates/tree-ring-memory-cli/src/actions/recall.rs
  • crates/tree-ring-memory-cli/src/actions/remember.rs
  • crates/tree-ring-memory-cli/src/commands/mod.rs
  • crates/tree-ring-memory-cli/src/commands/scriptable.rs
  • crates/tree-ring-memory-cli/src/main.rs
  • crates/tree-ring-memory-cli/src/tui/app.rs
  • crates/tree-ring-memory-sqlite/src/lib.rs
  • crates/tree-ring-memory-sqlite/src/lifecycle.rs
  • crates/tree-ring-memory-sqlite/src/schema.rs
  • crates/tree-ring-memory-sqlite/src/search.rs
  • crates/tree-ring-memory-sqlite/src/write.rs
  • docs/architecture/rust-core-status.md
  • docs/superpowers/plans/2026-07-09-tree-ring-shared-action-foundation-implementation-plan.md
  • docs/superpowers/specs/2026-07-09-tree-ring-shared-action-foundation-design.md

Comment on lines +135 to +159
#[test]
fn import_action_dry_run_validates_without_writing() {
let dir = tempdir().unwrap();
let mut source = SQLiteMemoryStore::open(dir.path().join("source.sqlite")).unwrap();
let target = SQLiteMemoryStore::open(dir.path().join("target.sqlite")).unwrap();
source
.put(&MemoryEvent::new("Import through shared action.", "lesson").unwrap())
.unwrap();
let (jsonl, _) = source.export_jsonl(false, false).unwrap();
let input = dir.path().join("input.jsonl");
fs::write(&input, jsonl).unwrap();

let report = import_jsonl(
None,
ImportActionRequest {
path: input,
dry_run: true,
replace_existing: false,
},
)
.unwrap();

assert_eq!(report.report.valid_count, 1);
assert_eq!(target.list_all(true).unwrap().len(), 0);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Test doesn't actually exercise the "no write" guarantee it claims to verify.

target is created but never passed into import_jsonl(None, ...) — the call always uses None for the store. So assert_eq!(target.list_all(true).unwrap().len(), 0) is trivially true regardless of dry-run behavior; target is untouched by the code under test. This gives false confidence that dry-run correctly avoids writes when a store is provided.

🧪 Suggested fix: exercise the store-provided dry-run path
-        let report = import_jsonl(
-            None,
-            ImportActionRequest {
-                path: input,
-                dry_run: true,
-                replace_existing: false,
-            },
-        )
-        .unwrap();
-
-        assert_eq!(report.report.valid_count, 1);
-        assert_eq!(target.list_all(true).unwrap().len(), 0);
+        let mut target = target;
+        let report = import_jsonl(
+            Some(&mut target),
+            ImportActionRequest {
+                path: input,
+                dry_run: true,
+                replace_existing: false,
+            },
+        )
+        .unwrap();
+
+        assert_eq!(report.report.valid_count, 1);
+        assert_eq!(target.list_all(true).unwrap().len(), 0);
📝 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
#[test]
fn import_action_dry_run_validates_without_writing() {
let dir = tempdir().unwrap();
let mut source = SQLiteMemoryStore::open(dir.path().join("source.sqlite")).unwrap();
let target = SQLiteMemoryStore::open(dir.path().join("target.sqlite")).unwrap();
source
.put(&MemoryEvent::new("Import through shared action.", "lesson").unwrap())
.unwrap();
let (jsonl, _) = source.export_jsonl(false, false).unwrap();
let input = dir.path().join("input.jsonl");
fs::write(&input, jsonl).unwrap();
let report = import_jsonl(
None,
ImportActionRequest {
path: input,
dry_run: true,
replace_existing: false,
},
)
.unwrap();
assert_eq!(report.report.valid_count, 1);
assert_eq!(target.list_all(true).unwrap().len(), 0);
}
#[test]
fn import_action_dry_run_validates_without_writing() {
let dir = tempdir().unwrap();
let mut source = SQLiteMemoryStore::open(dir.path().join("source.sqlite")).unwrap();
let target = SQLiteMemoryStore::open(dir.path().join("target.sqlite")).unwrap();
source
.put(&MemoryEvent::new("Import through shared action.", "lesson").unwrap())
.unwrap();
let (jsonl, _) = source.export_jsonl(false, false).unwrap();
let input = dir.path().join("input.jsonl");
fs::write(&input, jsonl).unwrap();
let mut target = target;
let report = import_jsonl(
Some(&mut target),
ImportActionRequest {
path: input,
dry_run: true,
replace_existing: false,
},
)
.unwrap();
assert_eq!(report.report.valid_count, 1);
assert_eq!(target.list_all(true).unwrap().len(), 0);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tree-ring-memory-cli/src/actions/export_import.rs` around lines 135 -
159, The dry-run test in import_action_dry_run_validates_without_writing does
not exercise the actual write path because import_jsonl is called with None, so
target is never used. Update the test to pass the created SQLiteMemoryStore into
import_jsonl through the shared import action path, then verify that the store
remains empty after a dry run. Keep the assertions tied to import_jsonl and
ImportActionRequest so the test validates the real no-write behavior rather than
an untouched target.

Comment on lines +75 to +89
pub fn maintain(
db_path: &Path,
store: Option<&mut SQLiteMemoryStore>,
request: MaintainActionRequest,
) -> ActionResult<MaintenanceReport> {
let request = maintenance_request(request);
if request.dry_run && !db_path.exists() {
return Ok(plan_maintenance(&[], &request));
}
if let Some(store) = store {
return store.maintain(&request).map_err(|err| err.to_string());
}
let mut store = SQLiteMemoryStore::open_read_only(db_path).map_err(|err| err.to_string())?;
store.maintain(&request).map_err(|err| err.to_string())
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

maintain opens read-only even when dry_run=false and no store is provided.

When store is None and request.dry_run is false (i.e., apply flags are set), the function unconditionally opens the database with open_read_only (line 87) but then calls store.maintain(&request) which will attempt writes. This will fail at runtime on a read-only SQLite connection.

While current CLI callers always pass Some(&mut store), the public signature permits store=None with apply flags set, making this a latent runtime failure path.

🔒 Proposed fix: conditionally open read-only vs read-write
     if let Some(store) = store {
         return store.maintain(&request).map_err(|err| err.to_string());
     }
-    let mut store = SQLiteMemoryStore::open_read_only(db_path).map_err(|err| err.to_string())?;
+    let mut store = if request.dry_run {
+        SQLiteMemoryStore::open_read_only(db_path)
+    } else {
+        SQLiteMemoryStore::open(db_path)
+    }
+    .map_err(|err| err.to_string())?;
     store.maintain(&request).map_err(|err| err.to_string())
📝 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
pub fn maintain(
db_path: &Path,
store: Option<&mut SQLiteMemoryStore>,
request: MaintainActionRequest,
) -> ActionResult<MaintenanceReport> {
let request = maintenance_request(request);
if request.dry_run && !db_path.exists() {
return Ok(plan_maintenance(&[], &request));
}
if let Some(store) = store {
return store.maintain(&request).map_err(|err| err.to_string());
}
let mut store = SQLiteMemoryStore::open_read_only(db_path).map_err(|err| err.to_string())?;
store.maintain(&request).map_err(|err| err.to_string())
}
pub fn maintain(
db_path: &Path,
store: Option<&mut SQLiteMemoryStore>,
request: MaintainActionRequest,
) -> ActionResult<MaintenanceReport> {
let request = maintenance_request(request);
if request.dry_run && !db_path.exists() {
return Ok(plan_maintenance(&[], &request));
}
if let Some(store) = store {
return store.maintain(&request).map_err(|err| err.to_string());
}
let mut store = if request.dry_run {
SQLiteMemoryStore::open_read_only(db_path)
} else {
SQLiteMemoryStore::open(db_path)
}
.map_err(|err| err.to_string())?;
store.maintain(&request).map_err(|err| err.to_string())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tree-ring-memory-cli/src/actions/lifecycle.rs` around lines 75 - 89,
The maintain path in maintainer::maintain should not always open
SQLiteMemoryStore in read-only mode when store is None. Update the logic to
choose the connection mode based on request.dry_run: keep open_read_only for dry
runs, but open a writable store for apply mode before calling
store.maintain(&request). Make sure the behavior is handled in maintain and any
helper used by SQLiteMemoryStore so the None-store fallback cannot attempt
writes on a read-only connection.

Comment on lines +17 to +33
pub(crate) fn open_read_only_connection(path: &Path) -> TreeRingResult<Connection> {
let path = path
.canonicalize()
.map_err(|err| sqlite_error(err.to_string()))?;
let normalized_path = normalize_sqlite_uri_path(&path.to_string_lossy());
let uri = format!(
"file:{}?mode=ro&immutable=1",
sqlite_uri_path(&normalized_path)
);
let connection = Connection::open_with_flags(
uri,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_URI,
)
.map_err(sqlite_error_from_rusqlite)?;
configure_connection(&connection)?;
Ok(connection)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does SQLite require a leading slash before a Windows drive letter in a file: URI for it to be treated as an absolute path?

💡 Result:

Yes, SQLite requires a leading forward slash before a Windows drive letter in a file: URI for it to be treated as an absolute path [1][2][3]. When using the file: URI syntax to specify a database file on Windows, SQLite follows specific rules to interpret the path component [1]. If the authority section of the URI is omitted, the path is interpreted as an absolute path only if it begins with a "/" character [1][2]. For Windows specifically, if the path begins with "/X:/", where X is a drive letter, SQLite recognizes the "X:" as the drive volume rather than the top-level directory [4][1]. To correctly convert a Windows absolute filename into a URI, the official SQLite documentation recommends prepending a single forward slash to the path if it begins with a drive letter [1][2][3]. Additionally, all backslashes in the Windows path should be converted to forward slashes [1][2]. Failure to include the leading slash when the authority is omitted may result in the path being interpreted as a relative pathname [1][2].

Citations:


🏁 Script executed:

git ls-files crates/tree-ring-memory-sqlite/src
sed -n '1,220p' crates/tree-ring-memory-sqlite/src/schema.rs

Repository: TerminallyLazy/Tree-Ring-Memory

Length of output: 3847


🏁 Script executed:

rg -n "open_read_only_connection|open_read_only|audit|maintain|normalize_sqlite_uri_path|sqlite_uri_path" crates/tree-ring-memory-sqlite/src

Repository: TerminallyLazy/Tree-Ring-Memory

Length of output: 3858


🌐 Web query:

SQLite URI UNC path authority Windows SQLITE_ALLOW_URI_AUTHORITY server share file://server/share

💡 Result:

In SQLite, the authority component of a URI (e.g., in file://authority/path) is subject to strict validation by default [1][2]. Under normal circumstances, the authority must either be empty, omitted, or set to "localhost"; any other value will trigger an error [1][3]. To use a Windows UNC path (e.g., //server/share/file.db) within a SQLite URI, SQLite must be compiled with the SQLITE_ALLOW_URI_AUTHORITY preprocessor macro [1][2]. When this macro is enabled, any authority value other than "localhost" is treated as the server component of a UNC path and passed directly to the underlying operating system [1]. Key technical details regarding this behavior include: - Default Behavior: Without the SQLITE_ALLOW_URI_AUTHORITY macro, you cannot use arbitrary server names in the authority section of a URI [1][2]. - UNC Path Handling: When enabled, the authority becomes part of the UNC path string, allowing SQLite to interact with network shares [1][4]. - Security/Compatibility: This mechanism is intended to bridge the gap between standard URI syntax and Windows-specific network path conventions [1][2]. Note that even when using UNC paths, SQLite's performance over network shares (SMB) can be significantly affected by network latency and the locking semantics of the underlying file system, as SQLite relies on mandatory file locking to maintain database integrity [5]. If you are experiencing issues with UNC paths in libraries like System.Data.SQLite, ensure that your path strings are being correctly escaped or passed to the underlying SQLite API to avoid accidental path manipulation by the wrapper library [6].

Citations:


🏁 Script executed:

rg -n "SQLITE_ALLOW_URI_AUTHORITY|allow_uri_authority|uri_authority|sqlite3.*compile|rusqlite" .

Repository: TerminallyLazy/Tree-Ring-Memory

Length of output: 9853


🏁 Script executed:

python3 - <<'PY'
def normalize_sqlite_uri_path(path: str) -> str:
    if path.startswith(r"\\?\UNC\\"):
        rest = path[len(r"\\?\UNC\\"):]
        return r"\\\\" + rest.replace("\\", "/")
    elif path.startswith(r"\\?\\" ):
        rest = path[len(r"\\?\\"):]
        return rest.replace("\\", "/")
    else:
        return path.replace("\\", "/")

def sqlite_uri_path(path: str) -> str:
    out = []
    for ch in path.encode():
        if (65 <= ch <= 90) or (97 <= ch <= 122) or (48 <= ch <= 57) or ch in b"/-._~":
            out.append(chr(ch))
        else:
            out.append(f"%{ch:02X}")
    return "".join(out)

for sample in [r"C:\Users\lazy\memory.sqlite", r"\\?\C:\Users\lazy\memory.sqlite", r"\\?\UNC\server\share\memory.sqlite"]:
    normalized = normalize_sqlite_uri_path(sample)
    uri = f"file:{sqlite_uri_path(normalized)}?mode=ro&immutable=1"
    print(sample)
    print(" normalized:", normalized)
    print(" uri:", uri)
PY

Repository: TerminallyLazy/Tree-Ring-Memory

Length of output: 594


Windows read-only paths need URI normalization

open_read_only_connection turns C:\... into file:C%3A/..., which SQLite treats as a relative path. UNC paths become file://server/..., which also depends on URI-authority support. Prepend / for drive-letter paths and handle UNC separately before building the URI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tree-ring-memory-sqlite/src/schema.rs` around lines 17 - 33, The URI
built in open_read_only_connection is not normalized correctly for Windows
paths, so drive-letter paths and UNC paths can be misinterpreted by SQLite.
Update the path-to-URI handling in open_read_only_connection, using
normalize_sqlite_uri_path and sqlite_uri_path, so drive-letter paths are
prefixed with a leading slash and UNC paths are handled separately before
constructing the file: URI. Keep the existing Connection::open_with_flags flow
and adjust only the URI assembly logic to preserve correct read-only behavior on
Windows.

Comment on lines +24 to +40
pub(crate) fn push_in_filter(
sql: &mut String,
parameters: &mut Vec<Value>,
column_name: &str,
values: &[String],
) {
sql.push_str(" AND ");
sql.push_str(column_name);
sql.push_str(" IN (");
sql.push_str(
&std::iter::repeat_n("?", values.len())
.collect::<Vec<_>>()
.join(", "),
);
sql.push(')');
parameters.extend(values.iter().cloned().map(Value::Text));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard against empty values in push_in_filter.

If called with an empty slice, this generates invalid SQL (AND col IN ()). Current callers appear to guard before calling, but the function itself should be defensive against future misuse.

🛡️ Proposed fix
 pub(crate) fn push_in_filter(
     sql: &mut String,
     parameters: &mut Vec<Value>,
     column_name: &str,
     values: &[String],
 ) {
+    if values.is_empty() {
+        return;
+    }
     sql.push_str(" AND ");
     sql.push_str(column_name);
     sql.push_str(" IN (");
📝 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
pub(crate) fn push_in_filter(
sql: &mut String,
parameters: &mut Vec<Value>,
column_name: &str,
values: &[String],
) {
sql.push_str(" AND ");
sql.push_str(column_name);
sql.push_str(" IN (");
sql.push_str(
&std::iter::repeat_n("?", values.len())
.collect::<Vec<_>>()
.join(", "),
);
sql.push(')');
parameters.extend(values.iter().cloned().map(Value::Text));
}
pub(crate) fn push_in_filter(
sql: &mut String,
parameters: &mut Vec<Value>,
column_name: &str,
values: &[String],
) {
if values.is_empty() {
return;
}
sql.push_str(" AND ");
sql.push_str(column_name);
sql.push_str(" IN (");
sql.push_str(
&std::iter::repeat_n("?", values.len())
.collect::<Vec<_>>()
.join(", "),
);
sql.push(')');
parameters.extend(values.iter().cloned().map(Value::Text));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/tree-ring-memory-sqlite/src/search.rs` around lines 24 - 40,
push_in_filter currently emits invalid SQL when values is empty, so make the
function defensive even if callers forget to guard. Update push_in_filter in
search.rs to check values.is_empty() up front and return without modifying sql
or parameters; otherwise keep the existing IN clause and parameter population
behavior unchanged.

@TerminallyLazy TerminallyLazy merged commit 6e12500 into main Jul 9, 2026
2 checks passed
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.

1 participant