1use std::{sync::Arc, time::Duration};
21
22use anyhow::{Context, Result, bail};
23use arrow_array::{ArrayRef, Datum, RecordBatch, StringArray};
24use arrow_cast::{CastOptions, cast_with_options, pretty::pretty_format_batches};
25use arrow_flight::{
26 FlightInfo,
27 flight_service_client::FlightServiceClient,
28 sql::{CommandGetDbSchemas, CommandGetTables, client::FlightSqlServiceClient},
29};
30use arrow_schema::Schema;
31use clap::{Parser, Subcommand, ValueEnum};
32use core::str;
33use futures::TryStreamExt;
34use tonic::{
35 metadata::MetadataMap,
36 transport::{Channel, ClientTlsConfig, Endpoint},
37};
38use tracing_log::log::info;
39
40#[derive(Debug, Parser)]
42pub struct LoggingArgs {
43 #[clap(
52 short = 'v',
53 long = "verbose",
54 action = clap::ArgAction::Count,
55 )]
56 log_verbose_count: u8,
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
61pub enum CompressionEncoding {
62 Gzip,
63 Deflate,
64 Zstd,
65}
66
67impl From<CompressionEncoding> for tonic::codec::CompressionEncoding {
68 fn from(encoding: CompressionEncoding) -> Self {
69 match encoding {
70 CompressionEncoding::Gzip => Self::Gzip,
71 CompressionEncoding::Deflate => Self::Deflate,
72 CompressionEncoding::Zstd => Self::Zstd,
73 }
74 }
75}
76
77#[derive(Debug, Parser)]
78struct ClientArgs {
79 #[clap(long = "header", short = 'H', value_parser = parse_key_val)]
85 headers: Vec<(String, String)>,
86
87 #[clap(long, requires = "password")]
91 username: Option<String>,
92
93 #[clap(long, requires = "username")]
97 password: Option<String>,
98
99 #[clap(long)]
101 token: Option<String>,
102
103 #[clap(long)]
107 tls: bool,
108
109 #[clap(long, requires = "tls")]
115 key_log: bool,
116
117 #[clap(long)]
121 host: String,
122
123 #[clap(long)]
127 port: Option<u16>,
128
129 #[clap(long, value_delimiter = ',')]
136 accept_compression: Vec<CompressionEncoding>,
137
138 #[clap(long)]
155 send_compression: Option<CompressionEncoding>,
156}
157
158#[derive(Debug, Parser)]
159struct Args {
160 #[clap(flatten)]
162 logging_args: LoggingArgs,
163
164 #[clap(flatten)]
166 client_args: ClientArgs,
167
168 #[clap(subcommand)]
169 cmd: Command,
170}
171
172#[derive(Debug, Subcommand)]
174enum Command {
175 Catalogs,
177 DbSchemas {
179 catalog: String,
183 #[clap(short, long)]
189 db_schema_filter: Option<String>,
190 },
191 Tables {
193 catalog: String,
197 #[clap(short, long)]
203 db_schema_filter: Option<String>,
204 #[clap(short, long)]
210 table_filter: Option<String>,
211 #[clap(long)]
215 table_types: Vec<String>,
216 },
217 TableTypes,
219
220 StatementQuery {
222 query: String,
226 },
227
228 PreparedStatementQuery {
230 query: String,
238
239 #[clap(short, value_parser = parse_key_val)]
246 params: Vec<(String, String)>,
247 },
248}
249
250#[tokio::main]
251async fn main() -> Result<()> {
252 let args = Args::parse();
253 setup_logging(args.logging_args)?;
254 let mut client = setup_client(args.client_args)
255 .await
256 .context("setup client")?;
257
258 let flight_info = match args.cmd {
259 Command::Catalogs => client.get_catalogs().await.context("get catalogs")?,
260 Command::DbSchemas {
261 catalog,
262 db_schema_filter,
263 } => client
264 .get_db_schemas(CommandGetDbSchemas {
265 catalog: Some(catalog),
266 db_schema_filter_pattern: db_schema_filter,
267 })
268 .await
269 .context("get db schemas")?,
270 Command::Tables {
271 catalog,
272 db_schema_filter,
273 table_filter,
274 table_types,
275 } => client
276 .get_tables(CommandGetTables {
277 catalog: Some(catalog),
278 db_schema_filter_pattern: db_schema_filter,
279 table_name_filter_pattern: table_filter,
280 table_types,
281 include_schema: false,
285 })
286 .await
287 .context("get tables")?,
288 Command::TableTypes => client.get_table_types().await.context("get table types")?,
289 Command::StatementQuery { query } => client
290 .execute(query, None)
291 .await
292 .context("execute statement")?,
293 Command::PreparedStatementQuery { query, params } => {
294 let mut prepared_stmt = client
295 .prepare(query, None)
296 .await
297 .context("prepare statement")?;
298
299 if !params.is_empty() {
300 prepared_stmt
301 .set_parameters(
302 construct_record_batch_from_params(
303 ¶ms,
304 prepared_stmt
305 .parameter_schema()
306 .context("get parameter schema")?,
307 )
308 .context("construct parameters")?,
309 )
310 .context("bind parameters")?;
311 }
312
313 prepared_stmt
314 .execute()
315 .await
316 .context("execute prepared statement")?
317 }
318 };
319
320 let batches = execute_flight(&mut client, flight_info)
321 .await
322 .context("read flight data")?;
323
324 let res = pretty_format_batches(batches.as_slice()).context("format results")?;
325 println!("{res}");
326
327 Ok(())
328}
329
330async fn execute_flight(
331 client: &mut FlightSqlServiceClient<Channel>,
332 info: FlightInfo,
333) -> Result<Vec<RecordBatch>> {
334 let schema = Arc::new(Schema::try_from(info.clone()).context("valid schema")?);
335 let mut batches = Vec::with_capacity(info.endpoint.len() + 1);
336 batches.push(RecordBatch::new_empty(schema));
337 info!("decoded schema");
338
339 for endpoint in info.endpoint {
340 let Some(ticket) = &endpoint.ticket else {
341 bail!("did not get ticket");
342 };
343
344 let mut flight_data = client.do_get(ticket.clone()).await.context("do get")?;
345 log_metadata(flight_data.headers(), "header");
346
347 let mut endpoint_batches: Vec<_> = (&mut flight_data)
348 .try_collect()
349 .await
350 .context("collect data stream")?;
351 batches.append(&mut endpoint_batches);
352
353 if let Some(trailers) = flight_data.trailers() {
354 log_metadata(&trailers, "trailer");
355 }
356 }
357 info!("received data");
358
359 Ok(batches)
360}
361
362fn construct_record_batch_from_params(
363 params: &[(String, String)],
364 parameter_schema: &Schema,
365) -> Result<RecordBatch> {
366 let mut items = Vec::<(&String, ArrayRef)>::new();
367
368 for (name, value) in params {
369 let field = parameter_schema.field_with_name(name)?;
370 let value_as_array = StringArray::new_scalar(value);
371 let casted = cast_with_options(
372 value_as_array.get().0,
373 field.data_type(),
374 &CastOptions::default(),
375 )?;
376 items.push((name, casted))
377 }
378
379 Ok(RecordBatch::try_from_iter(items)?)
380}
381
382fn setup_logging(args: LoggingArgs) -> Result<()> {
383 use tracing_subscriber::{EnvFilter, FmtSubscriber, util::SubscriberInitExt};
384
385 tracing_log::LogTracer::init().context("tracing log init")?;
386
387 let filter = match args.log_verbose_count {
388 0 => "warn",
389 1 => "info",
390 2 => "debug",
391 _ => "trace",
392 };
393 let filter = EnvFilter::try_new(filter).context("set up log env filter")?;
394
395 let subscriber = FmtSubscriber::builder().with_env_filter(filter).finish();
396 subscriber.try_init().context("init logging subscriber")?;
397
398 Ok(())
399}
400
401async fn setup_client(args: ClientArgs) -> Result<FlightSqlServiceClient<Channel>> {
402 let port = args.port.unwrap_or(if args.tls { 443 } else { 80 });
403
404 let protocol = if args.tls { "https" } else { "http" };
405
406 let mut endpoint = Endpoint::new(format!("{}://{}:{}", protocol, args.host, port))
407 .context("create endpoint")?
408 .connect_timeout(Duration::from_secs(20))
409 .timeout(Duration::from_secs(20))
410 .tcp_nodelay(true) .tcp_keepalive(Option::Some(Duration::from_secs(3600)))
412 .http2_keep_alive_interval(Duration::from_secs(300))
413 .keep_alive_timeout(Duration::from_secs(20))
414 .keep_alive_while_idle(true);
415
416 if args.tls {
417 let mut tls_config = ClientTlsConfig::new().with_enabled_roots();
418 if args.key_log {
419 tls_config = tls_config.use_key_log();
420 }
421
422 endpoint = endpoint
423 .tls_config(tls_config)
424 .context("create TLS endpoint")?;
425 }
426
427 let channel = endpoint.connect().await.context("connect to endpoint")?;
428
429 let mut client = FlightServiceClient::new(channel);
430 for encoding in args.accept_compression {
431 client = client.accept_compressed(encoding.into());
432 }
433 if let Some(encoding) = args.send_compression {
434 client = client.send_compressed(encoding.into());
435 }
436 let mut client = FlightSqlServiceClient::new_from_inner(client);
437 info!("connected");
438
439 for (k, v) in args.headers {
440 client.set_header(k, v);
441 }
442
443 if let Some(token) = args.token {
444 client.set_token(token);
445 info!("token set");
446 }
447
448 match (args.username, args.password) {
449 (None, None) => {}
450 (Some(username), Some(password)) => {
451 client
452 .handshake(&username, &password)
453 .await
454 .context("handshake")?;
455 info!("performed handshake");
456 }
457 (Some(_), None) => {
458 bail!("when username is set, you also need to set a password")
459 }
460 (None, Some(_)) => {
461 bail!("when password is set, you also need to set a username")
462 }
463 }
464
465 Ok(client)
466}
467
468fn parse_key_val(s: &str) -> Result<(String, String), String> {
470 let pos = s
471 .find('=')
472 .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
473 Ok((s[..pos].to_owned(), s[pos + 1..].to_owned()))
474}
475
476fn log_metadata(map: &MetadataMap, what: &'static str) {
478 for k_v in map.iter() {
479 match k_v {
480 tonic::metadata::KeyAndValueRef::Ascii(k, v) => {
481 info!(
482 "{}: {}={}",
483 what,
484 k.as_str(),
485 v.to_str().unwrap_or("<invalid>"),
486 );
487 }
488 tonic::metadata::KeyAndValueRef::Binary(k, v) => {
489 info!(
490 "{}: {}={}",
491 what,
492 k.as_str(),
493 String::from_utf8_lossy(v.as_ref()),
494 );
495 }
496 }
497 }
498}