Skip to content

Feat/schema validators session and scopes 2#57

Merged
casibbald merged 25 commits into
mainfrom
feat/schema_validators_session_and_scopes_2
Mar 31, 2026
Merged

Feat/schema validators session and scopes 2#57
casibbald merged 25 commits into
mainfrom
feat/schema_validators_session_and_scopes_2

Conversation

@casibbald
Copy link
Copy Markdown
Contributor

@casibbald casibbald commented Mar 31, 2026

Note

Medium Risk
Medium risk because it introduces new lifeguard-derive attribute parsing and proc-macro exports (validate, validation_strategy, scope, DeriveMigrationName) that affect generated code and could break consumers if edge cases are missed; the rest is largely documentation reshaping.

Overview
Restructures top-level documentation by slimming README.md into a pitch + quick-start + doc index and moving detailed material into new/expanded docs: COMPARISON.md (repository truth + competitive matrix), VISION.md, ARCHITECTURE.md (full diagrams), OBSERVABILITY.md, and ROADMAP.md, with cross-links updated across planning docs.

Extends lifeguard-derive validation/scope support by parsing #[validate(custom = path)] on fields and a struct-level #[validation_strategy = "aggregate"|"fail_fast"], and by exposing new proc-macro entrypoints #[scope] and #[derive(DeriveMigrationName)] (plus updating derive attribute allow-lists to accept validate/validation_strategy).

Written by Cursor Bugbot for commit 79951ff. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added schema comparison CLI to reconcile live database against migrations
    • Introduced configurable validator strategies (fail-fast or aggregate error collection)
    • Added field-level custom validators for flexible validation logic
    • Added scope composition helpers (scope_or, scope_any) for complex queries
    • Added F-style UPDATE expressions for computed column updates
    • Introduced Session API for batched transactional persistence with dirty tracking
    • Added read preference control to route reads to primary or replica tier
    • Added exclusive primary-tier locking for consistency-critical operations
  • Documentation

    • Reorganized docs into focused guides: COMPARISON.md, ROADMAP.md, VISION.md, ARCHITECTURE.md, OBSERVABILITY.md
    • Simplified README with pointers to detailed documentation

…3, F() update test

- Add ValidateOp::Delete and run_validators on derive delete path
- SelectQuery::scope_or and scope_any with unit tests
- infer-schema table filter integration test (SI-3); migrate README touch-ups
- db_integration column_f_update for ColumnTrait::f_add UPDATE
- #[validate(custom = path)] on LifeRecord fields (V-5)
- DESIGN_FIND_RELATED_SCOPES, PRD and mapping docs, progress memory bank
…ttern

- Add db_integration column_f_where (Expr::expr + f_add, ExprTrait, order_by_expr)
- Extend ColumnTrait::f_add rustdoc for WHERE/ORDER BY
- PRD §8.7 Phase D + memory bank progress
- Add __update_exprs map and generated set_<field>_expr on LifeRecord
- UPDATE SET prefers expression over literal/save_as; setters clear expr
- dirty_fields / reset / set / take integrate with __update_exprs
- column_f_update: mutex + shared setup; record_set_n_expr_update_increments_on_postgres
- PRD §8.7, SEAORM mapping, derive rustdoc, progress memory bank
…EADME

- ModelIdentityMap::mark_dirty_key; session rustdoc + unit test
- Derived identity_map_key() via ActiveModelTrait::get (UFCS)
- insert() rejects non-empty __update_exprs (already in macro)
- README competitive table + bullets; PRD §7.7/§8.7/§9.7; DESIGN_SESSION_UOW
- DEVELOPMENT db_integration notes; SEAORM session row; F-3 rustdoc note on f_add
- Tests: insert_rejects_when_set_expr_pending, user_record_identity_map_key_*
- Session/ModelIdentityMap: register_pending_insert, flush_dirty_with_map_key, promote_pending_to_loaded; pooled/txn map-key flush; integration tests
- lifeguard-migrate: compare-schema vs merged generated SQL; infer-schema CLI subprocess e2e; smoke tests use dedicated PG schema
- Docs: README/SEAORM/PRD/DESIGN validators G6, F() PostgreSQL typing, Phase A closure; ColumnTrait f_add rustdoc; memory bank
…ntityModelCell

- Add Send wrapper for Rc<RefCell<Model>> and attach_session_with_model plumbing
- Update README, PRD §9, DESIGN_SESSION_UOW, integration test, memory bank
…oledLifeExecutor

- Add ReadPreference Default|Primary, read_pool_for, with_read_preference
- Integration test pooled_read_preference_primary_forces_primary_tier
- README, DESIGN_FIND_RELATED_SCOPES, memory bank
- Unit struct -> snake_case MIGRATION_NAME + MigrationName impl
- Re-export DeriveMigrationName from lifeguard::migration
- Tests and SEAORM mapping + README row
- Wrap SeaQuery with_cte so CTEs work with all/one (vs raw WithQuery)
- join_subquery; window; expr_window*; docs and README roadmap
- query/select: correct SelectQuery::all/one links; compile-safe sea_query examples for CTEs, joins, windows, window_function_cust; expr_window docs
- pool/pooled: ReadPreference when-to-use, PooledLifeExecutor routing, with_read_preference example
- lib.rs and query/mod.rs: explicit opt-in APIs and default vs advanced SQL
- memory-bank: rustdoc API surface audit snapshot
…pes vs find_related

- lifeguard-derive: backtick snake_case in DeriveMigrationName rustdoc (clippy::doc_markdown)
- lifeguard-migrate: composite PK emits #[primary_key] per column; column_map_from_merged_baseline; compare-schema column name drift for shared tables; PRD §5.7/§5.7a; DESIGN_SCHEMA; README; golden infer_composite_pk
- query::scope + FindRelated: document find_related does not inherit parent scopes; Related/LazyLoader links
- DESIGN_FIND_RELATED_SCOPES, PRD §7.7, SEAORM mapping; memory bank
Copy link
Copy Markdown

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

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 introduces a unit-of-work Session with identity mapping, a compare-schema utility for migration reconciliation, and expanded SelectQuery support for CTEs and window functions. The review identifies a critical unsoundness in the Send implementation for identity map cells and potential performance issues related to OS-thread blocking mutexes in the may coroutine runtime. Further feedback suggests optimizing session notification hooks to reduce cloning overhead and improving the efficiency of string validation predicates by avoiding redundant character counting.

