Skip to content

Major fixes 0814#1343

Merged
duckduckhero merged 5 commits intomainfrom
major-fixes-0814
Aug 14, 2025
Merged

Major fixes 0814#1343
duckduckhero merged 5 commits intomainfrom
major-fixes-0814

Conversation

@duckduckhero
Copy link
Contributor

@duckduckhero duckduckhero commented Aug 14, 2025

Summary by cubic

Fixed recurring event handling in Apple Calendar sync to transfer sessions from old non-recurring events and preserve data integrity. Improved is_recurring field support when saving events to the database.

  • Bug Fixes
  • Sessions linked to old non-recurring events now transfer correctly to new recurring events.
  • is_recurring is now set and updated consistently for events.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

📝 Walkthrough

Walkthrough

Adds is_recurring to Event and threads it through database update/upsert paths. Updates Apple Calendar sync to handle migration from non-recurring to recurring events, introducing session transfer logic and reordering sync steps: create/upsert, update, transfer sessions, then delete.

Changes

Cohort / File(s) Summary of changes
Event model and persistence
crates/db-user/src/events.rs, crates/db-user/src/events_ops.rs
Adds Event.is_recurring: bool; includes field in INSERT/UPSERT/UPDATE SQL and bindings; updates tests to initialize/assert is_recurring.
Apple Calendar sync logic
plugins/apple-calendar/src/sync.rs
Adds backward-compat path to replace prior non-recurring events with recurring ones; introduces session_transfers to relink sessions; reorders execute flow: upsert new, update existing, transfer sessions, delete old; uses composite tracking_id for recurring cases; minor refactors/formatting.

Sequence Diagram(s)

sequenceDiagram
  participant Calendar as Apple Calendar
  participant Sync as Sync Engine
  participant DB as Event DB
  participant Sessions as Session Store

  Calendar->>Sync: Fetch system events
  Sync->>Sync: Detect recurring vs non-recurring
  alt Recurring replacing old non-recurring
    Sync->>DB: Find matching non-recurring event (base_id/date/name)
    Sync->>DB: Upsert new event (new id, composite tracking_id, is_recurring=true)
    Sync->>Sessions: Record session transfer (old_session -> new_event)
  end
  Sync->>DB: Upsert new events
  Sync->>DB: Update existing events (incl. is_recurring)
  Sync->>Sessions: Relink sessions to new events
  Sync->>DB: Delete obsolete events
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch major-fixes-0814

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

cubic analysis

No issues found across 2 files. Review in cubic

Copy link
Contributor

@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: 0

🔭 Outside diff range comments (1)
plugins/apple-calendar/src/sync.rs (1)

255-271: Critical: Session transfer is skipped when the recurring instance already exists

The early continue on "already_exists" prevents the backward-compatibility session transfer from running. If the recurring instance is already in DB and the old non-recurring event has a session, you’ll delete the old event and orphan the session.

Move the backward-compatibility branch before the “already exists” check and, when the recurring instance already exists, re-link the session to that existing event instead of creating a duplicate.

Apply this diff:

