Skip to main content

connectorx/
partition.rs

1use std::convert::TryFrom;
2use std::sync::Arc;
3
4use crate::errors::{ConnectorXOutError, OutResult};
5use crate::source_router::{SourceConn, SourceType};
6#[cfg(feature = "src_bigquery")]
7use crate::sources::bigquery::BigQueryDialect;
8#[cfg(feature = "src_clickhouse")]
9use crate::sources::clickhouse::{ClickHouseSource, ClickHouseSourceError};
10#[cfg(feature = "src_mssql_tiberius")]
11use crate::sources::mssql::{mssql_config, FloatN, IntN, MsSQLTypeSystem};
12#[cfg(feature = "src_mysql")]
13use crate::sources::mysql::{build_opts, MySQLTypeSystem};
14#[cfg(feature = "src_oracle")]
15use crate::sources::oracle::{OracleDialect, OracleSource};
16#[cfg(feature = "src_postgres")]
17use crate::sources::postgres::{rewrite_tls_args, PostgresTypeSystem};
18#[cfg(feature = "src_trino")]
19use crate::sources::trino::TrinoDialect;
20#[cfg(feature = "src_sqlite")]
21use crate::sql::get_partition_range_query_sep;
22use crate::sql::{get_partition_range_query, single_col_partition_query, CXQuery};
23use anyhow::anyhow;
24use fehler::{throw, throws};
25#[cfg(feature = "src_bigquery")]
26use gcp_bigquery_client;
27#[cfg(feature = "src_mysql")]
28use r2d2_mysql::mysql::{prelude::Queryable, Pool, Row};
29#[cfg(feature = "src_sqlite")]
30use rusqlite::{types::Type, Connection};
31#[cfg(feature = "src_postgres")]
32use rust_decimal::{prelude::ToPrimitive, Decimal};
33#[cfg(feature = "src_postgres")]
34use rust_decimal_macros::dec;
35#[cfg(feature = "src_clickhouse")]
36use serde::Deserialize;
37#[cfg(feature = "src_clickhouse")]
38use serde_json::Value as JsonValue;
39#[cfg(feature = "src_clickhouse")]
40use sqlparser::dialect::ClickHouseDialect;
41#[cfg(feature = "src_mssql_common")]
42use sqlparser::dialect::MsSqlDialect;
43#[cfg(feature = "src_mysql")]
44use sqlparser::dialect::MySqlDialect;
45#[cfg(feature = "src_postgres")]
46use sqlparser::dialect::PostgreSqlDialect;
47#[cfg(feature = "src_sqlite")]
48use sqlparser::dialect::SQLiteDialect;
49#[cfg(feature = "src_mssql_tiberius")]
50use tiberius::Client;
51#[cfg(feature = "src_mssql_tiberius")]
52use tokio::net::TcpStream;
53#[cfg(any(
54    feature = "src_bigquery",
55    feature = "src_mssql_tiberius",
56    feature = "src_trino"
57))]
58use tokio::runtime::Runtime;
59#[cfg(feature = "src_mssql_tiberius")]
60use tokio_util::compat::TokioAsyncWriteCompatExt;
61use url::Url;
62
63pub struct PartitionQuery {
64    query: String,
65    column: String,
66    min: Option<i64>,
67    max: Option<i64>,
68    num: usize,
69}
70
71impl PartitionQuery {
72    pub fn new(query: &str, column: &str, min: Option<i64>, max: Option<i64>, num: usize) -> Self {
73        Self {
74            query: query.into(),
75            column: column.into(),
76            min,
77            max,
78            num,
79        }
80    }
81}
82
83pub fn partition(part: &PartitionQuery, source_conn: &SourceConn) -> OutResult<Vec<CXQuery>> {
84    let mut queries = vec![];
85
86    if part.num == 0 {
87        throw!(anyhow!("partition count (num) must be greater than zero"));
88    }
89
90    let num = i64::try_from(part.num).map_err(|_| {
91        anyhow!(
92            "partition count (num) is too large to represent safely: {}",
93            part.num
94        )
95    })?;
96
97    let (min, max) = match (part.min, part.max) {
98        (None, None) => get_col_range(source_conn, &part.query, &part.column)?,
99        (Some(min), Some(max)) => (min, max),
100        _ => throw!(anyhow!(
101            "partition_query range can not be partially specified",
102        )),
103    };
104
105    if max < min {
106        throw!(anyhow!(
107            "partition range is invalid: max ({}) must be greater than or equal to min ({})",
108            max,
109            min
110        ));
111    }
112
113    let range_len = max
114        .checked_sub(min)
115        .and_then(|value| value.checked_add(1))
116        .ok_or_else(|| {
117            anyhow!(
118                "partition range overflow: min={}, max={} is too large",
119                min,
120                max
121            )
122        })?;
123
124    let partition_size = range_len / num;
125
126    let final_upper = max.checked_add(1).ok_or_else(|| {
127        anyhow!(
128            "partition upper bound overflow: max={} cannot be incremented safely",
129            max
130        )
131    })?;
132
133    for i in 0i64..num {
134        let lower = i
135            .checked_mul(partition_size)
136            .and_then(|offset| min.checked_add(offset))
137            .ok_or_else(|| {
138                anyhow!(
139                    "partition lower bound overflow: min={}, step={}, partition_size={}",
140                    min,
141                    i,
142                    partition_size
143                )
144            })?;
145        let upper = if i == num - 1 {
146            final_upper
147        } else {
148            (i + 1)
149                .checked_mul(partition_size)
150                .and_then(|offset| min.checked_add(offset))
151                .ok_or_else(|| {
152                    anyhow!(
153                        "partition upper bound overflow: min={}, step={}, partition_size={}",
154                        min,
155                        i + 1,
156                        partition_size
157                    )
158                })?
159        };
160        let partition_query = get_part_query(source_conn, &part.query, &part.column, lower, upper)?;
161        queries.push(partition_query);
162    }
163    Ok(queries)
164}
165
166pub fn get_col_range(source_conn: &SourceConn, query: &str, col: &str) -> OutResult<(i64, i64)> {
167    match source_conn.ty {
168        #[cfg(feature = "src_postgres")]
169        SourceType::Postgres => pg_get_partition_range(&source_conn.conn, query, col),
170        #[cfg(feature = "src_sqlite")]
171        SourceType::SQLite => sqlite_get_partition_range(&source_conn.conn, query, col),
172        #[cfg(feature = "src_mysql")]
173        SourceType::MySQL => mysql_get_partition_range(&source_conn.conn, query, col),
174        #[cfg(all(feature = "src_mssql_tiberius", not(feature = "src_mssql_tds")))]
175        SourceType::MsSQL => mssql_get_partition_range(&source_conn.conn, query, col),
176        #[cfg(all(feature = "src_mssql_tiberius", feature = "src_mssql_tds"))]
177        SourceType::MsSQL => match crate::sources::mssql::active_driver() {
178            crate::sources::mssql::MsSQLDriverKind::Tiberius => {
179                mssql_get_partition_range(&source_conn.conn, query, col)
180            }
181            crate::sources::mssql::MsSQLDriverKind::MssqlTds => Ok(
182                crate::sources::mssql::tds_get_partition_range(&source_conn.conn, query, col)?,
183            ),
184        },
185        #[cfg(all(feature = "src_mssql_tds", not(feature = "src_mssql_tiberius")))]
186        SourceType::MsSQL => Ok(crate::sources::mssql::tds_get_partition_range(
187            &source_conn.conn,
188            query,
189            col,
190        )?),
191        #[cfg(feature = "src_oracle")]
192        SourceType::Oracle => oracle_get_partition_range(&source_conn.conn, query, col),
193        #[cfg(feature = "src_bigquery")]
194        SourceType::BigQuery => bigquery_get_partition_range(&source_conn.conn, query, col),
195        #[cfg(feature = "src_trino")]
196        SourceType::Trino => trino_get_partition_range(&source_conn.conn, query, col),
197        #[cfg(feature = "src_clickhouse")]
198        SourceType::ClickHouse => clickhouse_get_partition_range(&source_conn.conn, query, col),
199        _ => unimplemented!("{:?} not implemented!", source_conn.ty),
200    }
201}
202
203#[throws(ConnectorXOutError)]
204pub fn get_part_query(
205    source_conn: &SourceConn,
206    query: &str,
207    col: &str,
208    lower: i64,
209    upper: i64,
210) -> CXQuery<String> {
211    let query = match source_conn.ty {
212        #[cfg(feature = "src_postgres")]
213        SourceType::Postgres => {
214            single_col_partition_query(query, col, lower, upper, &PostgreSqlDialect {})?
215        }
216        #[cfg(feature = "src_sqlite")]
217        SourceType::SQLite => {
218            single_col_partition_query(query, col, lower, upper, &SQLiteDialect {})?
219        }
220        #[cfg(feature = "src_mysql")]
221        SourceType::MySQL => {
222            single_col_partition_query(query, col, lower, upper, &MySqlDialect {})?
223        }
224        #[cfg(feature = "src_mssql_common")]
225        SourceType::MsSQL => {
226            single_col_partition_query(query, col, lower, upper, &MsSqlDialect {})?
227        }
228        #[cfg(feature = "src_oracle")]
229        SourceType::Oracle => {
230            single_col_partition_query(query, col, lower, upper, &OracleDialect {})?
231        }
232        #[cfg(feature = "src_bigquery")]
233        SourceType::BigQuery => {
234            single_col_partition_query(query, col, lower, upper, &BigQueryDialect {})?
235        }
236        #[cfg(feature = "src_trino")]
237        SourceType::Trino => {
238            single_col_partition_query(query, col, lower, upper, &TrinoDialect {})?
239        }
240        #[cfg(feature = "src_clickhouse")]
241        SourceType::ClickHouse => {
242            single_col_partition_query(query, col, lower, upper, &ClickHouseDialect {})?
243        }
244        _ => unimplemented!("{:?} not implemented!", source_conn.ty),
245    };
246    CXQuery::Wrapped(query)
247}
248
249#[cfg(feature = "src_postgres")]
250#[throws(ConnectorXOutError)]
251fn pg_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
252    let (config, tls) = rewrite_tls_args(conn)?;
253    let mut client = match tls {
254        None => config.connect(postgres::NoTls)?,
255        Some(tls_conn) => config.connect(tls_conn)?,
256    };
257    let range_query = get_partition_range_query(query, col, &PostgreSqlDialect {})?;
258    let row = client.query_one(range_query.as_str(), &[])?;
259
260    let col_type = PostgresTypeSystem::from(row.columns()[0].type_());
261    let (min_v, max_v) = match col_type {
262        PostgresTypeSystem::Int2(_) => {
263            let min_v: Option<i16> = row.get(0);
264            let max_v: Option<i16> = row.get(1);
265            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
266        }
267        PostgresTypeSystem::Int4(_) => {
268            let min_v: Option<i32> = row.get(0);
269            let max_v: Option<i32> = row.get(1);
270            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
271        }
272        PostgresTypeSystem::Int8(_) => {
273            let min_v: Option<i64> = row.get(0);
274            let max_v: Option<i64> = row.get(1);
275            (min_v.unwrap_or(0), max_v.unwrap_or(0))
276        }
277        PostgresTypeSystem::Float4(_) => {
278            let min_v: Option<f32> = row.get(0);
279            let max_v: Option<f32> = row.get(1);
280            (min_v.unwrap_or(0.0) as i64, max_v.unwrap_or(0.0) as i64)
281        }
282        PostgresTypeSystem::Float8(_) => {
283            let min_v: Option<f64> = row.get(0);
284            let max_v: Option<f64> = row.get(1);
285            (min_v.unwrap_or(0.0) as i64, max_v.unwrap_or(0.0) as i64)
286        }
287        PostgresTypeSystem::Numeric(_) => {
288            let min_v: Option<Decimal> = row.get(0);
289            let max_v: Option<Decimal> = row.get(1);
290            (
291                min_v.unwrap_or(dec!(0.0)).to_i64().unwrap_or(0),
292                max_v.unwrap_or(dec!(0.0)).to_i64().unwrap_or(0),
293            )
294        }
295        _ => throw!(anyhow!(
296            "Partition can only be done on int or float columns"
297        )),
298    };
299
300    (min_v, max_v)
301}
302
303#[cfg(feature = "src_sqlite")]
304#[throws(ConnectorXOutError)]
305fn sqlite_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
306    // remove the first "sqlite://" manually since url.path is not correct for windows and for relative path
307    let conn = Connection::open(&conn.as_str()[9..])?;
308    // SQLite only optimize min max queries when there is only one aggregation
309    // https://www.sqlite.org/optoverview.html#minmax
310    let (min_query, max_query) = get_partition_range_query_sep(query, col, &SQLiteDialect {})?;
311    let mut error = None;
312    let min_v = conn.query_row(min_query.as_str(), [], |row| {
313        // declare type for count query will be None, only need to check the returned value type
314        let col_type = row.get_ref(0)?.data_type();
315        match col_type {
316            Type::Integer => row.get(0),
317            Type::Real => {
318                let v: f64 = row.get(0)?;
319                Ok(v as i64)
320            }
321            Type::Null => Ok(0),
322            _ => {
323                error = Some(anyhow!("Partition can only be done on integer columns"));
324                Ok(0)
325            }
326        }
327    })?;
328    match error {
329        None => {}
330        Some(e) => throw!(e),
331    }
332    let max_v = conn.query_row(max_query.as_str(), [], |row| {
333        let col_type = row.get_ref(0)?.data_type();
334        match col_type {
335            Type::Integer => row.get(0),
336            Type::Real => {
337                let v: f64 = row.get(0)?;
338                Ok(v as i64)
339            }
340            Type::Null => Ok(0),
341            _ => {
342                error = Some(anyhow!("Partition can only be done on integer columns"));
343                Ok(0)
344            }
345        }
346    })?;
347    match error {
348        None => {}
349        Some(e) => throw!(e),
350    }
351
352    (min_v, max_v)
353}
354
355#[cfg(feature = "src_mysql")]
356#[throws(ConnectorXOutError)]
357fn mysql_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
358    let pool = Pool::new(build_opts(conn.as_str())?)?;
359    let mut conn = pool.get_conn()?;
360    let range_query = get_partition_range_query(query, col, &MySqlDialect {})?;
361    let row: Row = conn
362        .query_first(range_query)?
363        .ok_or_else(|| anyhow!("mysql range: no row returns"))?;
364
365    let col_type = MySQLTypeSystem::from((
366        &row.columns()[0].column_type(),
367        &row.columns()[0].flags(),
368        row.columns()[0].character_set(),
369    ));
370
371    let (min_v, max_v) = match col_type {
372        MySQLTypeSystem::Tiny(_) => {
373            let min_v: Option<i8> = row
374                .get(0)
375                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
376            let max_v: Option<i8> = row
377                .get(1)
378                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
379            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
380        }
381        MySQLTypeSystem::Short(_) => {
382            let min_v: Option<i16> = row
383                .get(0)
384                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
385            let max_v: Option<i16> = row
386                .get(1)
387                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
388            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
389        }
390        MySQLTypeSystem::Int24(_) => {
391            let min_v: Option<i32> = row
392                .get(0)
393                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
394            let max_v: Option<i32> = row
395                .get(1)
396                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
397            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
398        }
399        MySQLTypeSystem::Long(_) => {
400            let min_v: Option<i64> = row
401                .get(0)
402                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
403            let max_v: Option<i64> = row
404                .get(1)
405                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
406            (min_v.unwrap_or(0), max_v.unwrap_or(0))
407        }
408        MySQLTypeSystem::LongLong(_) => {
409            let min_v: Option<i64> = row
410                .get(0)
411                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
412            let max_v: Option<i64> = row
413                .get(1)
414                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
415            (min_v.unwrap_or(0), max_v.unwrap_or(0))
416        }
417        MySQLTypeSystem::UTiny(_) => {
418            let min_v: Option<u8> = row
419                .get(0)
420                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
421            let max_v: Option<u8> = row
422                .get(1)
423                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
424            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
425        }
426        MySQLTypeSystem::UShort(_) => {
427            let min_v: Option<u16> = row
428                .get(0)
429                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
430            let max_v: Option<u16> = row
431                .get(1)
432                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
433            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
434        }
435        MySQLTypeSystem::UInt24(_) => {
436            let min_v: Option<u32> = row
437                .get(0)
438                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
439            let max_v: Option<u32> = row
440                .get(1)
441                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
442            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
443        }
444        MySQLTypeSystem::ULong(_) => {
445            let min_v: Option<u32> = row
446                .get(0)
447                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
448            let max_v: Option<u32> = row
449                .get(1)
450                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
451            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
452        }
453        MySQLTypeSystem::ULongLong(_) => {
454            let min_v: Option<u64> = row
455                .get(0)
456                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
457            let max_v: Option<u64> = row
458                .get(1)
459                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
460            (min_v.unwrap_or(0) as i64, max_v.unwrap_or(0) as i64)
461        }
462        MySQLTypeSystem::Float(_) => {
463            let min_v: Option<f32> = row
464                .get(0)
465                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
466            let max_v: Option<f32> = row
467                .get(1)
468                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
469            (min_v.unwrap_or(0.0) as i64, max_v.unwrap_or(0.0) as i64)
470        }
471        MySQLTypeSystem::Double(_) => {
472            let min_v: Option<f64> = row
473                .get(0)
474                .ok_or_else(|| anyhow!("mysql range: cannot get min value"))?;
475            let max_v: Option<f64> = row
476                .get(1)
477                .ok_or_else(|| anyhow!("mysql range: cannot get max value"))?;
478            (min_v.unwrap_or(0.0) as i64, max_v.unwrap_or(0.0) as i64)
479        }
480        _ => throw!(anyhow!("Partition can only be done on int columns")),
481    };
482
483    (min_v, max_v)
484}
485
486#[cfg(feature = "src_mssql_tiberius")]
487#[throws(ConnectorXOutError)]
488fn mssql_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
489    let rt = Runtime::new().expect("Failed to create runtime");
490    let config = mssql_config(conn)?;
491    let tcp = rt.block_on(TcpStream::connect(config.get_addr()))?;
492    tcp.set_nodelay(true)?;
493
494    let mut client = rt.block_on(Client::connect(config, tcp.compat_write()))?;
495
496    let range_query = get_partition_range_query(query, col, &MsSqlDialect {})?;
497    let query_result = rt.block_on(client.query(range_query.as_str(), &[]))?;
498    let row = rt.block_on(query_result.into_row())?.unwrap();
499
500    let col_type = MsSQLTypeSystem::from(&row.columns()[0].column_type());
501    let (min_v, max_v) = match col_type {
502        MsSQLTypeSystem::Tinyint(_) => {
503            let min_v: u8 = row.get(0).unwrap_or(0);
504            let max_v: u8 = row.get(1).unwrap_or(0);
505            (min_v as i64, max_v as i64)
506        }
507        MsSQLTypeSystem::Smallint(_) => {
508            let min_v: i16 = row.get(0).unwrap_or(0);
509            let max_v: i16 = row.get(1).unwrap_or(0);
510            (min_v as i64, max_v as i64)
511        }
512        MsSQLTypeSystem::Int(_) => {
513            let min_v: i32 = row.get(0).unwrap_or(0);
514            let max_v: i32 = row.get(1).unwrap_or(0);
515            (min_v as i64, max_v as i64)
516        }
517        MsSQLTypeSystem::Bigint(_) => {
518            let min_v: i64 = row.get(0).unwrap_or(0);
519            let max_v: i64 = row.get(1).unwrap_or(0);
520            (min_v, max_v)
521        }
522        MsSQLTypeSystem::Intn(_) => {
523            let min_v: IntN = row.get(0).unwrap_or(IntN(0));
524            let max_v: IntN = row.get(1).unwrap_or(IntN(0));
525            (min_v.0, max_v.0)
526        }
527        MsSQLTypeSystem::Float24(_) => {
528            let min_v: f32 = row.get(0).unwrap_or(0.0);
529            let max_v: f32 = row.get(1).unwrap_or(0.0);
530            (min_v as i64, max_v as i64)
531        }
532        MsSQLTypeSystem::Float53(_) => {
533            let min_v: f64 = row.get(0).unwrap_or(0.0);
534            let max_v: f64 = row.get(1).unwrap_or(0.0);
535            (min_v as i64, max_v as i64)
536        }
537        MsSQLTypeSystem::Floatn(_) => {
538            let min_v: FloatN = row.get(0).unwrap_or(FloatN(0.0));
539            let max_v: FloatN = row.get(1).unwrap_or(FloatN(0.0));
540            (min_v.0 as i64, max_v.0 as i64)
541        }
542        _ => throw!(anyhow!(
543            "Partition can only be done on int or float columns"
544        )),
545    };
546
547    (min_v, max_v)
548}
549
550#[cfg(feature = "src_oracle")]
551#[throws(ConnectorXOutError)]
552fn oracle_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
553    let source = OracleSource::new(conn.as_str(), 1)?;
554    let conn = source.get_conn()?;
555    let range_query = get_partition_range_query(query, col, &OracleDialect {})?;
556    let row = conn.query_row(range_query.as_str(), &[])?;
557    let min_v: i64 = row.get(0).unwrap_or(0);
558    let max_v: i64 = row.get(1).unwrap_or(0);
559    (min_v, max_v)
560}
561
562#[cfg(feature = "src_bigquery")]
563#[throws(ConnectorXOutError)] // TODO
564fn bigquery_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
565    let rt = Runtime::new().expect("Failed to create runtime");
566    let url = Url::parse(conn.as_str())?;
567    let sa_key_path = url.path();
568    let client = rt.block_on(gcp_bigquery_client::Client::from_service_account_key_file(
569        sa_key_path,
570    ))?;
571
572    let auth_data = std::fs::read_to_string(sa_key_path)?;
573    let auth_json: serde_json::Value = serde_json::from_str(&auth_data)?;
574    let project_id = auth_json
575        .get("project_id")
576        .ok_or_else(|| anyhow!("Cannot get project_id from auth file"))?
577        .as_str()
578        .ok_or_else(|| anyhow!("Cannot get project_id as string from auth file"))?;
579    let range_query = get_partition_range_query(query, col, &BigQueryDialect {})?;
580
581    let query_result = rt.block_on(client.job().query(
582        project_id,
583        gcp_bigquery_client::model::query_request::QueryRequest::new(range_query.as_str()),
584    ))?;
585    let mut rs = gcp_bigquery_client::model::query_response::ResultSet::new_from_query_response(
586        query_result,
587    );
588    rs.next_row();
589    let min_v = rs.get_i64(0)?.unwrap_or(0);
590    let max_v = rs.get_i64(1)?.unwrap_or(0);
591
592    (min_v, max_v)
593}
594
595#[cfg(feature = "src_trino")]
596#[throws(ConnectorXOutError)]
597fn trino_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
598    use crate::sources::trino::{build_client_from_url, TrinoDialect, TrinoPartitionQueryResult};
599
600    let rt = Runtime::new().expect("Failed to create runtime");
601
602    let client =
603        build_client_from_url(conn).map_err(|e| anyhow!("Failed to build Trino client: {}", e))?;
604
605    let range_query = get_partition_range_query(query, col, &TrinoDialect {})?;
606    let query_result = rt.block_on(client.get_all::<TrinoPartitionQueryResult>(range_query));
607
608    let query_result = match query_result {
609        Ok(query_result) => Ok(query_result.into_vec()),
610        Err(e) => match e {
611            prusto::error::Error::EmptyData => {
612                Ok(vec![TrinoPartitionQueryResult { _col0: 0, _col1: 0 }])
613            }
614            _ => Err(anyhow!("Failed to get query result: {}", e)),
615        },
616    }?;
617
618    let result = query_result
619        .first()
620        .unwrap_or(&TrinoPartitionQueryResult { _col0: 0, _col1: 0 });
621
622    (result._col0, result._col1)
623}
624
625#[cfg(feature = "src_clickhouse")]
626#[throws(ConnectorXOutError)]
627fn clickhouse_get_partition_range(conn: &Url, query: &str, col: &str) -> (i64, i64) {
628    use sqlparser::dialect::ClickHouseDialect;
629
630    let rt = Arc::new(tokio::runtime::Runtime::new().expect("Failed to create runtime"));
631    let clickhouse_source = ClickHouseSource::new(rt.clone(), conn.as_str())
632        .expect("Failed to create ClickHouse client");
633
634    let range_query = get_partition_range_query(query, col, &ClickHouseDialect {})?;
635
636    let response = rt.block_on(async {
637        let mut cursor = clickhouse_source
638            .client
639            .query(range_query.as_str())
640            .fetch_bytes("JSONCompact")
641            .map_err(|e| anyhow!("ClickHouse error: {}", e))?;
642        let bytes = cursor
643            .collect()
644            .await
645            .map_err(|e| anyhow!("ClickHouse error: {}", e))?;
646        Ok::<_, ClickHouseSourceError>(bytes)
647    })?;
648
649    #[derive(Debug, Deserialize)]
650    struct MinMaxResponse {
651        data: Vec<Vec<JsonValue>>,
652    }
653
654    let parsed: MinMaxResponse = serde_json::from_slice(&response)
655        .map_err(|e| anyhow!("Failed to parse min max response: {}", e))?;
656
657    let (min_v, max_v) = if let Some(row) = parsed.data.first() {
658        let min_v = row.first().and_then(|v| v.as_i64()).unwrap_or(0);
659        let max_v = row.get(1).and_then(|v| v.as_i64()).unwrap_or(0);
660
661        (min_v, max_v)
662    } else {
663        (0, 0)
664    };
665
666    (min_v, max_v)
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672
673    /// Builds a `SourceConn` backed by SQLite, which does not require a live
674    /// database connection to generate partition queries (the SQL is
675    /// generated purely from the parsed dialect).
676    fn sqlite_source_conn() -> SourceConn {
677        SourceConn::new(
678            SourceType::SQLite,
679            Url::parse("sqlite://test.db").unwrap(),
680            "binary".to_string(),
681        )
682    }
683
684    fn queries_as_strings(queries: &[CXQuery]) -> Vec<String> {
685        queries.iter().map(|q| q.to_string()).collect()
686    }
687
688    #[test]
689    fn partitions_evenly_divisible_range() {
690        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(0), Some(9), 2);
691        let source_conn = sqlite_source_conn();
692        let queries = partition(&part, &source_conn).unwrap();
693
694        assert_eq!(queries.len(), 2);
695        let strings = queries_as_strings(&queries);
696        assert!(strings[0].contains("0 <=") && strings[0].contains("< 5"));
697        assert!(strings[1].contains("5 <=") && strings[1].contains("< 10"));
698    }
699
700    #[test]
701    fn partitions_range_not_evenly_divisible() {
702        // Range [0, 10] has 11 values split across 3 partitions: sizes 3, 3, 3 with
703        // remainder handled by the last partition absorbing everything up to max + 1.
704        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(0), Some(10), 3);
705        let source_conn = sqlite_source_conn();
706        let queries = partition(&part, &source_conn).unwrap();
707
708        assert_eq!(queries.len(), 3);
709        let strings = queries_as_strings(&queries);
710        assert!(strings[0].contains("0 <=") && strings[0].contains("< 3"));
711        assert!(strings[1].contains("3 <=") && strings[1].contains("< 6"));
712        // Last partition always goes up to max + 1, regardless of even division.
713        assert!(strings[2].contains("6 <=") && strings[2].contains("< 11"));
714    }
715
716    #[test]
717    fn single_partition_covers_whole_range() {
718        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(5), Some(15), 1);
719        let source_conn = sqlite_source_conn();
720        let queries = partition(&part, &source_conn).unwrap();
721
722        assert_eq!(queries.len(), 1);
723        let strings = queries_as_strings(&queries);
724        assert!(strings[0].contains("5 <=") && strings[0].contains("< 16"));
725    }
726
727    #[test]
728    fn min_equals_max_single_row_range() {
729        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(7), Some(7), 1);
730        let source_conn = sqlite_source_conn();
731        let queries = partition(&part, &source_conn).unwrap();
732
733        assert_eq!(queries.len(), 1);
734        let strings = queries_as_strings(&queries);
735        assert!(strings[0].contains("7 <=") && strings[0].contains("< 8"));
736    }
737
738    #[test]
739    fn negative_range_partitions_correctly() {
740        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(-10), Some(-1), 2);
741        let source_conn = sqlite_source_conn();
742        let queries = partition(&part, &source_conn).unwrap();
743
744        assert_eq!(queries.len(), 2);
745        let strings = queries_as_strings(&queries);
746        assert!(strings[0].contains("-10 <=") && strings[0].contains("< -5"));
747        assert!(strings[1].contains("-5 <=") && strings[1].contains("< 0"));
748    }
749
750    #[test]
751    fn partially_specified_range_returns_error() {
752        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(0), None, 2);
753        let source_conn = sqlite_source_conn();
754        let result = partition(&part, &source_conn);
755
756        assert!(result.is_err());
757    }
758
759    #[test]
760    fn zero_partitions_returns_error_on_division_by_zero() {
761        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(0), Some(9), 0);
762        let source_conn = sqlite_source_conn();
763        let result = partition(&part, &source_conn);
764        assert!(result.is_err());
765        let err = result.unwrap_err().to_string();
766        assert!(err.contains("partition count (num) must be greater than zero"));
767    }
768
769    #[test]
770    fn inverted_range_returns_error() {
771        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(10), Some(0), 2);
772        let source_conn = sqlite_source_conn();
773        let result = partition(&part, &source_conn);
774        assert!(result.is_err());
775        let err = result.unwrap_err().to_string();
776        assert!(
777            err.contains("partition range is invalid"),
778            "unexpected error message: {}",
779            err
780        );
781    }
782
783    #[test]
784    fn more_partitions_than_range_values_preserves_partition_count() {
785        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(0), Some(1), 4);
786        let source_conn = sqlite_source_conn();
787        let queries = partition(&part, &source_conn).unwrap();
788
789        assert_eq!(queries.len(), 4);
790        let strings = queries_as_strings(&queries);
791        assert!(strings[0].contains("0 <=") && strings[0].contains("< 0"));
792        assert!(strings[1].contains("0 <=") && strings[1].contains("< 0"));
793        assert!(strings[2].contains("0 <=") && strings[2].contains("< 0"));
794        assert!(strings[3].contains("0 <=") && strings[3].contains("< 2"));
795    }
796
797    #[test]
798    fn large_range_does_not_overflow_with_small_num() {
799        // Sanity check that reasonably large but safe ranges partition correctly
800        // without overflow for a small number of partitions.
801        let part = PartitionQuery::new(
802            "SELECT * FROM test",
803            "id",
804            Some(0),
805            Some(1_000_000_000_000),
806            4,
807        );
808        let source_conn = sqlite_source_conn();
809        let queries = partition(&part, &source_conn).unwrap();
810
811        assert_eq!(queries.len(), 4);
812        let strings = queries_as_strings(&queries);
813        assert!(strings[0].contains("0 <="));
814        assert!(strings[3].contains("< 1000000000001"));
815    }
816
817    #[test]
818    fn many_small_partitions_produce_correct_count_and_bounds() {
819        let part = PartitionQuery::new("SELECT * FROM test", "id", Some(1), Some(100), 10);
820        let source_conn = sqlite_source_conn();
821        let queries = partition(&part, &source_conn).unwrap();
822
823        assert_eq!(queries.len(), 10);
824        let strings = queries_as_strings(&queries);
825        assert!(strings[0].contains("1 <=") && strings[0].contains("< 11"));
826        assert!(strings[9].contains("91 <=") && strings[9].contains("< 101"));
827    }
828
829    #[test]
830    fn partition_range_overflow() {
831        // (max - min + 1) overflows i64 when min is i64::MIN and max is i64::MAX
832        let part = PartitionQuery::new(
833            "SELECT * FROM test",
834            "id",
835            Some(i64::MIN),
836            Some(i64::MAX),
837            2,
838        );
839        let source_conn = sqlite_source_conn();
840        let res = partition(&part, &source_conn);
841        assert!(res.is_err());
842        let err = res.unwrap_err().to_string();
843        assert!(
844            err.contains("partition range overflow"),
845            "unexpected error message: {}",
846            err
847        );
848    }
849
850    #[test]
851    fn partition_upper_overflow_at_i64_max() {
852        // max is i64::MAX; range_len is 6 (does not overflow),
853        // but max + 1 for the partition upper bound overflows i64.
854        let part = PartitionQuery::new(
855            "SELECT * FROM test",
856            "id",
857            Some(i64::MAX - 5),
858            Some(i64::MAX),
859            1,
860        );
861        let source_conn = sqlite_source_conn();
862        let res = partition(&part, &source_conn);
863        assert!(res.is_err());
864        let err = res.unwrap_err().to_string();
865        assert!(
866            err.contains("partition upper bound overflow"),
867            "unexpected error message: {}",
868            err
869        );
870    }
871
872    #[test]
873    fn partition_upper_overflow_multi_partition() {
874        // Multi-partition where first partition succeeds but last partition overflows max + 1
875        let part = PartitionQuery::new(
876            "SELECT * FROM test",
877            "id",
878            Some(i64::MAX - 5),
879            Some(i64::MAX),
880            2,
881        );
882        let source_conn = sqlite_source_conn();
883        let res = partition(&part, &source_conn);
884        assert!(res.is_err());
885        let err = res.unwrap_err().to_string();
886        assert!(
887            err.contains("partition upper bound overflow"),
888            "unexpected error message: {}",
889            err
890        );
891    }
892}