eventcore_postgres/projections/
source.rs1use std::num::NonZeroU64;
2
3use eventcore_types::{
4 BatchSize, DeliveryPosition, DeliverySourceId, DeliveryUpperBound, EventTypeName,
5 PersistedEventEnvelope, PersistedEventId, ProjectionSelection, ProjectionSource,
6 ProjectionStreamFilter, StreamId, StreamVersion,
7};
8use serde_json::value::RawValue;
9use sqlx::{Pool, Postgres, QueryBuilder, Row, postgres::PgPoolOptions, query_scalar};
10use thiserror::Error;
11use uuid::Uuid;
12
13use super::migration::run_component_migration;
14use crate::PostgresConfig;
15
16const DELIVERY_SOURCE_COMPONENT: &str = "delivery-source";
17const DELIVERY_SOURCE_VERSION: i64 = 1;
18const DELIVERY_SOURCE_MIGRATION: &str =
19 include_str!("../../projection-migrations/0001_delivery_source.sql");
20
21#[derive(Debug, Error)]
23#[non_exhaustive]
24pub enum PostgresProjectionSourceError {
25 #[error("failed to connect projection source")]
27 ConnectionFailed(#[source] sqlx::Error),
28 #[error("failed to migrate projection source")]
30 MigrationFailed(#[source] sqlx::Error),
31 #[error("failed to read projection delivery source")]
33 ReadFailed(#[source] sqlx::Error),
34 #[error("invalid persisted {field}: {detail}")]
36 InvalidPersistedValue {
37 field: &'static str,
39 detail: String,
41 },
42}
43
44#[derive(Debug, Clone)]
46pub struct PostgresProjectionSource {
47 pool: Pool<Postgres>,
48 source_id: DeliverySourceId,
49}
50
51impl PostgresProjectionSource {
52 pub async fn new<S: Into<String>>(
54 connection_string: S,
55 source_id: DeliverySourceId,
56 ) -> Result<Self, PostgresProjectionSourceError> {
57 Self::with_config(connection_string, PostgresConfig::default(), source_id).await
58 }
59
60 pub async fn with_config<S: Into<String>>(
62 connection_string: S,
63 config: PostgresConfig,
64 source_id: DeliverySourceId,
65 ) -> Result<Self, PostgresProjectionSourceError> {
66 let max_connections: std::num::NonZeroU32 = config.max_connections.into();
67 let pool = PgPoolOptions::new()
68 .max_connections(max_connections.get())
69 .acquire_timeout(config.acquire_timeout)
70 .idle_timeout(config.idle_timeout)
71 .connect(&connection_string.into())
72 .await
73 .map_err(PostgresProjectionSourceError::ConnectionFailed)?;
74 Ok(Self::from_pool(pool, source_id))
75 }
76
77 pub fn from_pool(pool: Pool<Postgres>, source_id: DeliverySourceId) -> Self {
79 Self { pool, source_id }
80 }
81
82 pub async fn migrate(&self) -> Result<(), PostgresProjectionSourceError> {
84 run_component_migration(
85 &self.pool,
86 DELIVERY_SOURCE_COMPONENT,
87 DELIVERY_SOURCE_VERSION,
88 DELIVERY_SOURCE_MIGRATION,
89 )
90 .await
91 .map_err(PostgresProjectionSourceError::MigrationFailed)
92 }
93}
94
95impl ProjectionSource for PostgresProjectionSource {
96 type Error = PostgresProjectionSourceError;
97
98 fn source_id(&self) -> &DeliverySourceId {
99 &self.source_id
100 }
101
102 async fn high_watermark(&self) -> Result<Option<DeliveryPosition>, Self::Error> {
103 let watermark: Option<i64> = query_scalar(
104 "SELECT NULLIF(last_position, 0) FROM eventcore_projection_delivery_frontier WHERE singleton = TRUE",
105 )
106 .fetch_one(&self.pool)
107 .await
108 .map_err(PostgresProjectionSourceError::ReadFailed)?;
109
110 watermark.map(delivery_position).transpose()
111 }
112
113 async fn read_envelopes(
114 &self,
115 selection: &ProjectionSelection,
116 after: Option<DeliveryPosition>,
117 through: DeliveryUpperBound,
118 limit: BatchSize,
119 ) -> Result<Vec<PersistedEventEnvelope>, Self::Error> {
120 let mut query = QueryBuilder::<Postgres>::new(
121 "SELECT delivery.delivery_position, events.event_id, events.stream_id, \
122 events.stream_version, events.event_type, events.event_data::TEXT AS event_data, \
123 events.metadata::TEXT AS metadata \
124 FROM eventcore_projection_delivery AS delivery \
125 INNER JOIN eventcore_events AS events ON events.event_id = delivery.event_id \
126 WHERE TRUE",
127 );
128
129 if let Some(after) = after {
130 let _ = query
131 .push(" AND delivery.delivery_position > ")
132 .push_bind(i64_from_position(after)?);
133 }
134 if let DeliveryUpperBound::Inclusive(through) = through {
135 let _ = query
136 .push(" AND delivery.delivery_position <= ")
137 .push_bind(i64_from_position(through)?);
138 }
139
140 match selection.stream_filter() {
141 ProjectionStreamFilter::All => {}
142 ProjectionStreamFilter::Prefix(prefix) => {
143 let _ = query
144 .push(" AND LEFT(events.stream_id, char_length(")
145 .push_bind(prefix.as_ref().to_string())
146 .push(")) = ")
147 .push_bind(prefix.as_ref().to_string());
148 }
149 ProjectionStreamFilter::Pattern(pattern) => {
150 let _ = query
151 .push(" AND events.stream_id ~ ")
152 .push_bind(glob_to_anchored_regex(pattern.as_ref()));
153 }
154 }
155
156 let _ = query.push(" AND events.event_type IN (");
157 let mut separated = query.separated(", ");
158 for event_type in selection.event_types() {
159 let _ = separated.push_bind(event_type.as_ref());
160 }
161 let _ = separated.push_unseparated(")");
162 let _ = query
163 .push(" ORDER BY delivery.delivery_position ASC LIMIT ")
164 .push_bind(i64::try_from(usize::from(limit)).map_err(|error| {
165 PostgresProjectionSourceError::InvalidPersistedValue {
166 field: "batch limit",
167 detail: error.to_string(),
168 }
169 })?);
170
171 let rows = query
172 .build()
173 .fetch_all(&self.pool)
174 .await
175 .map_err(PostgresProjectionSourceError::ReadFailed)?;
176
177 rows.into_iter()
178 .map(|row| envelope_from_row(row, self.source_id.clone()))
179 .collect()
180 }
181}
182
183fn envelope_from_row(
184 row: sqlx::postgres::PgRow,
185 source_id: DeliverySourceId,
186) -> Result<PersistedEventEnvelope, PostgresProjectionSourceError> {
187 let position: i64 = row
188 .try_get("delivery_position")
189 .map_err(PostgresProjectionSourceError::ReadFailed)?;
190 let event_id: Uuid = row
191 .try_get("event_id")
192 .map_err(PostgresProjectionSourceError::ReadFailed)?;
193 let stream_id: String = row
194 .try_get("stream_id")
195 .map_err(PostgresProjectionSourceError::ReadFailed)?;
196 let stream_version: i64 = row
197 .try_get("stream_version")
198 .map_err(PostgresProjectionSourceError::ReadFailed)?;
199 let event_type: String = row
200 .try_get("event_type")
201 .map_err(PostgresProjectionSourceError::ReadFailed)?;
202 let payload: String = row
203 .try_get("event_data")
204 .map_err(PostgresProjectionSourceError::ReadFailed)?;
205 let metadata: String = row
206 .try_get("metadata")
207 .map_err(PostgresProjectionSourceError::ReadFailed)?;
208
209 let stream_id = StreamId::try_new(stream_id).map_err(|error| {
210 PostgresProjectionSourceError::InvalidPersistedValue {
211 field: "stream_id",
212 detail: error.to_string(),
213 }
214 })?;
215 let stream_version = usize::try_from(stream_version).map_err(|error| {
216 PostgresProjectionSourceError::InvalidPersistedValue {
217 field: "stream_version",
218 detail: error.to_string(),
219 }
220 })?;
221 let event_type = EventTypeName::try_new(event_type).map_err(|error| {
222 PostgresProjectionSourceError::InvalidPersistedValue {
223 field: "event_type",
224 detail: error.to_string(),
225 }
226 })?;
227 let payload = RawValue::from_string(payload).map_err(|error| {
228 PostgresProjectionSourceError::InvalidPersistedValue {
229 field: "event_data",
230 detail: error.to_string(),
231 }
232 })?;
233 let metadata = RawValue::from_string(metadata).map_err(|error| {
234 PostgresProjectionSourceError::InvalidPersistedValue {
235 field: "metadata",
236 detail: error.to_string(),
237 }
238 })?;
239 Ok(PersistedEventEnvelope::new(
240 source_id,
241 delivery_position(position)?,
242 PersistedEventId::new(event_id),
243 stream_id,
244 StreamVersion::new(stream_version),
245 event_type,
246 payload,
247 metadata,
248 ))
249}
250
251fn delivery_position(value: i64) -> Result<DeliveryPosition, PostgresProjectionSourceError> {
252 let value = u64::try_from(value).map_err(|error| {
253 PostgresProjectionSourceError::InvalidPersistedValue {
254 field: "delivery_position",
255 detail: error.to_string(),
256 }
257 })?;
258 let value = NonZeroU64::new(value).ok_or_else(|| {
259 PostgresProjectionSourceError::InvalidPersistedValue {
260 field: "delivery_position",
261 detail: "must be positive".to_string(),
262 }
263 })?;
264 Ok(DeliveryPosition::new(value))
265}
266
267fn i64_from_position(value: DeliveryPosition) -> Result<i64, PostgresProjectionSourceError> {
268 i64::try_from(value.get()).map_err(|error| {
269 PostgresProjectionSourceError::InvalidPersistedValue {
270 field: "delivery_position",
271 detail: error.to_string(),
272 }
273 })
274}
275
276fn glob_to_anchored_regex(glob: &str) -> String {
277 let mut regex = String::with_capacity(glob.len() + 2);
278 regex.push('^');
279
280 let mut chars = glob.chars().peekable();
281 while let Some(character) = chars.next() {
282 match character {
283 '*' => regex.push_str(".*"),
284 '?' => regex.push('.'),
285 '[' => {
286 let mut class = String::new();
287 let mut closed = false;
288 let negated = matches!(chars.peek(), Some('!'));
289 if negated {
290 let _ = chars.next();
291 class.push('^');
292 }
293 for class_character in chars.by_ref() {
294 if class_character == ']' {
295 closed = true;
296 break;
297 }
298 class.push(class_character);
299 }
300 if closed {
301 regex.push('[');
302 for (index, class_character) in class.chars().enumerate() {
303 if !negated && index == 0 && class_character == '^' {
304 regex.push_str("\\^");
305 } else if class_character == '\\' {
306 regex.push_str("\\\\");
307 } else {
308 regex.push(class_character);
309 }
310 }
311 regex.push(']');
312 } else {
313 regex.push_str("\\[");
314 regex.push_str(&class);
315 }
316 }
317 '\\' | '^' | '$' | '.' | '+' | '(' | ')' | '|' | '{' | '}' => {
318 regex.push('\\');
319 regex.push(character);
320 }
321 other => regex.push(other),
322 }
323 }
324
325 regex.push('$');
326 regex
327}