use super::plan::SnapshotTable;
use crate::column_value::RowData;
use crate::connection::PgReplicationConnection;
use crate::copy_text::{decode_line, TextRowDecoder};
use crate::error::{ReplicationError, Result};
use crate::prelude::*;
use crate::protocol::TupleData;
use crate::stream::LogicalReplicationStream;
use crate::types::Lsn;
use tokio_util::sync::CancellationToken;
const STREAM_TAKEN: &str = "the stream is moved out only by run/finish/abandon, which consume self";
fn next_buffered_row(
decoder: &mut TextRowDecoder,
table: &SnapshotTable,
lsn: Lsn,
) -> Result<Option<SnapshotRow>> {
let Some(line) = decoder.next_line() else {
return Ok(None);
};
let columns = decode_line(&line, table.relation.columns.len())?;
Ok(Some(SnapshotRow {
relation: Arc::clone(&table.relation),
lsn,
data: TupleData::from_smallvec(columns).into_row_data(&table.relation),
}))
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SnapshotRow {
pub relation: Arc<crate::protocol::RelationInfo>,
pub data: RowData,
pub lsn: Lsn,
}
#[allow(clippy::large_enum_variant)]
enum RowSource {
Copy {
reader: PgReplicationConnection,
decoder: TextRowDecoder,
remaining: Vec<SnapshotTable>,
current: Option<SnapshotTable>,
},
#[cfg(test)]
Mock {
outcomes: alloc::collections::VecDeque<Result<()>>,
delivered: usize,
},
}
#[cfg(test)]
fn synthetic_row(index: usize, lsn: Lsn) -> SnapshotRow {
use crate::column_value::ColumnValue;
use crate::protocol::{ColumnInfo, RelationInfo};
let relation = Arc::new(RelationInfo::new(
42,
"public".to_string(),
"users".to_string(),
b'd',
vec![ColumnInfo::new(0, "id".to_string(), 23, -1)],
));
let mut data = RowData::new();
data.push(Arc::from("id"), ColumnValue::text(&(index + 1).to_string()));
SnapshotRow {
relation,
data,
lsn,
}
}
#[must_use = "the replication stream is inside this value; consume it with \
run/finish/abandon to get it back"]
pub struct SnapshotRows {
stream: Option<LogicalReplicationStream>,
source: RowSource,
consistent_point: Lsn,
cancellation_token: CancellationToken,
completed: bool,
poisoned: bool,
}
impl SnapshotRows {
pub(super) fn new(
stream: LogicalReplicationStream,
reader: PgReplicationConnection,
tables: Vec<SnapshotTable>,
consistent_point: Lsn,
cancellation_token: CancellationToken,
) -> Self {
let mut remaining = tables;
remaining.reverse();
Self {
stream: Some(stream),
source: RowSource::Copy {
reader,
decoder: TextRowDecoder::new(),
remaining,
current: None,
},
consistent_point,
cancellation_token,
completed: false,
poisoned: false,
}
}
#[inline]
pub fn stream(&self) -> &LogicalReplicationStream {
self.stream.as_ref().expect(STREAM_TAKEN)
}
pub async fn next_row(&mut self) -> Result<Option<SnapshotRow>> {
if self.poisoned {
return Err(Self::poisoned_err());
}
if self.completed {
return Ok(None);
}
let result = self.next_row_inner().await;
match &result {
Ok(None) => self.completed = true,
Ok(Some(_)) => {}
Err(_) => self.poisoned = true,
}
result
}
async fn next_row_inner(&mut self) -> Result<Option<SnapshotRow>> {
let consistent_point = self.consistent_point;
match &mut self.source {
#[cfg(test)]
RowSource::Mock {
outcomes,
delivered,
} => match outcomes.pop_front() {
None => Ok(None),
Some(Err(e)) => Err(e),
Some(Ok(())) => {
let row = synthetic_row(*delivered, consistent_point);
*delivered += 1;
Ok(Some(row))
}
},
RowSource::Copy {
reader,
decoder,
remaining,
current,
} => {
loop {
if let Some(table) = current.as_ref() {
if let Some(row) = next_buffered_row(decoder, table, consistent_point)? {
return Ok(Some(row));
}
match reader.copy_out_next(&self.cancellation_token).await? {
Some(frame) => {
decoder.push_frame(frame);
continue;
}
None => {
decoder.finish()?;
*current = None;
}
}
}
if !remaining.is_empty() && self.cancellation_token.is_cancelled() {
return Err(ReplicationError::Cancelled(
"snapshot cancelled".to_string(),
));
}
match remaining.pop() {
Some(table) => {
reader.copy_out_begin(&table.copy_sql)?;
*decoder = TextRowDecoder::new();
*current = Some(table);
}
None => return Ok(None),
}
}
}
}
}
pub async fn run<F, Fut>(mut self, mut handler: F) -> Result<LogicalReplicationStream>
where
F: FnMut(SnapshotRow) -> Fut,
Fut: core::future::Future<Output = Result<()>>,
{
while let Some(row) = self.next_row().await? {
handler(row).await?;
}
self.finish().await
}
pub async fn finish(mut self) -> Result<LogicalReplicationStream> {
if self.poisoned {
return Err(Self::poisoned_err());
}
if !self.completed {
return Err(ReplicationError::protocol(
"snapshot was not fully consumed; finish() would leave a gap between \
the snapshot and the stream. Consume every row, or use abandon() \
instead of finish()."
.to_string(),
));
}
self.close_reader();
Ok(self.take_stream())
}
pub async fn abandon(mut self) -> Result<LogicalReplicationStream> {
if self.poisoned {
return Err(Self::poisoned_err());
}
self.close_reader();
Ok(self.take_stream())
}
#[inline]
fn take_stream(&mut self) -> LogicalReplicationStream {
self.stream.take().expect(STREAM_TAKEN)
}
fn discard_slot(&mut self) {
self.close_reader();
if let Some(stream) = self.stream.as_mut() {
stream.discard_replication_slot();
}
}
fn close_reader(&mut self) {
match &mut self.source {
RowSource::Copy { reader, .. } => reader.copy_out_abort(),
#[cfg(test)]
RowSource::Mock { .. } => {}
}
}
#[cold]
#[inline(never)]
fn poisoned_err() -> ReplicationError {
ReplicationError::protocol(
"snapshot handle was poisoned by an earlier error; the exported snapshot \
cannot be resumed from, so restart the snapshot from the beginning"
.to_string(),
)
}
#[cfg(test)]
pub(super) fn mock(
stream: LogicalReplicationStream,
outcomes: alloc::vec::Vec<Result<()>>,
consistent_point: Lsn,
) -> Self {
Self {
stream: Some(stream),
source: RowSource::Mock {
outcomes: outcomes.into_iter().collect(),
delivered: 0,
},
consistent_point,
cancellation_token: CancellationToken::new(),
completed: false,
poisoned: false,
}
}
}
impl Drop for SnapshotRows {
fn drop(&mut self) {
if self.stream.is_none() {
return;
}
self.discard_slot();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::column_value::ColumnValue;
use crate::copy_text::TextRowDecoder;
use crate::protocol::{ColumnInfo, RelationInfo};
use crate::stream::{tests::create_test_stream, ReplicationStreamConfig};
fn expect_err<T>(result: Result<T>) -> ReplicationError {
match result {
Ok(_) => panic!("expected an error"),
Err(e) => e,
}
}
fn mock(outcomes: Vec<Result<()>>) -> SnapshotRows {
let config = ReplicationStreamConfig::builder("s", "p");
SnapshotRows::mock(create_test_stream(config), outcomes, Lsn::new(0x1234))
}
fn table(columns: &[&str]) -> SnapshotTable {
SnapshotTable {
relation: Arc::new(RelationInfo::new(
42,
"public".to_string(),
"users".to_string(),
b'd',
columns
.iter()
.map(|c| ColumnInfo::new(0, c.to_string(), 25, -1))
.collect(),
)),
copy_sql: String::new(),
}
}
#[test]
fn buffered_row_is_none_until_a_full_line_arrives() {
let mut decoder = TextRowDecoder::new();
let t = table(&["id", "name"]);
decoder.push_frame(bytes::Bytes::from_static(b"1\tAli"));
assert!(next_buffered_row(&mut decoder, &t, Lsn::new(7))
.unwrap()
.is_none());
decoder.push_frame(bytes::Bytes::from_static(b"ce\n"));
let row = next_buffered_row(&mut decoder, &t, Lsn::new(7))
.unwrap()
.unwrap();
assert_eq!(row.data.get("id").unwrap().as_str().unwrap(), "1");
assert_eq!(row.data.get("name").unwrap().as_str().unwrap(), "Alice");
}
#[test]
fn buffered_row_shares_the_relation_arc() {
let mut decoder = TextRowDecoder::new();
let t = table(&["id"]);
decoder.push_frame(bytes::Bytes::from_static(b"1\n2\n"));
let a = next_buffered_row(&mut decoder, &t, Lsn::new(7))
.unwrap()
.unwrap();
let b = next_buffered_row(&mut decoder, &t, Lsn::new(7))
.unwrap()
.unwrap();
assert!(
Arc::ptr_eq(&a.relation, &b.relation),
"column names must be shared across rows, not re-allocated"
);
}
#[test]
fn buffered_row_decodes_null_and_empty_distinctly() {
let mut decoder = TextRowDecoder::new();
let t = table(&["a", "b"]);
decoder.push_frame(bytes::Bytes::from_static(b"\\N\t\n"));
let row = next_buffered_row(&mut decoder, &t, Lsn::new(7))
.unwrap()
.unwrap();
assert!(matches!(row.data.get("a").unwrap(), ColumnValue::Null));
assert_eq!(row.data.get("b").unwrap().as_str().unwrap(), "");
}
#[test]
fn buffered_row_rejects_a_column_count_mismatch() {
let mut decoder = TextRowDecoder::new();
let t = table(&["a", "b"]);
decoder.push_frame(bytes::Bytes::from_static(b"1\t2\t3\n"));
let err = next_buffered_row(&mut decoder, &t, Lsn::new(7)).unwrap_err();
assert!(format!("{err}").contains("expected 2"), "{err}");
}
#[test]
fn buffered_row_handles_an_empty_decoder() {
let mut decoder = TextRowDecoder::new();
assert!(
next_buffered_row(&mut decoder, &table(&["id"]), Lsn::new(7))
.unwrap()
.is_none()
);
}
#[tokio::test]
async fn yields_rows_then_none() {
let mut rows = mock(vec![Ok(()), Ok(())]);
assert!(rows.next_row().await.unwrap().is_some());
assert!(rows.next_row().await.unwrap().is_some());
assert!(rows.next_row().await.unwrap().is_none());
assert!(rows.next_row().await.unwrap().is_none());
}
#[tokio::test]
async fn every_row_carries_the_consistent_point() {
let mut rows = mock(vec![Ok(()), Ok(())]);
while let Some(row) = rows.next_row().await.unwrap() {
assert_eq!(row.lsn, Lsn::new(0x1234));
}
}
#[tokio::test]
async fn finish_after_full_consumption_returns_the_stream() {
let mut rows = mock(vec![Ok(())]);
while rows.next_row().await.unwrap().is_some() {}
rows.finish().await.unwrap();
}
#[tokio::test]
async fn finish_before_completion_errors() {
let mut rows = mock(vec![Ok(()), Ok(())]);
rows.next_row().await.unwrap();
let err = expect_err(rows.finish().await);
assert!(format!("{err}").contains("not fully consumed"), "{err}");
}
#[tokio::test]
async fn abandon_returns_the_stream_at_any_point() {
let mut rows = mock(vec![Ok(()), Ok(())]);
rows.next_row().await.unwrap();
rows.abandon().await.unwrap();
}
#[tokio::test]
async fn error_poisons_the_handle() {
let mut rows = mock(vec![
Ok(()),
Err(ReplicationError::protocol("boom".to_string())),
Ok(()),
]);
assert!(rows.next_row().await.unwrap().is_some());
assert!(rows.next_row().await.is_err());
let err = rows.next_row().await.unwrap_err();
assert!(format!("{err}").contains("poisoned"), "{err}");
let err = expect_err(rows.finish().await);
assert!(
format!("{err}").contains("poisoned"),
"a failed snapshot must not yield the stream: {err}"
);
}
#[tokio::test]
async fn abandon_rejects_a_poisoned_handle() {
let mut rows = mock(vec![
Ok(()),
Err(ReplicationError::protocol("boom".to_string())),
]);
assert!(rows.next_row().await.unwrap().is_some());
assert!(rows.next_row().await.is_err());
let err = expect_err(rows.abandon().await);
assert!(
format!("{err}").contains("poisoned"),
"abandoning a failed snapshot must not look like a deliberate skip: {err}"
);
}
#[tokio::test]
async fn abandon_returns_the_stream_when_not_poisoned() {
let rows = mock(vec![Ok(()), Ok(())]);
rows.abandon().await.expect("a clean handle must abandon");
}
#[tokio::test]
async fn run_consumes_everything_and_returns_the_stream() {
let seen = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
let sink = std::sync::Arc::clone(&seen);
mock(vec![Ok(()), Ok(())])
.run(move |r| {
let sink = std::sync::Arc::clone(&sink);
async move {
sink.lock()
.unwrap()
.push(r.data.get("id").unwrap().as_str().unwrap().to_string());
Ok(())
}
})
.await
.unwrap();
assert_eq!(
*seen.lock().unwrap(),
vec!["1", "2"],
"run must pass every row to the handler, in order"
);
}
#[tokio::test]
async fn run_propagates_a_handler_error() {
let err = mock(vec![Ok(())])
.run(|_| async { Err(ReplicationError::protocol("handler failed".to_string())) })
.await;
let err = expect_err(err);
assert!(format!("{err}").contains("handler failed"), "{err}");
}
#[tokio::test]
async fn run_propagates_a_source_error() {
let err = mock(vec![Err(ReplicationError::protocol(
"read failed".to_string(),
))])
.run(|_| async { Ok(()) })
.await;
let err = expect_err(err);
assert!(
format!("{err}").contains("read failed"),
"slot cleanup must not mask the original error: {err}"
);
}
#[tokio::test]
async fn run_over_an_empty_snapshot_succeeds() {
mock(vec![]).run(|_| async { Ok(()) }).await.unwrap();
}
}