laminar_connectors/mongodb/source/
history.rs1use std::sync::Arc;
7
8use arrow_array::builder::{
9 Int32Builder, Int64Builder, StringBuilder, TimestampMicrosecondBuilder,
10};
11use arrow_array::{ArrayRef, RecordBatch};
12use arrow_schema::{DataType, Field, Schema, SchemaRef, TimeUnit};
13use mongodb::bson::{RawBsonRef, RawDocumentBuf, Timestamp};
14use sha2::{Digest, Sha256};
15use uuid::Uuid;
16
17use super::super::change_event::canonical_document_extjson;
18use super::buffering::{BufferedMongoPayload, ChangeRecord, SnapshotRecord};
19use super::checkpoint::encode_timestamp;
20use super::decoding::{decode_change, optional_extjson};
21use super::ConnectorError;
22
23pub const MONGODB_HISTORY_VERSION: i32 = 1;
25pub const SNAPSHOT_OPERATION: &str = "snapshot";
27
28#[must_use]
50pub fn mongodb_history_schema() -> SchemaRef {
51 let utf8 = |name: &str, nullable| Field::new(name, DataType::Utf8, nullable);
52 Arc::new(Schema::new(vec![
53 utf8("event_id", false),
54 Field::new("event_version", DataType::Int32, false),
55 utf8("operation", false),
56 utf8("database", false),
57 utf8("collection", true),
58 utf8("collection_uuid", false),
59 utf8("document_key", true),
60 utf8("full_document", true),
61 utf8("update_description", true),
62 utf8("event_details", true),
63 utf8("resume_token", true),
64 Field::new("cluster_time_seconds", DataType::Int64, false),
65 Field::new("cluster_time_increment", DataType::Int64, false),
66 Field::new(
67 "wall_time",
68 DataType::Timestamp(TimeUnit::Microsecond, None),
70 true,
71 ),
72 Field::new("txn_number", DataType::Int64, true),
73 utf8("lsid", true),
74 utf8("snapshot_id", true),
75 ]))
76}
77
78pub(super) struct HistoryIdentity<'a> {
80 pub(super) deployment: &'a str,
81 pub(super) collection_uuid: Uuid,
82 pub(super) database: &'a str,
83 pub(super) collection: &'a str,
84}
85
86impl HistoryIdentity<'_> {
87 fn digest(&self, kind: &[u8], parts: &[&[u8]]) -> String {
88 let mut digest = Sha256::new();
89 digest.update(b"laminardb-mongodb-event-v1\0");
90 digest.update(kind);
91 digest.update(self.deployment.as_bytes());
92 digest.update(self.collection_uuid.as_bytes());
93 for part in parts {
94 digest.update(u64::try_from(part.len()).unwrap_or(u64::MAX).to_be_bytes());
95 digest.update(part);
96 }
97 format!("{:x}", digest.finalize())
98 }
99}
100
101struct HistoryBuilders {
102 event_id: StringBuilder,
103 version: Int32Builder,
104 operation: StringBuilder,
105 database: StringBuilder,
106 collection: StringBuilder,
107 collection_uuid: StringBuilder,
108 document_key: StringBuilder,
109 full_document: StringBuilder,
110 update_description: StringBuilder,
111 details: StringBuilder,
112 resume_token: StringBuilder,
113 seconds: Int64Builder,
114 increment: Int64Builder,
115 wall_time: TimestampMicrosecondBuilder,
116 txn_number: Int64Builder,
117 lsid: StringBuilder,
118 snapshot_id: StringBuilder,
119}
120
121impl HistoryBuilders {
122 fn with_capacity(rows: usize) -> Self {
123 let text = || StringBuilder::with_capacity(rows, rows * 64);
124 Self {
125 event_id: StringBuilder::with_capacity(rows, rows * 64),
126 version: Int32Builder::with_capacity(rows),
127 operation: text(),
128 database: text(),
129 collection: text(),
130 collection_uuid: StringBuilder::with_capacity(rows, rows * 36),
131 document_key: text(),
132 full_document: StringBuilder::with_capacity(rows, rows * 256),
133 update_description: text(),
134 details: text(),
135 resume_token: text(),
136 seconds: Int64Builder::with_capacity(rows),
137 increment: Int64Builder::with_capacity(rows),
138 wall_time: TimestampMicrosecondBuilder::with_capacity(rows),
139 txn_number: Int64Builder::with_capacity(rows),
140 lsid: text(),
141 snapshot_id: text(),
142 }
143 }
144
145 fn append_time(&mut self, at: Timestamp) {
146 self.seconds.append_value(i64::from(at.time));
147 self.increment.append_value(i64::from(at.increment));
148 }
149
150 fn append_change(
151 &mut self,
152 record: &ChangeRecord,
153 identity: &HistoryIdentity<'_>,
154 ) -> Result<(), ConnectorError> {
155 let change = decode_change(&record.raw)?;
156 if change
157 .collection_uuid
158 .is_some_and(|uuid| uuid != identity.collection_uuid)
159 {
160 return Err(ConnectorError::ReadError(format!(
161 "MongoDB change event for {}.{} carries a different collection UUID than the \
162 bound collection {}",
163 identity.database, identity.collection, identity.collection_uuid
164 )));
165 }
166 let cluster_time = change.cluster_time.ok_or_else(|| {
167 ConnectorError::ReadError("MongoDB change event omitted clusterTime".into())
168 })?;
169 self.event_id
170 .append_value(identity.digest(b"change\0", &[record.token.as_bytes()]));
171 self.version.append_value(MONGODB_HISTORY_VERSION);
172 self.operation.append_value(change.operation);
173 self.database
174 .append_value(change.database.unwrap_or(identity.database));
175 self.collection.append_option(change.collection);
176 self.collection_uuid
177 .append_value(identity.collection_uuid.hyphenated().to_string());
178 self.document_key
179 .append_option(optional_extjson(change.document_key)?);
180 self.full_document
181 .append_option(optional_extjson(change.full_document)?);
182 self.update_description
183 .append_option(optional_extjson(change.update_description)?);
184 self.details
185 .append_option(optional_extjson(change.details.as_deref())?);
186 self.resume_token.append_value(&record.token);
187 self.append_time(cluster_time);
188 self.wall_time.append_option(change.wall_time_us);
189 self.txn_number.append_option(change.txn_number);
190 self.lsid.append_option(optional_extjson(change.lsid)?);
191 self.snapshot_id.append_null();
192 Ok(())
193 }
194
195 fn append_snapshot(
196 &mut self,
197 record: &SnapshotRecord,
198 snapshot_at: Timestamp,
199 identity: &HistoryIdentity<'_>,
200 ) -> Result<(), ConnectorError> {
201 let snapshot_id = encode_timestamp(snapshot_at);
202 let id = record
203 .raw
204 .get("_id")
205 .map_err(|error| ConnectorError::ReadError(format!("snapshot document _id: {error}")))?
206 .ok_or_else(|| ConnectorError::ReadError("snapshot document has no _id".into()))?;
207 let key = id_key(id);
208 self.event_id.append_value(identity.digest(
209 b"snapshot\0",
210 &[snapshot_id.as_bytes(), record.key.as_bytes()],
211 ));
212 self.version.append_value(MONGODB_HISTORY_VERSION);
213 self.operation.append_value(SNAPSHOT_OPERATION);
214 self.database.append_value(identity.database);
215 self.collection.append_value(identity.collection);
216 self.collection_uuid
217 .append_value(identity.collection_uuid.hyphenated().to_string());
218 self.document_key
219 .append_value(canonical_document_extjson(&key)?);
220 self.full_document
221 .append_value(canonical_document_extjson(&record.raw)?);
222 self.update_description.append_null();
223 self.details.append_null();
224 self.resume_token.append_null();
225 self.append_time(snapshot_at);
226 self.wall_time.append_null();
227 self.txn_number.append_null();
228 self.lsid.append_null();
229 self.snapshot_id.append_value(snapshot_id);
230 Ok(())
231 }
232
233 fn finish(mut self, schema: &SchemaRef) -> Result<RecordBatch, ConnectorError> {
234 let columns: Vec<ArrayRef> = vec![
235 Arc::new(self.event_id.finish()),
236 Arc::new(self.version.finish()),
237 Arc::new(self.operation.finish()),
238 Arc::new(self.database.finish()),
239 Arc::new(self.collection.finish()),
240 Arc::new(self.collection_uuid.finish()),
241 Arc::new(self.document_key.finish()),
242 Arc::new(self.full_document.finish()),
243 Arc::new(self.update_description.finish()),
244 Arc::new(self.details.finish()),
245 Arc::new(self.resume_token.finish()),
246 Arc::new(self.seconds.finish()),
247 Arc::new(self.increment.finish()),
248 Arc::new(self.wall_time.finish()),
249 Arc::new(self.txn_number.finish()),
250 Arc::new(self.lsid.finish()),
251 Arc::new(self.snapshot_id.finish()),
252 ];
253 RecordBatch::try_new(Arc::clone(schema), columns)
254 .map_err(|error| ConnectorError::Internal(format!("MongoDB history batch: {error}")))
255 }
256}
257
258pub(super) fn history_batch<'a>(
260 records: impl ExactSizeIterator<Item = &'a BufferedMongoPayload>,
261 schema: &SchemaRef,
262 identity: &HistoryIdentity<'_>,
263 snapshot_at: Option<Timestamp>,
264) -> Result<RecordBatch, ConnectorError> {
265 let mut builders = HistoryBuilders::with_capacity(records.len());
266 for payload in records {
267 match payload {
268 BufferedMongoPayload::Change(record) => builders.append_change(record, identity)?,
269 BufferedMongoPayload::Snapshot(record) => {
270 let at = snapshot_at.ok_or_else(|| {
271 ConnectorError::Internal("snapshot row without a snapshot time".into())
272 })?;
273 builders.append_snapshot(record, at, identity)?;
274 }
275 BufferedMongoPayload::HighWatermark { .. } | BufferedMongoPayload::SnapshotComplete => {
276 return Err(ConnectorError::Internal(
277 "MongoDB history batch received a progress marker as a record".into(),
278 ));
279 }
280 }
281 }
282 builders.finish(schema)
283}
284
285pub(super) fn id_key(id: RawBsonRef<'_>) -> RawDocumentBuf {
287 let mut key = RawDocumentBuf::new();
288 key.append("_id", id.to_raw_bson());
289 key
290}