use super::super::generated::gapic_storage::client::BigQueryWrite;
use super::super::transport::Transport;
use super::{BufferedWriter, CommittedWriter, DefaultWriter, PendingWriter, Writer};
use crate::model::write_stream::Type;
use crate::model::{ArrowSchema, WriteStream};
use crate::write::error::{AttachError, AttachResult};
use crate::{Error, Result};
use gaxi::path_parameter::{PathMismatchBuilder, try_match};
use gaxi::routing_parameter::Segment;
use google_cloud_gax::error::binding::BindingError;
use std::sync::Arc;
#[derive(Clone, Debug)]
pub struct WriterBuilder {
inner: Arc<Transport>,
schema: ArrowSchema,
}
impl WriterBuilder {
pub(crate) fn new(inner: Arc<Transport>, schema: ArrowSchema) -> Self {
Self { inner, schema }
}
pub fn default<T: Into<String>>(self, table: T) -> Result<DefaultWriter> {
let table = table.into();
validate_table(table.as_str())?;
let mut write_stream = table;
write_stream.push_str("/streams/_default");
Ok(DefaultWriter::new(self.inner, write_stream, self.schema))
}
pub async fn pending<T: Into<String>>(self, table: T) -> Result<PendingWriter> {
let table = table.into();
validate_table(table.as_str())?;
let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
let write_stream = client
.create_write_stream()
.set_parent(table)
.set_write_stream(WriteStream::new().set_type(Type::Pending))
.send()
.await?;
Ok(PendingWriter::new(
self.inner,
write_stream.name,
self.schema,
))
}
pub async fn committed<T: Into<String>>(self, table: T) -> Result<CommittedWriter> {
let table = table.into();
validate_table(table.as_str())?;
let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
let write_stream = client
.create_write_stream()
.set_parent(table)
.set_write_stream(WriteStream::new().set_type(Type::Committed))
.send()
.await?;
Ok(CommittedWriter::new(
self.inner,
write_stream.name,
self.schema,
))
}
pub async fn buffered<T: Into<String>>(self, table: T) -> Result<BufferedWriter> {
let table = table.into();
validate_table(table.as_str())?;
let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
let write_stream = client
.create_write_stream()
.set_parent(table)
.set_write_stream(WriteStream::new().set_type(Type::Buffered))
.send()
.await?;
Ok(BufferedWriter::new(
self.inner,
write_stream.name,
self.schema,
))
}
pub async fn attach<U: Writer, S: Into<String>>(self, write_stream: S) -> AttachResult<U> {
let write_stream = write_stream.into();
validate_stream(write_stream.as_str())?;
let client = BigQueryWrite::from_stub::<Transport>(self.inner.clone());
let stream = client
.get_write_stream()
.set_name(&write_stream)
.send()
.await?;
let stream_type = stream.r#type.clone();
if stream_type != U::STREAM_TYPE {
return Err(AttachError::TypeMismatch {
expected: U::STREAM_TYPE,
actual: stream_type,
});
}
Ok(U::build(self.inner, write_stream, self.schema))
}
}
fn validate_table(table: &str) -> Result<()> {
let segments = &[
Segment::Literal("projects/"),
Segment::SingleWildcard,
Segment::Literal("/datasets/"),
Segment::SingleWildcard,
Segment::Literal("/tables/"),
Segment::SingleWildcard,
];
try_match(Some(table), segments)
.ok_or_else(|| {
let builder = PathMismatchBuilder::default().maybe_add(
Some(table),
segments,
"table",
"projects/*/datasets/*/tables/*",
);
Error::binding(BindingError {
paths: vec![builder.build()],
})
})
.map(|_| ())
}
fn validate_stream(stream: &str) -> crate::Result<()> {
let segments = &[
Segment::Literal("projects/"),
Segment::SingleWildcard,
Segment::Literal("/datasets/"),
Segment::SingleWildcard,
Segment::Literal("/tables/"),
Segment::SingleWildcard,
Segment::Literal("/streams/"),
Segment::SingleWildcard,
];
try_match(Some(stream), segments)
.ok_or_else(|| {
let builder = gaxi::path_parameter::PathMismatchBuilder::default();
let builder = builder.maybe_add(
Some(stream),
segments,
"write_stream",
"projects/*/datasets/*/tables/*/streams/*",
);
Error::binding(BindingError {
paths: vec![builder.build()],
})
})
.map(|_| ())
}
#[cfg(test)]
mod tests {
use super::super::super::transport::tests::test_transport;
use super::*;
use bigquery_grpc_mock::google::cloud::bigquery::storage::v1::WriteStream as MockWriteStream;
use bigquery_grpc_mock::{MockBigQueryWrite, start};
use test_case::test_case;
use tokio::task::JoinHandle;
#[tokio::test]
async fn pending_success() -> anyhow::Result<()> {
let mut mock = MockBigQueryWrite::new();
mock.expect_create_write_stream().return_once(|req| {
let req = req.into_inner();
assert_eq!(req.parent, "projects/p/datasets/d/tables/t");
let ws = req.write_stream.expect("write_stream populated");
assert_eq!(Type::from(ws.r#type), Type::Pending);
Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
..Default::default()
}))
});
let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
let transport = Arc::new(test_transport(endpoint).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer = builder.pending("projects/p/datasets/d/tables/t").await?;
assert_eq!(
writer.inner.write_stream,
"projects/p/datasets/d/tables/t/streams/s"
);
assert_eq!(writer.inner.schema, schema);
Ok(())
}
#[test_case("projects/p")]
#[test_case("projects/p/tables/t")]
#[test_case("projects/p/datasets/d/tables/")]
#[tokio::test]
async fn pending_bad_table_format(table: &str) -> anyhow::Result<()> {
let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let err = builder
.pending(table)
.await
.expect_err("should fail locally on bad format");
assert!(err.is_binding(), "{err:?}");
Ok(())
}
#[tokio::test]
async fn committed_success() -> anyhow::Result<()> {
let mut mock = MockBigQueryWrite::new();
mock.expect_create_write_stream().return_once(|req| {
let req = req.into_inner();
assert_eq!(req.parent, "projects/p/datasets/d/tables/t");
let ws = req.write_stream.expect("write_stream populated");
assert_eq!(Type::from(ws.r#type), Type::Committed);
Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
..Default::default()
}))
});
let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
let transport = Arc::new(test_transport(endpoint).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer = builder.committed("projects/p/datasets/d/tables/t").await?;
assert_eq!(
writer.inner.write_stream,
"projects/p/datasets/d/tables/t/streams/s"
);
assert_eq!(writer.inner.schema, schema);
Ok(())
}
#[test_case("projects/p")]
#[test_case("projects/p/tables/t")]
#[test_case("projects/p/datasets/d/tables/")]
#[tokio::test]
async fn committed_bad_table_format(table: &str) -> anyhow::Result<()> {
let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let err = builder
.committed(table)
.await
.expect_err("should fail locally on bad format");
assert!(err.is_binding(), "{err:?}");
Ok(())
}
#[tokio::test]
async fn default() -> anyhow::Result<()> {
let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer = builder.default("projects/p/datasets/d/tables/t")?;
assert_eq!(
writer.write_stream,
"projects/p/datasets/d/tables/t/streams/_default"
);
assert_eq!(writer.schema, schema);
Ok(())
}
#[test_case("projects/p")]
#[test_case("projects/p/tables/t")]
#[test_case("projects/p/datasets/d/tables/")]
#[test_case("projects/p/instances/i/tables/t")]
#[test_case("projects/p/datasets/d/tables/t/streams")]
#[test_case("projects/p/datasets/d/tables/t/streams/_default")]
#[tokio::test]
async fn bad_table_format(table: &str) -> anyhow::Result<()> {
let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let err = builder
.default(table)
.expect_err("should fail locally on bad format");
assert!(err.is_binding(), "{err:?}");
Ok(())
}
#[tokio::test]
async fn buffered_success() -> anyhow::Result<()> {
let mut mock = MockBigQueryWrite::new();
mock.expect_create_write_stream().return_once(|req| {
let req = req.into_inner();
assert_eq!(req.parent, "projects/p/datasets/d/tables/t");
let ws = req.write_stream.expect("write_stream populated");
assert_eq!(Type::from(ws.r#type), Type::Buffered);
Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
..Default::default()
}))
});
let (endpoint, _server) = start("0.0.0.0:0", mock).await?;
let transport = Arc::new(test_transport(endpoint).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer = builder.buffered("projects/p/datasets/d/tables/t").await?;
assert_eq!(
writer.inner.write_stream,
"projects/p/datasets/d/tables/t/streams/s"
);
assert_eq!(writer.inner.schema, schema);
Ok(())
}
#[test_case("projects/p")]
#[test_case("projects/p/tables/t")]
#[test_case("projects/p/datasets/d/tables/")]
#[tokio::test]
async fn buffered_bad_table_format(table: &str) -> anyhow::Result<()> {
let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let err = builder
.buffered(table)
.await
.expect_err("should fail locally on bad format");
assert!(err.is_binding(), "{err:?}");
Ok(())
}
async fn attach_mock(stream_type: Type) -> anyhow::Result<(Arc<Transport>, JoinHandle<()>)> {
let mut mock = MockBigQueryWrite::new();
mock.expect_get_write_stream().return_once(move |req| {
let req = req.into_inner();
assert_eq!(req.name, "projects/p/datasets/d/tables/t/streams/s");
Ok(gaxi::grpc::tonic::Response::new(MockWriteStream {
name: "projects/p/datasets/d/tables/t/streams/s".to_string(),
r#type: stream_type.value().expect("known enum value"),
..Default::default()
}))
});
let (endpoint, server) = start("0.0.0.0:0", mock).await?;
let transport = Arc::new(test_transport(endpoint).await?);
Ok((transport, server))
}
#[tokio::test]
async fn attach_committed_success() -> anyhow::Result<()> {
let (transport, _server) = attach_mock(Type::Committed).await?;
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer: CommittedWriter = builder
.attach("projects/p/datasets/d/tables/t/streams/s")
.await?;
assert_eq!(
writer.inner.write_stream,
"projects/p/datasets/d/tables/t/streams/s"
);
assert_eq!(writer.inner.schema, schema);
Ok(())
}
#[tokio::test]
async fn attach_pending_success() -> anyhow::Result<()> {
let (transport, _server) = attach_mock(Type::Pending).await?;
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer: PendingWriter = builder
.attach("projects/p/datasets/d/tables/t/streams/s")
.await?;
assert_eq!(
writer.inner.write_stream,
"projects/p/datasets/d/tables/t/streams/s"
);
assert_eq!(writer.inner.schema, schema);
Ok(())
}
#[tokio::test]
async fn attach_buffered_success() -> anyhow::Result<()> {
let (transport, _server) = attach_mock(Type::Buffered).await?;
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let writer: BufferedWriter = builder
.attach("projects/p/datasets/d/tables/t/streams/s")
.await?;
assert_eq!(
writer.inner.write_stream,
"projects/p/datasets/d/tables/t/streams/s"
);
assert_eq!(writer.inner.schema, schema);
Ok(())
}
#[test_case("projects/p")]
#[test_case("projects/p/tables/t")]
#[test_case("projects/p/datasets/d/tables/t")]
#[test_case("projects/p/datasets/d/tables/t/streams/")]
#[tokio::test]
async fn attach_bad_stream_format(stream: &str) -> anyhow::Result<()> {
let transport = Arc::new(test_transport("http://ignored:1".to_string()).await?);
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let err = builder
.attach::<CommittedWriter, _>(stream)
.await
.expect_err("should fail locally on bad format");
assert!(matches!(err, AttachError::Rpc { source: e } if e.is_binding()));
Ok(())
}
#[tokio::test]
async fn attach_stream_type_mismatch() -> anyhow::Result<()> {
let (transport, _server) = attach_mock(Type::Buffered).await?;
let schema = ArrowSchema::new().set_serialized_schema("test");
let builder = WriterBuilder::new(transport, schema.clone());
let err = builder
.attach::<CommittedWriter, _>("projects/p/datasets/d/tables/t/streams/s")
.await
.expect_err("should return type mismatch error");
assert!(matches!(err, AttachError::TypeMismatch { .. }));
assert!(err.to_string().contains("stream type mismatch: requested"));
Ok(())
}
}