From a492e29f8bdb1b791dcc43bcf9c8a66ca8ef11de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20Moravec?= Date: Sun, 28 Jan 2024 21:41:19 +0100 Subject: [PATCH 1/4] refactor: cleanup client output --- client/src/main.rs | 128 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 98 insertions(+), 30 deletions(-) diff --git a/client/src/main.rs b/client/src/main.rs index 66b281d..e0fd46b 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,3 +1,4 @@ +use std::io::Write; use clap::Parser; use proto::handshake::client::do_client_handshake; use proto::handshake::request::HandshakeRequest; @@ -5,7 +6,7 @@ use proto::reader::protoreader::ProtoReader; use proto::writer::protowriter::{ProtoFlush, ProtoWriter}; use tokio::io::{BufReader, BufWriter}; use tokio::net::TcpStream; -use proto::message::backend::{BackendMessage, DataRowData, RowDescriptionData}; +use proto::message::backend::{BackendMessage, CommandCompleteData, DataRowData, ErrorResponseData, RowDescriptionData}; use proto::message::frontend::{FrontendMessage, QueryData}; use proto::reader::oneway::OneWayProtoReader; use proto::writer::oneway::OneWayProtoWriter; @@ -19,12 +20,17 @@ struct Cli { /// Host name or IP address of the server. #[arg(long, default_value = "127.0.0.1", help = "Host name or IP address of the server")] host: String, + + /// User name sent to the server. + #[arg(long, default_value = "minisql user", help = "User name")] + username: String, } #[tokio::main] async fn main() -> anyhow::Result<()> { let cli = Cli::parse(); let addr = format!("{}:{}", cli.host, cli.port); + println!("Connecting to {}", addr); let mut stream = TcpStream::connect(addr).await?; let (reader, writer) = stream.split(); @@ -33,42 +39,55 @@ async fn main() -> anyhow::Result<()> { let mut reader = ProtoReader::new(BufReader::new(reader), 1024); let request = HandshakeRequest::new(196608) - .parameter("user", "test user") + .parameter("user", cli.username.as_str()) .parameter("client_encoding", "UTF8"); - let response = do_client_handshake(&mut writer, &mut reader, request).await?; + let _ = do_client_handshake(&mut writer, &mut reader, request).await?; + println!("Connected to the server"); - println!("Handshake complete:\n{response:?}"); + let mut exit = false; + let command = prompt()?; + if let Some(cmd) = command { + writer.write_proto(FrontendMessage::Query(QueryData { + query: cmd.into(), + })).await?; + writer.flush().await?; + } else { + exit = true; + } - writer.write_proto(FrontendMessage::Query(QueryData { - query: "SELECT * FROM users;".to_string().into(), - })).await?; - writer.flush().await?; - let mut line = String::new(); - loop { + while !exit { let msg: BackendMessage = reader.read_proto().await?; match msg { BackendMessage::RowDescription(data) => { - print_header(data); + print_row_description(data); }, BackendMessage::DataRow(data) => { - print_row(data); + print_row_data(data); }, BackendMessage::CommandComplete(data) => { - println!("Command complete: {:?}", data); + print_command_complete(data); + }, + BackendMessage::ErrorResponse(data) => { + print_error_response(data); + }, + BackendMessage::EmptyQueryResponse => { + println!("Empty query response"); + }, + BackendMessage::NoData => { + println!("No data"); }, BackendMessage::ReadyForQuery(data) => { - println!("Ready for query: {:?}", data); - line.clear(); - let res = std::io::stdin().read_line(&mut line); - if let Ok(_) = res { - if line.eq("exit") { - break; - } + println!("Ready for next query ({})", data.status); + + let command = prompt()?; + if let Some(cmd) = command { writer.write_proto(FrontendMessage::Query(QueryData { - query: line.clone().into(), + query: cmd.into(), })).await?; writer.flush().await?; + } else { + exit = true; } }, m => { @@ -83,21 +102,70 @@ async fn main() -> anyhow::Result<()> { Ok(()) } -fn print_header(header: RowDescriptionData) { - print!("Header -> "); - for column in Vec::from(header.columns) { - print!("{} | ", column.name.as_str()); +fn prompt() -> std::io::Result> { + print!("> "); + std::io::stdout().flush()?; + + let mut line = String::new(); + let _ = std::io::stdin().read_line(&mut line)?; + + let line = line.trim(); + + if line.is_empty() { + return prompt(); + } + + if line == "exit" || line == "quit" { + return Ok(None); + } + + Ok(Some(line.trim().to_string())) +} + +fn print_error_response(data: ErrorResponseData) { + println!("Error with code {}: {}", data.code, data.message.as_str()); +} + +fn print_command_complete(data: CommandCompleteData) { + println!("Result: {}", data.tag.as_str()); +} + +fn print_row_description(data: RowDescriptionData) { + let mut lengths = vec![]; + + let columns = Vec::from(data.columns); + for i in 0..columns.len() { + let column_name = columns[i].name.as_str(); + lengths.push(column_name.len()); + print!("{}", column_name); + if i < columns.len() - 1 { + print!(" | "); + } + } + + println!(); + for i in 0..lengths.len() { + for _ in 0..lengths[i] { + print!("-"); + } + if i < lengths.len() - 1 { + print!("-+-"); + } } println!(); } -fn print_row(row: DataRowData) { - print!("Row -> "); - for column in Vec::from(row.columns) { - let bytes = Vec::from(column); +fn print_row_data(data: DataRowData) { + let columns = Vec::from(data.columns); + let length = columns.len(); + for column in columns.into_iter().enumerate() { + let bytes = Vec::from(column.1); let string = String::from_utf8(bytes).unwrap(); - print!("{} | ", string); + print!("{}", string); + if column.0 < length - 1 { + print!(" | "); + } } println!(); } From 832ed8170b75a613700b473883528d5d52d894d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20Moravec?= Date: Sun, 28 Jan 2024 21:56:02 +0100 Subject: [PATCH 2/4] fix: utf8 decoding and add test to prevent it --- proto/src/message/primitive/pgstring.rs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/proto/src/message/primitive/pgstring.rs b/proto/src/message/primitive/pgstring.rs index 58fad78..c768528 100644 --- a/proto/src/message/primitive/pgstring.rs +++ b/proto/src/message/primitive/pgstring.rs @@ -41,15 +41,33 @@ impl Encode for PgString { impl Decode for PgString { fn decode(decoder: &mut D) -> Result { - let mut string = String::new(); + let mut bytes = Vec::new(); loop { let byte = u8::decode(decoder)?; if byte == 0 { break; } - string.push(byte as char); + bytes.push(byte); } + let string = String::from_utf8(bytes) + .map_err(|e| DecodeError::Utf8 { inner: e.utf8_error() })?; Ok(PgString(string)) } } + +#[cfg(test)] +mod tests { + use crate::message::primitive::data::MessageData; + use super::*; + + #[test] + fn test_encode_decode_utf8() { + let pg_string = PgString::from("áhój jěžkó"); + let encoded = pg_string.serialize().unwrap(); + let decoded: PgString = PgString::deserialize(&encoded).unwrap(); + + let actual = decoded.as_str(); + assert_eq!("áhój jěžkó", actual); + } +} From 402251aa5c0f10c818c11fc0e5df91a7db1f301a Mon Sep 17 00:00:00 2001 From: Maxim Svistunov Date: Sun, 28 Jan 2024 22:10:33 +0100 Subject: [PATCH 3/4] Add demo DB with two tables --- demo.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 demo.json diff --git a/demo.json b/demo.json new file mode 100644 index 0000000..f7ac84a --- /dev/null +++ b/demo.json @@ -0,0 +1 @@ +{"table_name_position_mapping":{"users":0,"cars":1},"tables":[{"schema":{"table_name":"users","primary_key":0,"column_name_position_mapping":{"id":0,"name":1,"surname":2,"email":3},"types":["Uuid","String","String","String"]},"rows":{"1":[{"Indexable":{"Uuid":1}},{"Indexable":{"String":"Isabelle"}},{"Indexable":{"String":"Oppie"}},{"Indexable":{"String":"ioppie0@mapquest.com"}}],"2":[{"Indexable":{"Uuid":2}},{"Indexable":{"String":"Frederik"}},{"Indexable":{"String":"Vardy"}},{"Indexable":{"String":"fvardy1@bloomberg.com"}}],"3":[{"Indexable":{"Uuid":3}},{"Indexable":{"String":"Ronna"}},{"Indexable":{"String":"Faiers"}},{"Indexable":{"String":"rfaiers2@netlog.com"}}],"4":[{"Indexable":{"Uuid":4}},{"Indexable":{"String":"Wash"}},{"Indexable":{"String":"Dirkin"}},{"Indexable":{"String":"wdirkin3@google.com.au"}}],"5":[{"Indexable":{"Uuid":5}},{"Indexable":{"String":"Dusty"}},{"Indexable":{"String":"Ayshford"}},{"Indexable":{"String":"dayshford4@discuz.net"}}],"6":[{"Indexable":{"Uuid":6}},{"Indexable":{"String":"Brenn"}},{"Indexable":{"String":"McGucken"}},{"Indexable":{"String":"bmcgucken5@networksolutions.com"}}],"7":[{"Indexable":{"Uuid":7}},{"Indexable":{"String":"Elwyn"}},{"Indexable":{"String":"Padbery"}},{"Indexable":{"String":"epadbery6@dion.ne.jp"}}],"8":[{"Indexable":{"Uuid":8}},{"Indexable":{"String":"Jobyna"}},{"Indexable":{"String":"Playhill"}},{"Indexable":{"String":"jplayhill7@virginia.edu"}}],"9":[{"Indexable":{"Uuid":9}},{"Indexable":{"String":"Ermina"}},{"Indexable":{"String":"Ledekker"}},{"Indexable":{"String":"eledekker8@xing.com"}}],"10":[{"Indexable":{"Uuid":10}},{"Indexable":{"String":"Saudra"}},{"Indexable":{"String":"Montague"}},{"Indexable":{"String":"smontague9@youtube.com"}}],"11":[{"Indexable":{"Uuid":11}},{"Indexable":{"String":"Gan"}},{"Indexable":{"String":"Levick"}},{"Indexable":{"String":"glevicka@nih.gov"}}],"12":[{"Indexable":{"Uuid":12}},{"Indexable":{"String":"Nicoline"}},{"Indexable":{"String":"Pike"}},{"Indexable":{"String":"npikeb@nymag.com"}}],"13":[{"Indexable":{"Uuid":13}},{"Indexable":{"String":"Netty"}},{"Indexable":{"String":"Jeandet"}},{"Indexable":{"String":"njeandetc@slashdot.org"}}],"14":[{"Indexable":{"Uuid":14}},{"Indexable":{"String":"Erich"}},{"Indexable":{"String":"Blonfield"}},{"Indexable":{"String":"eblonfieldd@epa.gov"}}],"15":[{"Indexable":{"Uuid":15}},{"Indexable":{"String":"Natividad"}},{"Indexable":{"String":"Doddemeede"}},{"Indexable":{"String":"ndoddemeedee@blinklist.com"}}],"16":[{"Indexable":{"Uuid":16}},{"Indexable":{"String":"Jobi"}},{"Indexable":{"String":"Gore"}},{"Indexable":{"String":"jgoref@oaic.gov.au"}}],"17":[{"Indexable":{"Uuid":17}},{"Indexable":{"String":"Christina"}},{"Indexable":{"String":"Lisciandri"}},{"Indexable":{"String":"clisciandrig@pcworld.com"}}],"18":[{"Indexable":{"Uuid":18}},{"Indexable":{"String":"Hallsy"}},{"Indexable":{"String":"Isaksson"}},{"Indexable":{"String":"hisakssonh@uiuc.edu"}}],"19":[{"Indexable":{"Uuid":19}},{"Indexable":{"String":"Isac"}},{"Indexable":{"String":"Edwick"}},{"Indexable":{"String":"iedwicki@over-blog.com"}}],"20":[{"Indexable":{"Uuid":20}},{"Indexable":{"String":"Beatrix"}},{"Indexable":{"String":"Durdy"}},{"Indexable":{"String":"bdurdyj@tuttocitta.it"}}],"21":[{"Indexable":{"Uuid":21}},{"Indexable":{"String":"Oralia"}},{"Indexable":{"String":"Venning"}},{"Indexable":{"String":"ovenningk@businesswire.com"}}],"22":[{"Indexable":{"Uuid":22}},{"Indexable":{"String":"Leese"}},{"Indexable":{"String":"Kitchaside"}},{"Indexable":{"String":"lkitchasidel@princeton.edu"}}],"23":[{"Indexable":{"Uuid":23}},{"Indexable":{"String":"Magda"}},{"Indexable":{"String":"Yurshev"}},{"Indexable":{"String":"myurshevm@washington.edu"}}],"24":[{"Indexable":{"Uuid":24}},{"Indexable":{"String":"Benyamin"}},{"Indexable":{"String":"Dominguez"}},{"Indexable":{"String":"bdominguezn@skyrock.com"}}],"25":[{"Indexable":{"Uuid":25}},{"Indexable":{"String":"Jorey"}},{"Indexable":{"String":"Benzie"}},{"Indexable":{"String":"jbenzieo@tripod.com"}}]},"indexes":{}},{"schema":{"table_name":"cars","primary_key":0,"column_name_position_mapping":{"id":0,"brand":2,"vid":1,"year":4,"model":3},"types":["Uuid","String","String","String","Int"]},"rows":{"1":[{"Indexable":{"Uuid":1}},{"Indexable":{"String":"5XYZG3AB1CG391341"}},{"Indexable":{"String":"Volkswagen"}},{"Indexable":{"String":"Golf"}},{"Indexable":{"Int":1939}}],"2":[{"Indexable":{"Uuid":2}},{"Indexable":{"String":"JN1CV6APXBM635254"}},{"Indexable":{"String":"Chevrolet"}},{"Indexable":{"String":"S10"}},{"Indexable":{"Int":1969}}],"3":[{"Indexable":{"Uuid":3}},{"Indexable":{"String":"4T1BF3EK4BU925939"}},{"Indexable":{"String":"Toyota"}},{"Indexable":{"String":"MR2"}},{"Indexable":{"Int":1958}}],"4":[{"Indexable":{"Uuid":4}},{"Indexable":{"String":"WDDJK7DA7EF523150"}},{"Indexable":{"String":"Toyota"}},{"Indexable":{"String":"MR2"}},{"Indexable":{"Int":1903}}],"5":[{"Indexable":{"Uuid":5}},{"Indexable":{"String":"KNADM5A3XD6039514"}},{"Indexable":{"String":"Hyundai"}},{"Indexable":{"String":"Santa Fe"}},{"Indexable":{"Int":1970}}],"6":[{"Indexable":{"Uuid":6}},{"Indexable":{"String":"WBAKB0C52AC111480"}},{"Indexable":{"String":"Mitsubishi"}},{"Indexable":{"String":"3000GT"}},{"Indexable":{"Int":1907}}],"7":[{"Indexable":{"Uuid":7}},{"Indexable":{"String":"WBAAW33401E562288"}},{"Indexable":{"String":"Dodge"}},{"Indexable":{"String":"Avenger"}},{"Indexable":{"Int":1967}}],"8":[{"Indexable":{"Uuid":8}},{"Indexable":{"String":"WBAFR7C54BC365585"}},{"Indexable":{"String":"Hyundai"}},{"Indexable":{"String":"Accent"}},{"Indexable":{"Int":2010}}],"9":[{"Indexable":{"Uuid":9}},{"Indexable":{"String":"WA1AY74L29D206107"}},{"Indexable":{"String":"Ford"}},{"Indexable":{"String":"Escort"}},{"Indexable":{"Int":1938}}],"10":[{"Indexable":{"Uuid":10}},{"Indexable":{"String":"JN8AS5MTXBW273113"}},{"Indexable":{"String":"Dodge"}},{"Indexable":{"String":"Intrepid"}},{"Indexable":{"Int":1925}}],"11":[{"Indexable":{"Uuid":11}},{"Indexable":{"String":"3D73Y4CL4BG009999"}},{"Indexable":{"String":"Toyota"}},{"Indexable":{"String":"Yaris"}},{"Indexable":{"Int":1906}}],"12":[{"Indexable":{"Uuid":12}},{"Indexable":{"String":"SCFFDABE1BG998717"}},{"Indexable":{"String":"Lamborghini"}},{"Indexable":{"String":"Diablo"}},{"Indexable":{"Int":2007}}],"13":[{"Indexable":{"Uuid":13}},{"Indexable":{"String":"1G4GG5E35DF715445"}},{"Indexable":{"String":"Lotus"}},{"Indexable":{"String":"Exige"}},{"Indexable":{"Int":1998}}],"14":[{"Indexable":{"Uuid":14}},{"Indexable":{"String":"1G6AY5S37E0510437"}},{"Indexable":{"String":"Land Rover"}},{"Indexable":{"String":"Discovery"}},{"Indexable":{"Int":1967}}],"15":[{"Indexable":{"Uuid":15}},{"Indexable":{"String":"KMHGH4JH4FU924859"}},{"Indexable":{"String":"Toyota"}},{"Indexable":{"String":"4Runner"}},{"Indexable":{"Int":1946}}],"16":[{"Indexable":{"Uuid":16}},{"Indexable":{"String":"JTMHY7AJ3D4338549"}},{"Indexable":{"String":"Audi"}},{"Indexable":{"String":"S5"}},{"Indexable":{"Int":2020}}],"17":[{"Indexable":{"Uuid":17}},{"Indexable":{"String":"3D4PG3FG7BT795377"}},{"Indexable":{"String":"GMC"}},{"Indexable":{"String":"Sierra 2500"}},{"Indexable":{"Int":1936}}],"18":[{"Indexable":{"Uuid":18}},{"Indexable":{"String":"SALAG2D44CA789334"}},{"Indexable":{"String":"Mitsubishi"}},{"Indexable":{"String":"Diamante"}},{"Indexable":{"Int":2019}}],"19":[{"Indexable":{"Uuid":19}},{"Indexable":{"String":"5N1AR1NB1CC618825"}},{"Indexable":{"String":"Infiniti"}},{"Indexable":{"String":"QX"}},{"Indexable":{"Int":1938}}],"20":[{"Indexable":{"Uuid":20}},{"Indexable":{"String":"1GKKRNED6DJ757464"}},{"Indexable":{"String":"Subaru"}},{"Indexable":{"String":"Baja"}},{"Indexable":{"Int":1969}}],"21":[{"Indexable":{"Uuid":21}},{"Indexable":{"String":"WAULD54B82N660784"}},{"Indexable":{"String":"Mercedes-Benz"}},{"Indexable":{"String":"C-Class"}},{"Indexable":{"Int":2001}}],"22":[{"Indexable":{"Uuid":22}},{"Indexable":{"String":"3C63D2CL1CG144067"}},{"Indexable":{"String":"BMW"}},{"Indexable":{"String":"8 Series"}},{"Indexable":{"Int":1978}}],"23":[{"Indexable":{"Uuid":23}},{"Indexable":{"String":"SCFAD22363K791284"}},{"Indexable":{"String":"Chrysler"}},{"Indexable":{"String":"300"}},{"Indexable":{"Int":1939}}],"24":[{"Indexable":{"Uuid":24}},{"Indexable":{"String":"WAUML54D11N926735"}},{"Indexable":{"String":"Buick"}},{"Indexable":{"String":"Regal"}},{"Indexable":{"Int":1935}}],"25":[{"Indexable":{"Uuid":25}},{"Indexable":{"String":"WUARL48H07K869442"}},{"Indexable":{"String":"Toyota"}},{"Indexable":{"String":"Camry"}},{"Indexable":{"Int":1988}}]},"indexes":{}}]} \ No newline at end of file From 65a469e8b9f654ded9f0915d57d2507dee2e1860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20Moravec?= Date: Sun, 28 Jan 2024 22:21:03 +0100 Subject: [PATCH 4/4] fix: cancellation before next query --- server/src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main.rs b/server/src/main.rs index da7a08a..b9fc5ae 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -167,6 +167,9 @@ async fn handle_query(writer: &mut W, state: &SharedDbState, query: String, t where W: BackendProtoWriter + ProtoFlush + Send, { + // Make sure token is reset before next query + token.reset(); + let operation = { let state = state.read().await; let db_schema = state.db_schema();