Skip to main content

arminius_lib_db_sync/
lib.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Queries related to syncing.
6
7pub mod syncv4;
8mod tests;
9
10use arminius_lib_db::{Database, DatabaseError, ResultExt as _};
11use arminius_lib_events::{RoomEvent, Verified};
12use arminius_lib_model::{event::EventVerifiedRow, types::StreamId};
13use ruma::{
14	EventId, OwnedEventId, OwnedRoomId, RoomId, UInt, UserId,
15	api::client::filter::{RoomEventFilter, RoomFilter},
16	events::room::member::MembershipState,
17};
18
19/// Maximum number (e.g. SQL `LIMIT`) of events retrieved by
20/// [`SyncQueries::fetch_sync_room_events`].
21pub const MAX_SYNCED_EVENTS: usize = 1000;
22
23/// [`MAX_SYNCED_EVENTS`], converted to `i64`.
24///
25/// There's no reasonable way to do this in Rust in compile-time.
26/// See <https://github.com/rust-lang/rust/issues/143874>
27/// But there's a test case that checks they're identical at least.
28const MAX_SYNCED_EVENTS_I64: i64 = 1000;
29
30/// Default number (e.g. SQL `LIMIT`) of events retrieved by
31/// [`SyncQueries::fetch_sync_room_events`].
32pub const DEFAULT_SYNCED_EVENTS: i64 = 10;
33
34/// Maximum number (e.g. SQL `LIMIT`) of messages retrieved by
35/// [`SyncQueries::fetch_room_messages_backwards`] and
36/// [`SyncQueries::fetch_room_messages_forwards`].
37pub const MAX_MESSAGES: usize = 1000;
38
39/// [`MAX_MESSAGES`], converted to `i64`.
40///
41/// There's no reasonable way to do this in Rust in compile-time.
42/// See <https://github.com/rust-lang/rust/issues/143874>
43/// But there's a test case that checks they're identical at least.
44const MAX_MESSAGES_I64: i64 = 1000;
45
46/// Structure used when querying events for joined rooms. Used for sync
47#[derive(Debug)]
48pub struct SyncEvent {
49	/// The stream_id of the event
50	pub stream_id: StreamId,
51	/// The event_id of the event
52	pub event_id: OwnedEventId,
53	/// The ID of the room associated with the event.
54	pub room_id: OwnedRoomId,
55	/// The users membership of the room, at the point the sync event was
56	/// fetched
57	pub membership: MembershipState,
58	/// The event JSON
59	pub event: RoomEvent<Verified>,
60}
61
62/// Queries related to sync
63// Queries are usually used as extension traits so suggest `import as _`.
64#[rust_analyzer::prefer_underscore_import]
65pub trait SyncQueries: Database {
66	/// Fetches the earliest membership event for a user in a room, since
67	/// `from_stream_id`
68	#[tracing::instrument(skip(self))]
69	async fn fetch_new_room_membership_event(
70		&mut self,
71		room_id: &RoomId,
72		user_id: &UserId,
73		from_stream_id: StreamId,
74	) -> Result<Option<RoomEvent<Verified>>, DatabaseError> {
75		sqlx::query_as!(
76			EventVerifiedRow,
77			r#"
78			SELECT
79				events.event_json AS "event_json: Box<sqlx::types::JsonRawValue>",
80				rooms.room_version,
81				verification.attempted_at,
82				verification.signature_verification,
83				verification.soft_failed
84			FROM
85				events
86				JOIN rooms ON events.room_nid = rooms.nid
87				JOIN verification_metadata verification ON verification.event_nid = events.nid
88			WHERE
89				rooms.room_id = $1
90				AND (events.event_json ->> 'type') = 'm.room.member'
91				AND (events.event_json ->> 'state_key') = $2
92				AND events.stream_id > $3
93				AND events.stream_id <= max_safe_seq ('eventids')
94			ORDER BY
95				stream_id ASC
96			LIMIT 1
97			"#,
98			room_id.as_str(),
99			user_id.as_str(),
100			from_stream_id
101		)
102		.fetch_optional(self.connection().await?)
103		.await?
104		.map(EventVerifiedRow::try_into_event)
105		.transpose()
106	}
107
108	/// Fetches all room events the user is able to see, starting at
109	/// `from_stream_id`.
110	#[tracing::instrument(skip(self))]
111	async fn fetch_sync_room_events(
112		&mut self,
113		user: &UserId,
114		from_stream_id: StreamId,
115		room_filters: Option<&RoomFilter>,
116	) -> Result<Vec<SyncEvent>, DatabaseError> {
117		let limit = room_filters
118			.and_then(|room_filter| room_filter.timeline.limit.map(i64::from))
119			.map_or(DEFAULT_SYNCED_EVENTS, |client_requested_limit| {
120				i64::min(MAX_SYNCED_EVENTS_I64, client_requested_limit)
121			});
122		let included_rooms = room_filters.and_then(|room_filter| {
123			room_filter.rooms.as_ref().map(|included_rooms| {
124				included_rooms.iter().map(OwnedRoomId::to_string).collect::<Vec<_>>()
125			})
126		});
127		let excluded_rooms = room_filters
128			.map(|room_filter| {
129				room_filter.not_rooms.iter().map(OwnedRoomId::to_string).collect::<Vec<_>>()
130			})
131			.unwrap_or_default();
132		let included_event_types =
133			room_filters.and_then(|room_filter| room_filter.timeline.types.clone());
134		let blocked_event_types = room_filters
135			.map(|room_filter| room_filter.timeline.not_types.clone())
136			.unwrap_or_default();
137
138		let included_senders = room_filters.and_then(|room_filter| {
139			room_filter
140				.clone()
141				.timeline
142				.senders
143				.map(|senders| senders.iter().map(ToString::to_string).collect::<Vec<_>>())
144		});
145		let excluded_senders = room_filters
146			.map(|room_filter| {
147				room_filter.timeline.not_senders.iter().map(ToString::to_string).collect::<Vec<_>>()
148			})
149			.unwrap_or_default();
150
151		sqlx::query_file!(
152			"src/sync.sql",
153			user.as_str(),
154			from_stream_id,
155			limit,
156			included_rooms.as_deref(),
157			&excluded_rooms,
158			included_event_types.as_deref(),
159			&blocked_event_types,
160			included_senders.as_deref(),
161			&excluded_senders
162		)
163		.fetch_all(self.connection().await?)
164		.await?
165		.into_iter()
166		.map(|row| -> Result<SyncEvent, DatabaseError> {
167			let event_row = EventVerifiedRow {
168				event_json: row.event_json,
169				room_version: row.room_version,
170				attempted_at: row.attempted_at,
171				signature_verification: row.signature_verification,
172				soft_failed: row.soft_failed,
173			};
174
175			let event = event_row.try_into_event()?;
176
177			Ok(SyncEvent {
178				stream_id: row.stream_id,
179				event_id: row.event_id.parse().invalid_data("Invalid event ID in database")?,
180				room_id: row.room_id.parse().invalid_data("Invalid room ID in database")?,
181				membership: MembershipState::from(row.membership),
182				event,
183			})
184		})
185		.collect::<Result<Vec<_>, _>>()
186	}
187
188	/// Fetch messages forwards starting from the given event ID. If no event ID
189	/// is given, the starting point is the earliest event visible to the given
190	/// user.
191	///
192	/// The returned boolean is `true` if there are more than `limit` messages.
193	#[tracing::instrument(skip(self))]
194	async fn fetch_room_messages_forwards(
195		&mut self,
196		user: &UserId,
197		room: &RoomId,
198		from_event_id: Option<&EventId>,
199		limit: UInt,
200		room_filters: &RoomEventFilter,
201	) -> Result<(Vec<RoomEvent<Verified>>, bool), DatabaseError> {
202		// We try to get n+1  messages to know if there is a need to paginate.
203		let limit = i64::min(i64::from(limit), MAX_MESSAGES_I64) + 1;
204		let included_senders = room_filters
205			.clone()
206			.senders
207			.map(|senders| senders.iter().map(ToString::to_string).collect::<Vec<_>>());
208		let excluded_senders =
209			room_filters.not_senders.iter().map(ToString::to_string).collect::<Vec<_>>();
210
211		// NOTE: This query is hand-formatted because the formatter chokes on
212		// the comments
213		let mut events = sqlx::query_as!(
214			EventVerifiedRow,
215			"
216			SELECT
217				event_json AS \"event_json!: Box<sqlx::types::JsonRawValue>\",
218				rooms.room_version,
219				verification.attempted_at,
220				verification.signature_verification,
221				verification.soft_failed
222			FROM
223				visible_timeline_events ($2, $1) events
224				JOIN event_ordering ON events.nid = event_ordering.event_nid
225				JOIN rooms ON rooms.nid = events.room_nid
226				JOIN verification_metadata verification ON verification.event_nid = events.nid
227			WHERE
228				-- The ordering value for the event should be greater than the one of from_event_id
229				-- if given
230				($3::text IS NULL OR ordering > (
231					SELECT
232						ordering
233					FROM
234						event_ordering
235						JOIN events ON event_ordering.event_nid = events.nid
236					WHERE
237						events.event_id = $3::text))
238				-- The type should match the allowlist in the room filter if given
239				AND ($5::text[] IS NULL
240					OR events.event_json ->> 'type' = ANY ($5::text[]))
241				-- The type should not be in the denylist from the room filter
242				AND NOT (events.event_json ->> 'type' = ANY ($6::text[]))
243				-- The sender should match the allowlist given in the room filter
244				AND ($7::text[] IS NULL
245					OR events.event_json ->> 'sender' = ANY ($7::text[]))
246				-- The sender should not be in the denylist given in the room filter
247				AND NOT (events.event_json ->> 'sender' = ANY ($8::text[]))
248				AND events.stream_id <= max_safe_seq ('eventids')
249			ORDER BY
250				ordering ASC
251			LIMIT $4
252			",
253			room.as_str(),
254			user.as_str(),
255			from_event_id.map(EventId::as_str),
256			limit,
257			room_filters.types.as_deref(),
258			&room_filters.not_types,
259			included_senders.as_deref(),
260			&excluded_senders
261		)
262		.fetch_all(self.connection().await?)
263		.await?
264		.into_iter()
265		.map(EventVerifiedRow::try_into_event)
266		.collect::<Result<Vec<_>, _>>()?;
267
268		let there_are_more = events.len() == limit as usize;
269		if there_are_more {
270			events.pop();
271		}
272
273		Ok((events, there_are_more))
274	}
275
276	/// Fetch messages backwards starting from the given event ID. If no event
277	/// ID is given, the starting point is the last event visible to the given
278	/// user.
279	///
280	/// The returned boolean is `true` if there are more than `limit` messages.
281	#[tracing::instrument(skip(self))]
282	async fn fetch_room_messages_backwards(
283		&mut self,
284		user: &UserId,
285		room: &RoomId,
286		from_event_id: Option<&EventId>,
287		limit: UInt,
288		room_filters: &RoomEventFilter,
289	) -> Result<(Vec<RoomEvent<Verified>>, bool), DatabaseError> {
290		// We try to get n+1  messages to know if there is a need to paginate.
291		let limit = i64::min(i64::from(limit), MAX_MESSAGES_I64) + 1;
292		let included_senders = room_filters
293			.clone()
294			.senders
295			.map(|senders| senders.iter().map(ToString::to_string).collect::<Vec<_>>());
296		let excluded_senders =
297			room_filters.not_senders.iter().map(ToString::to_string).collect::<Vec<_>>();
298
299		// NOTE: This query is hand-formatted because the formatter chokes on
300		// the comments
301		let mut events = sqlx::query_as!(
302			EventVerifiedRow,
303			"
304			SELECT
305			    event_json AS \"event_json!: Box<sqlx::types::JsonRawValue>\",
306			    rooms.room_version,
307			    verification.attempted_at,
308			    verification.signature_verification,
309			    verification.soft_failed
310			FROM
311				visible_timeline_events ($2, $1) events
312				JOIN event_ordering ON events.nid = event_ordering.event_nid
313				JOIN rooms ON rooms.nid = events.room_nid
314				JOIN verification_metadata verification ON verification.event_nid = events.nid
315			WHERE
316			    -- The ordering value for the event should be less than the one of from_event_id
317			    -- if given
318				($3::text IS NULL OR ordering < (
319					SELECT
320						ordering
321					FROM
322						event_ordering
323						JOIN events ON event_ordering.event_nid = events.nid
324					WHERE
325						events.event_id = $3::text))
326				-- The type should match the allowlist in the room filter if given
327				AND ($5::text[] IS NULL
328					OR events.event_json ->> 'type' = ANY ($5::text[]))
329				-- The type should not be in the denylist from the room filter
330				AND NOT (events.event_json ->> 'type' = ANY ($6::text[]))
331				-- The sender should match the allowlist given in the room filter
332				AND ($7::text[] IS NULL
333					OR events.event_json ->> 'sender' = ANY ($7::text[]))
334				-- The sender should not be in the denylist given in the room filter
335				AND NOT (events.event_json ->> 'sender' = ANY ($8::text[]))
336				AND events.stream_id <= max_safe_seq ('eventids')
337			ORDER BY
338				ordering DESC
339			LIMIT $4
340			",
341			room.as_str(),
342			user.as_str(),
343			from_event_id.map(EventId::as_str),
344			limit,
345			room_filters.types.as_deref(),
346			&room_filters.not_types,
347			included_senders.as_deref(),
348			&excluded_senders
349		)
350		.fetch_all(self.connection().await?)
351		.await?
352		.into_iter()
353		.map(EventVerifiedRow::try_into_event)
354		.collect::<Result<Vec<_>, _>>()?;
355
356		let there_are_more = events.len() == limit as usize;
357		if there_are_more {
358			events.pop();
359		}
360
361		Ok((events, there_are_more))
362	}
363
364	/// Get all state changes between two points in the timeline
365	#[tracing::instrument(skip(self))]
366	async fn fetch_incremental_room_state_for_user(
367		&mut self,
368		room_id: &RoomId,
369		from_event_id: StreamId,
370		to_stream_id: StreamId,
371	) -> Result<Vec<RoomEvent<Verified>>, DatabaseError> {
372		// FIXME(#370): Implement event visibility rules (passing an additional
373		// `user_id`).
374		sqlx::query_as!(
375			EventVerifiedRow,
376			r#"
377			SELECT DISTINCT ON (state_events.nid)
378				state_events.event_json AS "event_json!: Box<sqlx::types::JsonRawValue>",
379				rooms.room_version,
380				verification.attempted_at,
381				verification.signature_verification,
382				verification.soft_failed
383			FROM
384				events
385				JOIN rooms ON rooms.nid = events.room_nid
386				CROSS JOIN LATERAL (
387					SELECT
388						event_state.nid,
389						event_state.event_id,
390						event_state.depth,
391						event_state.event_json::text,
392						event_state.received_ts
393					FROM
394						state_before (events.nid) event_state
395					WHERE
396						event_state.stream_id >= $2) AS state_events
397				JOIN verification_metadata verification ON verification.event_nid = state_events.nid
398			WHERE
399				events.room_nid = (
400					SELECT
401						nid
402					FROM
403						rooms
404					WHERE
405						room_id = $1)
406				AND events.stream_id >= $2
407				AND events.stream_id <= $3
408			ORDER BY
409				state_events.nid
410			"#,
411			room_id.to_string(),
412			from_event_id,
413			to_stream_id,
414		)
415		.fetch_all(self.connection().await?)
416		.await?
417		.into_iter()
418		.map(EventVerifiedRow::try_into_event)
419		.collect()
420	}
421}
422
423/// Make the query available on types implementing the
424/// Database trait.
425impl<T> SyncQueries for T where T: Database {}