1use std::collections::HashMap;
2use std::fmt;
3use std::num::NonZeroU32;
4use std::sync::Arc;
5
6use chrono::{DateTime, Utc};
7use rusqlite::types::Value;
8
9use eventuary_core::partition::{PartitionHasher, PartitionKeyResolver, PartitionStrategy};
10use eventuary_core::{Error, Result, SerializedEvent, SerializedPayload};
11
12use crate::database::SqliteConn;
13use crate::event_log::{SqliteEventLogSchema, SqliteEventLogSchemaConfig};
14use crate::relation::SqliteRelationName;
15
16pub struct SqlitePartitionBackfillConfig {
17 pub events_relation: SqliteRelationName,
18 pub partition_count: NonZeroU32,
19 pub key_resolver: Arc<dyn PartitionKeyResolver>,
20 pub hasher: Arc<dyn PartitionHasher>,
21 pub batch_size: usize,
22}
23
24impl SqlitePartitionBackfillConfig {
25 pub fn new(
26 events_relation: SqliteRelationName,
27 partition_count: NonZeroU32,
28 key_resolver: impl PartitionKeyResolver + 'static,
29 hasher: impl PartitionHasher + 'static,
30 batch_size: usize,
31 ) -> Result<Self> {
32 if batch_size == 0 {
33 return Err(Error::Config(
34 "partition backfill batch size must be greater than zero".to_owned(),
35 ));
36 }
37
38 Ok(Self {
39 events_relation,
40 partition_count,
41 key_resolver: Arc::new(key_resolver),
42 hasher: Arc::new(hasher),
43 batch_size,
44 })
45 }
46}
47
48impl fmt::Debug for SqlitePartitionBackfillConfig {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 f.debug_struct("SqlitePartitionBackfillConfig")
51 .field("events_relation", &self.events_relation)
52 .field("partition_count", &self.partition_count)
53 .field("batch_size", &self.batch_size)
54 .finish()
55 }
56}
57
58#[derive(Debug, Clone, Default)]
59pub struct BackfillReport {
60 pub rows_updated: u64,
61 pub batches: u32,
62}
63
64pub struct SqlitePartitionBackfill {
65 conn: SqliteConn,
66 config: SqlitePartitionBackfillConfig,
67}
68
69impl SqlitePartitionBackfill {
70 pub fn new(conn: SqliteConn, config: SqlitePartitionBackfillConfig) -> Self {
71 Self { conn, config }
72 }
73
74 pub fn connect(conn: SqliteConn, config: SqlitePartitionBackfillConfig) -> Result<Self> {
75 Self::prepare_schema(&conn, &config)?;
76 Ok(Self::new(conn, config))
77 }
78
79 pub fn prepare_schema(conn: &SqliteConn, config: &SqlitePartitionBackfillConfig) -> Result<()> {
80 let guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
81 SqliteEventLogSchema::prepare(
82 &guard,
83 &SqliteEventLogSchemaConfig {
84 events_relation: config.events_relation.clone(),
85 },
86 )
87 }
88
89 pub fn schema_sql(config: &SqlitePartitionBackfillConfig) -> String {
90 SqliteEventLogSchema::schema_sql(&SqliteEventLogSchemaConfig {
91 events_relation: config.events_relation.clone(),
92 })
93 }
94
95 pub async fn run(&self) -> Result<BackfillReport> {
96 let events = self.config.events_relation.render();
97 let batch_size = self.config.batch_size;
98 let partition_count = self.config.partition_count;
99 let key_resolver = Arc::clone(&self.config.key_resolver);
100 let hasher = Arc::clone(&self.config.hasher);
101 let conn = Arc::clone(&self.conn);
102
103 let fetch_sql = format!(
104 "SELECT sequence, id, organization, namespace, topic, event_key, payload, \
105 content_type, metadata, timestamp, version, parent_id, correlation_id, causation_id \
106 FROM {events} \
107 WHERE partition_id IS NULL \
108 ORDER BY sequence \
109 LIMIT ?1"
110 );
111
112 let update_sql = format!(
113 "UPDATE {events} \
114 SET partition_key = ?1, \
115 partition_hash = ?2, \
116 partition_id = ?3, \
117 partition_count = ?4, \
118 partition_strategy = ?5 \
119 WHERE sequence = ?6 AND partition_id IS NULL"
120 );
121
122 tokio::task::spawn_blocking(move || {
123 let mut report = BackfillReport::default();
124 let mut guard = conn.lock().map_err(|e| Error::Store(e.to_string()))?;
125
126 loop {
127 let rows = {
128 let mut stmt = guard
129 .prepare(&fetch_sql)
130 .map_err(|e| Error::Store(e.to_string()))?;
131 let mapped = stmt
132 .query_map([Value::Integer(batch_size as i64)], decode_row)
133 .map_err(|e| Error::Store(e.to_string()))?;
134 let mut out: Vec<(i64, SerializedEvent)> = Vec::new();
135 for row in mapped {
136 out.push(row.map_err(|e| Error::Store(e.to_string()))?);
137 }
138 out
139 };
140
141 if rows.is_empty() {
142 break;
143 }
144
145 let tx = guard
146 .transaction()
147 .map_err(|e| Error::Store(e.to_string()))?;
148
149 for (sequence, serialized) in &rows {
150 let event = serialized.to_event()?;
151 let partition_key = key_resolver.partition_key(&event)?;
152 let partition_hash = hasher.hash(&partition_key);
153 let partition = hasher.partition_for(&partition_key, partition_count);
154 let partition_strategy = PartitionStrategy::new(hasher.strategy())?;
155
156 let updated = tx
157 .execute(
158 &update_sql,
159 rusqlite::params![
160 partition_key.as_str(),
161 partition_hash.to_sql_i64(),
162 partition.id() as i64,
163 partition.count() as i64,
164 partition_strategy.as_str(),
165 *sequence,
166 ],
167 )
168 .map_err(|e| Error::Store(e.to_string()))?;
169 report.rows_updated += updated as u64;
170 }
171
172 tx.commit().map_err(|e| Error::Store(e.to_string()))?;
173 report.batches += 1;
174 }
175
176 Ok(report)
177 })
178 .await
179 .map_err(|e| Error::Store(format!("blocking task panicked: {e}")))?
180 }
181}
182
183fn decode_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<(i64, SerializedEvent)> {
184 let sequence: i64 = row.get(0)?;
185 let id: String = row.get(1)?;
186 let organization: String = row.get(2)?;
187 let namespace: String = row.get(3)?;
188 let topic: String = row.get(4)?;
189 let key: String = row.get(5)?;
190 let payload_str: String = row.get(6)?;
191 let _content_type: String = row.get(7)?;
192 let metadata_str: String = row.get(8)?;
193 let timestamp_str: String = row.get(9)?;
194 let version: i64 = row.get(10)?;
195 let parent_id: Option<String> = row.get(11)?;
196 let correlation_id: Option<String> = row.get(12)?;
197 let causation_id: Option<String> = row.get(13)?;
198
199 let id = uuid::Uuid::parse_str(&id).map_err(|e| {
200 rusqlite::Error::FromSqlConversionFailure(
201 1,
202 rusqlite::types::Type::Text,
203 Box::new(std::io::Error::other(format!("decode id: {e}"))),
204 )
205 })?;
206 let parent_id = parent_id
207 .as_deref()
208 .map(uuid::Uuid::parse_str)
209 .transpose()
210 .map_err(|e| {
211 rusqlite::Error::FromSqlConversionFailure(
212 11,
213 rusqlite::types::Type::Text,
214 Box::new(std::io::Error::other(format!("decode parent_id: {e}"))),
215 )
216 })?;
217 let payload: SerializedPayload = serde_json::from_str(&payload_str).map_err(|e| {
218 rusqlite::Error::FromSqlConversionFailure(
219 6,
220 rusqlite::types::Type::Text,
221 Box::new(std::io::Error::other(format!("decode payload: {e}"))),
222 )
223 })?;
224 let metadata: HashMap<String, String> = serde_json::from_str(&metadata_str).map_err(|e| {
225 rusqlite::Error::FromSqlConversionFailure(
226 8,
227 rusqlite::types::Type::Text,
228 Box::new(std::io::Error::other(format!("decode metadata: {e}"))),
229 )
230 })?;
231 let timestamp = DateTime::parse_from_rfc3339(×tamp_str)
232 .map(|d| d.with_timezone(&Utc))
233 .map_err(|e| {
234 rusqlite::Error::FromSqlConversionFailure(
235 9,
236 rusqlite::types::Type::Text,
237 Box::new(std::io::Error::other(format!("decode timestamp: {e}"))),
238 )
239 })?;
240
241 Ok((
242 sequence,
243 SerializedEvent {
244 id,
245 organization,
246 namespace,
247 topic,
248 payload,
249 metadata,
250 timestamp,
251 version: version as u64,
252 key,
253 parent_id,
254 correlation_id,
255 causation_id,
256 },
257 ))
258}