Skip to content

Commit 69b5e19

Browse files
committed
feat: show info on repl startup
1 parent 8ced9ef commit 69b5e19

4 files changed

Lines changed: 242 additions & 3 deletions

File tree

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ syntect = "5.3.0"
2525
terminal-colorsaurus = "1.0.3"
2626

2727
[dev-dependencies]
28+
arrow-buffer = "58.1.0"
2829
tempfile = "3.27.0"

src/database.rs

Lines changed: 176 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,99 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
use crate::cli::ConnectionSource;
5-
use adbc_core::options::{AdbcVersion, OptionDatabase, OptionValue};
6-
use adbc_core::{Database, Driver, LOAD_FLAG_DEFAULT, Statement};
5+
use adbc_core::options::{AdbcVersion, InfoCode, OptionDatabase, OptionValue};
6+
use adbc_core::{Connection, Database, Driver, LOAD_FLAG_DEFAULT, Statement};
77
use adbc_driver_manager::profile::{
88
ConnectionProfile, ConnectionProfileProvider, FilesystemProfileProvider, process_profile_value,
99
};
1010
use adbc_driver_manager::{ManagedConnection, ManagedDriver};
11-
use arrow_array::RecordBatch;
11+
use arrow_array::cast::AsArray;
12+
use arrow_array::{Array, RecordBatch, UnionArray};
13+
use std::collections::HashSet;
14+
15+
/// Vendor (database product) metadata reported by an ADBC driver.
16+
#[derive(Debug, Default)]
17+
pub struct VendorInfo {
18+
pub name: Option<String>,
19+
pub version: Option<String>,
20+
}
21+
22+
/// Query the connected driver for the database vendor name and version.
23+
///
24+
/// This is best-effort: any failure (the driver does not implement `get_info`,
25+
/// returns unexpected data, etc.) yields a `VendorInfo` with `None` fields so
26+
/// that callers can continue without the metadata.
27+
pub fn get_vendor_info(connection: &impl Connection) -> VendorInfo {
28+
let codes = HashSet::from([InfoCode::VendorName, InfoCode::VendorVersion]);
29+
let reader = match connection.get_info(Some(codes)) {
30+
Ok(reader) => reader,
31+
Err(_) => return VendorInfo::default(),
32+
};
33+
let batches: Vec<RecordBatch> = match reader.collect::<Result<_, _>>() {
34+
Ok(batches) => batches,
35+
Err(_) => return VendorInfo::default(),
36+
};
37+
parse_vendor_info(&batches)
38+
}
39+
40+
/// Extract the vendor name and version from the record batches returned by
41+
/// [`Connection::get_info`].
42+
///
43+
/// The batches follow the ADBC `get_info` schema: an `info_name` (`u32`) column
44+
/// and an `info_value` dense-union column. Vendor name/version are utf8 values
45+
/// stored in the union's `string_value` child (type id 0).
46+
fn parse_vendor_info(batches: &[RecordBatch]) -> VendorInfo {
47+
// Derive the info codes from the same enum used in the `get_info` request
48+
// so the parse cannot drift from the query.
49+
let vendor_name_code = u32::from(&InfoCode::VendorName);
50+
let vendor_version_code = u32::from(&InfoCode::VendorVersion);
51+
52+
let mut info = VendorInfo::default();
53+
54+
for batch in batches {
55+
let Some(info_names) = batch
56+
.column_by_name("info_name")
57+
.and_then(|col| col.as_primitive_opt::<arrow_array::types::UInt32Type>())
58+
else {
59+
continue;
60+
};
61+
let Some(info_values) = batch
62+
.column_by_name("info_value")
63+
.and_then(|col| col.as_any().downcast_ref::<UnionArray>())
64+
else {
65+
continue;
66+
};
67+
68+
for row in 0..batch.num_rows() {
69+
if info_names.is_null(row) {
70+
continue;
71+
}
72+
let code = info_names.value(row);
73+
if code != vendor_name_code && code != vendor_version_code {
74+
continue;
75+
}
76+
77+
let value = info_values.value(row);
78+
let Some(strings) = value.as_string_opt::<i32>() else {
79+
continue;
80+
};
81+
if strings.is_empty() || strings.is_null(0) {
82+
continue;
83+
}
84+
let text = strings.value(0).to_string();
85+
86+
// The guard above ensures `code` is one of the two vendor codes,
87+
// so `else` unambiguously means the version.
88+
if code == vendor_name_code {
89+
info.name = Some(text);
90+
} else {
91+
info.version = Some(text);
92+
}
93+
}
94+
}
95+
96+
info
97+
}
1298

