Skip to content

Commit 131eef5

Browse files
committed
Add REPL example
1 parent 72ae69a commit 131eef5

File tree

4 files changed

+270
-0
lines changed

4 files changed

+270
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@
2525
crates/libduckdb-sys/duckdb-sources/*
2626
crates/libduckdb-sys/duckdb/
2727
crates/libduckdb-sys/._duckdb
28+
29+
.duckdb_rs_history

Cargo.lock

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

crates/duckdb/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ doc-comment = { workspace = true }
6161
polars-core = { workspace = true }
6262
pretty_assertions = { workspace = true }
6363
rand = { workspace = true }
64+
rustyline = { version = "15", features = ["case_insensitive_history_search"] }
6465
tempfile = { workspace = true }
6566
uuid = { workspace = true, features = ["v4"] }
6667

crates/duckdb/examples/repl.rs

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Usage:
2+
// cargo run --example repl
3+
// cargo run --example repl path/to/database.db
4+
5+
use duckdb::{
6+
arrow::{record_batch::RecordBatch, util::pretty::print_batches},
7+
Connection, Result as DuckResult,
8+
};
9+
use rustyline::{error::ReadlineError, history::DefaultHistory, Config, Editor};
10+
11+
const HISTORY_FILE: &str = ".duckdb_rs_history";
12+
13+
struct SqlRepl {
14+
conn: Connection,
15+
editor: Editor<(), DefaultHistory>,
16+
}
17+
18+
impl SqlRepl {
19+
fn new() -> DuckResult<Self> {
20+
Self::new_with_file(":memory:")
21+
}
22+
23+
fn new_with_file(path: &str) -> DuckResult<Self> {
24+
let conn = Connection::open(path)?;
25+
let editor = {
26+
let config = Config::builder().auto_add_history(true).build();
27+
let mut editor = Editor::with_config(config).expect("Failed to create editor");
28+
let _ = editor.load_history(HISTORY_FILE); // Load history, might not exist yet
29+
editor
30+
};
31+
Ok(SqlRepl { conn, editor })
32+
}
33+
34+
fn run(&mut self) -> DuckResult<()> {
35+
println!("duckdb-rs REPL");
36+
println!("Type '.help' for help.");
37+
38+
loop {
39+
match self.editor.readline("> ") {
40+
Ok(line) => {
41+
let line = line.trim();
42+
if line.is_empty() {
43+
continue;
44+
}
45+
match line {
46+
".quit" | ".exit" => break,
47+
".help" => self.show_help(),
48+
".schema" => {
49+
if let Err(e) = self.show_schema() {
50+
eprintln!("Error showing schema: {e}");
51+
}
52+
}
53+
".tables" => {
54+
if let Err(e) = self.show_tables() {
55+
eprintln!("Error showing tables: {e}");
56+
}
57+
}
58+
_ => {
59+
if let Err(e) = self.execute_sql(line) {
60+
eprintln!("{e}");
61+
}
62+
}
63+
}
64+
}
65+
Err(ReadlineError::Interrupted) => {
66+
continue;
67+
}
68+
Err(ReadlineError::Eof) => {
69+
break;
70+
}
71+
Err(err) => {
72+
eprintln!("Error: {err}");
73+
break;
74+
}
75+
}
76+
}
77+
78+
if let Err(e) = self.editor.save_history(HISTORY_FILE) {
79+
eprintln!("Warning: Failed to save history: {e}");
80+
}
81+
82+
Ok(())
83+
}
84+
85+
fn show_help(&self) {
86+
println!("Available commands:");
87+
println!(" .help - Show this help message");
88+
println!(" .quit - Exit the REPL");
89+
println!(" .exit - Exit the REPL");
90+
println!(" .schema - Show database schema");
91+
println!(" .tables - Show all tables");
92+
println!();
93+
println!("Keyboard shortcuts:");
94+
println!(" Up/Down - Navigate command history");
95+
println!(" Ctrl+R - Search command history");
96+
println!(" Ctrl+C - Cancel current input");
97+
println!(" Ctrl+D - Exit REPL");
98+
println!();
99+
println!("Enter any SQL statement to execute it.");
100+
println!();
101+
println!("Examples:");
102+
println!(" SELECT 1 + 1;");
103+
println!(" CREATE TABLE test (id INTEGER, name TEXT);");
104+
println!(" INSERT INTO test VALUES (1, 'hello');");
105+
println!(" SELECT * FROM test;");
106+
}
107+
108+
fn show_schema(&self) -> DuckResult<()> {
109+
let mut stmt = self.conn.prepare("SELECT sql FROM sqlite_master WHERE type='table'")?;
110+
let rbs: Vec<RecordBatch> = stmt.query_arrow([])?.collect();
111+
112+
if rbs.is_empty() || rbs[0].num_rows() == 0 {
113+
println!("No tables found in database.");
114+
} else {
115+
print_batches(&rbs).unwrap();
116+
}
117+
118+
Ok(())
119+
}
120+
121+
fn show_tables(&self) -> DuckResult<()> {
122+
let mut stmt = self.conn.prepare("SHOW TABLES")?;
123+
let rbs: Vec<RecordBatch> = stmt.query_arrow([])?.collect();
124+
125+
if rbs.is_empty() || rbs[0].num_rows() == 0 {
126+
println!("No tables found in database.");
127+
} else {
128+
print_batches(&rbs).unwrap();
129+
}
130+
131+
Ok(())
132+
}
133+
134+
fn execute_sql(&self, sql: &str) -> DuckResult<()> {
135+
// Check if it's a statement that returns results
136+
let sql_upper = sql.trim().to_uppercase();
137+
let is_query = sql_upper.starts_with("SELECT")
138+
|| sql_upper.starts_with("FROM")
139+
|| sql_upper.starts_with("SHOW")
140+
|| sql_upper.starts_with("DESCRIBE")
141+
|| sql_upper.starts_with("EXPLAIN")
142+
|| sql_upper.starts_with("PRAGMA")
143+
|| sql_upper.starts_with("WITH");
144+
145+
if is_query {
146+
let mut stmt = self.conn.prepare(sql)?;
147+
let rbs: Vec<RecordBatch> = stmt.query_arrow([])?.collect();
148+
149+
if rbs.is_empty() || rbs[0].num_rows() == 0 {
150+
println!("No results returned.");
151+
} else {
152+
print_batches(&rbs).unwrap();
153+
}
154+
} else {
155+
// Execute non-query statements
156+
self.conn.execute_batch(sql)?;
157+
}
158+
159+
Ok(())
160+
}
161+
}
162+
163+
fn main() -> DuckResult<()> {
164+
let args: Vec<String> = std::env::args().collect();
165+
166+
let mut repl = if args.len() > 1 {
167+
let db_path = &args[1];
168+
SqlRepl::new_with_file(db_path)?
169+
} else {
170+
SqlRepl::new()?
171+
};
172+
173+
repl.run()
174+
}

0 commit comments

Comments
 (0)