diff --git a/Cargo.toml b/Cargo.toml index e240a461..fe7bb0ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["codegen", "examples", "performance_measurement", "performance_measur [package] name = "worktable" -version = "0.9.0-beta0.2.1" +version = "0.9.0-beta0.2.2" edition = "2024" authors = ["Handy-caT"] license = "MIT" @@ -46,7 +46,7 @@ tracing = "0.1" url = { version = "2", optional = true } uuid = { version = "1.10.0", features = ["v4", "v7"] } walkdir = { version = "2", optional = true } -worktable_codegen = { path = "codegen", version = "=0.9.0-beta0.2.1" } +worktable_codegen = { path = "codegen", version = "=0.9.0-beta0.2.2" } [dev-dependencies] chrono = "0.4.43" diff --git a/codegen/Cargo.toml b/codegen/Cargo.toml index d4acd9a0..1ef6c1c9 100644 --- a/codegen/Cargo.toml +++ b/codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "worktable_codegen" -version = "0.9.0-beta0.2.1" +version = "0.9.0-beta0.2.2" edition = "2024" license = "MIT" description = "WorkTable codegeneration crate" diff --git a/codegen/src/generators/in_memory/table/impls.rs b/codegen/src/generators/in_memory/table/impls.rs index 18e11790..e3c26d25 100644 --- a/codegen/src/generators/in_memory/table/impls.rs +++ b/codegen/src/generators/in_memory/table/impls.rs @@ -1,4 +1,5 @@ -use proc_macro2::TokenStream; +use convert_case::{Case, Casing}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use crate::common::model::GeneratorType; @@ -83,6 +84,18 @@ impl InMemoryGenerator { let column_range_type = name_generator.get_column_range_type_ident(); let row_fields_ident = name_generator.get_row_fields_enum_ident(); + let pk_sorted_by = if self.columns.primary_keys.len() == 1 { + let pk_field = &self.columns.primary_keys[0]; + let pk_pascal = Ident::new(&pk_field.to_string().to_case(Case::Pascal), Span::mixed_site()); + quote! { + SelectQueryBuilder::new_sorted(rows, #row_fields_ident::#pk_pascal) + } + } else { + quote! { + SelectQueryBuilder::new(rows) + } + }; + quote! { pub fn select_by_pk_range(&self, range: R) -> SelectQueryBuilder<#row_type, impl DoubleEndedIterator + '_, @@ -101,7 +114,7 @@ impl InMemoryGenerator { .range(converted_range) .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); - SelectQueryBuilder::new(rows) + #pk_sorted_by } } } diff --git a/codegen/src/generators/in_memory/table/index_fns.rs b/codegen/src/generators/in_memory/table/index_fns.rs index 16ca307a..0545d63b 100644 --- a/codegen/src/generators/in_memory/table/index_fns.rs +++ b/codegen/src/generators/in_memory/table/index_fns.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use convert_case::{Case, Casing}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -130,6 +131,7 @@ impl InMemoryGenerator { let type_ = columns_map.get(i).ok_or(syn::Error::new(i.span(), "Row not found"))?; let fn_name = Ident::new(format!("select_by_{i}_range").as_str(), Span::mixed_site()); let field_ident = &idx.name; + let column_pascal = Ident::new(&i.to_string().to_case(Case::Pascal), Span::mixed_site()); let (range_bounds, range_arg) = if is_float(type_.to_string().as_str()) { ( @@ -157,7 +159,7 @@ impl InMemoryGenerator { .range(#range_arg) .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); - SelectQueryBuilder::new(rows) + SelectQueryBuilder::new_sorted(rows, #row_fields_ident::#column_pascal) } }) } diff --git a/codegen/src/generators/in_memory/table/select_executor.rs b/codegen/src/generators/in_memory/table/select_executor.rs index 65155f4c..ba980e86 100644 --- a/codegen/src/generators/in_memory/table/select_executor.rs +++ b/codegen/src/generators/in_memory/table/select_executor.rs @@ -155,6 +155,20 @@ impl InMemoryGenerator { } }; + let fallback_sort = quote! { + let mut items: Vec<#row_type> = iter.collect(); + items.sort_by(|a, b| { + for (order, col) in &self.params.order { + match col { + #(#order_matches)* + _ => continue, + } + } + std::cmp::Ordering::Equal + }); + iter = Box::new(items.into_iter()); + }; + quote! { impl SelectQueryExecutor<#row_type, I, #column_range_type, #row_fields_ident> for SelectQueryBuilder<#row_type, I, #column_range_type, #row_fields_ident> @@ -181,19 +195,30 @@ impl InMemoryGenerator { #range if !self.params.order.is_empty() { - let mut items: Vec<#row_type> = iter.collect(); - - items.sort_by(|a, b| { - for (order, col) in &self.params.order { - match col { - #(#order_matches)* - _ => continue, + // Optimization: single order on pre-sorted column with no additional range filters + let can_optimize = self.params.sorted_by.is_some() + && self.params.range.is_empty() + && self.params.order.len() == 1; + + if can_optimize { + let (order, col) = &self.params.order[0]; + let sorted_col = self.params.sorted_by.as_ref().unwrap(); + + if col == sorted_col { + match order { + Order::Desc => { + iter = Box::new(iter.rev()); + } + Order::Asc => { + // Already sorted correctly, no action needed + } } + } else { + #fallback_sort } - std::cmp::Ordering::Equal - }); - - iter = Box::new(items.into_iter()); + } else { + #fallback_sort + } } let iter_result: Box> = if let Some(offset) = self.params.offset { diff --git a/codegen/src/generators/persist/table/impls.rs b/codegen/src/generators/persist/table/impls.rs index c325b667..b241e6f8 100644 --- a/codegen/src/generators/persist/table/impls.rs +++ b/codegen/src/generators/persist/table/impls.rs @@ -1,4 +1,5 @@ -use proc_macro2::TokenStream; +use convert_case::{Case, Casing}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use crate::common::model::GeneratorType; @@ -167,6 +168,18 @@ impl PersistGenerator { let column_range_type = name_generator.get_column_range_type_ident(); let row_fields_ident = name_generator.get_row_fields_enum_ident(); + let pk_sorted_by = if self.columns.primary_keys.len() == 1 { + let pk_field = &self.columns.primary_keys[0]; + let pk_pascal = Ident::new(&pk_field.to_string().to_case(Case::Pascal), Span::mixed_site()); + quote! { + SelectQueryBuilder::new_sorted(rows, #row_fields_ident::#pk_pascal) + } + } else { + quote! { + SelectQueryBuilder::new(rows) + } + }; + quote! { pub fn select_by_pk_range(&self, range: R) -> SelectQueryBuilder<#row_type, impl DoubleEndedIterator + '_, @@ -185,7 +198,7 @@ impl PersistGenerator { .range(converted_range) .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); - SelectQueryBuilder::new(rows) + #pk_sorted_by } } } diff --git a/codegen/src/generators/persist/table/index_fns.rs b/codegen/src/generators/persist/table/index_fns.rs index 9613c8b7..d682a6ac 100644 --- a/codegen/src/generators/persist/table/index_fns.rs +++ b/codegen/src/generators/persist/table/index_fns.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use convert_case::{Case, Casing}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -130,6 +131,7 @@ impl PersistGenerator { let type_ = columns_map.get(i).ok_or(syn::Error::new(i.span(), "Row not found"))?; let fn_name = Ident::new(format!("select_by_{i}_range").as_str(), Span::mixed_site()); let field_ident = &idx.name; + let column_pascal = Ident::new(&i.to_string().to_case(Case::Pascal), Span::mixed_site()); let (range_bounds, range_arg) = if is_float(type_.to_string().as_str()) { ( @@ -157,7 +159,7 @@ impl PersistGenerator { .range(#range_arg) .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); - SelectQueryBuilder::new(rows) + SelectQueryBuilder::new_sorted(rows, #row_fields_ident::#column_pascal) } }) } diff --git a/codegen/src/generators/persist/table/select_executor.rs b/codegen/src/generators/persist/table/select_executor.rs index bc7bf1a9..4505202d 100644 --- a/codegen/src/generators/persist/table/select_executor.rs +++ b/codegen/src/generators/persist/table/select_executor.rs @@ -155,6 +155,20 @@ impl PersistGenerator { } }; + let fallback_sort = quote! { + let mut items: Vec<#row_type> = iter.collect(); + items.sort_by(|a, b| { + for (order, col) in &self.params.order { + match col { + #(#order_matches)* + _ => continue, + } + } + std::cmp::Ordering::Equal + }); + iter = Box::new(items.into_iter()); + }; + quote! { impl SelectQueryExecutor<#row_type, I, #column_range_type, #row_fields_ident> for SelectQueryBuilder<#row_type, I, #column_range_type, #row_fields_ident> @@ -181,19 +195,30 @@ impl PersistGenerator { #range if !self.params.order.is_empty() { - let mut items: Vec<#row_type> = iter.collect(); - - items.sort_by(|a, b| { - for (order, col) in &self.params.order { - match col { - #(#order_matches)* - _ => continue, + // Optimization: single order on pre-sorted column with no additional range filters + let can_optimize = self.params.sorted_by.is_some() + && self.params.range.is_empty() + && self.params.order.len() == 1; + + if can_optimize { + let (order, col) = &self.params.order[0]; + let sorted_col = self.params.sorted_by.as_ref().unwrap(); + + if col == sorted_col { + match order { + Order::Desc => { + iter = Box::new(iter.rev()); + } + Order::Asc => { + // Already sorted correctly, no action needed + } } + } else { + #fallback_sort } - std::cmp::Ordering::Equal - }); - - iter = Box::new(items.into_iter()); + } else { + #fallback_sort + } } let iter_result: Box> = if let Some(offset) = self.params.offset { diff --git a/codegen/src/generators/read_only/table/impls.rs b/codegen/src/generators/read_only/table/impls.rs index b9eba858..3389ad7d 100644 --- a/codegen/src/generators/read_only/table/impls.rs +++ b/codegen/src/generators/read_only/table/impls.rs @@ -1,4 +1,5 @@ -use proc_macro2::TokenStream; +use convert_case::{Case, Casing}; +use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; use crate::common::name_generator::{WorktableNameGenerator, is_unsized_vec}; @@ -163,6 +164,18 @@ impl ReadOnlyGenerator { let column_range_type = name_generator.get_column_range_type_ident(); let row_fields_ident = name_generator.get_row_fields_enum_ident(); + let pk_sorted_by = if self.columns.primary_keys.len() == 1 { + let pk_field = &self.columns.primary_keys[0]; + let pk_pascal = Ident::new(&pk_field.to_string().to_case(Case::Pascal), Span::mixed_site()); + quote! { + SelectQueryBuilder::new_sorted(rows, #row_fields_ident::#pk_pascal) + } + } else { + quote! { + SelectQueryBuilder::new(rows) + } + }; + quote! { pub fn select_by_pk_range(&self, range: R) -> SelectQueryBuilder<#row_type, impl DoubleEndedIterator + '_, @@ -181,7 +194,7 @@ impl ReadOnlyGenerator { .range(converted_range) .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); - SelectQueryBuilder::new(rows) + #pk_sorted_by } } } diff --git a/codegen/src/generators/read_only/table/index_fns.rs b/codegen/src/generators/read_only/table/index_fns.rs index f8e113f0..819490d3 100644 --- a/codegen/src/generators/read_only/table/index_fns.rs +++ b/codegen/src/generators/read_only/table/index_fns.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use convert_case::{Case, Casing}; use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; @@ -130,6 +131,7 @@ impl ReadOnlyGenerator { let type_ = columns_map.get(i).ok_or(syn::Error::new(i.span(), "Row not found"))?; let fn_name = Ident::new(format!("select_by_{i}_range").as_str(), Span::mixed_site()); let field_ident = &idx.name; + let column_pascal = Ident::new(&i.to_string().to_case(Case::Pascal), Span::mixed_site()); let (range_bounds, range_arg) = if is_float(type_.to_string().as_str()) { ( @@ -157,7 +159,7 @@ impl ReadOnlyGenerator { .range(#range_arg) .filter_map(|(_, link)| self.0.data.select_non_ghosted(link.0).ok()); - SelectQueryBuilder::new(rows) + SelectQueryBuilder::new_sorted(rows, #row_fields_ident::#column_pascal) } }) } diff --git a/codegen/src/generators/read_only/table/select_executor.rs b/codegen/src/generators/read_only/table/select_executor.rs index d346e9b5..fbcec3f3 100644 --- a/codegen/src/generators/read_only/table/select_executor.rs +++ b/codegen/src/generators/read_only/table/select_executor.rs @@ -155,6 +155,20 @@ impl ReadOnlyGenerator { } }; + let fallback_sort = quote! { + let mut items: Vec<#row_type> = iter.collect(); + items.sort_by(|a, b| { + for (order, col) in &self.params.order { + match col { + #(#order_matches)* + _ => continue, + } + } + std::cmp::Ordering::Equal + }); + iter = Box::new(items.into_iter()); + }; + quote! { impl SelectQueryExecutor<#row_type, I, #column_range_type, #row_fields_ident> for SelectQueryBuilder<#row_type, I, #column_range_type, #row_fields_ident> @@ -181,19 +195,30 @@ impl ReadOnlyGenerator { #range if !self.params.order.is_empty() { - let mut items: Vec<#row_type> = iter.collect(); - - items.sort_by(|a, b| { - for (order, col) in &self.params.order { - match col { - #(#order_matches)* - _ => continue, + // Optimization: single order on pre-sorted column with no additional range filters + let can_optimize = self.params.sorted_by.is_some() + && self.params.range.is_empty() + && self.params.order.len() == 1; + + if can_optimize { + let (order, col) = &self.params.order[0]; + let sorted_col = self.params.sorted_by.as_ref().unwrap(); + + if col == sorted_col { + match order { + Order::Desc => { + iter = Box::new(iter.rev()); + } + Order::Asc => { + // Already sorted correctly, no action needed + } } + } else { + #fallback_sort } - std::cmp::Ordering::Equal - }); - - iter = Box::new(items.into_iter()); + } else { + #fallback_sort + } } let iter_result: Box> = if let Some(offset) = self.params.offset { diff --git a/src/table/select/mod.rs b/src/table/select/mod.rs index da75b6be..5b9fc6e6 100644 --- a/src/table/select/mod.rs +++ b/src/table/select/mod.rs @@ -16,4 +16,5 @@ pub struct QueryParams { pub offset: Option, pub order: VecDeque<(Order, RowFields)>, pub range: VecDeque<(ColumnRange, RowFields)>, + pub sorted_by: Option, } diff --git a/src/table/select/query.rs b/src/table/select/query.rs index ca7bf40d..2b2f3f66 100644 --- a/src/table/select/query.rs +++ b/src/table/select/query.rs @@ -22,6 +22,20 @@ where offset: None, order: VecDeque::new(), range: VecDeque::new(), + sorted_by: None, + }, + iter, + } + } + + pub fn new_sorted(iter: I, sorted_by: RowFields) -> Self { + Self { + params: QueryParams { + limit: None, + offset: None, + order: VecDeque::new(), + range: VecDeque::new(), + sorted_by: Some(sorted_by), }, iter, } @@ -38,6 +52,9 @@ where } pub fn order_on(mut self, column: RowFields, order: Order) -> Self { + if !self.params.order.is_empty() { + self.params.sorted_by = None; + } self.params.order.push_back((order, column)); self } @@ -46,6 +63,7 @@ where where R: Into, { + self.params.sorted_by = None; self.params.range.push_back((range.into(), column)); self }