fix(528): pass2 add recurrence_id field

This commit is contained in:
Edouard Vanbelle
2026-07-14 17:27:16 +02:00
parent 184b9dfab6
commit 02f67f7a5c
3 changed files with 318 additions and 51 deletions
@@ -30,10 +30,11 @@ impl CalendarEventRepository for CalendarEventPgRepository {
sqlx::query(
r#"
INSERT INTO caldav.calendar_events (
id, calendar_id, summary, description, location, start_time, end_time,
all_day, rrule, created_at, updated_at, ical_uid, ical_data
id, calendar_id, summary, description, location, start_time, end_time,
all_day, rrule, created_at, updated_at, ical_uid, ical_data,
recurrence_id
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
"#,
)
.bind(event.id())
@@ -49,6 +50,10 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.bind(event.updated_at())
.bind(event.ical_uid())
.bind(event.ical_data())
// NULL on masters, non-NULL on exception overrides — see the
// `20260913000001_calendar_events_recurrence_id.sql` migration
// and `docs/architecture/rebac-authorization.md` follow-up doc.
.bind(event.recurrence_id().copied())
.execute(&*self.pool)
.await
.map_err(|e| {
@@ -68,16 +73,17 @@ impl CalendarEventRepository for CalendarEventPgRepository {
sqlx::query(
r#"
UPDATE caldav.calendar_events
SET summary = $1,
description = $2,
location = $3,
start_time = $4,
end_time = $5,
all_day = $6,
SET summary = $1,
description = $2,
location = $3,
start_time = $4,
end_time = $5,
all_day = $6,
rrule = $7,
ical_data = $8,
updated_at = $9
WHERE id = $10
recurrence_id = $9,
updated_at = $10
WHERE id = $11
"#,
)
.bind(event.summary())
@@ -88,6 +94,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.bind(event.all_day())
.bind(event.rrule())
.bind(event.ical_data())
.bind(event.recurrence_id().copied())
.bind(now)
.bind(event.id())
.execute(&*self.pool)
@@ -126,12 +133,12 @@ impl CalendarEventRepository for CalendarEventPgRepository {
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let rows = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data, recurrence_id
FROM caldav.calendar_events
WHERE calendar_id = $1
WHERE calendar_id = $1
AND (
(start_time >= $2 AND start_time < $3) OR
(end_time > $2 AND end_time <= $3) OR
@@ -152,7 +159,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
let mut events = Vec::new();
for row in rows {
let event = CalendarEvent::with_id(
let mut event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
@@ -170,6 +177,11 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
// Rehydrate the RECURRENCE-ID after entity construction —
// `with_id` initialises to `None` because the field predates
// the rest of the constructor signature (#528). Keeping
// `with_id` unchanged avoids ripple-changing every caller.
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
events.push(event);
}
@@ -179,10 +191,10 @@ impl CalendarEventRepository for CalendarEventPgRepository {
async fn find_event_by_id(&self, id: &Uuid) -> CalendarEventRepositoryResult<CalendarEvent> {
let row = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data, recurrence_id
FROM caldav.calendar_events
WHERE id = $1
"#,
@@ -195,11 +207,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
})?
.ok_or_else(|| DomainError::not_found("Calendar Event", id.to_string()))?;
// In a real implementation, we would build a complete CalendarEvent object
// For simplicity, we create an object with default values to
// demonstrate the approach without macros
let event = CalendarEvent::with_id(
let mut event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
@@ -217,6 +225,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
Ok(event)
}
@@ -227,10 +236,10 @@ impl CalendarEventRepository for CalendarEventPgRepository {
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
let rows = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data, recurrence_id
FROM caldav.calendar_events
WHERE calendar_id = $1
ORDER BY start_time
@@ -245,7 +254,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
let mut events = Vec::new();
for row in rows {
let event = CalendarEvent::with_id(
let mut event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
@@ -263,6 +272,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
events.push(event);
}
@@ -278,10 +288,10 @@ impl CalendarEventRepository for CalendarEventPgRepository {
let rows = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data, recurrence_id
FROM caldav.calendar_events
WHERE calendar_id = $1 AND summary ILIKE $2
ORDER BY start_time
@@ -297,7 +307,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
let mut events = Vec::new();
for row in rows {
let event = CalendarEvent::with_id(
let mut event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
@@ -315,6 +325,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
events.push(event);
}
@@ -326,14 +337,21 @@ impl CalendarEventRepository for CalendarEventPgRepository {
calendar_id: &Uuid,
ical_uid: &str,
) -> CalendarEventRepositoryResult<Option<CalendarEvent>> {
// Phase 2 note: this method looks up "an event with this UID"
// — the SELECT still isn't filtered on `recurrence_id IS NULL`
// because the phase-3 handler routing (which will distinguish
// master vs. exception override at PUT time) is where the
// filter actually needs to live. For phase 2 the invariant is
// enforced only at INSERT time via the two partial unique
// indexes; reads see whatever's there.
let row_opt = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data, recurrence_id
FROM caldav.calendar_events
WHERE calendar_id = $1 AND ical_uid = $2
WHERE calendar_id = $1 AND ical_uid = $2 AND recurrence_id IS NULL
"#,
)
.bind(calendar_id)
@@ -346,7 +364,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
match row_opt {
Some(row) => {
let event = CalendarEvent::with_id(
let mut event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
@@ -364,6 +382,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
Ok(Some(event))
}
None => Ok(None),
@@ -375,12 +394,18 @@ impl CalendarEventRepository for CalendarEventPgRepository {
calendar_id: &Uuid,
ical_uids: &[String],
) -> CalendarEventRepositoryResult<Vec<CalendarEvent>> {
// Batch UID lookup returns ALL rows for the given UIDs, both
// masters and exception overrides. Callers that want just
// masters filter downstream. Same phase-2 policy as the
// single-UID variant — read-side filtering is a phase-3
// concern; the DB unique indexes are what guarantee at most
// one master + N distinct exceptions per (calendar, UID).
let rows = sqlx::query(
r#"
SELECT
id, calendar_id, summary, description, location,
start_time, end_time, all_day, rrule,
created_at, updated_at, ical_uid, ical_data
created_at, updated_at, ical_uid, ical_data, recurrence_id
FROM caldav.calendar_events
WHERE calendar_id = $1 AND ical_uid = ANY($2)
ORDER BY start_time
@@ -396,7 +421,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
let mut events = Vec::new();
for row in rows {
let event = CalendarEvent::with_id(
let mut event = CalendarEvent::with_id(
row.get("id"),
row.get("calendar_id"),
row.get("summary"),
@@ -414,6 +439,7 @@ impl CalendarEventRepository for CalendarEventPgRepository {
.map_err(|e| {
DomainError::database_error(format!("Error creating calendar event: {}", e))
})?;
event.set_recurrence_id(row.get::<Option<DateTime<Utc>>, _>("recurrence_id"));
events.push(event);
}