From 008521a85bfaf6a4ef29f6c454f0710d8a1a2b9d Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:28:31 +0200 Subject: [PATCH 01/29] Add modelardb_embedded as a dependency instead of arrow and tonic --- Cargo.lock | 6 ++---- crates/modelardb_client/Cargo.toml | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 832c23c9..858e319e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3524,14 +3524,12 @@ name = "modelardb_client" version = "0.1.0" dependencies = [ "arrow", - "arrow-flight", - "bytes", "clap", "dirs", - "modelardb_auth", + "futures", + "modelardb_embedded", "rustyline", "tokio", - "tonic", ] [[package]] diff --git a/crates/modelardb_client/Cargo.toml b/crates/modelardb_client/Cargo.toml index dd768b13..0e9cc18a 100644 --- a/crates/modelardb_client/Cargo.toml +++ b/crates/modelardb_client/Cargo.toml @@ -24,12 +24,10 @@ name = "modelardb" path = "src/main.rs" [dependencies] -arrow-flight.workspace = true arrow = { workspace = true, features = ["prettyprint"] } -bytes.workspace = true clap.workspace = true dirs.workspace = true -modelardb_auth.workspace = true +futures.workspace = true +modelardb_embedded.workspace = true rustyline.workspace = true tokio.workspace = true -tonic.workspace = true From 7f65d872543f5865cfbb465a5e44a66e71fd3663 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:28:56 +0200 Subject: [PATCH 02/29] Remove tonic errors and add ModelarDbEmbeddedError --- crates/modelardb_client/src/error.rs | 33 ++++++++++------------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/crates/modelardb_client/src/error.rs b/crates/modelardb_client/src/error.rs index 9cb489d6..18b59d54 100644 --- a/crates/modelardb_client/src/error.rs +++ b/crates/modelardb_client/src/error.rs @@ -21,9 +21,8 @@ use std::io::Error as IoError; use std::result::Result as StdResult; use arrow::error::ArrowError; +use modelardb_embedded::error::ModelarDbEmbeddedError; use rustyline::error::ReadlineError as RustyLineError; -use tonic::Status as TonicStatusError; -use tonic::transport::Error as TonicTransportError; /// Result type used throughout `modelardb_client`. pub type Result = StdResult; @@ -37,12 +36,10 @@ pub enum ModelarDbClientError { InvalidArgument(String), /// Error returned from IO operations. Io(IoError), + /// Error returned by modelardb_embedded. + ModelarDbEmbedded(ModelarDbEmbeddedError), /// Error returned by RustyLine. RustyLine(RustyLineError), - /// Status returned by Tonic. - TonicStatus(Box), - /// Error returned by Tonic. - TonicTransport(TonicTransportError), } impl Display for ModelarDbClientError { @@ -51,9 +48,8 @@ impl Display for ModelarDbClientError { Self::Arrow(reason) => write!(f, "Arrow Error: {reason}"), Self::InvalidArgument(reason) => write!(f, "Invalid Argument Error: {reason}"), Self::Io(reason) => write!(f, "Io Error: {reason}"), + Self::ModelarDbEmbedded(reason) => write!(f, "ModelarDB Embedded Error: {reason}"), Self::RustyLine(reason) => write!(f, "RustyLine Error: {reason}"), - Self::TonicStatus(reason) => write!(f, "Tonic Status Error: {reason}"), - Self::TonicTransport(reason) => write!(f, "Tonic Transport Error: {reason}"), } } } @@ -65,9 +61,8 @@ impl Error for ModelarDbClientError { Self::Arrow(reason) => Some(reason), Self::InvalidArgument(_reason) => None, Self::Io(reason) => Some(reason), + Self::ModelarDbEmbedded(reason) => Some(reason), Self::RustyLine(reason) => Some(reason), - Self::TonicStatus(reason) => Some(reason), - Self::TonicTransport(reason) => Some(reason), } } } @@ -84,20 +79,14 @@ impl From for ModelarDbClientError { } } -impl From for ModelarDbClientError { - fn from(error: RustyLineError) -> Self { - Self::RustyLine(error) +impl From for ModelarDbClientError { + fn from(error: ModelarDbEmbeddedError) -> Self { + Self::ModelarDbEmbedded(error) } } -impl From for ModelarDbClientError { - fn from(error: TonicStatusError) -> Self { - Self::TonicStatus(Box::new(error)) - } -} - -impl From for ModelarDbClientError { - fn from(error: TonicTransportError) -> Self { - Self::TonicTransport(error) +impl From for ModelarDbClientError { + fn from(error: RustyLineError) -> Self { + Self::RustyLine(error) } } From 615a533c9ab36ac24b12b610d2f4393e1ad5eda3 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:42:51 +0200 Subject: [PATCH 03/29] Connect to embedded Client instead of arrow flight client --- crates/modelardb_client/src/main.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 0cab02b0..cd64e58c 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -18,29 +18,20 @@ mod error; mod helper; -use std::collections::HashMap; -use std::convert::TryFrom; use std::fs::File; use std::io::{self, BufRead, BufReader, IsTerminal, Write}; use std::path::{Path as StdPath, PathBuf}; use std::process; -use std::sync::Arc; use std::time::Instant; -use arrow::array::ArrayRef; -use arrow::datatypes::Schema; -use arrow::ipc::convert; use arrow::util::pretty; -use arrow_flight::flight_service_client::FlightServiceClient; -use arrow_flight::{Action, Criteria, FlightData, FlightDescriptor, Ticket, utils}; -use bytes::Bytes; use clap::Parser; -use modelardb_auth::BearerInterceptor; +use futures::StreamExt; +use modelardb_embedded::error::ModelarDbEmbeddedError; +use modelardb_embedded::operations::Operations; +use modelardb_embedded::operations::client::Client; use rustyline::Editor; use rustyline::history::FileHistory; -use tonic::codegen::InterceptedService; -use tonic::transport::{Channel, Endpoint}; -use tonic::{Request, Streaming}; use crate::error::{ModelarDbClientError, Result}; use crate::helper::ClientHelper; @@ -88,12 +79,15 @@ async fn main() -> Result<()> { // Parse the command line arguments. let args = ClientArgs::parse(); + // Connect to the server. + let url = format!("grpc://{}:{}", args.host, args.port); + let client = Client::connect(&url, args.token.as_deref()).await?; + // Execute the queries. - let flight_service_client = connect(&args.host, args.port, args.token).await?; if let Some(query_file) = args.query_file { - execute_queries_from_a_file(flight_service_client, &query_file).await + execute_queries_from_a_file(client, &query_file).await } else { - execute_queries_from_a_repl(flight_service_client).await + execute_queries_from_a_repl(client).await } } From 6c08f2dcc85403c1c7db9353468dc6b44f19021f Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:43:05 +0200 Subject: [PATCH 04/29] Remove now unused connect function --- crates/modelardb_client/src/main.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index cd64e58c..2e4f895f 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -91,25 +91,6 @@ async fn main() -> Result<()> { } } -/// Connect to the server at `host`:`port` with an optional bearer `maybe_token`. Returns -/// [`ModelarDbClientError`] if a connection to the server cannot be established or the token is -/// not a valid ASCII metadata value. -async fn connect( - host: &str, - port: u16, - maybe_token: Option, -) -> Result { - let interceptor = BearerInterceptor::try_new(maybe_token.as_deref())?; - - let address = format!("grpc://{host}:{port}"); - let connection = Endpoint::new(address)?.connect().await?; - - Ok(FlightServiceClient::with_interceptor( - connection, - interceptor, - )) -} - /// Execute the commands and queries in `query_file`. async fn execute_queries_from_a_file( mut flight_service_client: AuthenticatedFlightClient, From b6675fa4fe4a52fd4ca1d210f5fff3753a5454e7 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:46:43 +0200 Subject: [PATCH 05/29] Pass Client down instead of arrow flight client --- crates/modelardb_client/src/main.rs | 31 +++++++++-------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 2e4f895f..4879e1e9 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -92,10 +92,7 @@ async fn main() -> Result<()> { } /// Execute the commands and queries in `query_file`. -async fn execute_queries_from_a_file( - mut flight_service_client: AuthenticatedFlightClient, - query_file: &StdPath, -) -> Result<()> { +async fn execute_queries_from_a_file(mut client: Client, query_file: &StdPath) -> Result<()> { let file = File::open(query_file)?; let lines = BufReader::new(file).lines(); @@ -111,7 +108,7 @@ async fn execute_queries_from_a_file( // Execute the query. if !query.is_empty() { println!("{query}"); - execute_and_print_command_or_query(&mut flight_service_client, &query).await + execute_and_print_command_or_query(&mut client, &query).await } } @@ -119,12 +116,10 @@ async fn execute_queries_from_a_file( } /// Execute commands and queries in a read-eval-print loop. -async fn execute_queries_from_a_repl( - mut flight_service_client: AuthenticatedFlightClient, -) -> Result<()> { +async fn execute_queries_from_a_repl(mut client: Client) -> Result<()> { // Create the read-eval-print loop. let mut editor = Editor::::new()?; - let table_names = retrieve_table_names(&mut flight_service_client).await?; + let table_names = client.tables().await?; editor.set_helper(Some(ClientHelper::new(table_names))); // Read previously executed commands and queries from the history file. @@ -140,7 +135,7 @@ async fn execute_queries_from_a_repl( // Execute commands and queries and print the result. while let Ok(line) = editor.readline("ModelarDB> ") { editor.add_history_entry(line.as_str())?; - execute_and_print_command_or_query(&mut flight_service_client, &line).await + execute_and_print_command_or_query(&mut client, &line).await } // Append the executed commands and queries to the history file. @@ -154,17 +149,14 @@ async fn execute_queries_from_a_repl( /// Execute a command or a query. Returns [`ModelarDbClientError`] if the command or query could not /// be executed or their result could not be retrieved. -async fn execute_and_print_command_or_query( - flight_service_client: &mut AuthenticatedFlightClient, - command_or_query: &str, -) { +async fn execute_and_print_command_or_query(client: &mut Client, command_or_query: &str) { let start_time = Instant::now(); let command_or_query = command_or_query.trim(); let result = if command_or_query.starts_with('\\') { - execute_command(flight_service_client, command_or_query).await + execute_command(client, command_or_query).await } else { - execute_query_and_print_result(flight_service_client, command_or_query).await + execute_query_and_print_result(client, command_or_query).await }; if let Err(message) = result { @@ -178,12 +170,7 @@ async fn execute_and_print_command_or_query( /// * An incorrect argument for the command was provided. /// * The command could not be executed. /// * The result could not be retrieved. -async fn execute_command( - flight_service_client: &mut AuthenticatedFlightClient, - command_and_argument: &str, -) -> Result<()> { - let mut command_and_argument = command_and_argument.split(' '); - match command_and_argument +async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Result<()> { .next() .ok_or(ModelarDbClientError::InvalidArgument( "No command was provided.".to_owned(), From 372dd88268d5a5bcbf57eb7dd74e2bc460d9e850 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:47:04 +0200 Subject: [PATCH 06/29] Remove unused retrieve_table_names function --- crates/modelardb_client/src/main.rs | 31 ----------------------------- 1 file changed, 31 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 4879e1e9..230aa101 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -236,37 +236,6 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } } -/// Retrieve the names of the tables available on the server. Returns [`ModelarDbClientError`] if -/// the request could not be performed or the tables names could not be retrieved. -async fn retrieve_table_names( - flight_service_client: &mut AuthenticatedFlightClient, -) -> Result> { - let criteria = Criteria { - expression: Bytes::new(), - }; - let request = Request::new(criteria); - - let mut stream = flight_service_client - .list_flights(request) - .await? - .into_inner(); - - let flight_infos = stream - .message() - .await? - .ok_or(ModelarDbClientError::InvalidArgument( - TRANSPORT_ERROR.to_owned(), - ))?; - - let mut table_names = vec![]; - if let Some(flight_descriptor) = flight_infos.flight_descriptor { - for table_name in flight_descriptor.path { - table_names.push(table_name); - } - } - - Ok(table_names) -} /// Execute an action. Returns [`ModelarDbClientError`] if the action could not be executed. async fn execute_action( From dc0e0331d19dca2eab6b1c777934dc840cf2433b Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:47:23 +0200 Subject: [PATCH 07/29] Remove unused execute_action function --- crates/modelardb_client/src/main.rs | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 230aa101..9a02d580 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -237,28 +237,6 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } -/// Execute an action. Returns [`ModelarDbClientError`] if the action could not be executed. -async fn execute_action( - flight_service_client: &mut AuthenticatedFlightClient, - action_type: &str, - action_body: &str, -) -> Result<()> { - let action = Action { - r#type: action_type.to_owned(), - body: action_body.to_owned().into(), - }; - - let request = Request::new(action); - - flight_service_client - .do_action(request) - .await? - .into_inner() - .message() - .await?; - - Ok(()) -} /// Execute a query and print each batch in the result set. Returns [`ModelarDbClientError`] if the /// query could not be executed or the batches in the result set could not be printed. From ce7a6f7497ed185c0743a9b39de3457ab905d87f Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:48:16 +0200 Subject: [PATCH 08/29] Remove unused TRANSPORT_ERROR and AuthenticatedFlightClient --- crates/modelardb_client/src/main.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 9a02d580..3bd60bab 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -36,13 +36,6 @@ use rustyline::history::FileHistory; use crate::error::{ModelarDbClientError, Result}; use crate::helper::ClientHelper; -/// Error to emit when the server does not provide a response when one is expected. -const TRANSPORT_ERROR: &str = "transport error: no messages received."; - -/// [`FlightServiceClient`] with a [`BearerInterceptor`] that attaches an authorization header. -type AuthenticatedFlightClient = - FlightServiceClient>; - /// Command line arguments for the ModelarDB client. #[derive(Parser)] #[command( From d866c4e33bc3ff43c6df9f239b064f3bfe8b9b4a Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 07:51:49 +0200 Subject: [PATCH 09/29] Use Client in execute_command() --- crates/modelardb_client/src/main.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 3bd60bab..f3029edd 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -164,6 +164,8 @@ async fn execute_and_print_command_or_query(client: &mut Client, command_or_quer /// * The command could not be executed. /// * The result could not be retrieved. async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Result<()> { + let mut command_and_arguments = command_and_arguments.split_whitespace(); + match command_and_arguments .next() .ok_or(ModelarDbClientError::InvalidArgument( "No command was provided.".to_owned(), @@ -171,18 +173,13 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re // Print the schema of a table on the server. "\\d" => { let table_name = - command_and_argument + command_and_arguments .next() .ok_or(ModelarDbClientError::InvalidArgument( "No table name was provided.".to_owned(), ))?; - let flight_descriptor = FlightDescriptor::new_path(vec![table_name.to_owned()]); - let request = Request::new(flight_descriptor); - let schema_result = flight_service_client - .get_schema(request) - .await? - .into_inner(); - let schema = convert::try_schema_from_ipc_buffer(&schema_result.schema)?; + + let schema = client.schema(table_name).await?; for field in schema.fields() { print!("{}: {}", field.name(), field.data_type()); for (metadata_name, metadata_value) in field.metadata() { @@ -194,17 +191,15 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } // Print the name of the tables on the server. "\\dt" => { - if let Ok(tables) = retrieve_table_names(flight_service_client).await { - for table in tables { - println!("{table}"); - } + for table_name in client.tables().await? { + println!("{table_name}"); } Ok(()) } // Flushes all data the server currently has in memory to disk. - "\\f" => execute_action(flight_service_client, "FlushMemory", "").await, + "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. - "\\F" => execute_action(flight_service_client, "FlushNode", "").await, + "\\F" => client.flush_node().await.map_err(|error| error.into()), // Print helpful information, explanations with \\ must be indented more to be aligned. "\\h" => { println!( From b9d50c2e30dd81eebd41514dd0bb01cbe2ecd8e4 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 08:05:57 +0200 Subject: [PATCH 10/29] Simplify query execution and user confirmation flow --- crates/modelardb_client/src/main.rs | 102 +++++++++------------------- 1 file changed, 33 insertions(+), 69 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index f3029edd..d2b4c8f1 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -224,67 +224,23 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } } - - -/// Execute a query and print each batch in the result set. Returns [`ModelarDbClientError`] if the -/// query could not be executed or the batches in the result set could not be printed. -async fn execute_query_and_print_result( - flight_service_client: &mut AuthenticatedFlightClient, - query: &str, -) -> Result<()> { - // Execute the query. - let ticket = Ticket { - ticket: query.to_owned().into(), - }; - let mut stream = flight_service_client.do_get(ticket).await?.into_inner(); - - // Get the schema of the data in the query result. - let flight_data = stream - .message() - .await? - .ok_or(ModelarDbClientError::InvalidArgument( - TRANSPORT_ERROR.to_owned(), - ))?; - let schema = Arc::new(Schema::try_from(&flight_data)?); - let dictionaries_by_id = HashMap::new(); - - if io::stdout().is_terminal() { - print_batches_with_confirmation(stream, schema, &dictionaries_by_id).await - } else { - print_batches_without_confirmation(stream, schema, &dictionaries_by_id).await - } -} - -/// Print each batch in the result set with confirmation from the user before printing each batch. -/// Returns [`ModelarDbClientError`] if the batches in the result set could not be printed. -async fn print_batches_with_confirmation( - mut stream: Streaming, - schema: Arc, - dictionaries_by_id: &HashMap, -) -> Result<()> { - let mut user_input = String::new(); +/// Execute a query and print each batch in the result set. If standard output is a terminal, ask +/// the user for confirmation before printing each batch after the first. Returns +/// [`ModelarDbClientError`] if the query could not be executed or the batches in the result set +/// could not be printed. +async fn execute_query_and_print_result(client: &mut Client, query: &str) -> Result<()> { + let mut record_batch_stream = client.read(query).await?; + + let print_confirmation = io::stdout().is_terminal(); let mut multiple_batches = false; - while let Some(flight_data) = stream.message().await? { - let record_batch = - utils::flight_data_to_arrow_batch(&flight_data, schema.clone(), dictionaries_by_id)?; - + while let Some(record_batch) = record_batch_stream.next().await { // Only ask for confirmation to print the next batch if there are multiple batches. - if multiple_batches { - loop { - user_input.clear(); - print!("Press Enter for next batch and q+Enter to quit> "); - io::stdout().flush()?; - io::stdin().read_line(&mut user_input)?; - - match user_input.as_str() { - "\n" => break, - "q\n" => return Ok(()), - _ => (), - } - } + if print_confirmation && multiple_batches && !confirm_printing_next_batch()? { + return Ok(()); } + let record_batch = record_batch.map_err(ModelarDbEmbeddedError::from)?; pretty::print_batches(&[record_batch])?; multiple_batches = true; } @@ -292,19 +248,27 @@ async fn print_batches_with_confirmation( Ok(()) } -/// Print each batch in the result set without user input. Returns [`ModelarDbClientError`] if the -/// batches in the result set could not be printed. -async fn print_batches_without_confirmation( - mut stream: Streaming, - schema: Arc, - dictionaries_by_id: &HashMap, -) -> Result<()> { - while let Some(flight_data) = stream.message().await? { - let record_batch = - utils::flight_data_to_arrow_batch(&flight_data, schema.clone(), dictionaries_by_id)?; +/// Ask the user for confirmation before printing the next batch in a result set. Returns false if +/// the user chose to stop printing batches. Returns [`ModelarDbClientError`] if the input could not +/// be read. +fn confirm_printing_next_batch() -> Result { + let mut user_input = String::new(); - pretty::print_batches(&[record_batch])?; - } + loop { + user_input.clear(); + print!("Press Enter for next batch and q+Enter to quit> "); + io::stdout().flush()?; - Ok(()) + // A read of zero bytes means standard input reached end-of-file, so no more batches can be + // confirmed. + if io::stdin().read_line(&mut user_input)? == 0 { + return Ok(false); + } + + match user_input.as_str() { + "\n" => return Ok(true), + "q\n" => return Ok(false), + _ => (), + } + } } From c9b3f60651fac8e35492a842e4dd30e4b33b6a54 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Mon, 17 Aug 2026 09:21:38 +0200 Subject: [PATCH 11/29] Fix problem with line endings on Windows --- crates/modelardb_client/src/main.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index d2b4c8f1..f02b56e9 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -265,9 +265,10 @@ fn confirm_printing_next_batch() -> Result { return Ok(false); } - match user_input.as_str() { - "\n" => return Ok(true), - "q\n" => return Ok(false), + // The line includes the line ending, which is \r\n on Windows and \n everywhere else. + match user_input.trim() { + "" => return Ok(true), + "q" => return Ok(false), _ => (), } } From 9e3ca56d0ee5bfa8e9f68c28b39e1b37fc30333d Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:20:38 +0200 Subject: [PATCH 12/29] Add command to print configuration --- Cargo.lock | 1 + crates/modelardb_client/Cargo.toml | 1 + crates/modelardb_client/src/main.rs | 45 +++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 858e319e..3065b7dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3528,6 +3528,7 @@ dependencies = [ "dirs", "futures", "modelardb_embedded", + "modelardb_types", "rustyline", "tokio", ] diff --git a/crates/modelardb_client/Cargo.toml b/crates/modelardb_client/Cargo.toml index 0e9cc18a..a861e51c 100644 --- a/crates/modelardb_client/Cargo.toml +++ b/crates/modelardb_client/Cargo.toml @@ -29,5 +29,6 @@ clap.workspace = true dirs.workspace = true futures.workspace = true modelardb_embedded.workspace = true +modelardb_types.workspace = true rustyline.workspace = true tokio.workspace = true diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index f02b56e9..4b341fc9 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -30,6 +30,7 @@ use futures::StreamExt; use modelardb_embedded::error::ModelarDbEmbeddedError; use modelardb_embedded::operations::Operations; use modelardb_embedded::operations::client::Client; +use modelardb_types::flight::protocol; use rustyline::Editor; use rustyline::history::FileHistory; @@ -196,6 +197,12 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } Ok(()) } + // Print the configuration of the node. + "\\dc" => { + let configuration = client.configuration().await?; + print_configuration(&configuration); + Ok(()) + } // Flushes all data the server currently has in memory to disk. "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. @@ -273,3 +280,41 @@ fn confirm_printing_next_batch() -> Result { } } } + +/// Print each field in `configuration` on its own line. +fn print_configuration(configuration: &protocol::Configuration) { + println!( + "ingested_reserved_memory_in_bytes: {}", + configuration.ingested_reserved_memory_in_bytes + ); + println!( + "uncompressed_reserved_memory_in_bytes: {}", + configuration.uncompressed_reserved_memory_in_bytes + ); + println!( + "compressed_reserved_memory_in_bytes: {}", + configuration.compressed_reserved_memory_in_bytes + ); + + let transfer_batch_size_in_bytes = configuration + .transfer_batch_size_in_bytes + .map_or("not set".to_owned(), |value| value.to_string()); + println!("transfer_batch_size_in_bytes: {transfer_batch_size_in_bytes}"); + + println!( + "segment_size_threshold_in_bytes: {}", + configuration.segment_size_threshold_in_bytes + ); + println!( + "optimize_target_file_size_in_bytes: {}", + configuration.optimize_target_file_size_in_bytes + ); + println!( + "vacuum_retention_period_in_seconds: {}", + configuration.vacuum_retention_period_in_seconds + ); + println!("ingestion_threads: {}", configuration.ingestion_threads); + println!("compression_threads: {}", configuration.compression_threads); + println!("writer_threads: {}", configuration.writer_threads); + println!("wal_enabled: {}", configuration.wal_enabled); +} From 4d31cc1e49bbfb0e75a60dd8d4696a921f619d83 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:22:16 +0200 Subject: [PATCH 13/29] Add command to print nodes --- crates/modelardb_client/src/main.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 4b341fc9..81169bb4 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -203,6 +203,13 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re print_configuration(&configuration); Ok(()) } + // Print the nodes that are currently part of the cluster. + "\\dn" => { + for node in client.list_nodes().await? { + println!("{} ({})", node.url, node.mode); + } + Ok(()) + } // Flushes all data the server currently has in memory to disk. "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. From 3fece47bc6f9d8a418b5841030eeca6f84852aba Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:25:35 +0200 Subject: [PATCH 14/29] Add command to print metrics --- crates/modelardb_client/src/main.rs | 55 +++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 81169bb4..a9598f92 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -210,6 +210,12 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } Ok(()) } + // Print the resource usage metrics of the node. + "\\dm" => { + let node_metrics = client.node_metrics().await?; + print_node_metrics(&node_metrics); + Ok(()) + } // Flushes all data the server currently has in memory to disk. "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. @@ -325,3 +331,52 @@ fn print_configuration(configuration: &protocol::Configuration) { println!("writer_threads: {}", configuration.writer_threads); println!("wal_enabled: {}", configuration.wal_enabled); } + +/// Print each field in `node_metrics` on its own line. +fn print_node_metrics(node_metrics: &protocol::NodeMetrics) { + println!( + "cpu_usage_percentage: {}", + node_metrics.cpu_usage_percentage + ); + println!("cpu_count: {}", node_metrics.cpu_count); + println!( + "used_memory_in_bytes: {}", + node_metrics.used_memory_in_bytes + ); + println!( + "total_memory_in_bytes: {}", + node_metrics.total_memory_in_bytes + ); + println!( + "used_disk_space_in_bytes: {}", + node_metrics.used_disk_space_in_bytes + ); + println!( + "total_disk_space_in_bytes: {}", + node_metrics.total_disk_space_in_bytes + ); + println!( + "ingested_used_memory_in_bytes: {}", + node_metrics.ingested_used_memory_in_bytes + ); + println!( + "ingested_reserved_memory_in_bytes: {}", + node_metrics.ingested_reserved_memory_in_bytes + ); + println!( + "uncompressed_used_memory_in_bytes: {}", + node_metrics.uncompressed_used_memory_in_bytes + ); + println!( + "uncompressed_reserved_memory_in_bytes: {}", + node_metrics.uncompressed_reserved_memory_in_bytes + ); + println!( + "compressed_used_memory_in_bytes: {}", + node_metrics.compressed_used_memory_in_bytes + ); + println!( + "compressed_reserved_memory_in_bytes: {}", + node_metrics.compressed_reserved_memory_in_bytes + ); +} From 262aab93a8119e02d4f9fc1d07cec58b7b7e8f68 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:34:16 +0200 Subject: [PATCH 15/29] Add command to update setting --- crates/modelardb_client/src/main.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index a9598f92..82d0a3be 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -31,6 +31,7 @@ use modelardb_embedded::error::ModelarDbEmbeddedError; use modelardb_embedded::operations::Operations; use modelardb_embedded::operations::client::Client; use modelardb_types::flight::protocol; +use modelardb_types::flight::protocol::update_configuration::Setting; use rustyline::Editor; use rustyline::history::FileHistory; @@ -216,6 +217,32 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re print_node_metrics(&node_metrics); Ok(()) } + // Update a setting in the configuration of the node. + "\\s" => { + let name = + command_and_arguments + .next() + .ok_or(ModelarDbClientError::InvalidArgument( + "No setting was provided.".to_owned(), + ))?; + + let setting = Setting::from_str_name(&name.to_uppercase()).ok_or( + ModelarDbClientError::InvalidArgument(format!("Unknown setting: {name}.")), + )?; + + // Omitting the value unsets the setting if it is optional. + let new_value = match command_and_arguments.next() { + Some(value) => Some(value.parse::().map_err(|_error| { + ModelarDbClientError::InvalidArgument(format!( + "{value} is not a valid value for {name}." + )) + })?), + None => None, + }; + + client.update_configuration(setting, new_value).await?; + Ok(()) + } // Flushes all data the server currently has in memory to disk. "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. From 9a7b45572158f50856c3468723547b0b002d3a1f Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:36:54 +0200 Subject: [PATCH 16/29] Add command to kill the node and stop the process --- crates/modelardb_client/src/main.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 82d0a3be..d819a306 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -247,6 +247,11 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. "\\F" => client.flush_node().await.map_err(|error| error.into()), + // Kill the node and quit, the connection is dead once the node process exits. + "\\k" => { + client.kill_node().await?; + process::exit(0); + } // Print helpful information, explanations with \\ must be indented more to be aligned. "\\h" => { println!( From bb469b12a7c9cbd63d59e1795a9d5302376678ef Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:39:05 +0200 Subject: [PATCH 17/29] Add new commands to help message --- crates/modelardb_client/src/main.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index d819a306..229e4200 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -247,11 +247,6 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re "\\f" => client.flush_memory().await.map_err(|error| error.into()), // Flushes all data the server currently has in memory and disk to the object store. "\\F" => client.flush_node().await.map_err(|error| error.into()), - // Kill the node and quit, the connection is dead once the node process exits. - "\\k" => { - client.kill_node().await?; - process::exit(0); - } // Print helpful information, explanations with \\ must be indented more to be aligned. "\\h" => { println!( @@ -260,13 +255,23 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re SELECT Execute a SELECT statement.\n\ \\d TABLE_NAME Print the schema of a table with TABLE_NAME.\n\ \\dt Print the name of all the tables.\n\ + \\dc Print the configuration of the node.\n\ + \\dn Print the nodes in the cluster.\n\ + \\dm Print the resource usage metrics of the node.\n\ + \\s SETTING [VALUE] Set SETTING to VALUE, or unset SETTING if VALUE is omitted.\n\ \\f Flushes data in memory to disk.\n\ \\F Flushes data in memory and disk to the object store.\n\ \\h Print documentation for all supported commands.\n\ + \\k Kill the node and quit modelardb.\n\ \\q Quit modelardb." ); Ok(()) } + // Kill the node and quit, the connection is dead once the node process exits. + "\\k" => { + client.kill_node().await?; + process::exit(0); + } "\\q" => { process::exit(0); } From f65d7d1ecede8b0b30682f9f6f6b2ad3a40b88fb Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 09:47:30 +0200 Subject: [PATCH 18/29] Change \s command to \sc --- crates/modelardb_client/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 229e4200..d0ae74a7 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -218,7 +218,7 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re Ok(()) } // Update a setting in the configuration of the node. - "\\s" => { + "\\sc" => { let name = command_and_arguments .next() @@ -258,7 +258,7 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re \\dc Print the configuration of the node.\n\ \\dn Print the nodes in the cluster.\n\ \\dm Print the resource usage metrics of the node.\n\ - \\s SETTING [VALUE] Set SETTING to VALUE, or unset SETTING if VALUE is omitted.\n\ + \\sc SETTING [VALUE] Set SETTING to VALUE, or unset SETTING if VALUE is omitted.\n\ \\f Flushes data in memory to disk.\n\ \\F Flushes data in memory and disk to the object store.\n\ \\h Print documentation for all supported commands.\n\ From 5daa7ec474f743a3c1c923f9cb260905c93e1b53 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:19:18 +0200 Subject: [PATCH 19/29] No longer transfer in KillNode for consistency --- crates/modelardb_client/src/main.rs | 2 +- .../src/operations/client.rs | 10 ++++---- crates/modelardb_server/src/remote/mod.rs | 25 +++++++++---------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index d0ae74a7..f4c85296 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -262,7 +262,7 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re \\f Flushes data in memory to disk.\n\ \\F Flushes data in memory and disk to the object store.\n\ \\h Print documentation for all supported commands.\n\ - \\k Kill the node and quit modelardb.\n\ + \\k Flush data to disk, kill the node, and quit modelardb.\n\ \\q Quit modelardb." ); Ok(()) diff --git a/crates/modelardb_embedded/src/operations/client.rs b/crates/modelardb_embedded/src/operations/client.rs index 642bafd6..4de4d4af 100644 --- a/crates/modelardb_embedded/src/operations/client.rs +++ b/crates/modelardb_embedded/src/operations/client.rs @@ -112,11 +112,11 @@ impl Client { Ok(()) } - /// Flushes all data to disk, transfers it to the remote object store, removes the node from the - /// cluster if necessary, and kills the node process. Since the process is killed, a - /// conventional response cannot be returned, so a dropped connection is not treated as an - /// error. If the data could not be flushed before the node was killed, - /// [`ModelarDbEmbeddedError`] is returned. + /// Flushes all data to disk, removes the node from the cluster if necessary, and kills the node + /// process. Data is not transferred to the remote object store. Call [`Client::flush_node`] + /// first if that is required. Since the process is killed, a conventional response cannot be + /// returned, so a dropped connection is not treated as an error. If the data could not be + /// flushed before the node was killed, [`ModelarDbEmbeddedError`] is returned. pub async fn kill_node(&mut self) -> Result<()> { // The node exits while handling this action, so the response stream is dropped. let _ = self.send_action("KillNode", vec![]).await; diff --git a/crates/modelardb_server/src/remote/mod.rs b/crates/modelardb_server/src/remote/mod.rs index 65c776d4..01846d39 100644 --- a/crates/modelardb_server/src/remote/mod.rs +++ b/crates/modelardb_server/src/remote/mod.rs @@ -886,10 +886,11 @@ impl FlightService for FlightServiceHandler { /// currently in memory to disk and then flushes all compressed data on disk to the remote /// object store. Note that data is only transferred to the remote object store if one was /// provided when starting the server. - /// * `KillNode`: An extension of the `FlushNode` action that first flushes all data to disk, - /// then flushes all compressed data to the remote object store, then removes the node - /// from the cluster if necessary, and finally kills the process that is running the server. - /// Note that since the process is killed, a conventional response cannot be returned. + /// * `KillNode`: An extension of the `FlushMemory` action that first flushes all data that is + /// currently in memory to disk, then removes the node from the cluster if necessary, and + /// finally kills the process that is running the server. Data is not transferred to the remote + /// object store. Use `FlushNode` first if that is required. Note that since the process is + /// killed, a conventional response cannot be returned. /// * `GetConfiguration`: Get the current server configuration. The value of each setting in the /// configuration is returned in a [`Configuration`](protocol::Configuration) protobuf message. /// * `UpdateConfiguration`: Update a single setting in the configuration. The setting to update @@ -954,13 +955,11 @@ impl FlightService for FlightServiceHandler { // Confirm the data was flushed. Ok(Response::new(Box::pin(stream::empty()))) } else if action.r#type == "KillNode" { - let mut storage_engine = self.context.storage_engine.write().await; - storage_engine - .flush() + self.context + .storage_engine + .write() .await - .map_err(error_to_status_internal)?; - storage_engine - .transfer() + .flush() .await .map_err(error_to_status_internal)?; @@ -1134,9 +1133,9 @@ impl FlightService for FlightServiceHandler { let kill_node_action = ActionType { r#type: "KillNode".to_owned(), - description: "Flush uncompressed data to disk by compressing and saving the data, \ - transfer all compressed data to the remote object store, and kill the \ - process running the server." + description: "Flush uncompressed data to disk by compressing and saving the data and \ + kill the process running the server. Data is not transferred to the \ + remote object store. Use FlushNode first if that is required." .to_owned(), }; From 5fd2d1b4f79a99147934e76fd66518f02726839b Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:26:47 +0200 Subject: [PATCH 20/29] Fix alignment issue in help message --- crates/modelardb_client/src/main.rs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index f4c85296..2b7ea3c3 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -253,17 +253,17 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re "CREATE [TIME SERIES] TABLE Execute a CREATE TABLE or CREATE TIME SERIES TABLE statement.\n\ INSERT INTO Execute an INSERT INTO statement. Must include generated columns.\n\ SELECT Execute a SELECT statement.\n\ - \\d TABLE_NAME Print the schema of a table with TABLE_NAME.\n\ - \\dt Print the name of all the tables.\n\ - \\dc Print the configuration of the node.\n\ - \\dn Print the nodes in the cluster.\n\ - \\dm Print the resource usage metrics of the node.\n\ - \\sc SETTING [VALUE] Set SETTING to VALUE, or unset SETTING if VALUE is omitted.\n\ - \\f Flushes data in memory to disk.\n\ - \\F Flushes data in memory and disk to the object store.\n\ - \\h Print documentation for all supported commands.\n\ - \\k Flush data to disk, kill the node, and quit modelardb.\n\ - \\q Quit modelardb." + \\d TABLE_NAME Print the schema of a table with TABLE_NAME.\n\ + \\dt Print the name of all the tables.\n\ + \\dc Print the configuration of the node.\n\ + \\dn Print the nodes in the cluster.\n\ + \\dm Print the resource usage metrics of the node.\n\ + \\sc SETTING [VALUE] Set SETTING to VALUE, or unset SETTING if VALUE is omitted.\n\ + \\f Flushes data in memory to disk.\n\ + \\F Flushes data in memory and disk to the object store.\n\ + \\h Print documentation for all supported commands.\n\ + \\k Flush data to disk, kill the node, and quit modelardb.\n\ + \\q Quit modelardb." ); Ok(()) } From 97e4b86e0b21eb40effbeef491a2b41fc03dcc0b Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:02:45 +0200 Subject: [PATCH 21/29] Refresh the table names for tab-completion if a table may have been created or dropped --- crates/modelardb_client/src/main.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 2b7ea3c3..9cd1724c 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -130,7 +130,15 @@ async fn execute_queries_from_a_repl(mut client: Client) -> Result<()> { // Execute commands and queries and print the result. while let Ok(line) = editor.readline("ModelarDB> ") { editor.add_history_entry(line.as_str())?; - execute_and_print_command_or_query(&mut client, &line).await + execute_and_print_command_or_query(&mut client, &line).await; + + // Refresh the table names for tab-completion if a table may have been created or dropped. + let first_word = line.split_whitespace().next().unwrap_or(""); + if first_word.eq_ignore_ascii_case("CREATE") || first_word.eq_ignore_ascii_case("DROP") { + if let Ok(table_names) = client.tables().await { + editor.set_helper(Some(ClientHelper::new(table_names))); + } + } } // Append the executed commands and queries to the history file. From c20ea9efd28c40f8dbefd97fa40954d26d1eabf6 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:12:18 +0200 Subject: [PATCH 22/29] Add supported commands to user docs --- docs/user/README.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/user/README.md b/docs/user/README.md index 8a64f20f..e2d5bdce 100644 --- a/docs/user/README.md +++ b/docs/user/README.md @@ -263,10 +263,20 @@ instance executes the `SELECT` statement on the data it manages and forwards the the provided addresses. Afterwards, the `modelardbd` instance that initially received the query, unions the result from all `modelardbd` instances and returns it to the client. As an example, this can be used to execute a `SELECT` statement on both the data in the cloud and a specific edge node. Although, be aware that this does not make the edge node -transfer the data it manages to the cloud, only the result of the query. In addition to SQL statements, the REPL also -supports listing all tables using `\dt`, printing the schema of a table using `\d table_name`, flushing data in memory -to disk using `\f`, flushing data in memory and on disk to an object store using `\F`, and printing operations supported -by the client using `\h`. +transfer the data it manages to the cloud, only the result of the query. + +In addition to SQL statements, the REPL supports the following commands: +- `\d table_name` - Print the schema of the table with `table_name`. +- `\dt` - Print the name of all the tables. +- `\dc` - Print the configuration of the node. +- `\dn` - Print the nodes in the cluster. +- `\dm` - Print the resource usage metrics of the node. +- `\sc setting [value]` - Set `setting` to `value`, or unset `setting` if `value` is omitted. +- `\f` - Flush data in memory to disk. +- `\F` - Flush data in memory and on disk to an object store. +- `\h` - Print documentation for all supported commands. +- `\k` - Flush data to disk, kill the node, and quit `modelardb`. +- `\q` - Quit `modelardb`. ```sql ModelarDB> \dt From df5cb995a4a40286af2da31e430928a82256ed06 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:22:05 +0200 Subject: [PATCH 23/29] Fix clippy issue --- crates/modelardb_client/src/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 9cd1724c..a6145c22 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -114,8 +114,7 @@ async fn execute_queries_from_a_file(mut client: Client, query_file: &StdPath) - async fn execute_queries_from_a_repl(mut client: Client) -> Result<()> { // Create the read-eval-print loop. let mut editor = Editor::::new()?; - let table_names = client.tables().await?; - editor.set_helper(Some(ClientHelper::new(table_names))); + editor.set_helper(Some(ClientHelper::new(client.tables().await?))); // Read previously executed commands and queries from the history file. let history_file_name = ".modelardb_history"; @@ -134,10 +133,10 @@ async fn execute_queries_from_a_repl(mut client: Client) -> Result<()> { // Refresh the table names for tab-completion if a table may have been created or dropped. let first_word = line.split_whitespace().next().unwrap_or(""); - if first_word.eq_ignore_ascii_case("CREATE") || first_word.eq_ignore_ascii_case("DROP") { - if let Ok(table_names) = client.tables().await { - editor.set_helper(Some(ClientHelper::new(table_names))); - } + if (first_word.eq_ignore_ascii_case("CREATE") || first_word.eq_ignore_ascii_case("DROP")) + && let Ok(table_names) = client.tables().await + { + editor.set_helper(Some(ClientHelper::new(table_names))); } } From fa960bc15ab34e488086df3e4f97cd436535ee04 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:48:23 +0200 Subject: [PATCH 24/29] Use maybe_ for optional value for consistency --- crates/modelardb_client/src/main.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index a6145c22..f7ac908a 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -207,8 +207,7 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } // Print the configuration of the node. "\\dc" => { - let configuration = client.configuration().await?; - print_configuration(&configuration); + print_configuration(&client.configuration().await?); Ok(()) } // Print the nodes that are currently part of the cluster. @@ -220,8 +219,7 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re } // Print the resource usage metrics of the node. "\\dm" => { - let node_metrics = client.node_metrics().await?; - print_node_metrics(&node_metrics); + print_node_metrics(&client.node_metrics().await?); Ok(()) } // Update a setting in the configuration of the node. @@ -238,7 +236,7 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re )?; // Omitting the value unsets the setting if it is optional. - let new_value = match command_and_arguments.next() { + let maybe_new_value = match command_and_arguments.next() { Some(value) => Some(value.parse::().map_err(|_error| { ModelarDbClientError::InvalidArgument(format!( "{value} is not a valid value for {name}." @@ -247,7 +245,9 @@ async fn execute_command(client: &mut Client, command_and_arguments: &str) -> Re None => None, }; - client.update_configuration(setting, new_value).await?; + client + .update_configuration(setting, maybe_new_value) + .await?; Ok(()) } // Flushes all data the server currently has in memory to disk. From cc5cedc57fc29d3c2920c98ae0b86467e6d4fcc4 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:56:34 +0200 Subject: [PATCH 25/29] Prettify print functions --- crates/modelardb_client/src/main.rs | 133 ++++++++++++---------------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index f7ac908a..b34db4c0 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -340,87 +340,64 @@ fn confirm_printing_next_batch() -> Result { /// Print each field in `configuration` on its own line. fn print_configuration(configuration: &protocol::Configuration) { - println!( - "ingested_reserved_memory_in_bytes: {}", - configuration.ingested_reserved_memory_in_bytes - ); - println!( - "uncompressed_reserved_memory_in_bytes: {}", - configuration.uncompressed_reserved_memory_in_bytes - ); - println!( - "compressed_reserved_memory_in_bytes: {}", - configuration.compressed_reserved_memory_in_bytes - ); - - let transfer_batch_size_in_bytes = configuration - .transfer_batch_size_in_bytes - .map_or("not set".to_owned(), |value| value.to_string()); + let protocol::Configuration { + ingested_reserved_memory_in_bytes, + uncompressed_reserved_memory_in_bytes, + compressed_reserved_memory_in_bytes, + transfer_batch_size_in_bytes, + segment_size_threshold_in_bytes, + optimize_target_file_size_in_bytes, + vacuum_retention_period_in_seconds, + ingestion_threads, + compression_threads, + writer_threads, + wal_enabled, + } = configuration; + + // Data is only transferred on an explicit flush if the batch size is not set. + let transfer_batch_size_in_bytes = + transfer_batch_size_in_bytes.map_or("not set".to_owned(), |value| value.to_string()); + + println!("ingested_reserved_memory_in_bytes: {ingested_reserved_memory_in_bytes}"); + println!("uncompressed_reserved_memory_in_bytes: {uncompressed_reserved_memory_in_bytes}"); + println!("compressed_reserved_memory_in_bytes: {compressed_reserved_memory_in_bytes}"); println!("transfer_batch_size_in_bytes: {transfer_batch_size_in_bytes}"); - - println!( - "segment_size_threshold_in_bytes: {}", - configuration.segment_size_threshold_in_bytes - ); - println!( - "optimize_target_file_size_in_bytes: {}", - configuration.optimize_target_file_size_in_bytes - ); - println!( - "vacuum_retention_period_in_seconds: {}", - configuration.vacuum_retention_period_in_seconds - ); - println!("ingestion_threads: {}", configuration.ingestion_threads); - println!("compression_threads: {}", configuration.compression_threads); - println!("writer_threads: {}", configuration.writer_threads); - println!("wal_enabled: {}", configuration.wal_enabled); + println!("segment_size_threshold_in_bytes: {segment_size_threshold_in_bytes}"); + println!("optimize_target_file_size_in_bytes: {optimize_target_file_size_in_bytes}"); + println!("vacuum_retention_period_in_seconds: {vacuum_retention_period_in_seconds}"); + println!("ingestion_threads: {ingestion_threads}"); + println!("compression_threads: {compression_threads}"); + println!("writer_threads: {writer_threads}"); + println!("wal_enabled: {wal_enabled}"); } /// Print each field in `node_metrics` on its own line. fn print_node_metrics(node_metrics: &protocol::NodeMetrics) { - println!( - "cpu_usage_percentage: {}", - node_metrics.cpu_usage_percentage - ); - println!("cpu_count: {}", node_metrics.cpu_count); - println!( - "used_memory_in_bytes: {}", - node_metrics.used_memory_in_bytes - ); - println!( - "total_memory_in_bytes: {}", - node_metrics.total_memory_in_bytes - ); - println!( - "used_disk_space_in_bytes: {}", - node_metrics.used_disk_space_in_bytes - ); - println!( - "total_disk_space_in_bytes: {}", - node_metrics.total_disk_space_in_bytes - ); - println!( - "ingested_used_memory_in_bytes: {}", - node_metrics.ingested_used_memory_in_bytes - ); - println!( - "ingested_reserved_memory_in_bytes: {}", - node_metrics.ingested_reserved_memory_in_bytes - ); - println!( - "uncompressed_used_memory_in_bytes: {}", - node_metrics.uncompressed_used_memory_in_bytes - ); - println!( - "uncompressed_reserved_memory_in_bytes: {}", - node_metrics.uncompressed_reserved_memory_in_bytes - ); - println!( - "compressed_used_memory_in_bytes: {}", - node_metrics.compressed_used_memory_in_bytes - ); - println!( - "compressed_reserved_memory_in_bytes: {}", - node_metrics.compressed_reserved_memory_in_bytes - ); + let protocol::NodeMetrics { + cpu_usage_percentage, + cpu_count, + used_memory_in_bytes, + total_memory_in_bytes, + used_disk_space_in_bytes, + total_disk_space_in_bytes, + ingested_used_memory_in_bytes, + ingested_reserved_memory_in_bytes, + uncompressed_used_memory_in_bytes, + uncompressed_reserved_memory_in_bytes, + compressed_used_memory_in_bytes, + compressed_reserved_memory_in_bytes, + } = node_metrics; + + println!("cpu_usage_percentage: {cpu_usage_percentage}"); + println!("cpu_count: {cpu_count}"); + println!("used_memory_in_bytes: {used_memory_in_bytes}"); + println!("total_memory_in_bytes: {total_memory_in_bytes}"); + println!("used_disk_space_in_bytes: {used_disk_space_in_bytes}"); + println!("total_disk_space_in_bytes: {total_disk_space_in_bytes}"); + println!("ingested_used_memory_in_bytes: {ingested_used_memory_in_bytes}"); + println!("ingested_reserved_memory_in_bytes: {ingested_reserved_memory_in_bytes}"); + println!("uncompressed_used_memory_in_bytes: {uncompressed_used_memory_in_bytes}"); + println!("uncompressed_reserved_memory_in_bytes: {uncompressed_reserved_memory_in_bytes}"); + println!("compressed_used_memory_in_bytes: {compressed_used_memory_in_bytes}"); + println!("compressed_reserved_memory_in_bytes: {compressed_reserved_memory_in_bytes}"); } From 541f7fc6498af601b53816696852453a81101011 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 11:58:23 +0200 Subject: [PATCH 26/29] Remove unnecessary comment --- crates/modelardb_client/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index b34db4c0..43e48731 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -354,7 +354,6 @@ fn print_configuration(configuration: &protocol::Configuration) { wal_enabled, } = configuration; - // Data is only transferred on an explicit flush if the batch size is not set. let transfer_batch_size_in_bytes = transfer_batch_size_in_bytes.map_or("not set".to_owned(), |value| value.to_string()); From 763435529f79ed97e9b859ef4fc2a7c1aa4d8493 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:18:42 +0200 Subject: [PATCH 27/29] Handle empty inputs better --- crates/modelardb_client/src/main.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/modelardb_client/src/main.rs b/crates/modelardb_client/src/main.rs index 43e48731..d5b5b1f4 100644 --- a/crates/modelardb_client/src/main.rs +++ b/crates/modelardb_client/src/main.rs @@ -101,9 +101,9 @@ async fn execute_queries_from_a_file(mut client: Client, query_file: &StdPath) - }; // Execute the query. - if !query.is_empty() { + if !query.trim().is_empty() { println!("{query}"); - execute_and_print_command_or_query(&mut client, &query).await + execute_and_print_command_or_query(&mut client, &query).await; } } @@ -155,6 +155,11 @@ async fn execute_and_print_command_or_query(client: &mut Client, command_or_quer let start_time = Instant::now(); let command_or_query = command_or_query.trim(); + // Nothing to execute if Enter was pressed without any input. + if command_or_query.is_empty() { + return; + } + let result = if command_or_query.starts_with('\\') { execute_command(client, command_or_query).await } else { From ae91aada09442bb8be6508ad23140a10a5408da4 Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:23:35 +0200 Subject: [PATCH 28/29] Make it clear that we do not return errors in kill_node --- crates/modelardb_embedded/src/operations/client.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/modelardb_embedded/src/operations/client.rs b/crates/modelardb_embedded/src/operations/client.rs index 4de4d4af..5d92c7c8 100644 --- a/crates/modelardb_embedded/src/operations/client.rs +++ b/crates/modelardb_embedded/src/operations/client.rs @@ -114,9 +114,9 @@ impl Client { /// Flushes all data to disk, removes the node from the cluster if necessary, and kills the node /// process. Data is not transferred to the remote object store. Call [`Client::flush_node`] - /// first if that is required. Since the process is killed, a conventional response cannot be - /// returned, so a dropped connection is not treated as an error. If the data could not be - /// flushed before the node was killed, [`ModelarDbEmbeddedError`] is returned. + /// first if that is required. The node exits while handling the request, so a response cannot + /// be returned. Errors are therefore ignored and [`Ok`] is returned even if the node rejected + /// the request or failed to flush its data. pub async fn kill_node(&mut self) -> Result<()> { // The node exits while handling this action, so the response stream is dropped. let _ = self.send_action("KillNode", vec![]).await; From 8c97dfe4df35735115367b7e5b7cabfd2905062a Mon Sep 17 00:00:00 2001 From: CGodiksen <36046286+CGodiksen@users.noreply.github.com> Date: Tue, 18 Aug 2026 12:26:24 +0200 Subject: [PATCH 29/29] Add to KillNode description that it removes the node from the cluster --- crates/modelardb_server/src/remote/mod.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/modelardb_server/src/remote/mod.rs b/crates/modelardb_server/src/remote/mod.rs index 01846d39..0fae3abf 100644 --- a/crates/modelardb_server/src/remote/mod.rs +++ b/crates/modelardb_server/src/remote/mod.rs @@ -1133,9 +1133,10 @@ impl FlightService for FlightServiceHandler { let kill_node_action = ActionType { r#type: "KillNode".to_owned(), - description: "Flush uncompressed data to disk by compressing and saving the data and \ - kill the process running the server. Data is not transferred to the \ - remote object store. Use FlushNode first if that is required." + description: "Flush uncompressed data to disk by compressing and saving the data, \ + remove the node from the cluster if necessary, and kill the process \ + running the server. Data is not transferred to the remote object store. \ + Use FlushNode first if that is required." .to_owned(), };