-                // Skip if this event already exists in the database with the same tracking_id
-                let already_exists = db_events_with_session
-                    .iter()
-                    .any(|(db_event, _)| db_event.tracking_id == composite_tracking_id);
-                if already_exists {
-                    continue;
-                }
-
-                // Check for backward compatibility: recurring event replacing old non-recurring
-                if system_event.is_recurring {
-                    // Look for old format event with session
-                    if let Some((_, session)) =
-                        db_events_with_session.iter().find(|(db_event, session)| {
-                            db_event.tracking_id == system_event.id &&  // Old format used base ID only
-                        db_event.start_date == system_event.start_date &&
-                        db_event.name == system_event.name &&
-                        !db_event.is_recurring &&  // Was stored as non-recurring
-                        session.is_some() // Has a session
-                        })
-                    {
-                        // Store session ID for transfer after new event is created
-                        if let Some(session) = session {
-                            let new_event_id = uuid::Uuid::new_v4().to_string();
-                            state
-                                .session_transfers
-                                .push((session.id.clone(), new_event_id.clone()));
-
-                            let new_event = hypr_db_user::Event {
-                                id: new_event_id,
-                                tracking_id: composite_tracking_id,
-                                user_id: user_id.clone(),
-                                calendar_id: Some(db_calendar.id.clone()),
-                                name: system_event.name.clone(),
-                                note: system_event.note.clone(),
-                                start_date: system_event.start_date,
-                                end_date: system_event.end_date,
-                                google_event_url: None,
-                                participants: Some(
-                                    serde_json::to_string(&system_event.participants)
-                                        .unwrap_or_else(|_| "[]".to_string()),
-                                ),
-                                is_recurring: system_event.is_recurring,
-                            };
-
-                            state.to_upsert.push(new_event);
-                            continue;
-                        }
-                    }
-                }
+                // Backward-compatibility: if a recurring instance replaces a previously stored
+                // non-recurring event that had a session, transfer the session.
+                if system_event.is_recurring {
+                    // Locate the old-format event with a session
+                    if let Some((_, Some(session))) = db_events_with_session.iter().find(|(db_event, session)| {
+                        db_event.tracking_id == system_event.id &&            // old base ID
+                        db_event.name == system_event.name &&
+                        // allow small clock skew (<= 1 minute)
+                        (db_event.start_date - system_event.start_date).num_minutes().abs() <= 1 &&
+                        !db_event.is_recurring &&
+                        session.is_some()
+                    }) {
+                        // If the recurring instance already exists, just re-link the session to it.
+                        if let Some((existing_recurring, _)) = db_events_with_session
+                            .iter()
+                            .find(|(db_event, _)| db_event.tracking_id == composite_tracking_id)
+                        {
+                            state
+                                .session_transfers
+                                .push((session.id.clone(), existing_recurring.id.clone()));
+                            continue;
+                        }
+
+                        // Otherwise, create the recurring instance and re-link the session to it.
+                        let new_event_id = uuid::Uuid::new_v4().to_string();
+                        state
+                            .session_transfers
+                            .push((session.id.clone(), new_event_id.clone()));
+
+                        let new_event = hypr_db_user::Event {
+                            id: new_event_id,
+                            tracking_id: composite_tracking_id,
+                            user_id: user_id.clone(),
+                            calendar_id: Some(db_calendar.id.clone()),
+                            name: system_event.name.clone(),
+                            note: system_event.note.clone(),
+                            start_date: system_event.start_date,
+                            end_date: system_event.end_date,
+                            google_event_url: None,
+                            participants: Some(
+                                serde_json::to_string(&system_event.participants)
+                                    .unwrap_or_else(|_| "[]".to_string()),
+                            ),
+                            is_recurring: system_event.is_recurring,
+                        };
+                        state.to_upsert.push(new_event);
+                        continue;
+                    }
+                }
+
+                // Skip if this event already exists in the database with the same tracking_id
+                if db_events_with_session
+                    .iter()
+                    .any(|(db_event, _)| db_event.tracking_id == composite_tracking_id)
+                {
+                    continue;
+                }

Also applies to: 272-313

🧹 Nitpick comments (4)
crates/db-user/src/events_ops.rs (1)

250-251: Add assertion for is_recurring persistence in tests

You initialize is_recurring but don’t assert it. Add a check to ensure persistence is wired correctly.

You can extend the test like:

// after upsert
assert_eq!(event.is_recurring, false);
plugins/apple-calendar/src/sync.rs (3)

276-281: Relax exact start_date equality for robust matching

Exact equality on start_date is brittle due to timezone and rounding differences. A small tolerance (e.g., <= 1 minute) is safer for the BC matching condition.

The diff above already applies this relaxation:

