From b6dcc2515044713ad6ce6a72665587b2fb5d5336 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Sun, 31 Aug 2025 21:26:31 -0400 Subject: [PATCH 1/4] Create file for order by clause --- src/db/table/select/mod.rs | 7 ++++++- src/db/table/select/order_by_clause.rs | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src/db/table/select/order_by_clause.rs diff --git a/src/db/table/select/mod.rs b/src/db/table/select/mod.rs index 75f824e..16eb195 100644 --- a/src/db/table/select/mod.rs +++ b/src/db/table/select/mod.rs @@ -1,5 +1,6 @@ pub mod where_clause; pub mod limit_clause; +pub mod order_by_clause; use crate::db::table::{Table, Value}; use crate::cli::ast::SelectStatement; use crate::cli::ast::SelectStatementColumns; @@ -8,7 +9,11 @@ use crate::db::table::common::validate_and_clone_row; pub fn select(table: &Table, statement: SelectStatement) -> Result>, String> { let mut rows = get_initial_rows(table, &statement)?; - // Implement order by + + if let Some(order_by_clause) = statement.order_by_clause { + rows = order_by_clause::get_ordered_rows(rows, &order_by_clause)?; + } + if let Some(limit_clause) = &statement.limit_clause { rows = limit_clause::get_limited_rows(rows, limit_clause)?; } diff --git a/src/db/table/select/order_by_clause.rs b/src/db/table/select/order_by_clause.rs new file mode 100644 index 0000000..167d2ea --- /dev/null +++ b/src/db/table/select/order_by_clause.rs @@ -0,0 +1,6 @@ +use crate::cli::ast::OrderByClause; +use crate::db::table::Value; + +pub fn get_ordered_rows(rows: Vec>, order_by_clause: &Vec) -> Result>, String> { + Ok(rows) +} From 7a2c520adb10f0d576df4ceee30d7d57899cf95e Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Mon, 1 Sep 2025 21:36:59 -0400 Subject: [PATCH 2/4] First attempt at an order by clause --- src/db/table/common.rs | 9 +++++ src/db/table/mod.rs | 25 ++++++++++++++ src/db/table/select/mod.rs | 2 +- src/db/table/select/order_by_clause.rs | 48 ++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 3 deletions(-) diff --git a/src/db/table/common.rs b/src/db/table/common.rs index b379a42..e7f5514 100644 --- a/src/db/table/common.rs +++ b/src/db/table/common.rs @@ -13,4 +13,13 @@ pub fn validate_and_clone_row(table: &Table, row: &Vec) -> Result Result { + for (i, c) in table.columns.iter().enumerate() { + if c.name == *column { + return Ok(i); + } + } + return Err(format!("Column {} does not exist in table {}", column, table.name)); } \ No newline at end of file diff --git a/src/db/table/mod.rs b/src/db/table/mod.rs index 14758df..42fdf78 100644 --- a/src/db/table/mod.rs +++ b/src/db/table/mod.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + pub mod select; pub mod insert; pub mod common; @@ -43,6 +45,29 @@ impl Value { Value::Null => DataType::Null, } } + + pub fn cmp(&self, other: &Value) -> Ordering { + if self.get_type() != other.get_type() { + return Ordering::Equal; // Hacky + } + match self { + Value::Integer(_) => { + self.cmp(other) + }, + Value::Real(_) => { + self.cmp(other) + }, + Value::Text(_) => { + self.cmp(other) + } + Value::Blob(_) => { + self.cmp(other) + }, + Value::Null => { + Ordering::Equal + } + } + } } #[derive(Debug)] diff --git a/src/db/table/select/mod.rs b/src/db/table/select/mod.rs index 16eb195..94963c3 100644 --- a/src/db/table/select/mod.rs +++ b/src/db/table/select/mod.rs @@ -11,7 +11,7 @@ pub fn select(table: &Table, statement: SelectStatement) -> Result>, order_by_clause: &Vec) -> Result>, String> { - Ok(rows) +pub fn get_ordered_rows(table: &Table, rows: Vec>, order_by_clauses: &Vec) -> Result>, String> { + let comparisons = order_by_clauses.iter().map(|order_by_clause| get_index_of_column(&table, &order_by_clause.column)).collect::, String>>()?; + let sorted_rows = sort_rows(table, rows, order_by_clauses, &comparisons); + return sorted_rows; } + + +pub fn sort_rows(table: &Table, rows: Vec>, order_by_clause: OrderByClause, comparisons: &Vec) -> Result>, String> { + let mut null_rows = rows.iter().filter(|row| row[index] == Value::Null).collect::>>(); + let mut non_null_rows = rows.iter().filter(|row| row[index] != Value::Null).collect::>>(); + let sorted_rows = match order_by_clause.direction { + OrderByDirection::Asc => { + non_null_rows.sort_by(|a, b| perform_comparions(a, b, &comparisons)); + null_rows.extend(non_null_rows); + null_rows + }, + OrderByDirection::Desc => { + non_null_rows.sort_by(|a, b| perform_comparions(a, b, &comparisons)); + non_null_rows.extend(null_rows); + non_null_rows + }, + }; + + let mut result = vec![]; + for row in sorted_rows { + result.push(row.clone()); + } + return Ok(result); +} + +fn perform_comparions(row1: &Vec, row2: &Vec, comparisons: &Vec) -> Ordering { + let result = Ordering::Equal; + for comparison in comparisons { + let ordering = row1[*comparison].cmp(&row2[*comparison]); + if ordering != Ordering::Equal { + result = ordering; + break; + } + } + return result; + +} \ No newline at end of file From ce692b6abad24f4fec2aacc7710a769ac1471e00 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Tue, 2 Sep 2025 08:34:34 -0400 Subject: [PATCH 3/4] Working ORDER BY function --- src/db/table/common.rs | 9 -- src/db/table/mod.rs | 54 ++++++--- src/db/table/select/order_by_clause.rs | 155 ++++++++++++++++++++----- 3 files changed, 158 insertions(+), 60 deletions(-) diff --git a/src/db/table/common.rs b/src/db/table/common.rs index e7f5514..b379a42 100644 --- a/src/db/table/common.rs +++ b/src/db/table/common.rs @@ -13,13 +13,4 @@ pub fn validate_and_clone_row(table: &Table, row: &Vec) -> Result Result { - for (i, c) in table.columns.iter().enumerate() { - if c.name == *column { - return Ok(i); - } - } - return Err(format!("Column {} does not exist in table {}", column, table.name)); } \ No newline at end of file diff --git a/src/db/table/mod.rs b/src/db/table/mod.rs index 42fdf78..173bdb4 100644 --- a/src/db/table/mod.rs +++ b/src/db/table/mod.rs @@ -1,5 +1,7 @@ use std::cmp::Ordering; +use crate::cli::ast::OrderByDirection; + pub mod select; pub mod insert; pub mod common; @@ -46,26 +48,31 @@ impl Value { } } - pub fn cmp(&self, other: &Value) -> Ordering { - if self.get_type() != other.get_type() { - return Ordering::Equal; // Hacky - } - match self { - Value::Integer(_) => { - self.cmp(other) - }, - Value::Real(_) => { - self.cmp(other) - }, - Value::Text(_) => { - self.cmp(other) - } - Value::Blob(_) => { - self.cmp(other) + pub fn compare(&self, other: &Value, direction: &OrderByDirection) -> Ordering { + let result = match (self, other) { + (Value::Null, Value::Null) => Ordering::Equal, + (Value::Null, _) => Ordering::Less, + (_, Value::Null) => Ordering::Greater, + (Value::Integer(a), Value::Integer(b)) => a.cmp(b), + (Value::Real(a), Value::Real(b)) => { + if a > b { + Ordering::Greater + } else if a < b { + Ordering::Less + } else { + Ordering::Equal + } + }, - Value::Null => { - Ordering::Equal - } + (Value::Text(a), Value::Text(b)) => a.cmp(b), + (Value::Blob(a), Value::Blob(b)) => a.cmp(b), + _ => return Ordering::Equal, // Bad - returns equal if data types are different + }; + + if direction == &OrderByDirection::Asc { + return result; + } else { + return result.reverse(); } } } @@ -102,4 +109,13 @@ impl Table { fn width(&self) -> usize { self.columns.len() } + + pub fn get_index_of_column(&self, column: &String) -> Result { + for (i, c) in self.columns.iter().enumerate() { + if c.name == *column { + return Ok(i); + } + } + return Err(format!("Column {} does not exist in table {}", column, self.name)); + } } \ No newline at end of file diff --git a/src/db/table/select/order_by_clause.rs b/src/db/table/select/order_by_clause.rs index 00fd3d3..cf1572e 100644 --- a/src/db/table/select/order_by_clause.rs +++ b/src/db/table/select/order_by_clause.rs @@ -3,48 +3,139 @@ use std::cmp::Ordering; use crate::cli::ast::OrderByClause; use crate::db::table::Table; use crate::db::table::Value; -use crate::db::table::common::get_index_of_column; -use crate::cli::ast::OrderByDirection; -pub fn get_ordered_rows(table: &Table, rows: Vec>, order_by_clauses: &Vec) -> Result>, String> { - let comparisons = order_by_clauses.iter().map(|order_by_clause| get_index_of_column(&table, &order_by_clause.column)).collect::, String>>()?; - let sorted_rows = sort_rows(table, rows, order_by_clauses, &comparisons); - return sorted_rows; -} -pub fn sort_rows(table: &Table, rows: Vec>, order_by_clause: OrderByClause, comparisons: &Vec) -> Result>, String> { - let mut null_rows = rows.iter().filter(|row| row[index] == Value::Null).collect::>>(); - let mut non_null_rows = rows.iter().filter(|row| row[index] != Value::Null).collect::>>(); - let sorted_rows = match order_by_clause.direction { - OrderByDirection::Asc => { - non_null_rows.sort_by(|a, b| perform_comparions(a, b, &comparisons)); - null_rows.extend(non_null_rows); - null_rows - }, - OrderByDirection::Desc => { - non_null_rows.sort_by(|a, b| perform_comparions(a, b, &comparisons)); - non_null_rows.extend(null_rows); - non_null_rows - }, - }; - - let mut result = vec![]; - for row in sorted_rows { - result.push(row.clone()); - } - return Ok(result); +// This sorting algorithm will always return a stable sort, this is given by all of the order columns +// then the input order of the rows is maintained with any required tie breaking. +pub fn get_ordered_rows(table: &Table, mut rows: Vec>, order_by_clauses: &Vec) -> Result>, String> { + rows.sort_by(|a, b| { + perform_comparions(table, a, b, order_by_clauses) + }); + return Ok(rows); } -fn perform_comparions(row1: &Vec, row2: &Vec, comparisons: &Vec) -> Ordering { - let result = Ordering::Equal; - for comparison in comparisons { - let ordering = row1[*comparison].cmp(&row2[*comparison]); +fn perform_comparions(table: &Table, row1: &Vec, row2: &Vec, order_by_clauses: &Vec) -> Ordering { + let mut result = Ordering::Equal; + for comparison in order_by_clauses { + let index = table.get_index_of_column(&comparison.column); + let index = match index { + Ok(index) => index, + Err(_) => return Ordering::Equal, // Bad but should never happen because we've validated the columns in the parser + }; + let ordering = row1[index].compare(&row2[index], &comparison.direction); if ordering != Ordering::Equal { result = ordering; break; } } return result; +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::table::{Table, Value, DataType, ColumnDefinition}; + use crate::cli::ast::OrderByDirection; + fn default_table() -> Table { + Table { + name: "users".to_string(), + columns: vec![ + ColumnDefinition {name: "id".to_string(), data_type: DataType::Integer, constraints: vec![]}, + ColumnDefinition {name: "name".to_string(), data_type: DataType::Text, constraints: vec![]}, + ColumnDefinition {name: "money".to_string(), data_type: DataType::Real, constraints: vec![]}, + ColumnDefinition {name: "some_data".to_string(), data_type: DataType::Blob, constraints: vec![]}, + ], + rows: vec![], + } + } + + fn default_rows() -> Vec> { + vec![ + vec![Value::Integer(3), Value::Text("c_Jim".to_string()), Value::Real(3000.0), Value::Blob(b"0022".to_vec())], + vec![Value::Integer(1), Value::Text("a_John".to_string()), Value::Real(1000.0), Value::Blob(b"0000".to_vec())], + vec![Value::Null, Value::Null, Value::Null, Value::Null], + vec![Value::Integer(2), Value::Text("b_Jane".to_string()), Value::Real(2000.0), Value::Blob(b"0201".to_vec())], + vec![Value::Integer(3), Value::Text("b_Jim".to_string()), Value::Real(1500.0), Value::Blob(b"0102".to_vec())], + vec![Value::Integer(4), Value::Text("a_Jim".to_string()), Value::Real(500.0), Value::Blob(b"0101".to_vec())], + vec![Value::Integer(1), Value::Text("a_Jim".to_string()), Value::Real(5000.0), Value::Blob(b"0401".to_vec())], + ] + } + + #[test] + fn get_ordered_rows_returns_rows_with_id_column_returns_rows_in_correct_order() { + let table = default_table(); + let rows = default_rows(); + let order_by_clauses = vec![OrderByClause {column: "id".to_string(), direction: OrderByDirection::Asc}]; + let result = get_ordered_rows(&table, rows, &order_by_clauses); + assert!(result.is_ok()); + let expected = vec![ + vec![Value::Null, Value::Null, Value::Null, Value::Null], + vec![Value::Integer(1), Value::Text("a_John".to_string()), Value::Real(1000.0), Value::Blob(b"0000".to_vec())], + vec![Value::Integer(1), Value::Text("a_Jim".to_string()), Value::Real(5000.0), Value::Blob(b"0401".to_vec())], + vec![Value::Integer(2), Value::Text("b_Jane".to_string()), Value::Real(2000.0), Value::Blob(b"0201".to_vec())], + vec![Value::Integer(3), Value::Text("c_Jim".to_string()), Value::Real(3000.0), Value::Blob(b"0022".to_vec())], + vec![Value::Integer(3), Value::Text("b_Jim".to_string()), Value::Real(1500.0), Value::Blob(b"0102".to_vec())], + vec![Value::Integer(4), Value::Text("a_Jim".to_string()), Value::Real(500.0), Value::Blob(b"0101".to_vec())], + ]; + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn get_ordered_rows_returns_rows_with_name_column_returns_rows_in_correct_order() { + let table = default_table(); + let rows = default_rows(); + let order_by_clauses = vec![OrderByClause {column: "name".to_string(), direction: OrderByDirection::Asc}]; + let result = get_ordered_rows(&table, rows, &order_by_clauses); + assert!(result.is_ok()); + let expected = vec![ + vec![Value::Null, Value::Null, Value::Null, Value::Null], + vec![Value::Integer(4), Value::Text("a_Jim".to_string()), Value::Real(500.0), Value::Blob(b"0101".to_vec())], + vec![Value::Integer(1), Value::Text("a_Jim".to_string()), Value::Real(5000.0), Value::Blob(b"0401".to_vec())], + vec![Value::Integer(1), Value::Text("a_John".to_string()), Value::Real(1000.0), Value::Blob(b"0000".to_vec())], + vec![Value::Integer(2), Value::Text("b_Jane".to_string()), Value::Real(2000.0), Value::Blob(b"0201".to_vec())], + vec![Value::Integer(3), Value::Text("b_Jim".to_string()), Value::Real(1500.0), Value::Blob(b"0102".to_vec())], + vec![Value::Integer(3), Value::Text("c_Jim".to_string()), Value::Real(3000.0), Value::Blob(b"0022".to_vec())], + ]; + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn get_ordered_rows_ordered_descending_returns_rows_in_correct_order() { + let table = default_table(); + let rows = default_rows(); + let order_by_clauses = vec![OrderByClause {column: "money".to_string(), direction: OrderByDirection::Desc}]; + let result = get_ordered_rows(&table, rows, &order_by_clauses); + assert!(result.is_ok()); + let expected = vec![ + vec![Value::Integer(1), Value::Text("a_Jim".to_string()), Value::Real(5000.0), Value::Blob(b"0401".to_vec())], + vec![Value::Integer(3), Value::Text("c_Jim".to_string()), Value::Real(3000.0), Value::Blob(b"0022".to_vec())], + vec![Value::Integer(2), Value::Text("b_Jane".to_string()), Value::Real(2000.0), Value::Blob(b"0201".to_vec())], + vec![Value::Integer(3), Value::Text("b_Jim".to_string()), Value::Real(1500.0), Value::Blob(b"0102".to_vec())], + vec![Value::Integer(1), Value::Text("a_John".to_string()), Value::Real(1000.0), Value::Blob(b"0000".to_vec())], + vec![Value::Integer(4), Value::Text("a_Jim".to_string()), Value::Real(500.0), Value::Blob(b"0101".to_vec())], + vec![Value::Null, Value::Null, Value::Null, Value::Null], + ]; + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn get_ordered_rows_multiple_sort_orders_returns_rows_in_correct_order() { + let table = default_table(); + let rows = default_rows(); + let order_by_clauses = vec![OrderByClause {column: "name".to_string(), direction: OrderByDirection::Desc}, OrderByClause {column: "some_data".to_string(), direction: OrderByDirection::Asc}]; + let result = get_ordered_rows(&table, rows, &order_by_clauses); + assert!(result.is_ok()); + let expected = vec![ + vec![Value::Integer(3), Value::Text("c_Jim".to_string()), Value::Real(3000.0), Value::Blob(b"0022".to_vec())], + vec![Value::Integer(3), Value::Text("b_Jim".to_string()), Value::Real(1500.0), Value::Blob(b"0102".to_vec())], + vec![Value::Integer(2), Value::Text("b_Jane".to_string()), Value::Real(2000.0), Value::Blob(b"0201".to_vec())], + vec![Value::Integer(1), Value::Text("a_John".to_string()), Value::Real(1000.0), Value::Blob(b"0000".to_vec())], + vec![Value::Integer(4), Value::Text("a_Jim".to_string()), Value::Real(500.0), Value::Blob(b"0101".to_vec())], + vec![Value::Integer(1), Value::Text("a_Jim".to_string()), Value::Real(5000.0), Value::Blob(b"0401".to_vec())], + vec![Value::Null, Value::Null, Value::Null, Value::Null], + ]; + assert_eq!(expected, result.unwrap()); + } } \ No newline at end of file From 8e937099b87fd5f1fa14c9386a036485e83809ea Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Tue, 2 Sep 2025 08:36:48 -0400 Subject: [PATCH 4/4] Add test for whole ORDER BY statement with SELECT --- src/db/table/select/mod.rs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/db/table/select/mod.rs b/src/db/table/select/mod.rs index 94963c3..e95ab92 100644 --- a/src/db/table/select/mod.rs +++ b/src/db/table/select/mod.rs @@ -61,10 +61,7 @@ pub fn get_columns_from_row(table: &Table, row: &Vec, selected_columns: & mod tests { use super::*; use crate::db::table::{Table, Value, DataType, ColumnDefinition}; - use crate::cli::ast::SelectStatementColumns; - use crate::cli::ast::Operator; - use crate::cli::ast::WhereClause; - use crate::cli::ast::LimitClause; + use crate::cli::ast::{SelectStatementColumns, WhereClause, LimitClause, OrderByClause, OrderByDirection, Operator}; fn default_table() -> Table { Table { @@ -209,4 +206,25 @@ mod tests { assert!(result.is_err()); assert_eq!(result.unwrap_err(), "Column column_not_included does not exist in table users"); } + + #[test] + fn select_with_order_by_clause_is_generated_correctly() { + let table = default_table(); + let statement = SelectStatement { + table_name: "users".to_string(), + columns: SelectStatementColumns::All, + where_clause: None, + order_by_clause: Some(vec![OrderByClause {column: "money".to_string(), direction: OrderByDirection::Desc}]), + limit_clause: None, + }; + let result = select(&table, statement); + assert!(result.is_ok()); + let expected = vec![ + vec![Value::Integer(4), Value::Null, Value::Integer(40), Value::Real(4000.0)], + vec![Value::Integer(3), Value::Text("Jim".to_string()), Value::Integer(35), Value::Real(3000.0)], + vec![Value::Integer(2), Value::Text("Jane".to_string()), Value::Integer(30), Value::Real(2000.0)], + vec![Value::Integer(1), Value::Text("John".to_string()), Value::Integer(25), Value::Real(1000.0)], + ]; + assert_eq!(expected, result.unwrap()); + } } \ No newline at end of file