Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions linux/crates/app/src/ui/export/export_producer.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::sync::Arc;

use tablepro_core::browse::build_export;
use tablepro_core::browse::{DeclaredPrecisionSink, build_export};
use tablepro_core::column::ResultColumn;
use tablepro_core::{ReadStatement, ResultEvent, SinkFlow, StreamSummary, Truncation};
use tablepro_core::{ReadStatement, ResultEvent, ResultSink, SinkFlow, StreamSummary, Truncation};
use tablepro_session::runtime::{TaskResult, Tasks};
use tablepro_session::{OperationError, OperationKind, Session};
use tokio_util::sync::CancellationToken;
Expand Down Expand Up @@ -67,13 +67,14 @@ pub(crate) fn start_query_producer(
let parts = parts.clone();
let query = Arc::clone(&query);
async move {
let read = match &*query {
ExportQuery::Table(export) => {
ReadStatement::Generated(build_export(&*lease.dialect(), &export.request())?)
}
ExportQuery::Editor { sql } => ReadStatement::User(sql.clone()),
let (read, sink): (_, Box<dyn ResultSink>) = match &*query {
ExportQuery::Table(export) => (
ReadStatement::Generated(build_export(&*lease.dialect(), &export.request())?),
Box::new(DeclaredPrecisionSink::new(parts.sink(), Arc::clone(&export.columns))),
),
ExportQuery::Editor { sql } => (ReadStatement::User(sql.clone()), parts.sink()),
};
Ok(lease.stream_read(&read, parts.sink(), options).await?)
Ok(lease.stream_read(&read, sink, options).await?)
}
})
.await
Expand Down
274 changes: 274 additions & 0 deletions linux/crates/core/src/browse/declared_precision.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
use std::sync::Arc;

use async_trait::async_trait;

use crate::column::{ColumnInfo, ColumnKind, ResultColumn};
use crate::query_result::QueryResult;
use crate::result_event::ResultEvent;
use crate::result_sink::{ResultSink, SinkFlow};

/// The page, with the fractional digits and bit lengths its columns'
/// catalogue entries declare where the result's own metadata names
/// none.
///
/// MySQL's result metadata, as sqlx exposes it, carries neither a
/// DATETIME(6)'s digits nor a BIT(10)'s length, and the grid writes a
/// value at what its column declares. Only a column of the same kind is
/// refined: one the page projected as another type keeps the result's.
pub(super) fn with_declared_precision(result: QueryResult, catalog: &[ColumnInfo]) -> QueryResult {
match declared_columns(&result.columns, catalog) {
Some(columns) => QueryResult { columns, ..result },
None => result,
}
}

/// A table export's sink. Its columns take what the catalogue declares,
/// as a page's do, so the file holds what the grid showed.
pub struct DeclaredPrecisionSink {
inner: Box<dyn ResultSink>,
catalog: Arc<[ColumnInfo]>,
}

impl DeclaredPrecisionSink {
pub fn new(inner: Box<dyn ResultSink>, catalog: Arc<[ColumnInfo]>) -> Self {
Self { inner, catalog }
}
}

#[async_trait]
impl ResultSink for DeclaredPrecisionSink {
async fn accept(&mut self, event: ResultEvent) -> SinkFlow {
let event = match event {
ResultEvent::ResultSetStarted { columns } => ResultEvent::ResultSetStarted {
columns: declared_columns(&columns, &self.catalog).unwrap_or(columns),
},
ResultEvent::ColumnsResolved { set_index, columns } => ResultEvent::ColumnsResolved {
set_index,
columns: declared_columns(&columns, &self.catalog).unwrap_or(columns),
},
other => other,
};
self.inner.accept(event).await
}
}

/// `None` when no column takes anything from the catalogue.
fn declared_columns(columns: &[ResultColumn], catalog: &[ColumnInfo]) -> Option<Arc<[ResultColumn]>> {
let refined: Vec<(usize, ColumnKind)> = columns
.iter()
.zip(catalog)
.enumerate()
.filter(|(_, (column, info))| column.name == info.name)
.filter_map(|(index, (column, info))| {
refined(&column.column_type.kind, &info.column_type.kind).map(|kind| (index, kind))
})
.collect();
if refined.is_empty() {
return None;
}
let mut columns = columns.to_vec();
for (index, kind) in refined {
if let Some(column) = columns.get_mut(index) {
column.column_type.kind = kind;
}
}
Some(columns.into())
}

fn refined(result: &ColumnKind, declared: &ColumnKind) -> Option<ColumnKind> {
match (result, declared) {
(
ColumnKind::Timestamp {
fractional_digits: None,
zone,
},
ColumnKind::Timestamp {
fractional_digits: Some(digits),
..
},
) => Some(ColumnKind::Timestamp {
fractional_digits: Some(*digits),
zone: zone.clone(),
}),
(
ColumnKind::Time {
fractional_digits: None,
with_offset,
range,
},
ColumnKind::Time {
fractional_digits: Some(digits),
..
},
) => Some(ColumnKind::Time {
fractional_digits: Some(*digits),
with_offset: *with_offset,
range: *range,
}),
(
ColumnKind::BitString { length: None, varying },
ColumnKind::BitString {
length: Some(length), ..
},
) => Some(ColumnKind::BitString {
length: Some(*length),
varying: *varying,
}),
_ => None,
}
}