-                        db_event.start_date == system_event.start_date &&
+                        (db_event.start_date - system_event.start_date).num_minutes().abs() <= 1 &&

245-253: Consider extracting composite tracking ID logic

This formatting appears in multiple places. A small helper reduces duplication and drift.

For example:

fn compose_tracking_id(id: &str, start: chrono::DateTime<chrono::Utc>, is_recurring: bool) -> String {
    if is_recurring {
        format!("{}__HYPR__{}", id, start.format("%Y%m%d%H%M%S"))
    } else {
        id.to_string()
    }
}

609-623: Tests don’t cover the BC session transfer path

Add a case where a non-recurring DB event with a session is replaced by a recurring system event instance; assert the session is re-linked and the old event ends up in to_delete.

I can draft a focused unit test harnessing in-memory state to simulate this scenario. Want me to add it?

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 94e7a5e and c8f13a6.

📒 Files selected for processing (2)
  • crates/db-user/src/events_ops.rs (6 hunks)
  • plugins/apple-calendar/src/sync.rs (4 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{js,ts,tsx,rs}

⚙️ CodeRabbit Configuration File

**/*.{js,ts,tsx,rs}: 1. No error handling.
2. No unused imports, variables, or functions.
3. For comments, keep it minimal. It should be about "Why", not "What".

Files:

  • crates/db-user/src/events_ops.rs
  • plugins/apple-calendar/src/sync.rs
🔇 Additional comments (6)
crates/db-user/src/events_ops.rs (2)

43-45: is_recurring column & migration verified — OK to propagate in UPDATE

Confirmed in repo: a migration adds the column and code already binds/uses it. No code changes required.

  • crates/db-user/src/events_migration_2.sql — ALTER TABLE events ADD COLUMN is_recurring BOOLEAN DEFAULT FALSE;
  • crates/db-user/src/events_migration.sql — original CREATE TABLE (does not include is_recurring; migration covers the addition)
  • crates/db-user/src/events_ops.rs — UPDATE/UPSERT use is_recurring = :is_recurring and pass ":is_recurring": event.is_recurring (both occurrences updated)
  • crates/db-user/src/events_types.rs — pub is_recurring: bool
  • plugins/db/js/bindings.gen.ts — Event type includes is_recurring: boolean

Recommendation: ensure the migration has been applied in each deployment environment (operational check).


89-91: No change required — tracking_id is globally UNIQUE, ON CONFLICT(tracking_id) is correct

Verified: crates/db-user/src/events_migration.sql declares tracking_id TEXT NOT NULL UNIQUE, so ON CONFLICT(tracking_id) matches the schema and will not collide across users.

Files checked:

  • crates/db-user/src/events_migration.sql — tracking_id TEXT NOT NULL UNIQUE
  • crates/db-user/src/events_ops.rs — ON CONFLICT(tracking_id) DO UPDATE ... (around line ~103)
  • crates/db-user/src/calendars_migration.sql — tracking_id TEXT NOT NULL UNIQUE
  • crates/db-user/src/calendars_ops.rs — ON CONFLICT(tracking_id) DO UPDATE ... (around line ~99)

Action: ignore the suggested diff to switch to ON CONFLICT(user_id, tracking_id) — no change required.

plugins/apple-calendar/src/sync.rs (4)

544-545: Session transfer state is a clean extension

Adding session_transfers to EventSyncState is appropriate and keeps execution concerns localized.


565-571: Reordering: upsert before update is sound

Creating new events before updates avoids foreign-key issues with session re-linking.


579-585: Session transfers sequenced after upserts

Good call to transfer sessions after ensuring the target event exists.


586-590: Delete last to preserve referential integrity

Deleting old events after transfers prevents dangling references.

@duckduckhero duckduckhero merged commit 8bc7a5e into main Aug 14, 2025
7 checks passed
@ComputelessComputer ComputelessComputer deleted the major-fixes-0814 branch December 14, 2025 15:22
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