1399
pub fn initialize_connection(source: ConnectionSource) -> Result<ManagedConnection, String> {
14100
match source {
@@ -193,6 +279,93 @@ fn merge_options(
193279
#[cfg(test)]
194280
mod tests {
195281
use super::*;
282+
use adbc_core::schemas::GET_INFO_SCHEMA;
283+
use arrow_array::{StringArray, UInt32Array, UnionArray};
284+
use arrow_buffer::ScalarBuffer;
285+
use arrow_schema::DataType;
286+
use std::sync::Arc;
287+
288+
/// Build a RecordBatch matching the ADBC `get_info` schema from a list of
289+
/// `(info_name, string_value)` pairs. `None` values are stored as null
290+
/// entries in the union's `string_value` child.
291+
fn make_info_batch(rows: &[(u32, Option<&str>)]) -> RecordBatch {
292+
let info_names: Vec<u32> = rows.iter().map(|(name, _)| *name).collect();
293+
let string_values: Vec<Option<&str>> = rows.iter().map(|(_, value)| *value).collect();
294+
295+
// The `info_value` union child fields, per GET_INFO_SCHEMA. All rows use
296+
// the `string_value` child (type_id 0) in this helper.
297+
let DataType::Union(union_fields, _) = GET_INFO_SCHEMA.field(1).data_type().clone() else {
298+
panic!("info_value must be a union");
299+
};
300+
301+
let string_child = Arc::new(StringArray::from(string_values.clone()));
302+
let children = union_fields
303+
.iter()
304+
.map(|(type_id, field)| -> arrow_array::ArrayRef {
305+
if type_id == 0 {
306+
string_child.clone()
307+
} else {
308+
// Empty child of the correct type for the unused union branches.
309+
arrow_array::new_empty_array(field.data_type())
310+
}
311+
})
312+
.collect::<Vec<_>>();
313+
314+
let type_ids = ScalarBuffer::from(vec![0i8; rows.len()]);
315+
let offsets = ScalarBuffer::from((0..rows.len() as i32).collect::<Vec<_>>());
316+
let union =
317+
UnionArray::try_new(union_fields, type_ids, Some(offsets), children).unwrap();
318+
319+
RecordBatch::try_new(
320+
GET_INFO_SCHEMA.clone(),
321+
vec![Arc::new(UInt32Array::from(info_names)), Arc::new(union)],
322+
)
323+
.unwrap()
324+
}
325+
326+
#[test]
327+
fn test_parse_vendor_info_name_and_version() {
328+
let batch = make_info_batch(&[(0, Some("DuckDB")), (1, Some("v1.1.0"))]);
329+
let info = parse_vendor_info(&[batch]);
330+
assert_eq!(info.name.as_deref(), Some("DuckDB"));
331+
assert_eq!(info.version.as_deref(), Some("v1.1.0"));
332+
}
333+
334+
#[test]
335+
fn test_parse_vendor_info_name_only() {
336+
let batch = make_info_batch(&[(0, Some("PostgreSQL"))]);
337+
let info = parse_vendor_info(&[batch]);
338+
assert_eq!(info.name.as_deref(), Some("PostgreSQL"));
339+
assert_eq!(info.version, None);
340+
}
341+
342+
#[test]
343+
fn test_parse_vendor_info_ignores_other_codes() {
344+
// 100 = DriverName, 101 = DriverVersion; not vendor fields.
345+
let batch = make_info_batch(&[
346+
(100, Some("ADBC DuckDB Driver")),
347+
(1, Some("v1.1.0")),
348+
(0, Some("DuckDB")),
349+
]);
350+
let info = parse_vendor_info(&[batch]);
351+
assert_eq!(info.name.as_deref(), Some("DuckDB"));
352+
assert_eq!(info.version.as_deref(), Some("v1.1.0"));
353+
}
354+
355+
#[test]
356+
fn test_parse_vendor_info_null_value() {
357+
let batch = make_info_batch(&[(0, Some("DuckDB")), (1, None)]);
358+
let info = parse_vendor_info(&[batch]);
359+
assert_eq!(info.name.as_deref(), Some("DuckDB"));
360+
assert_eq!(info.version, None);
361+
}
362+
363+
#[test]
364+
fn test_parse_vendor_info_empty() {
365+
let info = parse_vendor_info(&[]);
366+
assert_eq!(info.name, None);
367+
assert_eq!(info.version, None);
368+
}
196369

197370
#[test]
198371
fn test_build_database_options_empty() {

src/repl.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,25 @@ impl Prompt for SqlPrompt {
5050
}
5151
}
5252

53+
/// Build the REPL startup banner. The first line always shows `databow` and its
54+
/// version. When the driver reports a vendor name, a second line shows the
55+
/// connected database, including its version when available.
56+
fn format_banner(version: &str, vendor: &database::VendorInfo) -> String {
57+
let mut banner = format!("databow {version}");
58+
if let Some(name) = &vendor.name {
59+
banner.push('\n');
60+
match &vendor.version {
61+
Some(vendor_version) => banner.push_str(&format!("Connected to {name} {vendor_version}")),
62+
None => banner.push_str(&format!("Connected to {name}")),
63+
}
64+
}
65+
banner
66+
}
67+
5368
pub fn run_repl(mut connection: impl Connection, table_mode: TableMode) {
69+
let vendor = database::get_vendor_info(&connection);
70+
println!("{}", format_banner(env!("CARGO_PKG_VERSION"), &vendor));
71+
5472
let mut line_editor = Reedline::create()
5573
.with_highlighter(Box::new(SyntectHighlighter::new()))
5674
.with_validator(Box::new(SqlValidator));
@@ -93,3 +111,49 @@ pub fn run_repl(mut connection: impl Connection, table_mode: TableMode) {
93111
}
94112
}
95113
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
use crate::database::VendorInfo;
119+
120+
#[test]
121+
fn test_format_banner_version_only() {
122+
let vendor = VendorInfo::default();
123+
assert_eq!(format_banner("0.1.2", &vendor), "databow 0.1.2");
124+
}
125+
126+
#[test]
127+
fn test_format_banner_with_vendor_name_and_version() {
128+
let vendor = VendorInfo {
129+
name: Some("DuckDB".to_string()),
130+
version: Some("v1.1.0".to_string()),
131+
};
132+
assert_eq!(
133+
format_banner("0.1.2", &vendor),
134+
"databow 0.1.2\nConnected to DuckDB v1.1.0"
135+
);
136+
}
137+
138+
#[test]
139+
fn test_format_banner_with_vendor_name_only() {
140+
let vendor = VendorInfo {
141+
name: Some("PostgreSQL".to_string()),
142+
version: None,
143+
};
144+
assert_eq!(
145+
format_banner("0.1.2", &vendor),
146+
"databow 0.1.2\nConnected to PostgreSQL"
147+
);
148+
}
149+
150+
#[test]
151+
fn test_format_banner_version_present_without_name_is_ignored() {
152+
// A version with no name should not produce a second line.
153+
let vendor = VendorInfo {
154+
name: None,
155+
version: Some("v1.1.0".to_string()),
156+
};
157+
assert_eq!(format_banner("0.1.2", &vendor), "databow 0.1.2");
158+
}
159+
}

0 commit comments

Comments
 (0)