Skip to content

Commit 81aedd2

Browse files
committed
feat: infer driver from URI scheme
1 parent f9f6ac7 commit 81aedd2

4 files changed

Lines changed: 177 additions & 28 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ Usage: databow [OPTIONS]
121121

122122
Options:
123123
--profile <profile> Connection profile name or path
124-
--driver <driver> Driver name (required if --profile not specified)
124+
--driver <driver> Driver name (inferred from --uri scheme or profile if not specified)
125125
--uri <uri> Database uniform resource identifier
126126
--username <username> Database user username
127127
--password <password> Database user password

src/cli.rs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub enum QuerySource {
1717
#[derive(Debug, Clone)]
1818
pub enum ConnectionSource {
1919
Direct {
20-
driver_name: String,
20+
driver_name: Option<String>,
2121
uri: Option<String>,
2222
username: Option<String>,
2323
password: Option<String>,
@@ -52,7 +52,7 @@ pub fn parse_args() -> AppConfig {
5252
.conflicts_with("driver"),
5353
Arg::new("driver")
5454
.long("driver")
55-
.help("Driver name (required if --profile not specified)"),
55+
.help("Driver name (inferred from --uri scheme or profile if not specified)"),
5656
Arg::new("uri")
5757
.long("uri")
5858
.help("Database uniform resource identifier"),
@@ -123,14 +123,28 @@ pub fn parse_args() -> AppConfig {
123123
}
124124
} else if let Some(driver_name) = driver_name {
125125
ConnectionSource::Direct {
126-
driver_name,
126+
driver_name: Some(driver_name),
127+
uri,
128+
username,
129+
password,
130+
options,
131+
}
132+
} else if uri.as_deref().is_some_and(uri_has_driver_scheme) {
133+
// The driver can be inferred from the URI scheme (e.g. `--uri duckdb://...`
134+
// implies the `duckdb` driver), so neither --driver nor --profile is
135+
// required. The raw URI is handed to `ManagedDatabase::from_uri`, which
136+
// performs the scheme parsing and driver selection.
137+
ConnectionSource::Direct {
138+
driver_name: None,
127139
uri,
128140
username,
129141
password,
130142
options,
131143
}
132144
} else {
133-
eprintln!("Error: Either --profile or --driver is required");
145+
eprintln!(
146+
"Error: Provide --driver, --profile, or a --uri with a scheme (e.g. duckdb://...)"
147+
);
134148
exit(1);
135149
};
136150

@@ -163,6 +177,13 @@ pub fn parse_args() -> AppConfig {
163177
}
164178
}
165179

180+
fn uri_has_driver_scheme(uri: &str) -> bool {
181+
let Some(idx) = uri.find(':') else {
182+
return false;
183+
};
184+
idx != 0
185+
}
186+
166187
fn parse_option(option: &str) -> Result<(String, String), String> {
167188
let parts: Vec<&str> = option.splitn(2, '=').collect();
168189
if parts.len() != 2 {
@@ -184,15 +205,15 @@ mod tests {
184205
#[test]
185206
fn test_connection_source_direct() {
186207
let source = ConnectionSource::Direct {
187-
driver_name: "duckdb".to_string(),
208+
driver_name: Some("duckdb".to_string()),
188209
uri: Some(":memory:".to_string()),
189210
username: None,
190211
password: None,
191212
options: vec![],
192213
};
193214
match source {
194215
ConnectionSource::Direct { driver_name, .. } => {
195-
assert_eq!(driver_name, "duckdb");
216+
assert_eq!(driver_name, Some("duckdb".to_string()));
196217
}
197218
_ => panic!("Expected Direct variant"),
198219
}
@@ -216,6 +237,36 @@ mod tests {
216237
}
217238
}
218239

240+
#[test]
241+
fn test_uri_has_driver_scheme_with_scheme() {
242+
assert!(uri_has_driver_scheme("duckdb://:memory:"));
243+
assert!(uri_has_driver_scheme("postgresql://host:5432/db"));
244+
assert!(uri_has_driver_scheme("sqlite:file::memory:"));
245+
assert!(uri_has_driver_scheme("sqlite::memory:"));
246+
// Bare `driver:` form: accepted, matching `parse_driver_uri`.
247+
assert!(uri_has_driver_scheme("duckdb:"));
248+
}
249+
250+
#[test]
251+
fn test_uri_has_driver_scheme_arbitrary_scheme() {
252+
assert!(uri_has_driver_scheme("DuckDB://x"));
253+
}
254+
255+
#[test]
256+
fn test_uri_has_driver_scheme_no_scheme() {
257+
// No colon at all.
258+
assert!(!uri_has_driver_scheme("plain_path"));
259+
// Empty scheme (leading colon).
260+
assert!(!uri_has_driver_scheme(":memory:"));
261+
}
262+
263+
#[test]
264+
fn test_uri_has_driver_scheme_accepts_profile_scheme() {
265+
// `profile://` is a valid scheme; `from_uri` resolves it to a profile.
266+
assert!(uri_has_driver_scheme("profile://my_database"));
267+
assert!(uri_has_driver_scheme("PROFILE://my_database"));
268+
}
269+
219270
#[test]
220271
fn test_connection_source_profile_with_path() {
221272
let source = ConnectionSource::Profile {
@@ -272,7 +323,7 @@ mod tests {
272323
fn test_app_config_with_direct_connection() {
273324
let config = AppConfig {
274325
connection: ConnectionSource::Direct {
275-
driver_name: "test_driver".to_string(),
326+
driver_name: Some("test_driver".to_string()),
276327
uri: Some("test_uri".to_string()),
277328
username: Some("test_user".to_string()),
278329
password: Some("test_pass".to_string()),
@@ -291,7 +342,7 @@ mod tests {
291342
password,
292343
options,
293344
} => {
294-
assert_eq!(driver_name, "test_driver");
345+
assert_eq!(driver_name, &Some("test_driver".to_string()));
295346
assert_eq!(*uri, Some("test_uri".to_string()));
296347
assert_eq!(*username, Some("test_user".to_string()));
297348
assert_eq!(*password, Some("test_pass".to_string()));

src/database.rs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use adbc_core::{Database, Driver, LOAD_FLAG_DEFAULT, Statement};
77
use adbc_driver_manager::profile::{
88
ConnectionProfile, ConnectionProfileProvider, FilesystemProfileProvider, process_profile_value,
99
};
10-
use adbc_driver_manager::{ManagedConnection, ManagedDriver};
10+
use adbc_driver_manager::{ManagedConnection, ManagedDatabase, ManagedDriver};
1111
use arrow_array::RecordBatch;
1212

1313
pub fn initialize_connection(source: ConnectionSource) -> Result<ManagedConnection, String> {
@@ -30,30 +30,58 @@ pub fn initialize_connection(source: ConnectionSource) -> Result<ManagedConnecti
3030
}
3131

3232
fn initialize_direct_connection(
33-
driver_name: String,
33+
driver_name: Option<String>,
3434
uri: Option<String>,
3535
username: Option<String>,
3636
password: Option<String>,
3737
options: Vec<(String, String)>,
3838
) -> Result<ManagedConnection, String> {
39-
let mut driver = ManagedDriver::load_from_name(
40-
&driver_name,
41-
None,
42-
AdbcVersion::default(),
43-
LOAD_FLAG_DEFAULT,
44-
None,
45-
)
46-
.map_err(|e| format!("Failed to load driver '{}': {}", driver_name, e))?;
47-
48-
let db_options = build_database_options(uri, username, password, options);
49-
50-
let database = driver
51-
.new_database_with_opts(db_options)
52-
.map_err(|e| format!("Failed to create database handle: {e}"))?;
39+
match driver_name {
40+
// Explicit driver: load it by name and hand it the URI as an option.
41+
Some(driver_name) => {
42+
let mut driver = ManagedDriver::load_from_name(
43+
&driver_name,
44+
None,
45+
AdbcVersion::default(),
46+
LOAD_FLAG_DEFAULT,
47+
None,
48+
)
49+
.map_err(|e| format!("Failed to load driver '{}': {}", driver_name, e))?;
50+
51+
let db_options = build_database_options(uri, username, password, options);
52+
53+
let database = driver
54+
.new_database_with_opts(db_options)
55+
.map_err(|e| format!("Failed to create database handle: {e}"))?;
56+
57+
database
58+
.new_connection()
59+
.map_err(|e| format!("Failed to create connection: {e}"))
60+
}
61+
// No driver given: infer it from the URI scheme via the driver manager.
62+
None => {
63+
let uri = uri
64+
.ok_or_else(|| "Internal error: URI is required to infer the driver".to_string())?;
65+
66+
// The URI itself selects the driver; the remaining CLI options are
67+
// layered on top (they override any options derived from the URI).
68+
let opts = build_database_options(None, username, password, options);
69+
70+
let database = ManagedDatabase::from_uri_with_opts(
71+
&uri,
72+
None,
73+
AdbcVersion::default(),
74+
LOAD_FLAG_DEFAULT,
75+
None,
76+
opts,
77+
)
78+
.map_err(|e| format!("Failed to load driver from URI '{}': {}", uri, e))?;
5379

54-
database
55-
.new_connection()
56-
.map_err(|e| format!("Failed to create connection: {e}"))
80+
database
81+
.new_connection()
82+
.map_err(|e| format!("Failed to create connection: {e}"))
83+
}
84+
}
5785
}
5886

5987
fn initialize_profile_connection(

tests/integration_test.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,41 @@ fn test_query_argument() {
6060
assert!(stdout.contains("42"));
6161
}
6262

63+
#[test]
64+
fn test_driver_inferred_from_uri_scheme() {
65+
// No --driver or --profile: the driver should be inferred from the
66+
// `sqlite:` URI scheme.
67+
let output = Command::new("cargo")
68+
.args([
69+
"run",
70+
"--",
71+
"--uri",
72+
"sqlite::memory:",
73+
"--query",
74+
"SELECT 42 AS answer",
75+
])
76+
.output()
77+
.expect("Failed to execute command");
78+
79+
assert!(output.status.success());
80+
let stdout = String::from_utf8_lossy(&output.stdout);
81+
assert!(stdout.contains("answer"));
82+
assert!(stdout.contains("42"));
83+
}
84+
85+
#[test]
86+
fn test_no_driver_no_scheme_uri_errors() {
87+
// A URI with no scheme and no --driver/--profile should still error.
88+
let output = Command::new("cargo")
89+
.args(["run", "--", "--uri", "plain_path", "--query", "SELECT 1"])
90+
.output()
91+
.expect("Failed to execute command");
92+
93+
assert!(!output.status.success());
94+
let stderr = String::from_utf8_lossy(&output.stderr);
95+
assert!(stderr.contains("--driver"));
96+
}
97+
6398
#[test]
6499
fn test_file_argument() {
65100
// Create a temporary SQL file
@@ -241,6 +276,41 @@ driver = "duckdb"
241276
assert!(stdout.contains("42"));
242277
}
243278

279+
#[test]
280+
fn test_uri_profile_scheme_from_file_path() {
281+
use std::io::Write;
282+
283+
// A `profile://<path>` URI (no --driver/--profile) should be resolved by
284+
// the driver manager's `from_uri`, which loads the referenced profile.
285+
let mut temp_file = NamedTempFile::with_suffix(".toml").expect("Failed to create temp file");
286+
let profile_path = temp_file.path().to_string_lossy().to_string();
287+
288+
temp_file
289+
.write_all(
290+
br#"profile_version = 1
291+
driver = "duckdb"
292+
293+
[Options]
294+
"#,
295+
)
296+
.expect("Failed to write to temp file");
297+
298+
let uri = format!("profile://{profile_path}");
299+
let output = Command::new("cargo")
300+
.args(["run", "--", "--uri", &uri, "--query", "SELECT 42 AS answer"])
301+
.output()
302+
.expect("Failed to execute command");
303+
304+
assert!(
305+
output.status.success(),
306+
"profile:// URI should resolve the profile. stderr: {}",
307+
String::from_utf8_lossy(&output.stderr)
308+
);
309+
let stdout = String::from_utf8_lossy(&output.stdout);
310+
assert!(stdout.contains("answer"));
311+
assert!(stdout.contains("42"));
312+
}
313+
244314
#[test]
245315
fn test_profile_with_uri_override() {
246316
use std::io::Write;

0 commit comments

Comments
 (0)