Skip to main content

risingwave_connector/sink/
big_query.rs

1// Copyright 2023 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use core::pin::Pin;
16use core::time::Duration;
17use std::collections::{BTreeMap, HashMap, VecDeque};
18
19use anyhow::{Context, anyhow};
20use async_trait::async_trait;
21use base64::Engine;
22use base64::prelude::BASE64_STANDARD;
23use futures::future::pending;
24use futures::prelude::Future;
25use futures::{Stream, StreamExt};
26use futures_async_stream::try_stream;
27use gcp_bigquery_client::Client;
28use gcp_bigquery_client::error::BQError;
29use gcp_bigquery_client::model::query_request::QueryRequest;
30use gcp_bigquery_client::model::query_response::ResultSet;
31use gcp_bigquery_client::model::table::Table;
32use gcp_bigquery_client::model::table_field_schema::TableFieldSchema;
33use gcp_bigquery_client::model::table_schema::TableSchema;
34use google_cloud_bigquery::grpc::apiv1::conn_pool::ConnectionManager;
35use google_cloud_gax::conn::{ConnectionOptions, Environment};
36use google_cloud_gax::grpc::{Request, Response, Status};
37use google_cloud_googleapis::cloud::bigquery::storage::v1::append_rows_request::{
38    MissingValueInterpretation, ProtoData, Rows as AppendRowsRequestRows,
39};
40use google_cloud_googleapis::cloud::bigquery::storage::v1::{
41    AppendRowsRequest, AppendRowsResponse, ProtoRows, ProtoSchema,
42};
43use google_cloud_pubsub::client::google_cloud_auth;
44use google_cloud_pubsub::client::google_cloud_auth::credentials::CredentialsFile;
45use phf::{Set, phf_set};
46use prost_reflect::{FieldDescriptor, MessageDescriptor};
47use prost_types::{
48    DescriptorProto, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet,
49    field_descriptor_proto,
50};
51use risingwave_common::array::{Op, StreamChunk};
52use risingwave_common::catalog::{Field, Schema};
53use risingwave_common::types::DataType;
54use serde::Deserialize;
55use serde_with::{DisplayFromStr, serde_as};
56use simd_json::prelude::ArrayTrait;
57use tokio::sync::mpsc;
58use url::Url;
59use uuid::Uuid;
60use with_options::WithOptions;
61use yup_oauth2::ServiceAccountKey;
62
63use super::encoder::{ProtoEncoder, ProtoHeader, RowEncoder, SerTo};
64use super::log_store::{LogStoreReadItem, TruncateOffset};
65use super::{
66    LogSinker, SINK_TYPE_APPEND_ONLY, SINK_TYPE_OPTION, SINK_TYPE_UPSERT, SinkError, SinkLogReader,
67};
68use crate::aws_utils::load_file_descriptor_from_s3;
69use crate::connector_common::AwsAuthProps;
70use crate::enforce_secret::EnforceSecret;
71use crate::sink::{Result, Sink, SinkParam, SinkWriterParam};
72
73pub const BIGQUERY_SINK: &str = "bigquery";
74pub const CHANGE_TYPE: &str = "_CHANGE_TYPE";
75const DEFAULT_GRPC_CHANNEL_NUMS: usize = 4;
76const CONNECT_TIMEOUT: Option<Duration> = Some(Duration::from_secs(30));
77const CONNECTION_TIMEOUT: Option<Duration> = None;
78const BIGQUERY_SEND_FUTURE_BUFFER_MAX_SIZE: usize = 65536;
79// < 10MB, we set 8MB
80const MAX_ROW_SIZE: usize = 8 * 1024 * 1024;
81
82#[serde_as]
83#[derive(Deserialize, Debug, Clone, WithOptions)]
84pub struct BigQueryCommon {
85    #[serde(rename = "bigquery.local.path")]
86    pub local_path: Option<String>,
87    #[serde(rename = "bigquery.s3.path")]
88    pub s3_path: Option<String>,
89    #[serde(rename = "bigquery.project")]
90    pub project: String,
91    #[serde(rename = "bigquery.dataset")]
92    pub dataset: String,
93    #[serde(rename = "bigquery.table")]
94    pub table: String,
95    #[serde(default, alias = "create_table_if_not_exists")] // default false
96    #[serde_as(as = "DisplayFromStr")]
97    pub auto_create: bool,
98    #[serde(rename = "bigquery.credentials")]
99    pub credentials: Option<String>,
100}
101
102impl EnforceSecret for BigQueryCommon {
103    const ENFORCE_SECRET_PROPERTIES: Set<&'static str> = phf_set! {
104        "bigquery.credentials",
105    };
106}
107
108struct BigQueryFutureManager {
109    // `offset_queue` holds the Some corresponding to each future.
110    // When TruncateOffset is barrier, the num is 0, we don't need to wait for the return of `resp_stream`.
111    // When TruncateOffset is chunk:
112    // 1. chunk has no rows. we didn't send, the num is 0, we don't need to wait for the return of `resp_stream`.
113    // 2. chunk is less than `MAX_ROW_SIZE`, we only sent once, the num is 1 and we only have to wait once for `resp_stream`.
114    // 3. chunk is less than `MAX_ROW_SIZE`, we only sent n, the num is n and we need to wait n times for r.
115    offset_queue: VecDeque<(TruncateOffset, usize)>,
116    resp_stream: Pin<Box<dyn Stream<Item = Result<()>> + Send>>,
117}
118impl BigQueryFutureManager {
119    pub fn new(
120        max_future_num: usize,
121        resp_stream: impl Stream<Item = Result<()>> + Send + 'static,
122    ) -> Self {
123        let offset_queue = VecDeque::with_capacity(max_future_num);
124        Self {
125            offset_queue,
126            resp_stream: Box::pin(resp_stream),
127        }
128    }
129
130    pub fn add_offset(&mut self, offset: TruncateOffset, resp_num: usize) {
131        self.offset_queue.push_back((offset, resp_num));
132    }
133
134    pub async fn next_offset(&mut self) -> Result<TruncateOffset> {
135        if let Some((_offset, remaining_resp_num)) = self.offset_queue.front_mut() {
136            if *remaining_resp_num == 0 {
137                return Ok(self.offset_queue.pop_front().unwrap().0);
138            }
139            while *remaining_resp_num > 0 {
140                self.resp_stream
141                    .next()
142                    .await
143                    .ok_or_else(|| SinkError::BigQuery(anyhow::anyhow!("end of stream")))??;
144                *remaining_resp_num -= 1;
145            }
146            Ok(self.offset_queue.pop_front().unwrap().0)
147        } else {
148            pending().await
149        }
150    }
151}
152pub struct BigQueryLogSinker {
153    writer: BigQuerySinkWriter,
154    bigquery_future_manager: BigQueryFutureManager,
155    future_num: usize,
156}
157impl BigQueryLogSinker {
158    pub fn new(
159        writer: BigQuerySinkWriter,
160        resp_stream: impl Stream<Item = Result<()>> + Send + 'static,
161        future_num: usize,
162    ) -> Self {
163        Self {
164            writer,
165            bigquery_future_manager: BigQueryFutureManager::new(future_num, resp_stream),
166            future_num,
167        }
168    }
169}
170
171#[async_trait]
172impl LogSinker for BigQueryLogSinker {
173    async fn consume_log_and_sink(mut self, mut log_reader: impl SinkLogReader) -> Result<!> {
174        log_reader.start_from(None).await?;
175        loop {
176            tokio::select!(
177                offset = self.bigquery_future_manager.next_offset() => {
178                        log_reader.truncate(offset?)?;
179                }
180                item_result = log_reader.next_item(), if self.bigquery_future_manager.offset_queue.len() <= self.future_num => {
181                    let (epoch, item) = item_result?;
182                    match item {
183                        LogStoreReadItem::StreamChunk { chunk_id, chunk } => {
184                            let resp_num = self.writer.write_chunk(chunk)?;
185                            self.bigquery_future_manager
186                                .add_offset(TruncateOffset::Chunk { epoch, chunk_id },resp_num);
187                        }
188                        LogStoreReadItem::Barrier { .. } => {
189                            self.bigquery_future_manager
190                                .add_offset(TruncateOffset::Barrier { epoch },0);
191                        }
192                    }
193                }
194            )
195        }
196    }
197}
198
199impl BigQueryCommon {
200    async fn build_client(&self, aws_auth_props: &AwsAuthProps) -> Result<Client> {
201        let auth_json = self.get_auth_json_from_path(aws_auth_props).await?;
202
203        let service_account =
204            if let Ok(auth_json_from_base64) = BASE64_STANDARD.decode(auth_json.clone()) {
205                serde_json::from_slice::<ServiceAccountKey>(&auth_json_from_base64)
206            } else {
207                serde_json::from_str::<ServiceAccountKey>(&auth_json)
208            }
209            .map_err(|e| SinkError::BigQuery(e.into()))?;
210
211        let client: Client = Client::from_service_account_key(service_account, false)
212            .await
213            .map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))?;
214        Ok(client)
215    }
216
217    async fn build_writer_client(
218        &self,
219        aws_auth_props: &AwsAuthProps,
220    ) -> Result<(StorageWriterClient, impl Stream<Item = Result<()>> + use<>)> {
221        let auth_json = self.get_auth_json_from_path(aws_auth_props).await?;
222
223        let credentials_file =
224            if let Ok(auth_json_from_base64) = BASE64_STANDARD.decode(auth_json.clone()) {
225                serde_json::from_slice::<CredentialsFile>(&auth_json_from_base64)
226            } else {
227                serde_json::from_str::<CredentialsFile>(&auth_json)
228            }
229            .map_err(|e| SinkError::BigQuery(e.into()))?;
230
231        StorageWriterClient::new(credentials_file).await
232    }
233
234    async fn get_auth_json_from_path(&self, aws_auth_props: &AwsAuthProps) -> Result<String> {
235        if let Some(credentials) = &self.credentials {
236            Ok(credentials.clone())
237        } else if let Some(local_path) = &self.local_path {
238            std::fs::read_to_string(local_path)
239                .map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))
240        } else if let Some(s3_path) = &self.s3_path {
241            let url =
242                Url::parse(s3_path).map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))?;
243            let auth_vec = load_file_descriptor_from_s3(&url, aws_auth_props)
244                .await
245                .map_err(|err| SinkError::BigQuery(anyhow::anyhow!(err)))?;
246            Ok(String::from_utf8(auth_vec).map_err(|e| SinkError::BigQuery(e.into()))?)
247        } else {
248            Err(SinkError::BigQuery(anyhow::anyhow!(
249                "`bigquery.local.path` and `bigquery.s3.path` set at least one, configure as needed."
250            )))
251        }
252    }
253}
254
255#[serde_as]
256#[derive(Clone, Debug, Deserialize, WithOptions)]
257pub struct BigQueryConfig {
258    #[serde(flatten)]
259    pub common: BigQueryCommon,
260    #[serde(flatten)]
261    pub aws_auth_props: AwsAuthProps,
262    pub r#type: String, // accept "append-only" or "upsert"
263}
264
265impl EnforceSecret for BigQueryConfig {
266    fn enforce_one(prop: &str) -> crate::error::ConnectorResult<()> {
267        BigQueryCommon::enforce_one(prop)?;
268        AwsAuthProps::enforce_one(prop)?;
269        Ok(())
270    }
271}
272
273impl BigQueryConfig {
274    pub fn from_btreemap(properties: BTreeMap<String, String>) -> Result<Self> {
275        let config =
276            serde_json::from_value::<BigQueryConfig>(serde_json::to_value(properties).unwrap())
277                .map_err(|e| SinkError::Config(anyhow!(e)))?;
278        if config.r#type != SINK_TYPE_APPEND_ONLY && config.r#type != SINK_TYPE_UPSERT {
279            return Err(SinkError::Config(anyhow!(
280                "`{}` must be {}, or {}",
281                SINK_TYPE_OPTION,
282                SINK_TYPE_APPEND_ONLY,
283                SINK_TYPE_UPSERT
284            )));
285        }
286        Ok(config)
287    }
288}
289
290#[derive(Debug)]
291pub struct BigQuerySink {
292    pub config: BigQueryConfig,
293    schema: Schema,
294    pk_indices: Vec<usize>,
295    is_append_only: bool,
296}
297
298impl EnforceSecret for BigQuerySink {
299    fn enforce_secret<'a>(
300        prop_iter: impl Iterator<Item = &'a str>,
301    ) -> crate::error::ConnectorResult<()> {
302        for prop in prop_iter {
303            BigQueryConfig::enforce_one(prop)?;
304        }
305        Ok(())
306    }
307}
308
309impl BigQuerySink {
310    pub fn new(
311        config: BigQueryConfig,
312        schema: Schema,
313        pk_indices: Vec<usize>,
314        is_append_only: bool,
315    ) -> Result<Self> {
316        Ok(Self {
317            config,
318            schema,
319            pk_indices,
320            is_append_only,
321        })
322    }
323}
324
325impl BigQuerySink {
326    fn is_decimal_type_compatible(bigquery_type: &str) -> bool {
327        // BigQuery INFORMATION_SCHEMA reports parameterized decimal columns as
328        // `NUMERIC(p, s)` or `BIGNUMERIC(p, s)`. RisingWave `Decimal` does not
329        // carry typmod, so schema validation accepts the BigQuery decimal type
330        // family instead of requiring exact string equality.
331        let normalized = bigquery_type.trim().to_ascii_uppercase();
332        matches!(
333            normalized
334                .split_once('(')
335                .map_or(normalized.as_str(), |(prefix, _)| prefix),
336            "NUMERIC" | "BIGNUMERIC"
337        )
338    }
339
340    fn is_data_type_compatible(rw_data_type: &DataType, bigquery_type: &str) -> Result<bool> {
341        if matches!(rw_data_type, DataType::Decimal) {
342            return Ok(Self::is_decimal_type_compatible(bigquery_type));
343        }
344
345        Ok(Self::get_string_and_check_support_from_datatype(rw_data_type)? == bigquery_type)
346    }
347
348    fn check_column_name_and_type(
349        &self,
350        big_query_columns_desc: HashMap<String, String>,
351    ) -> Result<()> {
352        let rw_fields_name = self.schema.fields();
353        if big_query_columns_desc.is_empty() {
354            return Err(SinkError::BigQuery(anyhow::anyhow!(
355                "Cannot find table in bigquery"
356            )));
357        }
358        if rw_fields_name.len().ne(&big_query_columns_desc.len()) {
359            return Err(SinkError::BigQuery(anyhow::anyhow!(
360                "The length of the RisingWave column {} must be equal to the length of the bigquery column {}",
361                rw_fields_name.len(),
362                big_query_columns_desc.len()
363            )));
364        }
365
366        for i in rw_fields_name {
367            let value = big_query_columns_desc.get(&i.name).ok_or_else(|| {
368                SinkError::BigQuery(anyhow::anyhow!(
369                    "Column `{:?}` on RisingWave side is not found on BigQuery side.",
370                    i.name
371                ))
372            })?;
373            let data_type_string = Self::get_string_and_check_support_from_datatype(&i.data_type)?;
374            if !Self::is_data_type_compatible(&i.data_type, value)? {
375                return Err(SinkError::BigQuery(anyhow::anyhow!(
376                    "Data type mismatch for column `{:?}`. BigQuery side: `{:?}`, RisingWave side: `{:?}`. ",
377                    i.name,
378                    value,
379                    data_type_string
380                )));
381            };
382        }
383        Ok(())
384    }
385
386    fn get_string_and_check_support_from_datatype(rw_data_type: &DataType) -> Result<String> {
387        match rw_data_type {
388            DataType::Boolean => Ok("BOOL".to_owned()),
389            DataType::Int16 => Ok("INT64".to_owned()),
390            DataType::Int32 => Ok("INT64".to_owned()),
391            DataType::Int64 => Ok("INT64".to_owned()),
392            DataType::Float32 => Err(SinkError::BigQuery(anyhow::anyhow!(
393                "REAL is not supported for BigQuery sink. Please convert to FLOAT64 or other supported types."
394            ))),
395            DataType::Float64 => Ok("FLOAT64".to_owned()),
396            DataType::Decimal => Ok("NUMERIC".to_owned()),
397            DataType::Date => Ok("DATE".to_owned()),
398            DataType::Varchar => Ok("STRING".to_owned()),
399            DataType::Time => Ok("TIME".to_owned()),
400            DataType::Timestamp => Ok("DATETIME".to_owned()),
401            DataType::Timestamptz => Ok("TIMESTAMP".to_owned()),
402            DataType::Interval => Ok("INTERVAL".to_owned()),
403            DataType::Struct(structs) => {
404                let mut elements_vec = vec![];
405                for (name, datatype) in structs.iter() {
406                    let element_string =
407                        Self::get_string_and_check_support_from_datatype(datatype)?;
408                    elements_vec.push(format!("{} {}", name, element_string));
409                }
410                Ok(format!("STRUCT<{}>", elements_vec.join(", ")))
411            }
412            DataType::List(l) => {
413                let element_string = Self::get_string_and_check_support_from_datatype(l.elem())?;
414                Ok(format!("ARRAY<{}>", element_string))
415            }
416            DataType::Bytea => Ok("BYTES".to_owned()),
417            DataType::Jsonb => Ok("JSON".to_owned()),
418            DataType::Serial => Ok("INT64".to_owned()),
419            DataType::Int256 => Err(SinkError::BigQuery(anyhow::anyhow!(
420                "INT256 is not supported for BigQuery sink."
421            ))),
422            DataType::Map(_) => Err(SinkError::BigQuery(anyhow::anyhow!(
423                "MAP is not supported for BigQuery sink."
424            ))),
425            DataType::Vector(_) => Err(SinkError::BigQuery(anyhow::anyhow!(
426                "VECTOR is not supported for BigQuery sink."
427            ))),
428        }
429    }
430
431    fn map_field(rw_field: &Field) -> Result<TableFieldSchema> {
432        let tfs = match &rw_field.data_type {
433            DataType::Boolean => TableFieldSchema::bool(&rw_field.name),
434            DataType::Int16 | DataType::Int32 | DataType::Int64 | DataType::Serial => {
435                TableFieldSchema::integer(&rw_field.name)
436            }
437            DataType::Float32 => {
438                return Err(SinkError::BigQuery(anyhow::anyhow!(
439                    "REAL is not supported for BigQuery sink. Please convert to FLOAT64 or other supported types."
440                )));
441            }
442            DataType::Float64 => TableFieldSchema::float(&rw_field.name),
443            DataType::Decimal => TableFieldSchema::numeric(&rw_field.name),
444            DataType::Date => TableFieldSchema::date(&rw_field.name),
445            DataType::Varchar => TableFieldSchema::string(&rw_field.name),
446            DataType::Time => TableFieldSchema::time(&rw_field.name),
447            DataType::Timestamp => TableFieldSchema::date_time(&rw_field.name),
448            DataType::Timestamptz => TableFieldSchema::timestamp(&rw_field.name),
449            DataType::Interval => {
450                return Err(SinkError::BigQuery(anyhow::anyhow!(
451                    "INTERVAL is not supported for BigQuery sink. Please convert to VARCHAR or other supported types."
452                )));
453            }
454            DataType::Struct(st) => {
455                let mut sub_fields = Vec::with_capacity(st.len());
456                for (name, dt) in st.iter() {
457                    let rw_field = Field::with_name(dt.clone(), name);
458                    let field = Self::map_field(&rw_field)?;
459                    sub_fields.push(field);
460                }
461                TableFieldSchema::record(&rw_field.name, sub_fields)
462            }
463            DataType::List(lt) => {
464                let inner_field =
465                    Self::map_field(&Field::with_name(lt.elem().clone(), &rw_field.name))?;
466                TableFieldSchema {
467                    mode: Some("REPEATED".to_owned()),
468                    ..inner_field
469                }
470            }
471
472            DataType::Bytea => TableFieldSchema::bytes(&rw_field.name),
473            DataType::Jsonb => TableFieldSchema::json(&rw_field.name),
474            DataType::Int256 => {
475                return Err(SinkError::BigQuery(anyhow::anyhow!(
476                    "INT256 is not supported for BigQuery sink."
477                )));
478            }
479            DataType::Map(_) => {
480                return Err(SinkError::BigQuery(anyhow::anyhow!(
481                    "MAP is not supported for BigQuery sink."
482                )));
483            }
484            DataType::Vector(_) => {
485                return Err(SinkError::BigQuery(anyhow::anyhow!(
486                    "VECTOR is not supported for BigQuery sink."
487                )));
488            }
489        };
490        Ok(tfs)
491    }
492
493    async fn create_table(
494        &self,
495        client: &Client,
496        project_id: &str,
497        dataset_id: &str,
498        table_id: &str,
499        fields: &Vec<Field>,
500    ) -> Result<Table> {
501        let dataset = client
502            .dataset()
503            .get(project_id, dataset_id)
504            .await
505            .map_err(|e| SinkError::BigQuery(e.into()))?;
506        let fields: Vec<_> = fields.iter().map(Self::map_field).collect::<Result<_>>()?;
507        let table = Table::from_dataset(&dataset, table_id, TableSchema::new(fields));
508
509        client
510            .table()
511            .create(table)
512            .await
513            .map_err(|e| SinkError::BigQuery(e.into()))
514    }
515}
516
517impl Sink for BigQuerySink {
518    type LogSinker = BigQueryLogSinker;
519
520    const SINK_NAME: &'static str = BIGQUERY_SINK;
521
522    async fn new_log_sinker(&self, _writer_param: SinkWriterParam) -> Result<Self::LogSinker> {
523        let (writer, resp_stream) = BigQuerySinkWriter::new(
524            self.config.clone(),
525            self.schema.clone(),
526            self.pk_indices.clone(),
527            self.is_append_only,
528        )
529        .await?;
530        Ok(BigQueryLogSinker::new(
531            writer,
532            resp_stream,
533            BIGQUERY_SEND_FUTURE_BUFFER_MAX_SIZE,
534        ))
535    }
536
537    async fn validate(&self) -> Result<()> {
538        risingwave_common::license::Feature::BigQuerySink
539            .check_available()
540            .map_err(|e| anyhow::anyhow!(e))?;
541        if !self.is_append_only && self.pk_indices.is_empty() {
542            return Err(SinkError::Config(anyhow!(
543                "Primary key not defined for upsert bigquery sink (please define in `primary_key` field)"
544            )));
545        }
546        let client = self
547            .config
548            .common
549            .build_client(&self.config.aws_auth_props)
550            .await?;
551        let BigQueryCommon {
552            project: project_id,
553            dataset: dataset_id,
554            table: table_id,
555            ..
556        } = &self.config.common;
557
558        if self.config.common.auto_create {
559            match client
560                .table()
561                .get(project_id, dataset_id, table_id, None)
562                .await
563            {
564                Err(BQError::ResponseError { error }) if error.error.code == 404 => {
565                    // early return: no need to query schema to check column and type
566                    return self
567                        .create_table(
568                            &client,
569                            project_id,
570                            dataset_id,
571                            table_id,
572                            &self.schema.fields,
573                        )
574                        .await
575                        .map(|_| ());
576                }
577                Err(e) => return Err(SinkError::BigQuery(e.into())),
578                _ => {}
579            }
580        }
581
582        let rs = client
583            .job()
584            .query(
585                &self.config.common.project,
586                QueryRequest::new(format!(
587                    "SELECT column_name, data_type FROM `{}.{}.INFORMATION_SCHEMA.COLUMNS` WHERE table_name = '{}'",
588                    project_id, dataset_id, table_id,
589                )),
590            ).await.map_err(|e| SinkError::BigQuery(e.into()))?;
591        let mut rs = ResultSet::new_from_query_response(rs);
592
593        let mut big_query_schema = HashMap::default();
594        while rs.next_row() {
595            big_query_schema.insert(
596                rs.get_string_by_name("column_name")
597                    .map_err(|e| SinkError::BigQuery(e.into()))?
598                    .ok_or_else(|| {
599                        SinkError::BigQuery(anyhow::anyhow!("Cannot find column_name"))
600                    })?,
601                rs.get_string_by_name("data_type")
602                    .map_err(|e| SinkError::BigQuery(e.into()))?
603                    .ok_or_else(|| {
604                        SinkError::BigQuery(anyhow::anyhow!("Cannot find column_name"))
605                    })?,
606            );
607        }
608
609        self.check_column_name_and_type(big_query_schema)?;
610        Ok(())
611    }
612}
613
614pub struct BigQuerySinkWriter {
615    pub config: BigQueryConfig,
616    #[expect(dead_code)]
617    schema: Schema,
618    #[expect(dead_code)]
619    pk_indices: Vec<usize>,
620    client: StorageWriterClient,
621    is_append_only: bool,
622    row_encoder: ProtoEncoder,
623    writer_pb_schema: ProtoSchema,
624    #[expect(dead_code)]
625    message_descriptor: MessageDescriptor,
626    write_stream: String,
627    proto_field: Option<FieldDescriptor>,
628}
629
630impl TryFrom<SinkParam> for BigQuerySink {
631    type Error = SinkError;
632
633    fn try_from(param: SinkParam) -> std::result::Result<Self, Self::Error> {
634        let schema = param.schema();
635        let pk_indices = param.downstream_pk_or_empty();
636        let config = BigQueryConfig::from_btreemap(param.properties)?;
637        BigQuerySink::new(config, schema, pk_indices, param.sink_type.is_append_only())
638    }
639}
640
641impl BigQuerySinkWriter {
642    pub async fn new(
643        config: BigQueryConfig,
644        schema: Schema,
645        pk_indices: Vec<usize>,
646        is_append_only: bool,
647    ) -> Result<(Self, impl Stream<Item = Result<()>>)> {
648        let (client, resp_stream) = config
649            .common
650            .build_writer_client(&config.aws_auth_props)
651            .await?;
652        let mut descriptor_proto = build_protobuf_schema(
653            schema
654                .fields()
655                .iter()
656                .map(|f| (f.name.as_str(), &f.data_type)),
657            config.common.table.clone(),
658        )?;
659
660        if !is_append_only {
661            let field = FieldDescriptorProto {
662                name: Some(CHANGE_TYPE.to_owned()),
663                number: Some((schema.len() + 1) as i32),
664                r#type: Some(field_descriptor_proto::Type::String.into()),
665                ..Default::default()
666            };
667            descriptor_proto.field.push(field);
668        }
669
670        let descriptor_pool = build_protobuf_descriptor_pool(&descriptor_proto)?;
671        let message_descriptor = descriptor_pool
672            .get_message_by_name(&config.common.table)
673            .ok_or_else(|| {
674                SinkError::BigQuery(anyhow::anyhow!(
675                    "Can't find message proto {}",
676                    config.common.table
677                ))
678            })?;
679        let proto_field = if !is_append_only {
680            let proto_field = message_descriptor
681                .get_field_by_name(CHANGE_TYPE)
682                .ok_or_else(|| {
683                    SinkError::BigQuery(anyhow::anyhow!("Can't find {}", CHANGE_TYPE))
684                })?;
685            Some(proto_field)
686        } else {
687            None
688        };
689        let row_encoder = ProtoEncoder::new(
690            schema.clone(),
691            None,
692            message_descriptor.clone(),
693            ProtoHeader::None,
694        )?;
695        Ok((
696            Self {
697                write_stream: format!(
698                    "projects/{}/datasets/{}/tables/{}/streams/_default",
699                    config.common.project, config.common.dataset, config.common.table
700                ),
701                config,
702                schema,
703                pk_indices,
704                client,
705                is_append_only,
706                row_encoder,
707                message_descriptor,
708                proto_field,
709                writer_pb_schema: ProtoSchema {
710                    proto_descriptor: Some(descriptor_proto.clone()),
711                },
712            },
713            resp_stream,
714        ))
715    }
716
717    fn append_only(&mut self, chunk: StreamChunk) -> Result<Vec<Vec<u8>>> {
718        let mut serialized_rows: Vec<Vec<u8>> = Vec::with_capacity(chunk.capacity());
719        for (op, row) in chunk.rows() {
720            if op != Op::Insert {
721                continue;
722            }
723            serialized_rows.push(self.row_encoder.encode(row)?.ser_to()?)
724        }
725        Ok(serialized_rows)
726    }
727
728    fn upsert(&mut self, chunk: StreamChunk) -> Result<Vec<Vec<u8>>> {
729        let mut serialized_rows: Vec<Vec<u8>> = Vec::with_capacity(chunk.capacity());
730        for (op, row) in chunk.rows() {
731            if op == Op::UpdateDelete {
732                continue;
733            }
734            let mut pb_row = self.row_encoder.encode(row)?;
735            match op {
736                Op::Insert => pb_row
737                    .message
738                    .try_set_field(
739                        self.proto_field.as_ref().unwrap(),
740                        prost_reflect::Value::String("UPSERT".to_owned()),
741                    )
742                    .map_err(|e| SinkError::BigQuery(e.into()))?,
743                Op::Delete => pb_row
744                    .message
745                    .try_set_field(
746                        self.proto_field.as_ref().unwrap(),
747                        prost_reflect::Value::String("DELETE".to_owned()),
748                    )
749                    .map_err(|e| SinkError::BigQuery(e.into()))?,
750                Op::UpdateDelete => continue,
751                Op::UpdateInsert => pb_row
752                    .message
753                    .try_set_field(
754                        self.proto_field.as_ref().unwrap(),
755                        prost_reflect::Value::String("UPSERT".to_owned()),
756                    )
757                    .map_err(|e| SinkError::BigQuery(e.into()))?,
758            };
759
760            serialized_rows.push(pb_row.ser_to()?)
761        }
762        Ok(serialized_rows)
763    }
764
765    fn write_chunk(&mut self, chunk: StreamChunk) -> Result<usize> {
766        let serialized_rows = if self.is_append_only {
767            self.append_only(chunk)?
768        } else {
769            self.upsert(chunk)?
770        };
771        if serialized_rows.is_empty() {
772            return Ok(0);
773        }
774        let mut result = Vec::new();
775        let mut result_inner = Vec::new();
776        let mut size_count = 0;
777        for i in serialized_rows {
778            size_count += i.len();
779            if size_count > MAX_ROW_SIZE {
780                result.push(result_inner);
781                result_inner = Vec::new();
782                size_count = i.len();
783            }
784            result_inner.push(i);
785        }
786        if !result_inner.is_empty() {
787            result.push(result_inner);
788        }
789        let len = result.len();
790        for serialized_rows in result {
791            let rows = AppendRowsRequestRows::ProtoRows(ProtoData {
792                writer_schema: Some(self.writer_pb_schema.clone()),
793                rows: Some(ProtoRows { serialized_rows }),
794            });
795            self.client.append_rows(rows, self.write_stream.clone())?;
796        }
797        Ok(len)
798    }
799}
800
801#[try_stream(ok = (), error = SinkError)]
802pub async fn resp_to_stream(
803    resp_stream: impl Future<
804        Output = std::result::Result<
805            Response<google_cloud_gax::grpc::Streaming<AppendRowsResponse>>,
806            Status,
807        >,
808    >
809    + 'static
810    + Send,
811) {
812    let mut resp_stream = resp_stream
813        .await
814        .map_err(|e| SinkError::BigQuery(e.into()))?
815        .into_inner();
816    loop {
817        match resp_stream
818            .message()
819            .await
820            .map_err(|e| SinkError::BigQuery(e.into()))?
821        {
822            Some(append_rows_response) => {
823                if !append_rows_response.row_errors.is_empty() {
824                    return Err(SinkError::BigQuery(anyhow::anyhow!(
825                        "bigquery insert error {:?}",
826                        append_rows_response.row_errors
827                    )));
828                }
829                if let Some(google_cloud_googleapis::cloud::bigquery::storage::v1::append_rows_response::Response::Error(status)) = append_rows_response.response{
830                            return Err(SinkError::BigQuery(anyhow::anyhow!(
831                                "bigquery insert error {:?}",
832                                status
833                            )));
834                        }
835                yield ();
836            }
837            None => {
838                return Err(SinkError::BigQuery(anyhow::anyhow!(
839                    "bigquery insert error: end of resp stream",
840                )));
841            }
842        }
843    }
844}
845
846struct StorageWriterClient {
847    #[expect(dead_code)]
848    environment: Environment,
849    request_sender: mpsc::UnboundedSender<AppendRowsRequest>,
850}
851impl StorageWriterClient {
852    pub async fn new(
853        credentials: CredentialsFile,
854    ) -> Result<(Self, impl Stream<Item = Result<()>>)> {
855        let ts_grpc = google_cloud_auth::token::DefaultTokenSourceProvider::new_with_credentials(
856            Self::bigquery_grpc_auth_config(),
857            Box::new(credentials),
858        )
859        .await
860        .map_err(|e| SinkError::BigQuery(e.into()))?;
861        let conn_options = ConnectionOptions {
862            connect_timeout: CONNECT_TIMEOUT,
863            timeout: CONNECTION_TIMEOUT,
864            ..Default::default()
865        };
866        let environment = Environment::GoogleCloud(Box::new(ts_grpc));
867        let conn = ConnectionManager::new(DEFAULT_GRPC_CHANNEL_NUMS, &environment, &conn_options)
868            .await
869            .map_err(|e| SinkError::BigQuery(e.into()))?;
870        let mut client = conn.writer();
871
872        let (tx, rx) = mpsc::unbounded_channel();
873        let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx);
874
875        let resp = async move { client.append_rows(Request::new(stream)).await };
876        let resp_stream = resp_to_stream(resp);
877
878        Ok((
879            StorageWriterClient {
880                environment,
881                request_sender: tx,
882            },
883            resp_stream,
884        ))
885    }
886
887    pub fn append_rows(&mut self, row: AppendRowsRequestRows, write_stream: String) -> Result<()> {
888        let append_req = AppendRowsRequest {
889            write_stream,
890            offset: None,
891            trace_id: Uuid::new_v4().hyphenated().to_string(),
892            missing_value_interpretations: HashMap::default(),
893            rows: Some(row),
894            default_missing_value_interpretation: MissingValueInterpretation::DefaultValue as i32,
895        };
896        self.request_sender
897            .send(append_req)
898            .map_err(|e| SinkError::BigQuery(e.into()))?;
899        Ok(())
900    }
901
902    fn bigquery_grpc_auth_config() -> google_cloud_auth::project::Config<'static> {
903        let mut auth_config = google_cloud_auth::project::Config::default();
904        auth_config =
905            auth_config.with_audience(google_cloud_bigquery::grpc::apiv1::conn_pool::AUDIENCE);
906        auth_config =
907            auth_config.with_scopes(&google_cloud_bigquery::grpc::apiv1::conn_pool::SCOPES);
908        auth_config
909    }
910}
911
912fn build_protobuf_descriptor_pool(desc: &DescriptorProto) -> Result<prost_reflect::DescriptorPool> {
913    let file_descriptor = FileDescriptorProto {
914        message_type: vec![desc.clone()],
915        name: Some("bigquery".to_owned()),
916        ..Default::default()
917    };
918
919    prost_reflect::DescriptorPool::from_file_descriptor_set(FileDescriptorSet {
920        file: vec![file_descriptor],
921    })
922    .context("failed to build descriptor pool")
923    .map_err(SinkError::BigQuery)
924}
925
926fn build_protobuf_schema<'a>(
927    fields: impl Iterator<Item = (&'a str, &'a DataType)>,
928    name: String,
929) -> Result<DescriptorProto> {
930    let mut proto = DescriptorProto {
931        name: Some(name),
932        ..Default::default()
933    };
934    let mut struct_vec = vec![];
935    let field_vec = fields
936        .enumerate()
937        .map(|(index, (name, data_type))| {
938            let (field, des_proto) =
939                build_protobuf_field(data_type, (index + 1) as i32, name.to_owned())?;
940            if let Some(sv) = des_proto {
941                struct_vec.push(sv);
942            }
943            Ok(field)
944        })
945        .collect::<Result<Vec<_>>>()?;
946    proto.field = field_vec;
947    proto.nested_type = struct_vec;
948    Ok(proto)
949}
950
951fn build_protobuf_field(
952    data_type: &DataType,
953    index: i32,
954    name: String,
955) -> Result<(FieldDescriptorProto, Option<DescriptorProto>)> {
956    let mut field = FieldDescriptorProto {
957        name: Some(name.clone()),
958        number: Some(index),
959        ..Default::default()
960    };
961    match data_type {
962        DataType::Boolean => field.r#type = Some(field_descriptor_proto::Type::Bool.into()),
963        DataType::Int32 => field.r#type = Some(field_descriptor_proto::Type::Int32.into()),
964        DataType::Int16 | DataType::Int64 => {
965            field.r#type = Some(field_descriptor_proto::Type::Int64.into())
966        }
967        DataType::Float64 => field.r#type = Some(field_descriptor_proto::Type::Double.into()),
968        DataType::Decimal => field.r#type = Some(field_descriptor_proto::Type::String.into()),
969        DataType::Date => field.r#type = Some(field_descriptor_proto::Type::Int32.into()),
970        DataType::Varchar => field.r#type = Some(field_descriptor_proto::Type::String.into()),
971        DataType::Time => field.r#type = Some(field_descriptor_proto::Type::String.into()),
972        DataType::Timestamp => field.r#type = Some(field_descriptor_proto::Type::String.into()),
973        DataType::Timestamptz => field.r#type = Some(field_descriptor_proto::Type::String.into()),
974        DataType::Interval => field.r#type = Some(field_descriptor_proto::Type::String.into()),
975        DataType::Struct(s) => {
976            field.r#type = Some(field_descriptor_proto::Type::Message.into());
977            let name = format!("Struct{}", name);
978            let sub_proto = build_protobuf_schema(s.iter(), name.clone())?;
979            field.type_name = Some(name);
980            return Ok((field, Some(sub_proto)));
981        }
982        DataType::List(l) => {
983            let (mut field, proto) = build_protobuf_field(l.elem(), index, name)?;
984            field.label = Some(field_descriptor_proto::Label::Repeated.into());
985            return Ok((field, proto));
986        }
987        DataType::Bytea => field.r#type = Some(field_descriptor_proto::Type::Bytes.into()),
988        DataType::Jsonb => field.r#type = Some(field_descriptor_proto::Type::String.into()),
989        DataType::Serial => field.r#type = Some(field_descriptor_proto::Type::Int64.into()),
990        DataType::Float32 | DataType::Int256 => {
991            return Err(SinkError::BigQuery(anyhow::anyhow!(
992                "Don't support Float32 and Int256"
993            )));
994        }
995        DataType::Map(_) => return Err(SinkError::BigQuery(anyhow::anyhow!("Don't support Map"))),
996        DataType::Vector(_) => {
997            return Err(SinkError::BigQuery(anyhow::anyhow!("Don't support Vector")));
998        }
999    }
1000    Ok((field, None))
1001}
1002
1003#[cfg(test)]
1004mod test {
1005
1006    use std::assert_matches;
1007    use std::collections::HashMap;
1008
1009    use risingwave_common::catalog::{Field, Schema};
1010    use risingwave_common::types::{DataType, StructType};
1011
1012    use crate::connector_common::AwsAuthProps;
1013    use crate::sink::big_query::{
1014        BigQueryCommon, BigQueryConfig, BigQuerySink, build_protobuf_descriptor_pool,
1015        build_protobuf_schema,
1016    };
1017
1018    #[tokio::test]
1019    async fn test_type_check() {
1020        let big_query_type_string = "ARRAY<STRUCT<v1 ARRAY<INT64>, v2 STRUCT<v1 INT64, v2 INT64>>>";
1021        let rw_datatype = DataType::list(DataType::Struct(StructType::new(vec![
1022            ("v1".to_owned(), DataType::Int64.list()),
1023            (
1024                "v2".to_owned(),
1025                DataType::Struct(StructType::new(vec![
1026                    ("v1".to_owned(), DataType::Int64),
1027                    ("v2".to_owned(), DataType::Int64),
1028                ])),
1029            ),
1030        ])));
1031        assert_eq!(
1032            BigQuerySink::get_string_and_check_support_from_datatype(&rw_datatype).unwrap(),
1033            big_query_type_string
1034        );
1035    }
1036
1037    #[tokio::test]
1038    async fn test_schema_check() {
1039        let schema = Schema {
1040            fields: vec![
1041                Field::with_name(DataType::Int64, "v1"),
1042                Field::with_name(DataType::Float64, "v2"),
1043                Field::with_name(
1044                    DataType::list(DataType::Struct(StructType::new(vec![
1045                        ("v1".to_owned(), DataType::Int64.list()),
1046                        (
1047                            "v3".to_owned(),
1048                            DataType::Struct(StructType::new(vec![
1049                                ("v1".to_owned(), DataType::Int64),
1050                                ("v2".to_owned(), DataType::Int64),
1051                            ])),
1052                        ),
1053                    ]))),
1054                    "v3",
1055                ),
1056            ],
1057        };
1058        let fields = schema
1059            .fields()
1060            .iter()
1061            .map(|f| (f.name.as_str(), &f.data_type));
1062        let desc = build_protobuf_schema(fields, "t1".to_owned()).unwrap();
1063        let pool = build_protobuf_descriptor_pool(&desc).unwrap();
1064        let t1_message = pool.get_message_by_name("t1").unwrap();
1065        assert_matches!(
1066            t1_message.get_field_by_name("v1").unwrap().kind(),
1067            prost_reflect::Kind::Int64
1068        );
1069        assert_matches!(
1070            t1_message.get_field_by_name("v2").unwrap().kind(),
1071            prost_reflect::Kind::Double
1072        );
1073        assert_matches!(
1074            t1_message.get_field_by_name("v3").unwrap().kind(),
1075            prost_reflect::Kind::Message(_)
1076        );
1077
1078        let v3_message = pool.get_message_by_name("t1.Structv3").unwrap();
1079        assert_matches!(
1080            v3_message.get_field_by_name("v1").unwrap().kind(),
1081            prost_reflect::Kind::Int64
1082        );
1083        assert!(v3_message.get_field_by_name("v1").unwrap().is_list());
1084
1085        let v3_v3_message = pool.get_message_by_name("t1.Structv3.Structv3").unwrap();
1086        assert_matches!(
1087            v3_v3_message.get_field_by_name("v1").unwrap().kind(),
1088            prost_reflect::Kind::Int64
1089        );
1090        assert_matches!(
1091            v3_v3_message.get_field_by_name("v2").unwrap().kind(),
1092            prost_reflect::Kind::Int64
1093        );
1094    }
1095
1096    #[test]
1097    fn test_decimal_type_family_compatibility() {
1098        assert!(BigQuerySink::is_decimal_type_compatible("NUMERIC"));
1099        assert!(BigQuerySink::is_decimal_type_compatible("numeric(31, 2)"));
1100        assert!(BigQuerySink::is_decimal_type_compatible("BIGNUMERIC"));
1101        assert!(BigQuerySink::is_decimal_type_compatible(
1102            "bignumeric(35, 12)"
1103        ));
1104        assert!(!BigQuerySink::is_decimal_type_compatible("STRING"));
1105    }
1106
1107    #[test]
1108    fn test_decimal_schema_check_accepts_parameterized_numeric_types() {
1109        let sink = BigQuerySink {
1110            config: BigQueryConfig {
1111                common: BigQueryCommon {
1112                    local_path: None,
1113                    s3_path: None,
1114                    project: "project".to_owned(),
1115                    dataset: "dataset".to_owned(),
1116                    table: "table".to_owned(),
1117                    auto_create: false,
1118                    credentials: None,
1119                },
1120                aws_auth_props: AwsAuthProps {
1121                    region: None,
1122                    endpoint: None,
1123                    access_key: None,
1124                    secret_key: None,
1125                    session_token: None,
1126                    arn: None,
1127                    external_id: None,
1128                    profile: None,
1129                    msk_signer_timeout_sec: None,
1130                },
1131                r#type: "append-only".to_owned(),
1132            },
1133            schema: Schema {
1134                fields: vec![Field::with_name(DataType::Decimal, "capitalizedcost")],
1135            },
1136            pk_indices: vec![],
1137            is_append_only: true,
1138        };
1139
1140        sink.check_column_name_and_type(HashMap::from([(
1141            "capitalizedcost".to_owned(),
1142            "NUMERIC(31, 2)".to_owned(),
1143        )]))
1144        .unwrap();
1145
1146        sink.check_column_name_and_type(HashMap::from([(
1147            "capitalizedcost".to_owned(),
1148            "BIGNUMERIC(35, 12)".to_owned(),
1149        )]))
1150        .unwrap();
1151    }
1152}