This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow-rs.git


The following commit(s) were added to refs/heads/master by this push:
     new 4aabd2c5de feat: document & streamline flight SQL CLI (#4912)
4aabd2c5de is described below

commit 4aabd2c5ded46d529cce0714a776f0a6336a9a89
Author: Marco Neumann <[email protected]>
AuthorDate: Tue Oct 10 20:14:14 2023 +0200

    feat: document & streamline flight SQL CLI (#4912)
    
    - add docs to README
    - add a few more clap features
    - document arguments
    - unify key-value parsing for headers and parameters
---
 arrow-flight/Cargo.toml                   |  2 +-
 arrow-flight/README.md                    | 32 +++++++++-
 arrow-flight/src/bin/flight_sql_client.rs | 97 ++++++++++++++++---------------
 3 files changed, 81 insertions(+), 50 deletions(-)

diff --git a/arrow-flight/Cargo.toml b/arrow-flight/Cargo.toml
index edaa7129dc..70227eedea 100644
--- a/arrow-flight/Cargo.toml
+++ b/arrow-flight/Cargo.toml
@@ -50,7 +50,7 @@ tonic = { version = "0.10.0", default-features = false, 
features = ["transport",
 
 # CLI-related dependencies
 anyhow = { version = "1.0", optional = true }
-clap = { version = "4.1", default-features = false, features = ["std", 
"derive", "env", "help", "error-context", "usage"], optional = true }
+clap = { version = "4.4.6", default-features = false, features = ["std", 
"derive", "env", "help", "error-context", "usage", "wrap_help", "color", 
"suggestions"], optional = true }
 tracing-log = { version = "0.1", optional = true }
 tracing-subscriber = { version = "0.3.1", default-features = false, features = 
["ansi", "env-filter", "fmt"], optional = true }
 
diff --git a/arrow-flight/README.md b/arrow-flight/README.md
index 9194b209fe..b80772ac92 100644
--- a/arrow-flight/README.md
+++ b/arrow-flight/README.md
@@ -44,5 +44,33 @@ that demonstrate how to build a Flight server implemented 
with [tonic](https://d
 ## Feature Flags
 
 - `flight-sql-experimental`: Enables experimental support for
-  [Apache Arrow 
FlightSQL](https://arrow.apache.org/docs/format/FlightSql.html),
-  a protocol for interacting with SQL databases.
+  [Apache Arrow FlightSQL], a protocol for interacting with SQL databases.
+
+## CLI
+
+This crates offers a basic [Apache Arrow FlightSQL] command line interface.
+
+The client can be installed from the repository:
+
+```console
+$ cargo install --features=cli,flight-sql-experimental,tls 
--bin=flight_sql_client --path=. --locked
+```
+
+The client comes with extensive help text:
+
+```console
+$ flight_sql_client help
+```
+
+A query can be executed using:
+
+```console
+$ flight_sql_client --host example.com statement-query "SELECT 1;"
++----------+
+| Int64(1) |
++----------+
+| 1        |
++----------+
+```
+
+[apache arrow flightsql]: https://arrow.apache.org/docs/format/FlightSql.html
diff --git a/arrow-flight/src/bin/flight_sql_client.rs 
b/arrow-flight/src/bin/flight_sql_client.rs
index df51530b3c..296efc1c30 100644
--- a/arrow-flight/src/bin/flight_sql_client.rs
+++ b/arrow-flight/src/bin/flight_sql_client.rs
@@ -15,7 +15,7 @@
 // specific language governing permissions and limitations
 // under the License.
 
-use std::{error::Error, sync::Arc, time::Duration};
+use std::{sync::Arc, time::Duration};
 
 use anyhow::{bail, Context, Result};
 use arrow_array::{ArrayRef, Datum, RecordBatch, StringArray};
@@ -30,45 +30,17 @@ use tonic::{
 };
 use tracing_log::log::info;
 
-/// A ':' separated key value pair
-#[derive(Debug, Clone)]
-struct KeyValue<K, V> {
-    pub key: K,
-    pub value: V,
-}
-
-impl<K, V> std::str::FromStr for KeyValue<K, V>
-where
-    K: std::str::FromStr,
-    V: std::str::FromStr,
-    K::Err: std::fmt::Display,
-    V::Err: std::fmt::Display,
-{
-    type Err = String;
-
-    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
-        let parts = s.splitn(2, ':').collect::<Vec<_>>();
-        match parts.as_slice() {
-            [key, value] => {
-                let key = K::from_str(key).map_err(|e| e.to_string())?;
-                let value = V::from_str(value.trim()).map_err(|e| 
e.to_string())?;
-                Ok(Self { key, value })
-            }
-            _ => Err(format!(
-                "Invalid key value pair - expected 'KEY:VALUE' got '{s}'"
-            )),
-        }
-    }
-}
-
 /// Logging CLI config.
 #[derive(Debug, Parser)]
 pub struct LoggingArgs {
     /// Log verbosity.
     ///
-    /// Use `-v for warn, `-vv for info, -vvv for debug, -vvvv for trace.
+    /// Defaults to "warn".
     ///
-    /// Note you can also set logging level using `RUST_LOG` environment 
variable: `RUST_LOG=debug`
+    /// Use `-v` for "info", `-vv` for "debug", `-vvv` for "trace".
+    ///
+    /// Note you can also set logging level using `RUST_LOG` environment 
variable:
+    /// `RUST_LOG=debug`.
     #[clap(
         short = 'v',
         long = "verbose",
@@ -81,16 +53,22 @@ pub struct LoggingArgs {
 struct ClientArgs {
     /// Additional headers.
     ///
-    /// Values should be key value pairs separated by ':'
-    #[clap(long, value_delimiter = ',')]
-    headers: Vec<KeyValue<String, String>>,
+    /// Can be given multiple times. Headers and values are separated by '='.
+    ///
+    /// Example: `-H foo=bar -H baz=42`
+    #[clap(long = "header", short = 'H', value_parser = parse_key_val)]
+    headers: Vec<(String, String)>,
 
-    /// Username
-    #[clap(long)]
+    /// Username.
+    ///
+    /// Optional. If given, `password` must also be set.
+    #[clap(long, requires = "password")]
     username: Option<String>,
 
-    /// Password
-    #[clap(long)]
+    /// Password.
+    ///
+    /// Optional. If given, `username` must also be set.
+    #[clap(long, requires = "username")]
     password: Option<String>,
 
     /// Auth token.
@@ -98,14 +76,20 @@ struct ClientArgs {
     token: Option<String>,
 
     /// Use TLS.
+    ///
+    /// If not provided, use cleartext connection.
     #[clap(long)]
     tls: bool,
 
     /// Server host.
+    ///
+    /// Required.
     #[clap(long)]
     host: String,
 
     /// Server port.
+    ///
+    /// Defaults to `443` if `tls` is set, otherwise defaults to `80`.
     #[clap(long)]
     port: Option<u16>,
 }
@@ -124,13 +108,34 @@ struct Args {
     cmd: Command,
 }
 
+/// Different available commands.
 #[derive(Debug, Subcommand)]
 enum Command {
+    /// Execute given statement.
     StatementQuery {
+        /// SQL query.
+        ///
+        /// Required.
         query: String,
     },
+
+    /// Prepare given statement and then execute it.
     PreparedStatementQuery {
+        /// SQL query.
+        ///
+        /// Required.
+        ///
+        /// Can contains placeholders like `$1`.
+        ///
+        /// Example: `SELECT * FROM t WHERE x = $1`
         query: String,
+
+        /// Additional parameters.
+        ///
+        /// Can be given multiple times. Names and values are separated by 
'='. Values will be
+        /// converted to the type that the server reported for the prepared 
statement.
+        ///
+        /// Example: `-p $1=42`
         #[clap(short, value_parser = parse_key_val)]
         params: Vec<(String, String)>,
     },
@@ -284,8 +289,8 @@ async fn setup_client(args: ClientArgs) -> 
Result<FlightSqlServiceClient<Channel
     let mut client = FlightSqlServiceClient::new(channel);
     info!("connected");
 
-    for kv in args.headers {
-        client.set_header(kv.key, kv.value);
+    for (k, v) in args.headers {
+        client.set_header(k, v);
     }
 
     if let Some(token) = args.token {
@@ -314,13 +319,11 @@ async fn setup_client(args: ClientArgs) -> 
Result<FlightSqlServiceClient<Channel
 }
 
 /// Parse a single key-value pair
-fn parse_key_val(
-    s: &str,
-) -> Result<(String, String), Box<dyn Error + Send + Sync + 'static>> {
+fn parse_key_val(s: &str) -> Result<(String, String), String> {
     let pos = s
         .find('=')
         .ok_or_else(|| format!("invalid KEY=value: no `=` found in `{s}`"))?;
-    Ok((s[..pos].parse()?, s[pos + 1..].parse()?))
+    Ok((s[..pos].to_owned(), s[pos + 1..].to_owned()))
 }
 
 /// Log headers/trailers.

Reply via email to