Skip to content

Commit 9ff7ab7

Browse files
committed
feat: add non-interactive usage modes
1 parent 128c663 commit 9ff7ab7

File tree

9 files changed

+350
-14
lines changed

9 files changed

+350
-14
lines changed

Cargo.lock

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,6 @@ nu-ansi-term = "0.50.3"
1616
reedline = "0.45.0"
1717
syntect = "5.3.0"
1818
terminal-colorsaurus = "1.0.3"
19+
20+
[dev-dependencies]
21+
tempfile = "3.14.0"

README.md

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ The binary will be built to `./target/release/adbcli`.
3131

3232
Install the DuckDB ADBC driver with [dbc](https://docs.columnar.tech/dbc/):
3333

34-
```sh
35-
dbc install duckdb
34+
```console
35+
$ dbc install duckdb
3636
```
3737

38+
### Interactive Usage
39+
3840
Connect to DuckDB (in-memory):
3941

40-
```sh
41-
adbcli --driver duckdb
42+
```console
43+
$ adbcli --driver duckdb
4244
```
4345

44-
Run SQL queries:
46+
Execute SQL queries:
4547

4648
```
4749
〉CREATE TABLE penguins AS FROM 'https://blobs.duckdb.org/data/penguins.csv';
@@ -62,6 +64,41 @@ Run SQL queries:
6264
└─────────┴───────────┴────────────────┴───────────────┴───────────────────┴─────────────┴────────┴──────┘
6365
```
6466

67+
### Non-interactive Usage
68+
69+
Execute a query directly and exit:
70+
71+
```console
72+
$ adbcli --driver duckdb --query "SELECT 2 AS favorite_num"
73+
┌──────────────┐
74+
│ favorite_num │
75+
╞══════════════╡
76+
│ 2 │
77+
└──────────────┘
78+
```
79+
80+
Execute a query from a file and exit:
81+
82+
```console
83+
$ adbcli --driver duckdb --file select_example.sql
84+
┌────────────┐
85+
│ the_answer │
86+
╞════════════╡
87+
│ 42 │
88+
└────────────┘
89+
```
90+
91+
Execute a query from stdin and exit:
92+
93+
```console
94+
$ echo "SELECT 'Emil' AS name" | adbcli --driver duckdb
95+
┌──────┐
96+
│ name │
97+
╞══════╡
98+
│ Emil │
99+
└──────┘
100+
```
101+
65102
## Reference
66103

67104
```console
@@ -76,6 +113,8 @@ Options:
76113
--username <username> Database user username
77114
--password <password> Database user password
78115
--option <option> Driver-specific database option
116+
--query <query> Execute query and exit
117+
--file <file> Read and execute file and exit
79118
-h, --help Print help
80119
-V, --version Print version
81120
```

docs/index.md

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,19 @@ A command-line tool for querying databases via [ADBC](https://arrow.apache.org/a
1818

1919
Install the DuckDB ADBC driver with [dbc](https://docs.columnar.tech/dbc/):
2020

21-
```sh
22-
dbc install duckdb
21+
```console
22+
$ dbc install duckdb
2323
```
2424

25+
### Interactive Usage
26+
2527
Connect to DuckDB (in-memory):
2628

27-
```sh
28-
adbcli --driver duckdb
29+
```console
30+
$ adbcli --driver duckdb
2931
```
3032

31-
Run SQL queries:
33+
Execute SQL queries:
3234

3335
```
3436
〉CREATE TABLE penguins AS FROM 'https://blobs.duckdb.org/data/penguins.csv';
@@ -48,3 +50,38 @@ Run SQL queries:
4850
│ Adelie │ Torgersen │ 36.7 │ 19.3 │ 193 │ 3450 │ female │ 2007 │
4951
└─────────┴───────────┴────────────────┴───────────────┴───────────────────┴─────────────┴────────┴──────┘
5052
```
53+
54+
### Non-interactive Usage
55+
56+
Execute a query directly and exit:
57+
58+
```console
59+
$ adbcli --driver duckdb --query "SELECT 2 AS favorite_num"
60+
┌──────────────┐
61+
│ favorite_num │
62+
╞══════════════╡
63+
│ 2 │
64+
└──────────────┘
65+
```
66+
67+
Execute a query from a file and exit:
68+
69+
```console
70+
$ adbcli --driver duckdb --file select_example.sql
71+
┌────────────┐
72+
│ the_answer │
73+
╞════════════╡
74+
│ 42 │
75+
└────────────┘
76+
```
77+
78+
Execute a query from stdin and exit:
79+
80+
```console
81+
$ echo "SELECT 'Emil' AS name" | adbcli --driver duckdb
82+
┌──────┐
83+
│ name │
84+
╞══════╡
85+
│ Emil │
86+
└──────┘
87+
```

docs/reference.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,22 @@ Driver-specific database option
4444
adbcli --driver duckdb --option path=file.db
4545
```
4646

47+
## --query
48+
49+
Execute query and exit
50+
51+
```sh
52+
adbcli --driver duckdb --query "SELECT 42 AS the_answer"
53+
```
54+
55+
## --file
56+
57+
Read and execute file and exit
58+
59+
```sh
60+
adbcli --driver duckdb --file select_example.sql
61+
```
62+
4763
## --help
4864

4965
Print the help message

src/cli.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,27 @@
11
use clap::{Arg, ArgAction, Command};
2+
use std::path::PathBuf;
23
use std::process::exit;
34

5+
#[derive(Debug, Clone)]
6+
pub enum QuerySource {
7+
Query(String),
8+
File(PathBuf),
9+
Stdin,
10+
Interactive,
11+
}
12+
413
pub struct DatabaseConfig {
514
pub driver_name: String,
615
pub uri: Option<String>,
716
pub username: Option<String>,
817
pub password: Option<String>,
918
pub options: Vec<(String, String)>,
19+
pub query_source: QuerySource,
20+
}
21+
22+
fn is_stdin_piped() -> bool {
23+
use std::io::IsTerminal;
24+
!std::io::stdin().is_terminal()
1025
}
1126

1227
pub fn parse_args() -> DatabaseConfig {
@@ -28,6 +43,14 @@ pub fn parse_args() -> DatabaseConfig {
2843
.long("option")
2944
.help("Driver-specific database option")
3045
.action(ArgAction::Append),
46+
Arg::new("query")
47+
.long("query")
48+
.help("Execute query and exit")
49+
.conflicts_with("file"),
50+
Arg::new("file")
51+
.long("file")
52+
.help("Read and execute file and exit")
53+
.conflicts_with("query"),
3154
];
3255
let command = Command::new("adbcli")
3356
.version(env!("CARGO_PKG_VERSION"))
@@ -61,12 +84,23 @@ pub fn parse_args() -> DatabaseConfig {
6184
}
6285
}
6386

87+
let query_source = if let Some(query) = matches.get_one::<String>("query") {
88+
QuerySource::Query(query.clone())
89+
} else if let Some(file) = matches.get_one::<String>("file") {
90+
QuerySource::File(PathBuf::from(file))
91+
} else if is_stdin_piped() {
92+
QuerySource::Stdin
93+
} else {
94+
QuerySource::Interactive
95+
};
96+
6497
DatabaseConfig {
6598
driver_name,
6699
uri,
67100
username,
68101
password,
69102
options,
103+
query_source,
70104
}
71105
}
72106

@@ -128,6 +162,7 @@ mod tests {
128162
username: Some("test_user".to_string()),
129163
password: Some("test_pass".to_string()),
130164
options: vec![("key1".to_string(), "val1".to_string())],
165+
query_source: QuerySource::Interactive,
131166
};
132167

133168
assert_eq!(config.driver_name, "test_driver");
@@ -146,6 +181,7 @@ mod tests {
146181
username: None,
147182
password: None,
148183
options: vec![],
184+
query_source: QuerySource::Interactive,
149185
};
150186

151187
assert_eq!(config.driver_name, "test_driver");

src/database.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::cli::DatabaseConfig;
2+
use crate::table;
23
use adbc_core::options::{AdbcVersion, OptionDatabase, OptionValue};
3-
use adbc_core::{Connection, Database, Driver, LOAD_FLAG_DEFAULT};
4+
use adbc_core::{Connection, Database, Driver, LOAD_FLAG_DEFAULT, Statement};
45
use adbc_driver_manager::ManagedDriver;
56
use std::process::exit;
67

@@ -53,3 +54,27 @@ pub fn initialize_connection(config: DatabaseConfig) -> impl Connection {
5354
}
5455
}
5556
}
57+
58+
pub fn execute_query(connection: &mut impl adbc_core::Connection, sql: &str) -> Result<(), String> {
59+
if sql.trim().is_empty() {
60+
return Ok(());
61+
}
62+
63+
let mut statement = connection
64+
.new_statement()
65+
.map_err(|e| format!("Failed to create statement: {e}"))?;
66+
67+
statement
68+
.set_sql_query(sql)
69+
.map_err(|e| format!("Failed to set SQL query: {e}"))?;
70+
71+
let reader = statement
72+
.execute()
73+
.map_err(|e| format!("Failed to execute statement: {e}"))?;
74+
75+
let batches: Vec<arrow_array::RecordBatch> = reader
76+
.collect::<Result<_, _>>()
77+
.map_err(|e| format!("Failed to collect batches: {e}"))?;
78+
79+
table::print_batches(&batches).map_err(|e| format!("Failed to print batches: {e}"))
80+
}

src/main.rs

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,51 @@ mod highlighter;
44
mod repl;
55
mod table;
66

7+
use cli::{QuerySource, parse_args};
8+
use std::io::Read;
9+
use std::process::exit;
10+
711
fn main() {
8-
let config = cli::parse_args();
9-
let connection = database::initialize_connection(config);
10-
repl::run_repl(connection);
12+
let config = parse_args();
13+
let query_source = config.query_source.clone();
14+
15+
match query_source {
16+
QuerySource::Query(sql) => {
17+
let mut connection = database::initialize_connection(config);
18+
if let Err(e) = database::execute_query(&mut connection, &sql) {
19+
eprintln!("{e}");
20+
exit(1);
21+
}
22+
}
23+
QuerySource::File(path) => {
24+
let sql = match std::fs::read_to_string(&path) {
25+
Ok(content) => content,
26+
Err(e) => {
27+
eprintln!("Failed to read file {}: {e}", path.display());
28+
exit(1);
29+
}
30+
};
31+
let mut connection = database::initialize_connection(config);
32+
if let Err(e) = database::execute_query(&mut connection, &sql) {
33+
eprintln!("{e}");
34+
exit(1);
35+
}
36+
}
37+
QuerySource::Stdin => {
38+
let mut sql = String::new();
39+
if let Err(e) = std::io::stdin().read_to_string(&mut sql) {
40+
eprintln!("Failed to read from stdin: {e}");
41+
exit(1);
42+
}
43+
let mut connection = database::initialize_connection(config);
44+
if let Err(e) = database::execute_query(&mut connection, &sql) {
45+
eprintln!("{e}");
46+
exit(1);
47+
}
48+
}
49+
QuerySource::Interactive => {
50+
let connection = database::initialize_connection(config);
51+
repl::run_repl(connection);
52+
}
53+
}
1154
}

0 commit comments

Comments
 (0)