Skip to main content

arminius_lib_db_sync/
syncv4.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Queries related to sync v4 (Simplified Sliding Sync).
6
7use std::collections::HashMap;
8
9use arminius_lib_db::{Database, DatabaseError, ResultExt as _, TransactedDb};
10use arminius_lib_events::{RoomEvent, Verified};
11use arminius_lib_model::{event::EventVerifiedRow, syncv4::StreamUpperBound, types::StreamId};
12use itertools::Itertools as _;
13use ruma::{
14	DeviceId, OwnedRoomId, RoomId, UserId,
15	events::{AnyStrippedStateEvent, StateEventType},
16	serde::Raw,
17};
18
19/// List of "bump events".
20///
21/// These events are used to compute the `bump_stamp` of rooms that are part of
22/// sync responses (see <https://github.com/matrix-org/matrix-spec-proposals/blob/main/proposals/4186-simplified-sliding-sync.md#common-fields>).
23///
24/// For each room, we fetch the stream ID of the latest event of such type
25/// (falls back to `0` if none), then we order the user's room using this stream
26/// ID, and finally we convert that stream ID to just an index in the ordered
27/// list.
28///
29/// ## Example
30///
31/// The user will get these 3 rooms as part of a sync request:
32///
33/// `room1(stream_id=4)`, `room2(stream_id=1)` and `room3(stream_id=8)`
34///
35/// The ordered list is therefore:
36///
37/// `[room2, room1, room3]`
38///
39/// And the answer will contain this array of rooms:
40///
41/// `[room2(bump_stamp=0), room1(bump_stamp=1), room3(bump_stamp=2)]`
42pub const BUMP_EVENTS_TYPES: [&str; 7] = [
43	"m.room.create",
44	"m.room.message",
45	"m.room.encrypted",
46	"m.sticker",
47	"m.call.invite",
48	"m.poll.start",
49	"m.beacon_info",
50];
51
52/// The various streams of our server.
53#[derive(Debug, Clone, sqlx::Type, PartialEq, Eq, Hash)]
54#[sqlx(type_name = "STREAM_NAME", rename_all = "snake_case")]
55pub enum DbStreamName {
56	/// Refers to the `eventids` sequence
57	Events,
58}
59
60/// Represents whether the stream got fully synced or up to a specific point.
61#[derive(Debug, Clone, sqlx::Type, PartialEq, Eq, Hash)]
62#[sqlx(type_name = "STREAM_STATUS", rename_all = "UPPERCASE")]
63pub enum DbStreamStatus {
64	/// The stream was fully up-to-date with the last request's token.
65	Live,
66	/// The stream got synced up to a certain `up_to` token.
67	Previously,
68}
69
70/// An existing connection in the database.
71///
72/// Connections are linked to `(user_id, device_id, conn_id)` triplets.
73#[derive(Debug, Clone)]
74pub struct DbSyncV4Connection {
75	// FIXME(#2147): Add `last_used_at` to this struct, and update it.
76}
77
78/// Struct used to fetch the server's current set of stream IDs.
79///
80/// These are all the maximum allotted stream IDs, per each stream.
81#[derive(Debug)]
82pub struct DbMaxStreamIds {
83	/// Maximum stream ID for the `events.stream_id`.
84	pub max_event_stream_id: StreamId,
85	/// Maximum stream ID for the `to_device_messages.stream_id`.
86	pub max_todevice_stream_id: StreamId,
87	/// Maximum stream ID for both `room_account_data.stream_id` and
88	/// `global_account_data.stream_id`.
89	pub max_account_data_stream_id: StreamId,
90	/// Maximum stream ID for the `user_keys.stream_id`.
91	pub max_user_keys_stream_id: StreamId,
92	/// Maximum stream ID for the `read_receipts.stream_id`.
93	pub max_read_receipts_stream_id: StreamId,
94	/// Maximum stream ID for the `federated_invites.stream_id`.
95	pub max_federated_invites_stream_id: StreamId,
96	/// Maximum stream ID for the `federated_knocks.stream_id`.
97	pub max_federated_knocks_stream_id: StreamId,
98}
99
100/// Information about a room, from a user's perspective.
101#[derive(Debug, Clone)]
102pub struct RoomInfo {
103	/// Whether the user ever joined the room or not
104	pub was_ever_joined: bool,
105	/// Stream ID of the most recent event of the room.
106	pub max_event_stream_id: StreamId,
107	/// Stream ID of the most recent bump event of the room. See
108	/// [`BUMP_EVENTS_TYPES`].
109	pub bump_event_stream_id: StreamId,
110}
111
112/// Struct used when fetching the timeline of synced room.
113// TODO: Add required states and fetch them at once ?
114#[derive(Debug, Default)]
115pub struct SyncedRoomTimeline {
116	/// The actual events of the timeline
117	pub timeline: Vec<RoomEvent<Verified>>,
118	/// Boolean indicating whether the room's `timeline_limit` was hit or not
119	/// (i.e. if there were actually more events in there).
120	pub limited: bool,
121	/// Number of events that occurred after the given `from`.
122	pub num_live: usize,
123}
124
125/// Contains the state of a room.
126#[derive(Debug)]
127pub struct RoomState {
128	/// The events stream status applying to the last time it was synced.
129	// TODO: When we add more than just the 'events' stream, this should become a
130	// `HashMap<DbStreamName, StreamStatus>` containing all of these streams statuses
131	pub events_stream_upper_bound: StreamUpperBound,
132}
133
134/// All the syncv4 related queries
135// Queries are usually used as extension traits so suggest `import as _`.
136#[rust_analyzer::prefer_underscore_import]
137pub trait TransactedSyncV4Queries: Database {
138	/// Inserts a new sync v4 connection given its `(user ID, device ID, connection ID)` triplet.
139	#[tracing::instrument(skip(self))]
140	async fn insert_syncv4_connection(
141		&mut self,
142		user_id: &UserId,
143		device_id: &DeviceId,
144		conn_id: Option<&str>,
145	) -> Result<(), DatabaseError> {
146		sqlx::query!(
147			r#"
148			INSERT INTO syncv4_connections (device_nid, conn_id)
149				VALUES ((
150						SELECT
151							devices.nid
152						FROM
153							devices
154							JOIN users ON devices.user_nid = users.nid
155						WHERE
156							users.user_id = $1
157							AND devices.device_id = $2),
158						$3)
159			"#,
160			user_id.to_string(),
161			device_id.to_string(),
162			conn_id
163		)
164		.execute(self.connection().await?)
165		.await?;
166
167		Ok(())
168	}
169
170	/// Deletes all the data associated to a connection.
171	#[tracing::instrument(skip(self))]
172	async fn reset_connection(
173		&mut self,
174		user_id: &UserId,
175		device_id: &DeviceId,
176		conn_id: Option<&str>,
177	) -> Result<(), DatabaseError> {
178		sqlx::query!(
179			r#"
180			DELETE FROM syncv4_positions USING syncv4_connections, devices, users
181			WHERE syncv4_positions.connection_nid = syncv4_connections.nid
182				AND syncv4_connections.device_nid = devices.nid
183				AND devices.user_nid = users.nid
184				AND users.user_id = $1
185				AND devices.device_id = $2
186				AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
187			"#,
188			user_id.as_str(),
189			device_id.as_str(),
190			conn_id
191		)
192		.execute(self.connection().await?)
193		.await?;
194
195		Ok(())
196	}
197
198	/// Tries to fetch a user's connection given its `(user ID, device ID,
199	/// connection ID)` triplet.
200	#[tracing::instrument(skip(self))]
201	async fn fetch_syncv4_connection(
202		&mut self,
203		user_id: &UserId,
204		device_id: &DeviceId,
205		conn_id: Option<&str>,
206	) -> Result<Option<DbSyncV4Connection>, DatabaseError> {
207		// FIXME(#2147): Update `last_used_at` of the connection if found.
208		Ok(sqlx::query_scalar!(
209			r#"
210			SELECT EXISTS (
211				SELECT 1
212				FROM syncv4_connections
213					JOIN devices ON syncv4_connections.device_nid = devices.nid
214					JOIN users ON devices.user_nid = users.nid
215				WHERE
216					users.user_id = $1
217					AND devices.device_id = $2
218					AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
219			) AS "exists!"
220			"#,
221			user_id.to_string(),
222			device_id.to_string(),
223			conn_id,
224		)
225		.fetch_one(self.connection().await?)
226		.await?
227		.then_some(DbSyncV4Connection {}))
228	}
229
230	/// Fetches a position that belongs to a specific connection.
231	///
232	/// Errors if the position was not found.
233	#[tracing::instrument(skip(self))]
234	async fn fetch_syncv4_position(
235		&mut self,
236		user_id: &UserId,
237		device_id: &DeviceId,
238		conn_id: Option<&str>,
239		position: i64,
240	) -> Result<(), DatabaseError> {
241		sqlx::query!(
242			r#"
243			SELECT
244				1 AS "exists"
245			FROM
246				syncv4_positions
247				JOIN syncv4_connections ON syncv4_positions.connection_nid = syncv4_connections.nid
248				JOIN devices ON syncv4_connections.device_nid = devices.nid
249				JOIN users ON devices.user_nid = users.nid
250			WHERE
251				users.user_id = $1
252				AND devices.device_id = $2
253				AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
254				AND syncv4_positions.position = $4
255			"#,
256			user_id.to_string(),
257			device_id.to_string(),
258			conn_id,
259			position
260		)
261		.fetch_one(self.connection().await?)
262		.await?;
263
264		Ok(())
265	}
266
267	/// Inserts a new position in the database, after having deleted any sibling
268	/// of its previous position, as well as the parent of that previous
269	/// position.
270	#[tracing::instrument(skip(self))]
271	async fn new_syncv4_position(
272		&mut self,
273		user_id: &UserId,
274		device_id: &DeviceId,
275		conn_id: Option<&str>,
276		prev_position: Option<i64>,
277	) -> Result<StreamId, DatabaseError> {
278		// Delete the sibling(s) and parent of the previous connection if any
279		sqlx::query!(
280			"
281   			DELETE FROM syncv4_positions USING syncv4_connections, users, devices
282   			WHERE syncv4_connections.nid = syncv4_positions.connection_nid
283						AND syncv4_connections.device_nid = devices.nid
284						AND devices.user_nid = users.nid
285						AND users.user_id = $1
286						AND devices.device_id = $2
287   			    AND conn_id IS NOT DISTINCT FROM $3
288   			    AND prev_position IS DISTINCT FROM $4
289   			    AND position IS DISTINCT FROM $4
290   			",
291			user_id.to_string(),
292			device_id.to_string(),
293			conn_id,
294			prev_position
295		)
296		.execute(self.connection().await?)
297		.await?;
298
299		let new_position = sqlx::query_scalar!(
300			r#"
301			INSERT INTO syncv4_positions (connection_nid, prev_position)
302			VALUES (
303				(
304					SELECT syncv4_connections.nid
305					FROM syncv4_connections
306						JOIN devices ON syncv4_connections.device_nid = devices.nid
307						JOIN users ON devices.user_nid = users.nid
308					WHERE
309						users.user_id = $1
310						AND devices.device_id = $2
311						AND syncv4_connections.conn_id IS NOT DISTINCT FROM $3
312				),
313				$4
314			)
315			RETURNING position
316			"#,
317			user_id.to_string(),
318			device_id.to_string(),
319			conn_id,
320			prev_position
321		)
322		.fetch_one(self.connection().await?)
323		.await?;
324
325		Ok(new_position)
326	}
327
328	/// Fetches all the maximum stream IDs of the various streams of Arminius.
329	///
330	/// This is used to compute the `next-pos` of sliding sync.
331	#[tracing::instrument(skip(self))]
332	async fn fetch_max_stream_ids(&mut self) -> Result<DbMaxStreamIds, DatabaseError> {
333		let row = sqlx::query!(
334			r#"
335			SELECT
336				(SELECT MAX(events.stream_id) FROM events) AS max_events_stream_id,
337				(SELECT MAX(to_device_messages.stream_id) FROM to_device_messages) AS max_todevice_stream_id,
338				(SELECT GREATEST(
339					(SELECT MAX(global_account_data.stream_id) FROM global_account_data),
340					(SELECT MAX(room_account_data.stream_id) FROM room_account_data)
341				)) AS max_account_data_stream_id,
342				(SELECT MAX(user_keys.stream_id) FROM user_keys) AS max_user_keys_stream_id,
343				(SELECT MAX(read_receipts.stream_id) FROM read_receipts) AS max_read_receipts_stream_id,
344				(SELECT MAX(federated_invites.stream_id) FROM federated_invites) AS max_federated_invites_stream_id,
345				(SELECT MAX(federated_knocks.stream_id) FROM federated_knocks) AS max_federated_knocks_stream_id
346			"#
347		)
348		.fetch_one(self.connection().await?)
349		.await?;
350
351		Ok(DbMaxStreamIds {
352			max_event_stream_id: row.max_events_stream_id.unwrap_or(0),
353			max_todevice_stream_id: row.max_todevice_stream_id.unwrap_or(0),
354			max_account_data_stream_id: row.max_account_data_stream_id.unwrap_or(0),
355			max_user_keys_stream_id: row.max_user_keys_stream_id.unwrap_or(0),
356			max_read_receipts_stream_id: row.max_read_receipts_stream_id.unwrap_or(0),
357			max_federated_invites_stream_id: row.max_federated_invites_stream_id.unwrap_or(0),
358			max_federated_knocks_stream_id: row.max_federated_knocks_stream_id.unwrap_or(0),
359		})
360	}
361
362	/// Fetches all the [`RoomInfo`] for all the rooms the user has a membership
363	/// event in.
364	#[tracing::instrument(skip(self))]
365	async fn fetch_user_room_info(
366		&mut self,
367		user_id: &UserId,
368	) -> Result<HashMap<OwnedRoomId, RoomInfo>, DatabaseError> {
369		// TODO: For rooms the user is not joined to, `bump_stream_id` should be that of the
370		// relevant membership event (e.g. when the user left the room).
371		sqlx::query!(
372			r#"
373			SELECT
374				room_membership_state.room_id AS "room_id!",
375				(room_membership_state.membership = 'join'
376					OR bool_or((events.event_json ->> 'type') = 'm.room.member'
377					AND (events.event_json ->> 'state_key') = $1
378					AND (events.event_json #>> '{content,membership}' = 'join'))) AS "has_ever_joined!",
379				MAX(events.stream_id) AS "max_event_stream_id!",
380				MAX(bump_events.stream_id) AS "bump_stream_id"
381			FROM
382				room_membership_state ($1)
383				JOIN events ON room_membership_state.room_nid = events.room_nid
384				-- PERF: This might need a pre-populated table to be less costly to fetch.
385				LEFT JOIN events bump_events ON room_membership_state.room_nid = bump_events.room_nid
386					AND (bump_events.event_json ->> 'type' = ANY ($2))
387			GROUP BY
388				room_membership_state.room_id,
389				room_membership_state.membership
390			"#,
391			user_id.to_string(),
392			&BUMP_EVENTS_TYPES as _
393		)
394		.fetch_all(self.connection().await?)
395		.await?
396		.into_iter()
397		.map(|row| {
398			let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
399
400			Ok((
401				room_id,
402				RoomInfo {
403					was_ever_joined: row.has_ever_joined,
404					max_event_stream_id: row.max_event_stream_id,
405					bump_event_stream_id: row.bump_stream_id.unwrap_or(0),
406				},
407			))
408		})
409		.collect::<Result<_, DatabaseError>>()
410	}
411
412	/// Fetches rooms timelines for the given user.
413	///
414	/// Parameters:
415	/// - `user_id`: The user for whom to fetch the visible timeline events.
416	/// - `rooms`: A map from room ID to `(start, limit)` where `start` is the stream ID to start
417	///   fetching events from, and `limit` is the maximum number of events to fetch for that room.
418	/// - `last_sync_stream_id`: The last sync's highest events stream ID. This is used to compute
419	///   `num_live` for each room.
420	/// - `to_stream_id`: The maximum stream ID to not go past by when fetching events.
421	///
422	/// Returns a mapping from room IDs to [`SyncedRoomTimeline`].
423	// TODO: Also fetch required states here !
424	#[tracing::instrument(skip(self))]
425	async fn fetch_syncv4_joined_rooms_timelines(
426		&mut self,
427		user_id: &UserId,
428		rooms: &HashMap<OwnedRoomId, (StreamId, i64)>,
429		last_sync_stream_id: StreamId,
430		to_stream_id: StreamId,
431	) -> Result<HashMap<OwnedRoomId, SyncedRoomTimeline>, DatabaseError> {
432		let (rooms_ids, rooms_from, rooms_limits): (Vec<String>, Vec<i64>, Vec<i64>) = rooms
433			.iter()
434			// We add 1 to the timeline limits here so that we can tell if we reached them or
435			// not
436			.map(|(room_id, (from_stream_id, timeline_limit))| {
437				(room_id.to_string(), from_stream_id, timeline_limit + 1)
438			})
439			.multiunzip();
440
441		let mut room_timelines = sqlx::query!(
442			r#"
443			SELECT
444				events.room_id AS "room_id!",
445				events.room_limit AS "room_limit!",
446				events.stream_id AS "stream_id!",
447				events.event_json::text AS "event_json!: Box<sqlx::types::JsonRawValue>",
448				events.room_version,
449				events.attempted_at,
450				events.signature_verification,
451				events.soft_failed
452			FROM
453				UNNEST($2::text[], $3::bigint[], $4::bigint[]) AS room_params (room_id,
454					room_from,
455					room_limit)
456				CROSS JOIN LATERAL (
457					SELECT
458						rooms.room_id,
459						room_params.room_limit,
460						events.stream_id,
461						events.event_json,
462						rooms.room_version,
463						verification_metadata.attempted_at,
464						verification_metadata.signature_verification,
465						verification_metadata.soft_failed
466					FROM
467						visible_timeline_events ($1, room_params.room_id) events
468						JOIN verification_metadata ON verification_metadata.event_nid = events.nid
469						JOIN rooms ON rooms.room_id = room_params.room_id
470					WHERE
471						events.stream_id > room_params.room_from
472						AND events.stream_id <= max_safe_seq ('eventids')
473						AND events.stream_id <= $5
474					ORDER BY
475						events.stream_id DESC
476					LIMIT room_params.room_limit) events
477			ORDER BY
478				events.stream_id DESC
479			"#,
480			user_id.to_string(),
481			&rooms_ids,
482			&rooms_from,
483			&rooms_limits,
484			to_stream_id
485		)
486		.fetch_all(self.connection().await?)
487		.await?
488		.into_iter()
489		.try_fold(
490			HashMap::new(),
491			|mut result: HashMap<OwnedRoomId, SyncedRoomTimeline>, row| {
492				let room_id =
493					RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
494
495				let room_timeline = result.entry(room_id).or_default();
496
497				// If we've hit the limit, ignore the event and set `limited` to
498				// true
499				if room_timeline.timeline.len() == (row.room_limit - 1) as usize {
500					room_timeline.limited = true;
501					return Ok(result);
502				}
503
504				room_timeline.timeline.push(
505					EventVerifiedRow {
506						event_json: row.event_json,
507						room_version: row.room_version,
508						attempted_at: row.attempted_at,
509						signature_verification: row.signature_verification,
510						soft_failed: row.soft_failed,
511					}
512					.try_into_event()?,
513				);
514
515				if row.stream_id > last_sync_stream_id {
516					room_timeline.num_live += 1;
517				}
518
519				Ok::<_, DatabaseError>(result)
520			},
521		)?;
522
523		// Reverse every timelines, as their last events is expected to be the
524		// most recent ones
525		for timeline in room_timelines.values_mut() {
526			timeline.timeline.reverse();
527		}
528
529		Ok(room_timelines)
530	}
531
532	/// Fetches the counts of 'join' and 'invite' users for a single room.
533	#[tracing::instrument(skip(self))]
534	async fn fetch_room_membership_counts(
535		&mut self,
536		room_id: &RoomId,
537	) -> Result<(u64, u64), DatabaseError> {
538		let counts = sqlx::query!(
539			r#"
540			SELECT
541				COUNT(*) FILTER (WHERE events.event_json #>> '{content,membership}' = 'join') AS "joined_count!",
542				COUNT(*) FILTER (WHERE events.event_json #>> '{content,membership}' = 'invite') AS "invited_count!"
543			FROM
544				rooms
545				JOIN room_state_snapshots ON room_state_snapshots.room_nid = rooms.nid
546				JOIN state_snapshot_state_events ON state_snapshot_state_events.state_snapshot_nid = room_state_snapshots.state_snapshot_nid
547				JOIN events ON events.nid = state_snapshot_state_events.state_event_nid
548			WHERE
549				events.event_json ->> 'type' = 'm.room.member'
550				AND rooms.room_id = $1
551				AND events.event_json #>> '{content, membership}' IN ('join', 'invite')
552			"#,
553			room_id.to_string()
554		).fetch_one(self.connection().await?).await?;
555
556		Ok((counts.joined_count as u64, counts.invited_count as u64))
557	}
558
559	/// Fetches the list of stripped state events consisting the state of all
560	/// the given `rooms`.
561	#[tracing::instrument(skip(self))]
562	async fn fetch_rooms_stripped_state(
563		&mut self,
564		rooms: &[OwnedRoomId],
565	) -> Result<HashMap<OwnedRoomId, Vec<Raw<AnyStrippedStateEvent>>>, DatabaseError> {
566		sqlx::query!(
567			r#"
568			SELECT
569				rooms.room_id,
570				rooms.room_version,
571				events.event_json::text AS "event_json!: Box<sqlx::types::JsonRawValue>",
572				verification_metadata.attempted_at,
573				verification_metadata.signature_verification,
574				verification_metadata.soft_failed
575			FROM
576				room_state_snapshots
577				JOIN state_snapshot_state_events ON state_snapshot_state_events.state_snapshot_nid = room_state_snapshots.state_snapshot_nid
578				JOIN events ON state_snapshot_state_events.state_event_nid = events.nid
579				JOIN verification_metadata ON verification_metadata.event_nid = events.nid
580				JOIN rooms ON events.room_nid = rooms.nid
581			WHERE
582				rooms.room_id = ANY ($1::text[])
583			"#,
584			&rooms.into_iter().map(ToString::to_string).collect::<Vec<_>>()
585		).fetch_all(self.connection().await?)
586			.await?
587			.into_iter()
588			.try_fold(HashMap::new(), |mut result, row| -> Result<_, DatabaseError> {
589				let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
590
591				let verified_event = EventVerifiedRow {
592					event_json: row.event_json,
593					room_version: row.room_version,
594					attempted_at: row.attempted_at,
595					signature_verification: row.signature_verification,
596					soft_failed: row.soft_failed
597				}.try_into_event()?;
598
599
600				let stripped_state_event = verified_event.to_stripped_state_event().invalid_data("Non state event stored as one in the database")?;
601
602				result.entry(room_id)
603					.and_modify(|room_events: &mut Vec<_>| room_events.push(stripped_state_event.clone()))
604					.or_insert_with(|| vec![stripped_state_event]);
605
606				Ok(result)
607		})
608	}
609
610	/// Fetches the list of required state events for a room.
611	#[tracing::instrument(skip(self))]
612	async fn fetch_rooms_required_states(
613		&mut self,
614		room_id: &RoomId,
615		required_state_config: &Vec<(StateEventType, String)>,
616		from_stream_id: StreamId,
617		to_stream_id: StreamId,
618	) -> Result<Vec<RoomEvent<Verified>>, DatabaseError> {
619		let (event_types, state_keys): (Vec<String>, Vec<String>) = required_state_config
620			.iter()
621			.map(|(event_type, state_key)| (event_type.to_string(), state_key.to_owned()))
622			.unzip();
623
624		sqlx::query_as!(
625			EventVerifiedRow,
626			r#"
627			WITH wanted_events AS (
628				SELECT
629					*
630				FROM
631					unnest($2::text[], $3::text[]) AS t (event_type,
632						state_key))
633			SELECT DISTINCT ON (events.nid)
634				events.event_json AS "event_json: Box<sqlx::types::JsonRawValue>",
635				rooms.room_version,
636				verification_metadata.attempted_at,
637				verification_metadata.signature_verification,
638				verification_metadata.soft_failed
639			FROM
640				room_state_snapshots
641				JOIN state_snapshot_state_events s ON room_state_snapshots.state_snapshot_nid = s.state_snapshot_nid
642				JOIN events ON s.state_event_nid = events.nid
643				JOIN verification_metadata ON verification_metadata.event_nid = events.nid
644				JOIN rooms ON events.room_nid = rooms.nid
645				JOIN wanted_events ON (wanted_events.event_type = '*'
646						OR wanted_events.event_type = event_json ->> 'type')
647					AND (wanted_events.state_key = '*'
648						OR wanted_events.state_key = '$LAZY'
649						OR wanted_events.state_key = event_json ->> 'state_key')
650			WHERE
651				rooms.room_id = $1
652				AND events.stream_id > $4
653				AND events.stream_id <= max_safe_seq ('eventids')
654				AND events.stream_id <= $5
655			"#,
656				room_id.as_str(),
657				&event_types,
658				&state_keys,
659				from_stream_id,
660				to_stream_id
661			)
662		.fetch_all(self.connection().await?)
663		.await?
664		.into_iter()
665		.map(EventVerifiedRow::try_into_event)
666		.collect()
667	}
668
669	/// Fetches all of a user's stored room streams, but only for the 'events'
670	/// stream.
671	#[tracing::instrument(skip(self))]
672	async fn fetch_events_room_streams(
673		&mut self,
674		position: i64,
675	) -> Result<HashMap<OwnedRoomId, StreamUpperBound>, DatabaseError> {
676		sqlx::query!(
677			r#"
678			SELECT
679				room_id,
680				stream_status AS "stream_status:DbStreamStatus",
681				up_to
682			FROM
683				syncv4_room_streams
684				JOIN rooms ON syncv4_room_streams.room_nid = rooms.nid
685				JOIN syncv4_positions ON syncv4_room_streams.position = syncv4_positions.position
686			WHERE
687				syncv4_positions.position = $1
688			"#,
689			position
690		)
691		.fetch_all(self.connection().await?)
692		.await?
693		.into_iter()
694		.map(|row| {
695			let room_id = RoomId::parse(row.room_id).invalid_data("Invalid room ID in database")?;
696
697			let event_stream_status = if row.stream_status == DbStreamStatus::Live {
698				StreamUpperBound::Live
699			} else {
700				let up_to = row.up_to.ok_or(DatabaseError::InvalidData(
701					"Entry with status 'previously' without its `up_to` set".to_owned(),
702				))?;
703				StreamUpperBound::Previously(up_to)
704			};
705
706			Ok((room_id, event_stream_status))
707		})
708		.collect::<Result<_, DatabaseError>>()
709	}
710
711	/// Updates the given room's `syncv4_room_streams` entries where
712	/// `stream_name='events'` to become `PREVIOUSLY(up_to)`.
713	#[tracing::instrument(skip(self))]
714	async fn update_rooms_events_streams_to_previously(
715		&mut self,
716		position: i64,
717		rooms_to_update: Vec<OwnedRoomId>,
718		new_up_to: StreamId,
719	) -> Result<(), DatabaseError> {
720		let room_ids: Vec<String> =
721			rooms_to_update.into_iter().map(|room_id| room_id.to_string()).collect();
722
723		sqlx::query!(
724			r#"
725			UPDATE
726				syncv4_room_streams
727			SET
728				stream_status = $4,
729				up_to = $5
730			FROM
731				rooms
732			WHERE
733				syncv4_room_streams.position = $1
734				AND syncv4_room_streams.stream_name = $2
735				AND rooms.room_id = ANY ($3::text[])
736			"#,
737			position,
738			DbStreamName::Events as _,
739			&room_ids,
740			DbStreamStatus::Previously as _,
741			new_up_to
742		)
743		.execute(self.connection().await?)
744		.await?;
745
746		Ok(())
747	}
748
749	/// Insert all the room streams associated to a position.
750	#[tracing::instrument(skip(self))]
751	async fn insert_rooms_events_streams(
752		&mut self,
753		new_position: i64,
754		rooms_streams: HashMap<OwnedRoomId, StreamUpperBound>,
755	) -> Result<(), DatabaseError> {
756		let (room_ids, stream_statuses, up_tos): (
757			Vec<String>,
758			Vec<DbStreamStatus>,
759			Vec<Option<i64>>,
760		) = rooms_streams
761			.into_iter()
762			.map(|(room_id, event_stream_status)| {
763				let (stream_status, up_to) = match event_stream_status {
764					StreamUpperBound::Previously(up_to) => {
765						(DbStreamStatus::Previously, Some(up_to))
766					}
767					StreamUpperBound::Live => (DbStreamStatus::Live, None),
768				};
769
770				(room_id.to_string(), stream_status, up_to)
771			})
772			.multiunzip();
773
774		sqlx::query!(
775			r#"
776			INSERT INTO syncv4_room_streams (position, stream_name, room_nid, stream_status, up_to)
777			SELECT
778				$1,
779				$2,
780				rooms.nid,
781				params.stream_status,
782				params.up_to
783			FROM
784				UNNEST($3::text[], $4::STREAM_STATUS[], $5::bigint[]) AS params (room_id,
785					stream_status,
786					up_to)
787				JOIN rooms ON params.room_id = rooms.room_id
788			"#,
789			new_position,
790			DbStreamName::Events as _,
791			&room_ids,
792			stream_statuses as _,
793			up_tos as _
794		)
795		.execute(self.connection().await?)
796		.await?;
797
798		Ok(())
799	}
800}
801
802impl<'c> TransactedSyncV4Queries for TransactedDb<'c> {}