#[cfg(test)]
mod tests {
use std::sync::Mutex;

use super::*;
use crate::column::{ColumnDefault, ColumnType, ReadForm, TemporalZone, TextLength, TimeRange};
use crate::value::Value;

fn typed(kind: ColumnKind) -> ColumnType {
ColumnType {
name: "t".to_owned(),
kind,
catalog: None,
dynamic_storage: false,
read_form: ReadForm::Native,
}
}

fn info(name: &str, kind: ColumnKind) -> ColumnInfo {
ColumnInfo {
name: name.to_owned(),
column_type: typed(kind),
nullable: true,
primary_key: false,
is_auto_increment: false,
is_generated: false,
default: ColumnDefault::None,
comment: None,
}
}

fn stamp(fractional_digits: Option<u8>, zone: TemporalZone) -> ColumnKind {
ColumnKind::Timestamp {
fractional_digits,
zone,
}
}

fn bits(length: Option<u32>) -> ColumnKind {
ColumnKind::BitString { length, varying: false }
}

fn result(columns: &[(&str, ColumnKind)]) -> QueryResult {
let columns: Vec<ResultColumn> = columns
.iter()
.map(|(name, kind)| ResultColumn::new(*name, typed(kind.clone())))
.collect();
QueryResult::new(columns, vec![vec![Value::Null; 2]])
}

fn kinds(result: &QueryResult) -> Vec<ColumnKind> {
result
.columns
.iter()
.map(|column| column.column_type.kind.clone())
.collect()
}

#[test]
fn a_result_without_digits_or_a_length_takes_the_catalogues() {
let read = result(&[("at", stamp(None, TemporalZone::Offset)), ("flag", bits(None))]);
let catalog = [
info("at", stamp(Some(3), TemporalZone::Naive)),
info("flag", bits(Some(1))),
];

let refined = with_declared_precision(read, &catalog);

assert_eq!(kinds(&refined), [stamp(Some(3), TemporalZone::Offset), bits(Some(1))]);
assert_eq!(refined.rows, [vec![Value::Null; 2]]);
}

#[test]
fn a_time_takes_the_digits_and_keeps_its_range() {
let time = |fractional_digits| ColumnKind::Time {
fractional_digits,
with_offset: false,
range: TimeRange::Duration,
};

let refined = with_declared_precision(result(&[("t", time(None))]), &[info("t", time(Some(6)))]);

assert_eq!(kinds(&refined), [time(Some(6))]);
}

#[test]
fn what_the_result_declares_or_projected_as_another_type_stays() {
let text = ColumnKind::Text {
length: TextLength::Unbounded,
};
let enumeration = ColumnKind::Enum {
labels: vec!["a".to_owned()],
multiple: false,
};
let read = result(&[("at", stamp(Some(6), TemporalZone::Naive)), ("mood", text.clone())]);
let catalog = [
info("at", stamp(Some(3), TemporalZone::Naive)),
info("mood", enumeration),
];

let refined = with_declared_precision(read, &catalog);

assert_eq!(kinds(&refined), [stamp(Some(6), TemporalZone::Naive), text]);
}

#[test]
fn a_column_under_another_name_is_not_the_catalogues() {
let read = result(&[("other", bits(None))]);

let refined = with_declared_precision(read, &[info("flag", bits(Some(1)))]);

assert_eq!(kinds(&refined), [bits(None)]);
}

#[derive(Clone, Default)]
struct Seen(Arc<Mutex<Vec<ResultEvent>>>);

#[async_trait]
impl ResultSink for Seen {
async fn accept(&mut self, event: ResultEvent) -> SinkFlow {
self.0.lock().expect("the events").push(event);
SinkFlow::Continue
}
}

#[tokio::test]
async fn an_export_takes_the_catalogues_digits_for_every_result_set() {
let seen = Seen::default();
let mut sink = DeclaredPrecisionSink::new(Box::new(seen.clone()), vec![info("flag", bits(Some(1)))].into());
let read: Arc<[ResultColumn]> = vec![ResultColumn::new("flag", typed(bits(None)))].into();

sink.accept(ResultEvent::ResultSetStarted { columns: read.clone() })
.await;
sink.accept(ResultEvent::ColumnsResolved {
set_index: 0,
columns: read,
})
.await;
sink.accept(ResultEvent::Rows(vec![vec![Value::Null]])).await;

let events = seen.0.lock().expect("the events");
let columns: Vec<ColumnKind> = events
.iter()
.filter_map(|event| match event {
ResultEvent::ResultSetStarted { columns } | ResultEvent::ColumnsResolved { columns, .. } => {
Some(columns[0].column_type.kind.clone())
}
_ => None,
})
.collect();
assert_eq!(columns, [bits(Some(1)), bits(Some(1))]);
assert!(matches!(events.last(), Some(ResultEvent::Rows(_))));
}
}
2 changes: 2 additions & 0 deletions linux/crates/core/src/browse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod browse_sql_error;
mod build_count;
mod build_export;
mod build_page;
mod declared_precision;
mod export_request;
mod page_layout;
mod page_request;
Expand All @@ -24,6 +25,7 @@ pub use browse_sql_error::BrowseSqlError;
pub use build_count::build_count;
pub use build_export::build_export;
pub use build_page::build_page;
pub use declared_precision::DeclaredPrecisionSink;
pub use export_request::ExportRequest;
pub use page_layout::PageLayout;
pub use page_request::PageRequest;
Expand Down
3 changes: 2 additions & 1 deletion linux/crates/core/src/browse/read_page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::query_result::QueryResult;
use crate::row_limit::RowLimit;
use crate::value::Value;

use super::declared_precision::with_declared_precision;
use super::split_page::key_of;
use super::{BrowseSqlError, PageLayout, PageRequest, PageSpec, PagingMode, RowKeyLayout, build_page, paging_mode};

Expand Down Expand Up @@ -164,7 +165,7 @@ where
.query_bound(&spec.statement, row_limit, options.clone())
.await?;
check_shape(&result, &spec.layout)?;
Ok((result, spec))
Ok((with_declared_precision(result, request.columns), spec))
}

/// Fill a page from block windows, each read in ctid order, until it
Expand Down
Loading
Loading