use crate::{Archive, DumpId, EntryReadLimits, PgDumpError, TableRef, TableSelector};
use std::{
collections::{HashSet, TryReserveError},
error::Error,
fmt,
hash::Hash,
io::{self, Read, Seek, Write},
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExtractionPlanError {
DuplicateSelector { selector: TableSelector },
DuplicateIndexAllocationFailed {
selector: TableSelector,
requested: usize,
},
}
impl ExtractionPlanError {
pub const fn selector(&self) -> &TableSelector {
match self {
Self::DuplicateSelector { selector }
| Self::DuplicateIndexAllocationFailed { selector, .. } => selector,
}
}
}
impl fmt::Display for ExtractionPlanError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateSelector { selector } => write!(
formatter,
"duplicate table selector in extraction plan: schema={:?}, table={:?}",
selector.schema(),
selector.name()
),
Self::DuplicateIndexAllocationFailed {
selector,
requested,
} => write!(
formatter,
"failed to reserve duplicate-selector index for {requested} extraction target(s): first schema={:?}, table={:?}",
selector.schema(),
selector.name()
),
}
}
}
impl Error for ExtractionPlanError {}
pub(crate) fn first_duplicate_index<I, K>(keys: I) -> Result<Option<usize>, TryReserveError>
where
I: ExactSizeIterator<Item = K>,
K: Eq + Hash,
{
let mut seen = HashSet::new();
seen.try_reserve(keys.len())?;
for (index, key) in keys.enumerate() {
if !seen.insert(key) {
return Ok(Some(index));
}
}
Ok(None)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractionTarget {
selector: TableSelector,
table_entry_id: DumpId,
data_entry_id: DumpId,
}
impl ExtractionTarget {
pub const fn selector(&self) -> &TableSelector {
&self.selector
}
pub const fn table_entry_id(&self) -> DumpId {
self.table_entry_id
}
pub const fn data_entry_id(&self) -> DumpId {
self.data_entry_id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractionOutcome {
target: ExtractionTarget,
copied_bytes: u64,
}
impl ExtractionOutcome {
pub const fn target(&self) -> &ExtractionTarget {
&self.target
}
pub const fn copied_bytes(&self) -> u64 {
self.copied_bytes
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ExtractionExecutionError {
Preflight { source: PgDumpError },
Target {
target: ExtractionTarget,
completed: Vec<ExtractionOutcome>,
source: PgDumpError,
},
}
impl ExtractionExecutionError {
pub fn completed(&self) -> &[ExtractionOutcome] {
match self {
Self::Preflight { .. } => &[],
Self::Target { completed, .. } => completed,
}
}
pub const fn failed_target(&self) -> Option<&ExtractionTarget> {
match self {
Self::Preflight { .. } => None,
Self::Target { target, .. } => Some(target),
}
}
pub const fn pgdump_error(&self) -> &PgDumpError {
match self {
Self::Preflight { source } | Self::Target { source, .. } => source,
}
}
}
impl fmt::Display for ExtractionExecutionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Preflight { source } => {
write!(formatter, "extraction plan preflight failed: {source}")
}
Self::Target {
target,
completed,
source,
} => write!(
formatter,
"extraction target failed after {} completed target(s): schema={:?}, table={:?}: {source}",
completed.len(),
target.selector().schema(),
target.selector().name()
),
}
}
}
impl Error for ExtractionExecutionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(self.pgdump_error())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExtractionPlan {
selectors: Vec<TableSelector>,
entry_read_limits: EntryReadLimits,
}
impl ExtractionPlan {
pub fn new(selectors: Vec<TableSelector>) -> Result<Self, ExtractionPlanError> {
Self::with_entry_read_limits(selectors, EntryReadLimits::default())
}
pub fn with_entry_read_limits(
selectors: Vec<TableSelector>,
entry_read_limits: EntryReadLimits,
) -> Result<Self, ExtractionPlanError> {
let duplicate_index = match first_duplicate_index(
selectors
.iter()
.map(|selector| (selector.schema(), selector.name())),
) {
Ok(duplicate_index) => duplicate_index,
Err(_) => {
let requested = selectors.len();
let selector = selectors
.into_iter()
.next()
.expect("a nonzero reservation failure has an input selector");
return Err(ExtractionPlanError::DuplicateIndexAllocationFailed {
selector,
requested,
});
}
};
if let Some(index) = duplicate_index {
let selector = selectors
.into_iter()
.nth(index)
.expect("duplicate index originated from the selector iterator");
return Err(ExtractionPlanError::DuplicateSelector { selector });
}
Ok(Self {
selectors,
entry_read_limits,
})
}
pub fn selectors(&self) -> &[TableSelector] {
&self.selectors
}
pub const fn entry_read_limits(&self) -> EntryReadLimits {
self.entry_read_limits
}
pub fn preflight<'a, R>(
&self,
archive: &'a Archive<R>,
) -> Result<ResolvedExtractionPlan<'a>, PgDumpError> {
let mut tables = Vec::with_capacity(self.selectors.len());
for selector in &self.selectors {
let table = archive
.resolve_table(selector)
.ok_or(PgDumpError::TableNotFound)?;
if table.data_entry_id().is_none() {
return Err(PgDumpError::TableDataEntryUnavailable {
table_id: table.table_entry_id().as_i32(),
});
}
tables.push(table);
}
Ok(ResolvedExtractionPlan {
tables,
entry_read_limits: self.entry_read_limits,
})
}
pub fn execute<R, W, F>(
&self,
archive: &mut Archive<R>,
mut destination_for: F,
) -> Result<Vec<ExtractionOutcome>, ExtractionExecutionError>
where
R: Read + Seek,
W: Write,
F: FnMut(&ExtractionTarget) -> io::Result<W>,
{
let targets = {
let resolved = self
.preflight(archive)
.map_err(|source| ExtractionExecutionError::Preflight { source })?;
let mut targets = Vec::with_capacity(resolved.tables().len());
for (selector, table) in self.selectors.iter().zip(resolved.tables()) {
let Some(data_entry_id) = table.data_entry_id() else {
return Err(ExtractionExecutionError::Preflight {
source: PgDumpError::TableDataEntryUnavailable {
table_id: table.table_entry_id().as_i32(),
},
});
};
targets.push(ExtractionTarget {
selector: selector.clone(),
table_entry_id: table.table_entry_id(),
data_entry_id,
});
}
targets
};
let mut completed = Vec::with_capacity(targets.len());
for target in targets {
let mut destination = match destination_for(&target) {
Ok(destination) => destination,
Err(source) => {
let error = PgDumpError::EntryOutputIo {
dump_id: target.data_entry_id().as_i32(),
written: 0,
source,
};
return Err(ExtractionExecutionError::Target {
target,
completed,
source: error,
});
}
};
let copied_bytes = match archive.copy_entry_to(
target.data_entry_id(),
&mut destination,
self.entry_read_limits,
) {
Ok(copied_bytes) => copied_bytes,
Err(source) => {
return Err(ExtractionExecutionError::Target {
target,
completed,
source,
});
}
};
if let Err(source) = destination.flush() {
let error = PgDumpError::EntryOutputIo {
dump_id: target.data_entry_id().as_i32(),
written: copied_bytes,
source,
};
return Err(ExtractionExecutionError::Target {
target,
completed,
source: error,
});
}
completed.push(ExtractionOutcome {
target,
copied_bytes,
});
}
Ok(completed)
}
}
#[derive(Debug, Clone)]
pub struct ResolvedExtractionPlan<'a> {
tables: Vec<TableRef<'a>>,
entry_read_limits: EntryReadLimits,
}
impl<'a> ResolvedExtractionPlan<'a> {
pub fn tables(&self) -> &[TableRef<'a>] {
&self.tables
}
pub const fn entry_read_limits(&self) -> EntryReadLimits {
self.entry_read_limits
}
}