Comment thread src/session/identity_model_cell.rs
Comment thread src/pool/pooled.rs
Comment thread lifeguard-derive/src/macros/life_record.rs
Comment thread src/active_model/predicates.rs Outdated
Comment on lines +16 to +23
match value {
Value::String(Some(s)) if s.chars().count() > max => Err(format!(
"must be at most {max} characters (got {})",
s.chars().count()
)),
Value::String(Some(_)) | Value::String(None) => Ok(()),
_ => Ok(()),
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

In string_utf8_chars_max, s.chars().count() is called twice: once in the match guard and once in the error message formatting. Since this is an $O(N)$ operation, it should be called once and the result reused.

    if let Value::String(Some(s)) = value {
        let count = s.chars().count();
        if count > max {
            return Err(format!("must be at most {max} characters (got {count})"));
        }
    }
    Ok(())

Comment thread src/session/uow.rs
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 31, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces substantial feature additions across validation orchestration, session/unit-of-work patterns, F-style expressions, query builders, pool executor enhancements, schema migration tooling, and comprehensive documentation reorganization. Key additions include DeriveMigrationName and #[scope] macros, configurable validation strategies with predicate helpers, session-based identity-map dirty tracking with transactional flush support, pool read-preference routing with exclusive primary-tier pinning, and new compare-schema CLI tooling for schema reconciliation.

Changes

Cohort / File(s) Summary
Derive macros: migration name and scope attribute
lifeguard-derive/src/lib.rs, lifeguard-derive/src/macros/migration_name_derive.rs, lifeguard-derive/src/macros/scope_attr.rs, lifeguard-derive/src/macros/mod.rs, lifeguard-derive/tests/test_derive_migration_name.rs
Added #[derive(DeriveMigrationName)] and #[scope] attribute macro implementations with compile-time validation; migration name derive enforces unit structs and emits snake_case constants; scope attribute renames functions and defaults visibility to pub.
Attribute parsing for validation
lifeguard-derive/src/attributes.rs
Extended TableAttributes with validation_strategy enum (FailFast/Aggregate) and added parse_field_validate_custom_paths to collect custom validator function paths from field-level #[validate] attributes.
LifeRecord derive expansion: F-style expressions, session binding, validation
lifeguard-derive/src/macros/life_record.rs
Major changes: added __update_exprs HashMap for buffering F-style update expressions, set_<field>_expr methods, session attachment via __lg_session_notifier and __lg_session_model (for auto-dirty notification), identity_map_key() when PKs exist, field-level validation via validate_fields_impl, and DELETE operation validator invocation via ValidateOp::Delete.
Validation framework: strategies and predicates
src/active_model/validate_op.rs, src/active_model/validation.rs, src/active_model/traits.rs, src/active_model/predicates.rs, src/active_model/mod.rs
Introduced ValidationStrategy enum (FailFast/Aggregate) controlling error aggregation, added ValidateOp::Delete variant, implemented run_validators_with_strategy for strategy-driven validation orchestration, new ActiveModelBehavior::validation_strategy hook, and built-in predicates module with string/byte/integer/float range validators.
Session and unit-of-work abstraction
src/session/uow.rs, src/session/identity_model_cell.rs, src/session/mod.rs
New session/UoW layer: Session<E> wraps identity map + pending dirty queue, SessionDirtyNotifier manages deduplicating dirty keys, SessionIdentityModelCell<M> provides Rc<RefCell<M>> wrapping for auto-sync, identity map extended with register_pending_insert, promote_pending_to_loaded, mark_dirty_key, flush_dirty_with_map_key, plus transaction/pool flush variants.
Query builder enhancements: scopes and advanced SQL
src/query/scope.rs, src/query/select.rs, src/query/mod.rs
Added scope_or, scope_any (OR composition), with_cte, join_subquery, window helpers (window, expr_window, expr_window_*); updated documentation on scope/find_related interaction and F-expression usage in WHERE/ORDER BY contexts.
Pool executor: read preference and primary pinning
src/pool/pooled.rs, src/pool/mod.rs
Introduced ReadPreference enum (Default/Primary) for per-executor read routing, ExclusivePrimaryLifeExecutor for single-connection primary-tier pinning via LifeguardPool::exclusive_primary_write_executor(), per-slot mutexes in WorkerPool, and read-preference dispatch helpers.
Migration tooling: schema inference and comparison
lifeguard-migrate/src/schema_infer.rs, lifeguard-migrate/src/generated_migration_diff.rs, lifeguard-migrate/src/schema_migration_compare.rs, lifeguard-migrate/src/main.rs, lifeguard-migrate/src/lib.rs, lifeguard-migrate/README.md
Removed composite PK TODO comment and now emit #[primary_key] per column; added column_map_from_merged_baseline utility; introduced new schema_migration_compare module with compare_generated_dir_to_live_db function; added compare-schema CLI subcommand with drift reporting.
Integration tests: F-expressions, pool, and session
tests/db_integration/column_f_update.rs, tests/db_integration/column_f_where.rs, tests/db_integration/pool_read_replica.rs, tests/db_integration/session_identity_flush.rs, tests/db_integration_suite.rs, lifeguard-derive/tests/test_minimal.rs
Added comprehensive integration tests covering F-style UPDATE expressions, WHERE/ORDER BY with F-expressions, pool read preference routing, session dirty tracking with transactional and pooled flush, pending insert workflow, and validation strategies; also added scope/validation/identity-map unit tests in test_minimal.rs.
Schema inference CLI and migration comparison tests
lifeguard-migrate/tests/infer_schema_cli_subprocess.rs, lifeguard-migrate/tests/infer_schema_postgres_smoke.rs, lifeguard-migrate/tests/infer_schema_table_filter_si3.rs, lifeguard-migrate/tests/migration_db_compare_smoke.rs, lifeguard-migrate/tests/golden/infer_composite_pk.expected.rs
Added live Postgres smoke tests for schema inference with URL fallback chain (TEST/DATABASE/LIFEGUARD_DATABASE_URL), CLI subprocess e2e tests, table filter validation (SI-3), migration comparison drift detection, and updated golden test for composite PK annotation.
Core library re-exports and public API
src/lib.rs, src/migration/mod.rs, src/migration/migration_name.rs
Expanded top-level re-exports: added pool (ExclusivePrimaryLifeExecutor, ReadPreference), validation (predicates, run_validators_with_strategy, ValidationStrategy), session (Session, SessionDirtyNotifier, SessionIdentityModelCell, pending-insert helpers), scope macro; introduced MigrationName trait and DeriveMigrationName inline re-export.
Documentation: roadmap, vision, architecture, observability, comparison
ROADMAP.md, VISION.md, ARCHITECTURE.md, OBSERVABILITY.md, COMPARISON.md, SECURITY_PROMPT.md, README.md
Added comprehensive new documentation files defining product roadmap (status table), long-form vision (ORM architecture, pooling, LifeReflector), architectural call flows (request routing, cache coherence), observability integrations (OTel tracing, Prometheus), competitive feature comparison (matrix vs SeaORM/Diesel/SQLx), and security assessment prompt; reorganized and streamlined README.md with pointers to new docs.
Planning and design documentation
.agent/memory-bank/activeContext.md, .agent/memory-bank/progress.md, DEVELOPMENT.md, docs/planning/DESIGN_FIND_RELATED_SCOPES.md, docs/planning/DESIGN_SCHEMA_INFERENCE_CLI_CODEGEN.md, docs/planning/DESIGN_SESSION_UOW.md, docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md, docs/planning/README.md, docs/planning/lifeguard-derive/SEAORM_LIFEGUARD_MAPPING.md
Updated agent context and progress tracking; added design docs for find_related/scope interaction (clarifying parent scopes are not merged), schema inference CLI/compare workflow, session/UoW architecture; expanded PRD with phase updates (composite PK support, validator strategies, scope delivery, F-expr limitations, session transaction variants); updated SeaORM mapping table with new capabilities and shipped status indicators.
Documentation: column expressions and query traits
src/query/column/column_trait.rs, src/query/traits.rs, src/logging/mod.rs, docs/OBSERVABILITY.md
Updated documentation on F-expression usage in WHERE/ORDER BY (with sea_query wrapping), PostgreSQL numeric promotion behavior, query builder architecture; fixed module-level intra-doc links and added pointer from nested OBSERVABILITY.md to root doc.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant Rec as LifeRecord
    participant Sess as Session
    participant Map as Identity Map
    participant Exec as LifeExecutor
    participant DB as PostgreSQL

    App->>Rec: mutate field
    Rec->>Rec: mark __update_exprs or literal
    Rec->>Sess: notify_identity_map_dirty
    Sess->>Map: mark_dirty_key or enqueue pending
    
    Note over App,Sess: Dirty tracking queued
    
    App->>Sess: flush_dirty(executor, closure)
    Sess->>Map: drain pending keys→mark_dirty_key
    Map->>Map: iterate dirty by map-key order
    
    loop Each dirty entry
        Map->>Exec: closure(executor, rc, key)
        Exec->>Rec: derive from model, call update()
        Rec->>Exec: SET literals + __update_exprs
        Exec->>DB: UPDATE with F-expressions
        DB-->>Exec: row updated
    end
    
    Exec-->>Map: Result
    Map->>Map: clear_dirty
    Map-->>Sess: Ok
    Sess-->>App: flush complete
Loading
sequenceDiagram
    participant App as Application
    participant Rec as LifeRecord
    participant Val as Validation
    participant VF as validate_fields
    participant VM as validate_model

    App->>Rec: set field value
    Rec->>Rec: accumulate validation errors
    
    Note over Rec: insert/update/delete triggered
    
    Rec->>Val: run_validators(record, op)
    Val->>Val: record.validation_strategy(op)
    
    alt FailFast
        Val->>VF: validate_fields
        VF-->>Val: first error?
        alt Error found
            Val-->>Rec: Err(ValidationError)
        else OK
            Val->>VM: validate_model
            VM-->>Val: result
            Val-->>Rec: result
        end
    else Aggregate
        par
            Val->>VF: validate_fields
            VF-->>Val: Vec of errors
        and
            Val->>VM: validate_model
            VM-->>Val: Vec of errors
        end
        
        Note over Val: Collect all errors
        Val-->>Rec: Err(Validation(combined))
    end
    
    Rec-->>App: validation result
Loading
sequenceDiagram
    participant App as Application
    participant Pool as LifeguardPool
    participant Exec as PooledLifeExecutor
    participant Dispatch as Dispatcher
    participant Primary as Primary Tier
    participant Replica as Replica Tier

    App->>Exec: with_read_preference(ReadPreference::Primary)
    Exec->>Exec: read_preference = Primary
    
    Note over Exec: New executor with Primary preference
    
    App->>Exec: query_all_values (read operation)
    Exec->>Dispatch: dispatch_read_with_preference
    
    alt ReadPreference::Primary
        Dispatch->>Primary: route to primary
        Primary-->>Dispatch: result
    else ReadPreference::Default
        Dispatch->>Replica: route to replica (if healthy)
        Replica-->>Dispatch: result
    end
    
    Dispatch-->>Exec: rows
    Exec-->>App: result set

    Note over App,Replica: Writes always route to primary tier
    App->>Exec: execute (write operation)
    Exec->>Primary: route to primary (always)
    Primary-->>Exec: rows affected
    Exec-->>App: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feat/schema validators session and scopes #56 — Implements the same primary feature set (schema inference/migration diff, validators with strategies, scopes, F-style expressions, and session/UoW identity-map with dirty/flush), directly sharing code-level changes across lifeguard-derive, lifeguard-migrate, and core library modules.
  • feat(pool): add LifeguardPool, PooledLifeExecutor, and ORM *_values path #55 — Related via pool/executor layer additions (LifeguardPool, PooledLifeExecutor, *_values APIs); both PRs modify derived CRUD paths and include replica/pooling integration tests.
  • Fix/jsf compliance issues #53 — Related via LifeRecord derive macro and ActiveModel validation behavior changes (validate_fields injection, lifecycle hooks, validation error handling).

Poem

🐰 Hop, hop—validators aggregate, scopes compose with grace!
Sessions track their dirty state, flush with expression-lace.
F-style updates bloom where PKs meet,
Pool reads take the replica's fleet—
Lifeguard's song now runs complete! 🌟

✨ 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 feat/schema_validators_session_and_scopes_2

Comment thread lifeguard-derive/src/macros/life_record.rs Outdated
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.

Actionable comments posted: 1

🧹 Nitpick comments (12)
lifeguard-migrate/src/main.rs (1)

237-242: Use named format string syntax per coding guidelines.

The format string uses a positional argument {} instead of named format syntax.

♻️ Suggested fix
     if !generated_dir.is_dir() {
         return Err(MigrationError::InvalidFormat(format!(
-            "compare-schema: not a directory: {}",
-            generated_dir.display()
+            "compare-schema: not a directory: {path}",
+            path = generated_dir.display()
         )));
     }

As per coding guidelines: "Use named format string syntax instead of positional arguments in format! and write! macros"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lifeguard-migrate/src/main.rs` around lines 237 - 242, Replace the positional
format! usage with named-format syntax when constructing the MigrationError for
a non-directory generated_dir: update the
MigrationError::InvalidFormat(format!(...)) call so the message uses a named
placeholder like "{path}" and pass generated_dir.display() as
path=generated_dir.display(); locate the block checking generated_dir.is_dir()
in main.rs and change the format invocation accordingly to follow the coding
guideline.
lifeguard-migrate/tests/infer_schema_cli_subprocess.rs (1)

35-39: Use named format string syntax per coding guidelines.

♻️ Suggested fix
     assert!(
         out.status.success(),
-        "infer-schema failed: stderr={}",
-        String::from_utf8_lossy(&out.stderr)
+        "infer-schema failed: stderr={stderr}",
+        stderr = String::from_utf8_lossy(&out.stderr)
     );

As per coding guidelines: "Use named format string syntax instead of positional arguments in format! and write! macros"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lifeguard-migrate/tests/infer_schema_cli_subprocess.rs` around lines 35 - 39,
The assert! uses a positional format argument; change it to named format syntax
per guidelines by updating the assertion message to use a named placeholder like
"infer-schema failed: stderr={stderr}" and pass stderr =
String::from_utf8_lossy(&out.stderr) as the named argument, referencing the
existing variable out in the assertion.
DEVELOPMENT.md (1)

42-42: Consider breaking up this documentation block for readability.

This line is extremely dense with test files, environment variables, and coverage details. Consider using a bullet list or sub-sections to improve scannability.

📝 Suggested restructuring
-- **Live Postgres tests (optional):** `infer_schema_postgres_smoke.rs` (**`infer_schema_rust`** on `public`); `infer_schema_table_filter_si3.rs` (SI-3); **`infer_schema_cli_subprocess.rs`** (full **`infer-schema`** binary via `CARGO_BIN_EXE_lifeguard-migrate`); **`migration_db_compare_smoke.rs`** (`compare_generated_dir_to_live_db` + **`compare-schema`** CLI). All require **`TEST_DATABASE_URL`**, **`DATABASE_URL`**, or **`LIFEGUARD_DATABASE_URL`** (and subprocess tests need the Cargo-built binary); otherwise they skip. **`db_integration_suite`** includes **`column_f_update`** (F-style `UPDATE SET`, derived `set_*_expr` / `update()`, insert guard for `__update_exprs`), **`column_f_where`** (`WHERE` / `ORDER BY` with `Expr::expr` + `ColumnTrait::f_*`), and **`session_identity_flush`** (`ModelIdentityMap` / `Session`, `attach_session`, `flush_dirty` / `flush_dirty_in_transaction`, `register_pending_insert` / `flush_dirty_with_map_key` / `flush_dirty_in_transaction_with_map_key` / `flush_dirty_in_transaction_pooled_with_map_key` / `promote_pending_to_loaded`, `LifeRecord::update` / `insert`, PRD §9).
+- **Live Postgres tests (optional):** All require `TEST_DATABASE_URL`, `DATABASE_URL`, or `LIFEGUARD_DATABASE_URL`; otherwise they skip.
+  - `infer_schema_postgres_smoke.rs` — `infer_schema_rust` on `public`
+  - `infer_schema_table_filter_si3.rs` — SI-3
+  - `infer_schema_cli_subprocess.rs` — full `infer-schema` binary via `CARGO_BIN_EXE_lifeguard-migrate`
+  - `migration_db_compare_smoke.rs` — `compare_generated_dir_to_live_db` + `compare-schema` CLI
+- **`db_integration_suite`** includes:
+  - `column_f_update` — F-style `UPDATE SET`, derived `set_*_expr` / `update()`, insert guard for `__update_exprs`
+  - `column_f_where` — `WHERE` / `ORDER BY` with `Expr::expr` + `ColumnTrait::f_*`
+  - `session_identity_flush` — `ModelIdentityMap` / `Session`, PRD §9
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DEVELOPMENT.md` at line 42, The single dense sentence in DEVELOPMENT.md
should be split into a short intro plus clear subsections or a bullet list:
create a "Live Postgres tests (optional)" bullet listing the test files
(infer_schema_postgres_smoke.rs, infer_schema_table_filter_si3.rs,
infer_schema_cli_subprocess.rs, migration_db_compare_smoke.rs) and the required
env vars (TEST_DATABASE_URL, DATABASE_URL, LIFEGUARD_DATABASE_URL) and note the
subprocess/CARGO_BIN_EXE_lifeguard-migrate requirement; then create a
"db_integration_suite" subsection or bullets that enumerate the included
tests/features (column_f_update with set_*_expr/update()/__update_exprs,
column_f_where using Expr::expr and ColumnTrait::f_*, and session_identity_flush
with ModelIdentityMap/Session, attach_session, flush_dirty*,
register_pending_insert, promote_pending_to_loaded, LifeRecord::update/insert).
Ensure each symbol from the diff (file names and function/type names) appears
verbatim to preserve searchability and break lines so each item is one bullet
for readability.
src/session/identity_model_cell.rs (1)

32-34: replace_with may panic if already borrowed.

The borrow_mut() call will panic if the RefCell is already borrowed. In a session identity-map context where multiple record references might exist, this could be triggered unexpectedly.

Consider using try_borrow_mut() and returning a Result to allow callers to handle borrow conflicts gracefully:

♻️ Proposed defensive change
-    pub fn replace_with(&self, model: M) {
-        *self.rc.borrow_mut() = model;
+    pub fn replace_with(&self, model: M) -> Result<(), std::cell::BorrowMutError> {
+        *self.rc.try_borrow_mut()? = model;
+        Ok(())
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/identity_model_cell.rs` around lines 32 - 34, The replace_with
method currently calls self.rc.borrow_mut() which will panic if the RefCell is
already borrowed; change replace_with to attempt a non-panicking borrow using
self.rc.try_borrow_mut() and return a Result<(), BorrowError> (or a
crate-appropriate error) so callers can handle borrow conflicts; update the
signature of replace_with(M) to return Result<(), _>, map the TryBorrowError to
your chosen error type and avoid unwraps or panicking operations in
replace_with, leaving callers responsible for retrying or propagating the error.
lifeguard-migrate/tests/migration_db_compare_smoke.rs (1)

24-25: Use named formatting here.

Line 25 uses positional formatting, and the repo guideline asks for named arguments in format!.

♻️ Proposed fix
 fn scratch_schema_name() -> String {
-    format!("lg_cmp_{}", Uuid::new_v4().simple())
+    let schema_id = Uuid::new_v4().simple();
+    format!("lg_cmp_{schema_id}")
 }

As per coding guidelines, **/*.rs: Use named format string syntax instead of positional arguments in format! and write! macros

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lifeguard-migrate/tests/migration_db_compare_smoke.rs` around lines 24 - 25,
The function scratch_schema_name uses positional formatting; update the format!
call in scratch_schema_name() to use named formatting per Rust guidelines (e.g.,
provide a named argument like id and pass Uuid::new_v4().simple() as its value)
so the format string reads with a named placeholder instead of positional
formatting.
src/active_model/predicates.rs (1)

15-24: Consider caching the character count to avoid redundant computation.

On the error path, s.chars().count() is called twice (once for the condition check at line 17, once for the error message at line 19). While this is only on the error path, for very long strings this could be noticeable.

♻️ Optional refactor to cache character count
 pub fn string_utf8_chars_max(value: &Value, max: usize) -> Result<(), String> {
     match value {
-        Value::String(Some(s)) if s.chars().count() > max => Err(format!(
-            "must be at most {max} characters (got {})",
-            s.chars().count()
-        )),
-        Value::String(Some(_)) | Value::String(None) => Ok(()),
+        Value::String(Some(s)) => {
+            let count = s.chars().count();
+            if count > max {
+                Err(format!("must be at most {max} characters (got {count})"))
+            } else {
+                Ok(())
+            }
+        }
+        Value::String(None) => Ok(()),
         _ => Ok(()),
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/active_model/predicates.rs` around lines 15 - 24, In
string_utf8_chars_max, avoid calling s.chars().count() twice by computing the
character count once into a local variable (e.g., let len = s.chars().count())
when Value::String(Some(s)) is matched; then use that len for both the
comparison and the error message in the Err branch of the function to eliminate
redundant iteration over the string.
src/session/uow.rs (4)

258-274: Same format string guideline applies here.

♻️ Proposed fix
         let tx = executor.begin().map_err(|e| {
-            ActiveModelError::DatabaseError(format!("begin transaction: {e}"))
+            ActiveModelError::DatabaseError(format!("begin transaction: {err}", err = e))
         })?;
         let ex: &dyn LifeExecutor = &tx;
         match self.flush_dirty_with_map_key(ex, |e, m, k| f(e, m, k)) {
             Ok(()) => tx.commit().map_err(|e| {
-                ActiveModelError::DatabaseError(format!("commit: {e}"))
+                ActiveModelError::DatabaseError(format!("commit: {err}", err = e))
             }),
             Err(e) => {
                 if let Err(rb_err) = tx.rollback() {
                     return Err(ActiveModelError::DatabaseError(format!(
-                        "flush failed: {e}; rollback failed: {rb_err}"
+                        "flush failed: {err}; rollback failed: {rollback_err}",
+                        err = e,
+                        rollback_err = rb_err
                     )));
                 }
                 Err(e)
             }
         }

As per coding guidelines: "Use named format string syntax instead of positional arguments in format! and write! macros".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/uow.rs` around lines 258 - 274, The format! calls inside the
transaction handling (the ActiveModelError::DatabaseError constructions in the
begin() map_err, commit() map_err, and the rollback error formatting in the Err
branch) use positional interpolation; change them to named format string syntax
(e.g., format!("begin transaction: {err}", err = e)) for each occurrence
(references: executor.begin() map_err closure, tx.commit() map_err closure, and
the rollback error formatting where "flush failed: {e}; rollback failed:
{rb_err}" is built) so they follow the named-argument guideline.

291-294: Same format string guideline applies to exclusive_primary_write_executor error handling.

♻️ Proposed fix
-        let exec = pool.exclusive_primary_write_executor().map_err(|e| {
-            ActiveModelError::DatabaseError(format!("exclusive primary executor: {e}"))
+        let exec = pool.exclusive_primary_write_executor().map_err(|err| {
+            ActiveModelError::DatabaseError(format!("exclusive primary executor: {err}", err = err))
         })?;

As per coding guidelines: "Use named format string syntax instead of positional arguments in format! and write! macros".

Also applies to: 306-309

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/uow.rs` around lines 291 - 294, Update the error formatting in
the closure that maps pool.exclusive_primary_write_executor() to
ActiveModelError::DatabaseError to use named format arguments instead of the
positional {e}; e.g., change format!("exclusive primary executor: {e}") to
format!("exclusive primary executor: {err}", err = e). Apply the same
named-argument change to the similar error formatting occurrences around lines
306-309 (the other ActiveModelError::DatabaseError constructions) and keep
references to exclusive_primary_write_executor and
flush_dirty_with_begin_commit_sql unchanged.

230-246: Use named format string arguments per coding guidelines.

The format! calls use positional arguments like {e} and {rb_err}. Per coding guidelines, use named format string syntax.

♻️ Proposed fix
         let tx = executor.begin().map_err(|e| {
-            ActiveModelError::DatabaseError(format!("begin transaction: {e}"))
+            ActiveModelError::DatabaseError(format!("begin transaction: {err}", err = e))
         })?;
         let ex: &dyn LifeExecutor = &tx;
         match self.flush_dirty(ex, |e, m| f(e, m)) {
             Ok(()) => tx.commit().map_err(|e| {
-                ActiveModelError::DatabaseError(format!("commit: {e}"))
+                ActiveModelError::DatabaseError(format!("commit: {err}", err = e))
             }),
             Err(e) => {
                 if let Err(rb_err) = tx.rollback() {
                     return Err(ActiveModelError::DatabaseError(format!(
-                        "flush failed: {e}; rollback failed: {rb_err}"
+                        "flush failed: {err}; rollback failed: {rollback_err}",
+                        err = e,
+                        rollback_err = rb_err
                     )));
                 }
                 Err(e)
             }
         }

As per coding guidelines: "Use named format string syntax instead of positional arguments in format! and write! macros".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/uow.rs` around lines 230 - 246, The format! calls in the
transaction handling block should use named format arguments rather than
positional ones; update the three ActiveModelError::DatabaseError uses inside
the begin/commit/rollback logic (the map_err after executor.begin(), the map_err
after tx.commit(), and the rollback error formatting inside the Err(e) arm) to
use named placeholders like {err} or {rb_err} and pass those names in the
format! call so the messages include the same values but follow the named format
string syntax; locate this in the function that calls executor.begin(), creates
tx (let tx = executor.begin()), calls self.flush_dirty(...), and invokes
tx.commit() or tx.rollback() to apply the changes.

335-353: Same format string guideline applies to the pooled transaction helper.

♻️ Proposed fix
-        executor.execute("BEGIN", &[]).map_err(|e| {
-            ActiveModelError::DatabaseError(format!("begin transaction: {e}"))
+        executor.execute("BEGIN", &[]).map_err(|err| {
+            ActiveModelError::DatabaseError(format!("begin transaction: {err}", err = err))
         })?;
         match session.flush_dirty_with_map_key(executor, |e, m, k| f(e, m, k)) {
             Ok(()) => {
-                executor.execute("COMMIT", &[]).map_err(|e| {
-                    ActiveModelError::DatabaseError(format!("commit: {e}"))
+                executor.execute("COMMIT", &[]).map_err(|err| {
+                    ActiveModelError::DatabaseError(format!("commit: {err}", err = err))
                 })?;
                 Ok(())
             }
             Err(e) => {
                 if let Err(rb_err) = executor.execute("ROLLBACK", &[]) {
                     return Err(ActiveModelError::DatabaseError(format!(
-                        "flush failed: {e}; rollback failed: {rb_err}"
+                        "flush failed: {err}; rollback failed: {rollback_err}",
+                        err = e,
+                        rollback_err = rb_err
                     )));
                 }
                 Err(e)
             }
         }

As per coding guidelines: "Use named format string syntax instead of positional arguments in format! and write! macros".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/uow.rs` around lines 335 - 353, Replace positional-format usage
in the pooled transaction helper by using named format parameters: change
format!("begin transaction: {e}") inside the map_err closure on executor.execute
to a named format like format!("begin transaction: {err}", err = e); do the same
for the commit map_err (format!("commit: {e}" -> format!("commit: {err}", err =
e))); and update the rollback-error composition to use named names
(format!("flush failed: {e}; rollback failed: {rb_err}" -> format!("flush
failed: {err}; rollback failed: {rb_err}", err = e, rb_err = rb_err)). Target
the calls around executor.execute, the map_err closures, and the Err branch that
constructs ActiveModelError::DatabaseError and the match over
session.flush_dirty_with_map_key.
lifeguard-derive/src/macros/life_record.rs (1)

903-914: Consider documenting the to_model() call cost in __lg_session_notify_dirty.

Each notifying mutation calls to_model() when __lg_session_model is set, which clones all fields. While this is documented in attach_session_with_model's rustdoc, a brief inline comment at the call site would help maintainers understand the performance tradeoff.

📝 Suggested inline comment
             #[doc(hidden)]
             #[inline]
             fn __lg_session_notify_dirty(&self) {
                 if let Some(ref n) = self.__lg_session_notifier {
                     n.notify_identity_map_dirty(self.identity_map_key());
                 }
                 if let Some(ref cell) = self.__lg_session_model {
+                    // Sync literal field values to the identity-map Rc; this calls to_model()
+                    // which clones all fields. F-style expressions are not represented on Model.
                     if let Ok(m) = self.to_model() {
                         cell.replace_with(m);
                     }
                 }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lifeguard-derive/src/macros/life_record.rs` around lines 903 - 914, The call
site in __lg_session_notify_dirty currently invokes to_model() when
__lg_session_model is set, which can be expensive because to_model() clones all
fields (as documented in attach_session_with_model); add a brief inline comment
immediately above the to_model() call in __lg_session_notify_dirty explaining
that to_model() performs a full clone of the record and may be costly on hot
paths, so maintainers know the performance tradeoff and can consider
alternatives (e.g., avoiding __lg_session_model, using a cheaper diff/partial
update, or caching) when modifying notification behavior.
src/session/mod.rs (1)

109-117: Consider documenting the theoretical u64 wraparound behavior.

wrapping_add(1) on next_pending_id is correct, but after 2^64 registrations without promotion/removal, a key collision could occur. This is practically unreachable in any real workload, but a brief doc comment noting this would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/mod.rs` around lines 109 - 117, Add a short doc comment on
register_pending_insert explaining that next_pending_id uses wrapping_add(1) and
therefore can theoretically wrap after 2^64 increments, which would cause key
reuse/collision of keys generated with PENDING_INSERT_KEY_PREFIX; mention this
is practically unreachable but intentional behavior and that callers should
promote/remove entries to avoid exhausting the counter if running an extremely
long-lived/high-throughput process. Reference register_pending_insert,
next_pending_id, and PENDING_INSERT_KEY_PREFIX in the comment.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/db_integration/pool_read_replica.rs`:
- Around line 323-325: The test is colliding with other replica tests because
setup_schema_on_primary drops/recreates pool_replica_test.t_pool_replica_smoke;
change the test to isolate its schema or serialize the module. Either (A)
generate a unique per-test schema/table suffix and pass it through
TestDatabase::with_url (or replace the TestDatabase::with_url call with a helper
that returns a DB bound to a unique schema) and update setup_schema_on_primary
to accept that schema name and create/target
pool_replica_test_<suffix>.t_pool_replica_smoke_<suffix>, or (B) add a
module-wide serial guard (e.g., a static Mutex/serial_test attribute) around
tests that use setup_schema_on_primary so they run one at a time; update
references to the table/schema in this test and in setup_schema_on_primary to
use the chosen schema identifier (TestDatabase::with_url,
setup_schema_on_primary, and pool_replica_test.t_pool_replica_smoke are the
symbols to modify).

---

Nitpick comments:
In `@DEVELOPMENT.md`:
- Line 42: The single dense sentence in DEVELOPMENT.md should be split into a
short intro plus clear subsections or a bullet list: create a "Live Postgres
tests (optional)" bullet listing the test files (infer_schema_postgres_smoke.rs,
infer_schema_table_filter_si3.rs, infer_schema_cli_subprocess.rs,
migration_db_compare_smoke.rs) and the required env vars (TEST_DATABASE_URL,
DATABASE_URL, LIFEGUARD_DATABASE_URL) and note the
subprocess/CARGO_BIN_EXE_lifeguard-migrate requirement; then create a
"db_integration_suite" subsection or bullets that enumerate the included
tests/features (column_f_update with set_*_expr/update()/__update_exprs,
column_f_where using Expr::expr and ColumnTrait::f_*, and session_identity_flush
with ModelIdentityMap/Session, attach_session, flush_dirty*,
register_pending_insert, promote_pending_to_loaded, LifeRecord::update/insert).
Ensure each symbol from the diff (file names and function/type names) appears
verbatim to preserve searchability and break lines so each item is one bullet
for readability.

In `@lifeguard-derive/src/macros/life_record.rs`:
- Around line 903-914: The call site in __lg_session_notify_dirty currently
invokes to_model() when __lg_session_model is set, which can be expensive
because to_model() clones all fields (as documented in
attach_session_with_model); add a brief inline comment immediately above the
to_model() call in __lg_session_notify_dirty explaining that to_model() performs
a full clone of the record and may be costly on hot paths, so maintainers know
the performance tradeoff and can consider alternatives (e.g., avoiding
__lg_session_model, using a cheaper diff/partial update, or caching) when
modifying notification behavior.

In `@lifeguard-migrate/src/main.rs`:
- Around line 237-242: Replace the positional format! usage with named-format
syntax when constructing the MigrationError for a non-directory generated_dir:
update the MigrationError::InvalidFormat(format!(...)) call so the message uses
a named placeholder like "{path}" and pass generated_dir.display() as
path=generated_dir.display(); locate the block checking generated_dir.is_dir()
in main.rs and change the format invocation accordingly to follow the coding
guideline.

In `@lifeguard-migrate/tests/infer_schema_cli_subprocess.rs`:
- Around line 35-39: The assert! uses a positional format argument; change it to
named format syntax per guidelines by updating the assertion message to use a
named placeholder like "infer-schema failed: stderr={stderr}" and pass stderr =
String::from_utf8_lossy(&out.stderr) as the named argument, referencing the
existing variable out in the assertion.

In `@lifeguard-migrate/tests/migration_db_compare_smoke.rs`:
- Around line 24-25: The function scratch_schema_name uses positional
formatting; update the format! call in scratch_schema_name() to use named
formatting per Rust guidelines (e.g., provide a named argument like id and pass
Uuid::new_v4().simple() as its value) so the format string reads with a named
placeholder instead of positional formatting.

In `@src/active_model/predicates.rs`:
- Around line 15-24: In string_utf8_chars_max, avoid calling s.chars().count()
twice by computing the character count once into a local variable (e.g., let len
= s.chars().count()) when Value::String(Some(s)) is matched; then use that len
for both the comparison and the error message in the Err branch of the function
to eliminate redundant iteration over the string.

In `@src/session/identity_model_cell.rs`:
- Around line 32-34: The replace_with method currently calls
self.rc.borrow_mut() which will panic if the RefCell is already borrowed; change
replace_with to attempt a non-panicking borrow using self.rc.try_borrow_mut()
and return a Result<(), BorrowError> (or a crate-appropriate error) so callers
can handle borrow conflicts; update the signature of replace_with(M) to return
Result<(), _>, map the TryBorrowError to your chosen error type and avoid
unwraps or panicking operations in replace_with, leaving callers responsible for
retrying or propagating the error.

In `@src/session/mod.rs`:
- Around line 109-117: Add a short doc comment on register_pending_insert
explaining that next_pending_id uses wrapping_add(1) and therefore can
theoretically wrap after 2^64 increments, which would cause key reuse/collision
of keys generated with PENDING_INSERT_KEY_PREFIX; mention this is practically
unreachable but intentional behavior and that callers should promote/remove
entries to avoid exhausting the counter if running an extremely
long-lived/high-throughput process. Reference register_pending_insert,
next_pending_id, and PENDING_INSERT_KEY_PREFIX in the comment.

In `@src/session/uow.rs`:
- Around line 258-274: The format! calls inside the transaction handling (the
ActiveModelError::DatabaseError constructions in the begin() map_err, commit()
map_err, and the rollback error formatting in the Err branch) use positional
interpolation; change them to named format string syntax (e.g., format!("begin
transaction: {err}", err = e)) for each occurrence (references: executor.begin()
map_err closure, tx.commit() map_err closure, and the rollback error formatting
where "flush failed: {e}; rollback failed: {rb_err}" is built) so they follow
the named-argument guideline.
- Around line 291-294: Update the error formatting in the closure that maps
pool.exclusive_primary_write_executor() to ActiveModelError::DatabaseError to
use named format arguments instead of the positional {e}; e.g., change
format!("exclusive primary executor: {e}") to format!("exclusive primary
executor: {err}", err = e). Apply the same named-argument change to the similar
error formatting occurrences around lines 306-309 (the other
ActiveModelError::DatabaseError constructions) and keep references to
exclusive_primary_write_executor and flush_dirty_with_begin_commit_sql
unchanged.
- Around line 230-246: The format! calls in the transaction handling block
should use named format arguments rather than positional ones; update the three
ActiveModelError::DatabaseError uses inside the begin/commit/rollback logic (the
map_err after executor.begin(), the map_err after tx.commit(), and the rollback
error formatting inside the Err(e) arm) to use named placeholders like {err} or
{rb_err} and pass those names in the format! call so the messages include the
same values but follow the named format string syntax; locate this in the
function that calls executor.begin(), creates tx (let tx = executor.begin()),
calls self.flush_dirty(...), and invokes tx.commit() or tx.rollback() to apply
the changes.
- Around line 335-353: Replace positional-format usage in the pooled transaction
helper by using named format parameters: change format!("begin transaction:
{e}") inside the map_err closure on executor.execute to a named format like
format!("begin transaction: {err}", err = e); do the same for the commit map_err
(format!("commit: {e}" -> format!("commit: {err}", err = e))); and update the
rollback-error composition to use named names (format!("flush failed: {e};
rollback failed: {rb_err}" -> format!("flush failed: {err}; rollback failed:
{rb_err}", err = e, rb_err = rb_err)). Target the calls around executor.execute,
the map_err closures, and the Err branch that constructs
ActiveModelError::DatabaseError and the match over
session.flush_dirty_with_map_key.
🪄 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: 07c0d58e-f837-4161-a559-d4a4cec1d982

📥 Commits

Reviewing files that changed from the base of the PR and between 4b74c74 and c42d101.

📒 Files selected for processing (52)
  • .agent/memory-bank/activeContext.md
  • .agent/memory-bank/progress.md
  • DEVELOPMENT.md
  • README.md
  • docs/planning/DESIGN_FIND_RELATED_SCOPES.md
  • docs/planning/DESIGN_SCHEMA_INFERENCE_CLI_CODEGEN.md
  • docs/planning/DESIGN_SESSION_UOW.md
  • docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md
  • docs/planning/README.md
  • docs/planning/lifeguard-derive/SEAORM_LIFEGUARD_MAPPING.md
  • lifeguard-derive/src/attributes.rs
  • lifeguard-derive/src/lib.rs
  • lifeguard-derive/src/macros/life_record.rs
  • lifeguard-derive/src/macros/migration_name_derive.rs
  • lifeguard-derive/src/macros/mod.rs
  • lifeguard-derive/src/macros/scope_attr.rs
  • lifeguard-derive/tests/test_derive_migration_name.rs
  • lifeguard-derive/tests/test_minimal.rs
  • lifeguard-migrate/README.md
  • lifeguard-migrate/src/generated_migration_diff.rs
  • lifeguard-migrate/src/lib.rs
  • lifeguard-migrate/src/main.rs
  • lifeguard-migrate/src/schema_infer.rs
  • lifeguard-migrate/src/schema_migration_compare.rs
  • lifeguard-migrate/tests/golden/infer_composite_pk.expected.rs
  • lifeguard-migrate/tests/infer_schema_cli_subprocess.rs
  • lifeguard-migrate/tests/infer_schema_postgres_smoke.rs
  • lifeguard-migrate/tests/infer_schema_table_filter_si3.rs
  • lifeguard-migrate/tests/migration_db_compare_smoke.rs
  • src/active_model/mod.rs
  • src/active_model/predicates.rs
  • src/active_model/traits.rs
  • src/active_model/validate_op.rs
  • src/active_model/validation.rs
  • src/lib.rs
  • src/migration/migration_name.rs
  • src/migration/mod.rs
  • src/pool/mod.rs
  • src/pool/pooled.rs
  • src/query/column/column_trait.rs
  • src/query/mod.rs
  • src/query/scope.rs
  • src/query/select.rs
  • src/relation/traits.rs
  • src/session/identity_model_cell.rs
  • src/session/mod.rs
  • src/session/uow.rs
  • tests/db_integration/column_f_update.rs
  • tests/db_integration/column_f_where.rs
  • tests/db_integration/pool_read_replica.rs
  • tests/db_integration/session_identity_flush.rs
  • tests/db_integration_suite.rs

Comment thread tests/db_integration/pool_read_replica.rs Outdated
…c fixes

- Add ARCHITECTURE.md with full diagrams; README summary + drill-down link
- README: competitive matrix and current status updates (prior session)
- Rustdoc: session/active_model/logging/predicates/query trait links and hygiene
- Memory bank: progress entries
Comment thread lifeguard-migrate/tests/migration_db_compare_smoke.rs
Relocate competitive matrix, roadmap, and repository-truth bullets from README.
Update ARCHITECTURE, PRD, SEAORM mapping, and memory bank cross-links.
Comment thread src/session/identity_model_cell.rs
Comment thread lifeguard-derive/src/macros/life_record.rs Outdated
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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.agent/memory-bank/progress.md (1)

193-200: ⚠️ Potential issue | 🟡 Minor

Stale metrics conflict with newer entries in this same file.

Lines 193-200 report outdated totals (for example test counts) that contradict newer completed entries above, so this section can no longer be treated as accurate status. Please either recompute these metrics or label them as historical snapshot only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.agent/memory-bank/progress.md around lines 193 - 200, Update the "Metrics"
section entries so they no longer conflict with newer entries: recompute and
correct the values for "Edge case coverage", "Test coverage", "Tests passing",
"Memory Bank files", "CRUD operations", "ActiveModel/Record Operations", and
"Query Builder Methods" to match the latest data elsewhere in the file, or else
prepend a clear "Historical snapshot" label to the entire Metrics block
indicating the values are outdated; ensure you update the specific lines
containing the bullet keys "Tests passing" and "CRUD operations: Fixed critical
bug in get() method" (and related bullets) so readers can unambiguously tell
whether those numbers are current or historical.
🧹 Nitpick comments (1)
docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md (1)

140-143: Schema reconciliation scope should explicitly include index-column validation follow-up.

This section documents table/column-name reconciliation only. Add a follow-on requirement/check for index definitions where every indexed column must map to an existing struct field (and migration generation validates CREATE INDEX succeeds), so schema parity catches index drift too.

Based on learnings: In lifeguard-derive and lifeguard-migrate code, consider implementing compile-time or migration-time validation to ensure every index column matches a struct field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md` around lines 140 -
143, Add index-column reconciliation: extend
lifeguard_migrate::schema_migration_compare (and the compare-schema CLI path) to
fetch index definitions (e.g., from information_schema/statistics or
pg_indexes), parse each index's column list and verify every indexed column
exists in the column_map_from_merged_baseline mapping and in the database
baseline; surface mismatches as failures/warnings and ensure migration
generation includes and validates CREATE INDEX operations (i.e., generated
migration should attempt to create the index and the test harness in
tests/migration_db_compare_smoke.rs should assert success/failure accordingly);
also add a follow-up validation hook in lifeguard-derive/migration-time checks
to ensure compile-time/migration-time mapping from struct fields to indexed
columns is enforced.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@COMPARISON.md`:
- Around line 137-140: Update COMPARISON.md to replace absolute language and
reflect precise compatibility: change the “You cannot mix and match” paragraph
to state factual compatibility statuses (e.g., “unsupported” vs “technically
incompatible”) and remove “must”/“fundamentally incompatible” wording; in the
comparison table split Diesel into “Diesel (sync/core)” and “Diesel-async”, mark
SeaORM as requiring Tokio runtime feature flags (e.g.,
runtime-tokio-native-tls), mark SQLx as unsupported/not recommended for
non-Tokio runtimes (may panic if no Tokio/async-std feature enabled) rather than
strictly incompatible, and mark Diesel (sync/core) as runtime-agnostic when used
via blocking calls while Diesel-async remains Tokio-focused—ensure you reference
BRRTRouter, Lifeguard, may, Tokio, async/await, SeaORM, SQLx, Diesel, and
Diesel-async in the updated text/table.

In `@docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md`:
- Around line 200-205: The documentation is inconsistent: the Types line lists
ValidateOp as `Insert | Update` while the Integration/Deletes bullets mention
`ValidateOp::Delete`; update the Types entry for `ValidateOp` to include
`Delete` (i.e., `Insert | Update | Delete`) and verify the Integration/“Delete”
bullet and V-3/other references (e.g., lifeguard-derive generated hooks,
ValidateOp, before_delete) remain consistent with that change so all bullets
reference the same set of operations.

---

Outside diff comments:
In @.agent/memory-bank/progress.md:
- Around line 193-200: Update the "Metrics" section entries so they no longer
conflict with newer entries: recompute and correct the values for "Edge case
coverage", "Test coverage", "Tests passing", "Memory Bank files", "CRUD
operations", "ActiveModel/Record Operations", and "Query Builder Methods" to
match the latest data elsewhere in the file, or else prepend a clear "Historical
snapshot" label to the entire Metrics block indicating the values are outdated;
ensure you update the specific lines containing the bullet keys "Tests passing"
and "CRUD operations: Fixed critical bug in get() method" (and related bullets)
so readers can unambiguously tell whether those numbers are current or
historical.

---

Nitpick comments:
In `@docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md`:
- Around line 140-143: Add index-column reconciliation: extend
lifeguard_migrate::schema_migration_compare (and the compare-schema CLI path) to
fetch index definitions (e.g., from information_schema/statistics or
pg_indexes), parse each index's column list and verify every indexed column
exists in the column_map_from_merged_baseline mapping and in the database
baseline; surface mismatches as failures/warnings and ensure migration
generation includes and validates CREATE INDEX operations (i.e., generated
migration should attempt to create the index and the test harness in
tests/migration_db_compare_smoke.rs should assert success/failure accordingly);
also add a follow-up validation hook in lifeguard-derive/migration-time checks
to ensure compile-time/migration-time mapping from struct fields to indexed
columns is enforced.
🪄 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: 5cf416b3-15c8-490c-bb19-f2b486e92902

📥 Commits

Reviewing files that changed from the base of the PR and between 0388b1e and d5994d1.

📒 Files selected for processing (12)
  • .agent/memory-bank/activeContext.md
  • .agent/memory-bank/progress.md
  • ARCHITECTURE.md
  • COMPARISON.md
  • OBSERVABILITY.md
  • README.md
  • ROADMAP.md
  • STATUS.md
  • VISION.md
  • docs/OBSERVABILITY.md
  • docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md
  • docs/planning/lifeguard-derive/SEAORM_LIFEGUARD_MAPPING.md
✅ Files skipped from review due to trivial changes (8)
  • docs/OBSERVABILITY.md
  • ROADMAP.md
  • OBSERVABILITY.md
  • STATUS.md
  • ARCHITECTURE.md
  • VISION.md
  • README.md
  • docs/planning/lifeguard-derive/SEAORM_LIFEGUARD_MAPPING.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • .agent/memory-bank/activeContext.md

Comment thread COMPARISON.md Outdated
Comment thread docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md Outdated
@casibbald casibbald force-pushed the feat/schema_validators_session_and_scopes_2 branch from d5994d1 to 7f9d826 Compare March 31, 2026 12:10
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 5 total unresolved issues (including 4 from previous reviews).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment thread lifeguard-derive/src/macros/life_record.rs
…ttribute

- Derive collects all #[validate(custom)] field errors when ValidationStrategy::Aggregate applies; add struct #[validation_strategy = "aggregate"|"fail_fast"] and register on LifeModel/LifeRecord derives.
- Tests for multi-field FailFast vs Aggregate; shared validators in test_minimal.
- Docs: STATUS merged into COMPARISON; README/ROADMAP/ARCHITECTURE/DEVELOPMENT/VISION updates; SECURITY_PROMPT; PRD validator section.
- Session identity cell docs, pool metrics, predicates, migrate CLI/tests, pool_read_replica isolation; memory bank update.
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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
COMPARISON.md (1)

65-65: ⚠️ Potential issue | 🟡 Minor

Keep runtime-compatibility labels consistent across the doc.

Line 65 still marks Diesel and SQLx as “Incompatible,” but Lines 148-159 already describe a more precise split (runtime-bound async crates vs runtime-agnostic sync Diesel). Align this row with the nuanced wording to avoid contradictory guidance.

📝 Suggested wording update
-| **Coroutine Runtime** | ✅✅✅✅ **Native support (UNIQUE)** | ✅ **Implemented** | ❌ Incompatible | ❌ Incompatible | ❌ Incompatible |
+| **Coroutine Runtime** | ✅✅✅✅ **Native support (UNIQUE)** | ✅ **Implemented** | ⚠️ Async runtime-bound (`runtime-*` features; not `may`-native) | ⚠️ Sync Diesel is runtime-agnostic (blocking API) | ⚠️ Async runtime-bound (Tokio/async-std features; not `may`-native) |
According to official docs, what are the runtime requirements/support statements for SeaORM, SQLx, Diesel (sync/core), and diesel-async (Tokio/async-std/runtime-agnostic distinctions)?

Also applies to: 148-159

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@COMPARISON.md` at line 65, Update the runtime-compatibility labels in the
COMPARISON.md table row that currently shows Diesel and SQLx as “Incompatible”
so they match the more nuanced descriptions later in the doc (lines describing
runtime-bound async crates vs sync Diesel and diesel-async). Specifically,
change the labels for SeaORM/SQLx/Diesel/diesel-async in the row under the
"Coroutine Runtime" column to reflect: SeaORM and SQLx as runtime-bound async
(needs an async runtime), Diesel (sync/core) as runtime-agnostic (works without
an async runtime), and diesel-async as runtime-specific (requires
Tokio/async-std or runtime adapter) so the table aligns with the detailed text
in the later section.
🧹 Nitpick comments (2)
src/session/mod.rs (1)

129-147: Clarify potential Rc orphaning on PK collision.

When promote_pending_to_loaded is called, if the model's PK already exists in the map (registered via another path), register_loaded returns the existing Rc, not a new one. Callers holding the original Rc from register_pending_insert would then have an orphaned reference not in the map.

This matches the documented "first load wins" semantics (line 70), but consider adding a note in the doc comment that the returned Rc may differ from the one originally returned by register_pending_insert in case of PK collision.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/session/mod.rs` around lines 129 - 147, Add a short doc-note to
promote_pending_to_loaded explaining that because register_loaded implements
"first load wins" semantics, if the model's primary key already exists in the
identity map the Rc returned by promote_pending_to_loaded may differ from the Rc
originally returned by register_pending_insert (callers could therefore hold an
orphaned Rc); reference the functions promote_pending_to_loaded,
register_loaded, and register_pending_insert and mention the
is_pending_insert_key/map collision scenario so callers know to compare identity
map entries instead of relying on the original Rc.
lifeguard-derive/src/macros/life_record.rs (1)

991-996: Silent replace_with failure may mask borrow conflicts.

When cell.replace_with(m) returns Err(BorrowMutError) (concurrent borrow), the error is silently discarded with let _ =. While documenting the single-threaded contract makes this "shouldn't happen" in correct usage, a debug-mode warning or debug_assert! could help catch contract violations during development.

🔧 Optional: Log or assert on unexpected borrow error
                     if let Ok(m) = self.to_model() {
-                        let _ = cell.replace_with(m);
+                        if let Err(_e) = cell.replace_with(m) {
+                            #[cfg(debug_assertions)]
+                            eprintln!("lifeguard: SessionIdentityModelCell replace_with failed (concurrent borrow?)");
+                        }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lifeguard-derive/src/macros/life_record.rs` around lines 991 - 996, The
current silent discard of replace_with errors hides BorrowMutError borrow
conflicts on self.__lg_session_model; update the block that calls
self.to_model() and cell.replace_with(m) to detect Err(e) and surface it in
debug builds (e.g., debug_assert!(false, ... ) or debug_warn/log) so development
catches unexpected concurrent-borrow violations: after calling self.to_model()
in the branch that checks self.__lg_session_model, match the Result from
cell.replace_with(m) and on Err(BorrowMutError) emit a debug_assert or a
debug-level log including the error and context (mention Self::to_model and
__lg_session_model) rather than silently ignoring it.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lifeguard-migrate/tests/migration_db_compare_smoke.rs`:
- Around line 120-129: The test currently forces UTF-8 with
dir.path().to_str().expect(...) when building the Command in the 'out' creation,
which can panic on non-UTF8 paths; replace that coerced string with the
Path/OsStr directly by removing the to_str().expect(...) and passing dir.path()
via .arg(dir.path()) (or using .arg(dir.path().as_os_str())) instead of
including it in the &str array passed to .args([...]) so the Command receives a
Path/OsStr and won't panic on non-UTF8 temp paths.

---

Duplicate comments:
In `@COMPARISON.md`:
- Line 65: Update the runtime-compatibility labels in the COMPARISON.md table
row that currently shows Diesel and SQLx as “Incompatible” so they match the
more nuanced descriptions later in the doc (lines describing runtime-bound async
crates vs sync Diesel and diesel-async). Specifically, change the labels for
SeaORM/SQLx/Diesel/diesel-async in the row under the "Coroutine Runtime" column
to reflect: SeaORM and SQLx as runtime-bound async (needs an async runtime),
Diesel (sync/core) as runtime-agnostic (works without an async runtime), and
diesel-async as runtime-specific (requires Tokio/async-std or runtime adapter)
so the table aligns with the detailed text in the later section.

---

Nitpick comments:
In `@lifeguard-derive/src/macros/life_record.rs`:
- Around line 991-996: The current silent discard of replace_with errors hides
BorrowMutError borrow conflicts on self.__lg_session_model; update the block
that calls self.to_model() and cell.replace_with(m) to detect Err(e) and surface
it in debug builds (e.g., debug_assert!(false, ... ) or debug_warn/log) so
development catches unexpected concurrent-borrow violations: after calling
self.to_model() in the branch that checks self.__lg_session_model, match the
Result from cell.replace_with(m) and on Err(BorrowMutError) emit a debug_assert
or a debug-level log including the error and context (mention Self::to_model and
__lg_session_model) rather than silently ignoring it.

In `@src/session/mod.rs`:
- Around line 129-147: Add a short doc-note to promote_pending_to_loaded
explaining that because register_loaded implements "first load wins" semantics,
if the model's primary key already exists in the identity map the Rc returned by
promote_pending_to_loaded may differ from the Rc originally returned by
register_pending_insert (callers could therefore hold an orphaned Rc); reference
the functions promote_pending_to_loaded, register_loaded, and
register_pending_insert and mention the is_pending_insert_key/map collision
scenario so callers know to compare identity map entries instead of relying on
the original Rc.
🪄 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: fee69f06-2830-471b-bb08-6185fbbaf986

📥 Commits

Reviewing files that changed from the base of the PR and between 7f9d826 and 79951ff.

📒 Files selected for processing (23)
  • .agent/memory-bank/activeContext.md
  • .agent/memory-bank/progress.md
  • ARCHITECTURE.md
  • COMPARISON.md
  • DEVELOPMENT.md
  • README.md
  • ROADMAP.md
  • SECURITY_PROMPT.md
  • VISION.md
  • docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md
  • lifeguard-derive/src/attributes.rs
  • lifeguard-derive/src/lib.rs
  • lifeguard-derive/src/macros/life_record.rs
  • lifeguard-derive/tests/test_minimal.rs
  • lifeguard-migrate/src/main.rs
  • lifeguard-migrate/tests/infer_schema_cli_subprocess.rs
  • lifeguard-migrate/tests/migration_db_compare_smoke.rs
  • src/active_model/predicates.rs
  • src/pool/pooled.rs
  • src/session/identity_model_cell.rs
  • src/session/mod.rs
  • src/session/uow.rs
  • tests/db_integration/pool_read_replica.rs
✅ Files skipped from review due to trivial changes (7)
  • lifeguard-migrate/tests/infer_schema_cli_subprocess.rs
  • SECURITY_PROMPT.md
  • ROADMAP.md
  • ARCHITECTURE.md
  • VISION.md
  • DEVELOPMENT.md
  • lifeguard-derive/src/lib.rs
🚧 Files skipped from review as they are similar to previous changes (7)
  • lifeguard-derive/src/attributes.rs
  • lifeguard-migrate/src/main.rs
  • tests/db_integration/pool_read_replica.rs
  • lifeguard-derive/tests/test_minimal.rs
  • src/active_model/predicates.rs
  • src/pool/pooled.rs
  • docs/planning/PRD_SCHEMA_VALIDATORS_SESSION_AND_SCOPES.md

Comment on lines +120 to +129
let out = std::process::Command::new(&bin)
.args([
"compare-schema",
"--database-url",
&url,
"--schema",
&schema,
"--generated-dir",
dir.path().to_str().expect("utf8 temp path"),
])
Copy link
Copy Markdown

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

🏁 Script executed:

#!/bin/bash
# Verify there are no remaining UTF-8 coercions for process args in this test file.
rg -n 'to_str\(\)\.expect\("utf8 temp path"\)|--generated-dir' --type=rust

Repository: microscaler/lifeguard

Length of output: 269


🏁 Script executed:

# Read the actual file to verify lines 120-129
sed -n '115,135p' lifeguard-migrate/tests/migration_db_compare_smoke.rs

Repository: microscaler/lifeguard

Length of output: 654


🏁 Script executed:

# Search for other instances of similar UTF-8 coercions in the file
rg -n 'to_str\(\)\.expect' lifeguard-migrate/tests/migration_db_compare_smoke.rs

Repository: microscaler/lifeguard

Length of output: 126


Remove unnecessary UTF-8 coercion for temp path argument.

Line 128 uses dir.path().to_str().expect("utf8 temp path"), which can panic on paths containing non-UTF8 sequences. Since Command::arg() accepts &Path directly via AsRef<OsStr>, pass the path without coercion:

Proposed fix
     let out = std::process::Command::new(&bin)
-        .args([
-            "compare-schema",
-            "--database-url",
-            &url,
-            "--schema",
-            &schema,
-            "--generated-dir",
-            dir.path().to_str().expect("utf8 temp path"),
-        ])
+        .arg("compare-schema")
+        .arg("--database-url")
+        .arg(&url)
+        .arg("--schema")
+        .arg(&schema)
+        .arg("--generated-dir")
+        .arg(dir.path())
         .output()
         .expect("spawn compare-schema");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lifeguard-migrate/tests/migration_db_compare_smoke.rs` around lines 120 -
129, The test currently forces UTF-8 with dir.path().to_str().expect(...) when
building the Command in the 'out' creation, which can panic on non-UTF8 paths;
replace that coerced string with the Path/OsStr directly by removing the
to_str().expect(...) and passing dir.path() via .arg(dir.path()) (or using
.arg(dir.path().as_os_str())) instead of including it in the &str array passed
to .args([...]) so the Command receives a Path/OsStr and won't panic on non-UTF8
temp paths.

@casibbald casibbald merged commit b31a811 into main Mar 31, 2026
9 checks passed
@casibbald casibbald deleted the feat/schema_validators_session_and_scopes_2 branch March 31, 2026 14:47
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