From f2db010059206a0b215e986cad349da40bdab425 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Sat, 13 Dec 2025 08:07:24 -0500 Subject: [PATCH 1/3] Date and Time Function Modifiers and start of implementation Fix issue with seconds calculation Refactor datetime to build a julian day object Simple integration tests for the initial implementation of datetime functions Move datetime builder into jdn Parsing implemented for simple modifiers Fix issue with testing fix fmt Add todo list fix backup PR review service --- .github/workflows/rust.yml | 4 +- .gitignore | 3 +- AGENTS.md | 199 ++++++++++ src/db/table/operations/helpers/common.rs | 8 +- .../helpers/datetime_functions/date.rs | 6 - .../helpers/datetime_functions/julian_day.rs | 141 +++++++ .../helpers/datetime_functions/mod.rs | 75 +++- .../helpers/datetime_functions/modifiers.rs | 350 ++++++++++++++++++ .../helpers/datetime_functions/time_values.rs | 179 ++++----- .../helpers/datetime_functions/todo.md | 11 + .../helpers/selectables/get_selectables.rs | 1 - src/interpreter/ast/mod.rs | 5 +- src/interpreter/tokenizer/scanner.rs | 5 + tests/main_tests.rs | 1 + tests/suites/datetime_operations.rs | 71 ++++ 15 files changed, 955 insertions(+), 104 deletions(-) create mode 100644 AGENTS.md delete mode 100644 src/db/table/operations/helpers/datetime_functions/date.rs create mode 100644 src/db/table/operations/helpers/datetime_functions/julian_day.rs create mode 100644 src/db/table/operations/helpers/datetime_functions/modifiers.rs create mode 100644 src/db/table/operations/helpers/datetime_functions/todo.md create mode 100644 tests/suites/datetime_operations.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 4175569..c14bd0a 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -88,7 +88,7 @@ jobs: exclude_paths: target/ fail_action_if_review_failed: true - - name: AI Code Review Fallback (gemini-1.5-flash) + - name: AI Code Review Fallback (gemini-2.5-flash-lite) if: steps.review_primary.outcome == 'failure' uses: Wandalen/wretry.action@v3.5.0 with: @@ -100,7 +100,7 @@ jobs: pr_number: ${{ github.event.number }} ai_provider: google google_api_key: ${{ secrets.GEMINI_API_KEY }} - google_model: gemini-1.5-flash + google_model: gemini-2.5-flash-lite include_extensions: .rs,.toml exclude_paths: target/ fail_action_if_review_failed: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 36a17f3..a1e28be 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .vscode/ .claude/ .gemini/ -temp/ \ No newline at end of file +temp/ +AGENTS.md \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ac90940 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,199 @@ +# AGENT GUIDELINES + +## Working with an AI Agent on MollyCache + +This document outlines how an AI assistant and the development team collaborate on MollyCache to bring this high-performance, in-memory SQL database to market. + +## Project Context + +**What is MollyCache?** +MollyCache is a from-scratch, in-memory SQL database built in Rust with SQLite compatibility. Unlike traditional query caches that store complete query results, MollyCache implements intelligent row-based caching where individual rows are cached and evicted, enabling superior memory efficiency when queries share overlapping data. + +**Current Status:** Early development (v0.1.0) + +**Target Market:** Applications requiring high-performance in-memory data access with SQL compatibility, particularly those with overlapping query patterns where traditional query caching falls short. + +## Core Development Philosophy + +When working on MollyCache, these principles are non-negotiable: + +1. **Performance First**: Code must be optimized for in-memory performance comparable to Memcached/Redis, not disk-based databases +2. **In-Memory First**: All data lives in RAM; disk I/O only on explicit user request +3. **SQLite Compatibility**: Complete parity with SQLite queries and results +4. **High Test Coverage**: Maintain >75% test coverage at all times +5. **Zero Dependencies**: Use only Rust standard library - no external crates +6. **Clean, Minimal Code**: Straightforward implementations over clever abstractions + +## Development Workflow + +### 1. Understanding Tasks +- Check GitHub Issues for context and related discussions +- Review existing code in relevant modules before making changes +- Ask clarifying questions if requirements are ambiguous +- Consider how changes fit into the broader architecture + +### 2. Code Standards + +**Rust Style:** +- Use Rust Edition 2024 +- Follow standard Rust naming conventions (snake_case for functions/variables, PascalCase for types) +- Prefer explicit error handling with `Result` +- Keep functions focused and modular +- Use descriptive variable and function names +- **No code comments**: Write self-documenting code with clear variable/function names instead of explanatory comments + +**Architecture Patterns:** +- **Stack-based state management** for transactions (rows, columns, table names all maintain stacks) +- **Separation of concerns**: Tokenizer → Parser → AST → Database Executor +- **Helper functions** for shared logic (e.g., row filtering, order-by evaluation) +- **Type casting** for SQLite compatibility across Value types + +**Error Messages:** +- Clear, actionable error messages +- Include context (table names, column names, etc.) +- Parser errors should include line/column information + +### 3. Testing Requirements + +**Every code change must include tests:** +- Unit tests embedded in source files with `#[cfg(test)]` +- Integration tests in `/tests` directory for user-facing features +- Use test utilities in `test_utils.rs` for assertions +- Verify edge cases and error conditions +- Ensure tests pass before committing: `cargo test` + +**Test Coverage:** +- Run `cargo tarpaulin` to verify coverage remains >75% +- Don't merge code that drops coverage below target + +### 5. Commit Messages +- Use clear, descriptive commit messages +- Focus on the "why" not just the "what" +- Follow existing commit style in git log +- One logical change per commit when possible + +### 6. Pull Requests +- Reference related GitHub Issues +- Explain architectural decisions in PR description +- Include test plan or verification steps +- Ensure CI passes before requesting review + +## How an AI Agent Should Approach Tasks + +### Research First +- Read relevant source files before making changes +- Understand existing patterns and architecture +- Check test files to understand expected behavior +- Review recent commits for context + +### Propose Solutions +- Present multiple approaches when applicable +- Explain trade-offs (performance, maintainability, complexity) +- Consider impact on existing functionality +- Think about future extensibility + +### Implement Incrementally +- Break large features into smaller, testable pieces +- Implement core functionality first, then edge cases +- Keep commits focused and atomic +- Test continuously during development + +### Communication Style +- Be concise and technical +- Focus on facts and implementation details +- Avoid unnecessary praise or filler +- Ask specific questions when blocked +- Provide code examples when explaining concepts + +## Key Architectural Components + +### Database Layer (`src/db/`) +- **Database** (`database.rs`): Central executor, routes SQL statements to appropriate handlers +- **Table** (`table/core/table.rs`): Manages rows and columns with stack-based history +- **Row** (`table/core/row.rs`): Vector of Values with transaction stack support +- **Column** (`table/core/column.rs`): Column definitions with types and constraints +- **Value** (`table/core/value.rs`): Typed data (Integer, Real, Text, Blob, Null) with casting +- **Operations** (`table/operations/`): CRUD implementations (CREATE, INSERT, SELECT, UPDATE, DELETE, DROP, ALTER) +- **Transactions** (`transactions/`): Transaction log, COMMIT, ROLLBACK, SAVEPOINT + +### Interpreter Layer (`src/interpreter/`) +- **Tokenizer** (`tokenizer/`): Lexical analysis of SQL strings +- **AST** (`ast/`): Abstract Syntax Tree definitions and parsing +- **Parser** (`ast/parser.rs`): Converts tokens to structured SQL statements + +### CLI Layer (`src/cli/`) +- **CLI** (`cli/mod.rs`): Interactive REPL interface + +## Common Development Scenarios + +### Adding a New SQL Feature +1. Add tokens to `tokenizer/token.rs` if needed +2. Update parser in `interpreter/ast/` to recognize syntax +3. Add statement type to `SqlStatement` enum +4. Implement execution logic in appropriate `operations/` module +5. Add integration tests in `/tests` +6. Update README if user-facing + +### Fixing a Bug +1. Write a failing test that reproduces the bug +2. Identify root cause through code review and debugging +3. Implement minimal fix +4. Verify test passes and no regressions +5. Consider edge cases and add additional tests + +### Optimizing Performance +1. Benchmark current implementation +2. Profile to identify bottleneck +3. Implement optimization +4. Verify correctness with existing tests +5. Benchmark improvement +6. Document trade-offs in commit message + +## Working with Zero Dependencies + +Since MollyCache uses only Rust standard library: +- No external crates for parsing, data structures, or algorithms +- Implement solutions from first principles +- Optimize using std library features (HashMap, Vec, etc.) +- Consider performance implications of standard collections + +## Future Roadmap Awareness + +When making changes, consider these planned features: +- **Row-based caching with eviction policies**: Data structures should support efficient eviction +- **SQLite snapshots**: Architecture should support loading from disk +- **Concurrent reads**: Code should be thread-safe where applicable +- **Multi-table JOINs**: Query execution should be extensible for joins +- **Query optimization**: Consider where indexes or query planning might fit + +## Getting Unstuck + +If you encounter challenges: +1. Review similar implementations in the codebase +2. Check SQLite documentation for expected behavior +3. Look at test files for usage examples +4. Ask the development team specific technical questions +5. Propose multiple solution approaches with trade-offs + +## Goals for Market Launch + +To get MollyCache to market, focus areas include: + +1. **Core SQL Completeness**: Full SQLite compatibility for common queries +2. **Row-Based Caching**: Implement intelligent caching with eviction policies +3. **Performance Benchmarks**: Demonstrate performance parity with Redis/Memcached +4. **Production Readiness**: Error handling, edge cases, memory safety +5. **Documentation**: Clear usage examples and API documentation +6. **Real-World Testing**: Validate with actual use cases and workloads + +## Questions? + +When in doubt: +- Check existing code patterns in the repository +- Review test files for examples +- Ask the development team +- Refer to SQLite documentation for compatibility questions + +--- + +**Remember**: MollyCache aims to revolutionize query caching through intelligent row-based storage. Every line of code should serve that mission while maintaining the principles of performance, simplicity, and SQLite compatibility. diff --git a/src/db/table/operations/helpers/common.rs b/src/db/table/operations/helpers/common.rs index 8911920..20d6133 100644 --- a/src/db/table/operations/helpers/common.rs +++ b/src/db/table/operations/helpers/common.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::collections::HashSet; use crate::db::table::core::{row::Row, table::Table, value::DataType, value::Value}; -use crate::db::table::operations::helpers::datetime_functions::date::get_date; +use crate::db::table::operations::helpers::datetime_functions::build_julian_day; use crate::db::table::operations::helpers::order_by_clause::apply_order_by_from_precomputed; use crate::interpreter::ast::{ FunctionName, LimitClause, LogicalOperator, MathOperator, Operator, OrderByClause, @@ -120,7 +120,11 @@ pub fn get_column( SelectableStackElement::Function(func) => { let args = &func.arguments; let res = match func.name { - FunctionName::Date => get_date(args)?, + FunctionName::DateTime => Value::Text(build_julian_day(args)?.as_datetime()), + FunctionName::Date => Value::Text(build_julian_day(args)?.as_date()), + FunctionName::Time => Value::Text(build_julian_day(args)?.as_time()), + FunctionName::JulianDay => Value::Real(build_julian_day(args)?.value()), + FunctionName::UnixEpoch => Value::Real(build_julian_day(args)?.as_unix_epoch()), _ => return Err(format!("Unsupported function: {:?}", func.name)), }; row_values.push(res); diff --git a/src/db/table/operations/helpers/datetime_functions/date.rs b/src/db/table/operations/helpers/datetime_functions/date.rs deleted file mode 100644 index 5ee3c50..0000000 --- a/src/db/table/operations/helpers/datetime_functions/date.rs +++ /dev/null @@ -1,6 +0,0 @@ -use crate::db::table::core::value::Value; -use crate::interpreter::ast::SelectableColumn; - -pub fn get_date(_args: &Vec) -> Result { - todo!() -} diff --git a/src/db/table/operations/helpers/datetime_functions/julian_day.rs b/src/db/table/operations/helpers/datetime_functions/julian_day.rs new file mode 100644 index 0000000..d2d0dd8 --- /dev/null +++ b/src/db/table/operations/helpers/datetime_functions/julian_day.rs @@ -0,0 +1,141 @@ +const JULIAN_DAY_NOON_OFFSET: f64 = 0.5; +const UNIX_EPOCH_JULIAN_DAY: f64 = 2440587.5; +const JULIAN_DAY_EPOCH_OFFSET: i64 = 32045; +const YEAR_OFFSET: i64 = 4800; +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct JulianDay { + jdn: f64, + is_subsecond: bool, +} + +impl JulianDay { + pub fn as_date(&self) -> String { + let jdn_value = self.value(); + let jd_int: i64 = ((jdn_value + JULIAN_DAY_NOON_OFFSET).floor()) as i64; + + let day = ((5 + * (((4 * (jd_int + 1401 + (((4 * jd_int + 274277) / 146097) * 3) / 4 - 38) + 3) + % 1461) + / 4) + + 2) + % 153) + / 5 + + 1; + let month = ((5 + * (((4 * (jd_int + 1401 + (((4 * jd_int + 274277) / 146097) * 3) / 4 - 38) + 3) + % 1461) + / 4) + + 2) + / 153 + + 2) + % 12 + + 1; + let year = (4 * (jd_int + 1401 + (((4 * jd_int + 274277) / 146097) * 3) / 4 - 38) + 3) + / 1461 + - 4716 + + (12 + 2 - month) / 12; + format!("{:04}-{:02}-{:02}", year, month, day) + } + + pub fn as_time(&self) -> String { + let jdn_value = self.value(); + let jd_int = ((jdn_value + JULIAN_DAY_NOON_OFFSET).floor()) as i64; + let jd_fractional = (jdn_value + JULIAN_DAY_NOON_OFFSET) - (jd_int as f64); + let total_seconds = jd_fractional * 86400.0; + let hour = (total_seconds / 3600.0).floor() as i64; + let minute = ((total_seconds % 3600.0) / 60.0).floor() as i64; + let second_with_fraction = (total_seconds % 3600.0) % 60.0; + let second = second_with_fraction.floor() as i64; + + if self.is_subsecond { + let fractional_seconds = second_with_fraction - second as f64; + let milliseconds = (fractional_seconds * 1000.0).round() as i64; + format!( + "{:02}:{:02}:{:02}.{:03}", + hour, minute, second, milliseconds + ) + } else { + format!("{:02}:{:02}:{:02}", hour, minute, second) + } + } + + pub fn as_datetime(&self) -> String { + format!("{} {}", self.as_date(), self.as_time()) + } + + pub fn as_unix_epoch(&self) -> f64 { + let jdn_value = self.value(); + if self.is_subsecond { + (jdn_value - UNIX_EPOCH_JULIAN_DAY) * 86400000.0 + } else { + (jdn_value - UNIX_EPOCH_JULIAN_DAY) * 86400.0 + } + } + + pub fn new(jdn: f64) -> Self { + Self { + jdn, + is_subsecond: false, + } + } + + // https://en.wikipedia.org/wiki/Julian_day + // This function accepts negative and positive float values for all of the params. + // If a None is passed in for the year, it will be treated as julian year 0. + pub fn new_from_datetime_vals( + year: f64, + month: f64, + day: f64, + hour: f64, + minute: f64, + second: f64, + subsecond: f64, + ) -> Self { + let total_seconds = hour * 3600.0 + minute * 60.0 + second + subsecond; + let time_fraction = total_seconds / 86400.0; + + let year_int = year.floor() as i64; + let month_int = month.floor() as i64; + let day_int = day.floor() as i64; + let day_fraction = day - day.floor(); + let a = (14 - month_int) / 12; + let y = year_int + YEAR_OFFSET - a; + let m = month_int + 12 * a - 3; + + let jdn_int = day_int + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 + - JULIAN_DAY_EPOCH_OFFSET; + + let jdn = (jdn_int as f64) + day_fraction + time_fraction - JULIAN_DAY_NOON_OFFSET; + Self { + jdn, + is_subsecond: false, + } + } + + pub fn new_relative_from_datetime_vals( + y: f64, + m: f64, + d: f64, + h: f64, + mi: f64, + s: f64, + fs: f64, + ) -> Self { + let jdn = Self::new_from_datetime_vals(y, m, d, h, mi, s, fs).value(); + let gregorian_year_zero = + Self::new_from_datetime_vals(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0).value(); + let jdn = jdn - gregorian_year_zero; + Self { + jdn, + is_subsecond: false, + } + } + + pub fn value(&self) -> f64 { + self.jdn + } + + pub fn value_mut(&mut self) -> &mut f64 { + &mut self.jdn + } +} diff --git a/src/db/table/operations/helpers/datetime_functions/mod.rs b/src/db/table/operations/helpers/datetime_functions/mod.rs index 7d28cb1..0251f87 100644 --- a/src/db/table/operations/helpers/datetime_functions/mod.rs +++ b/src/db/table/operations/helpers/datetime_functions/mod.rs @@ -1,2 +1,75 @@ -pub mod date; +pub mod julian_day; +pub mod modifiers; pub mod time_values; + +use crate::db::table::core::value::Value; +use crate::db::table::operations::helpers::datetime_functions::julian_day::JulianDay; +use crate::db::table::operations::helpers::datetime_functions::modifiers::DateTimeModifier; +use crate::db::table::operations::helpers::datetime_functions::modifiers::parse_modifier; +use crate::db::table::operations::helpers::datetime_functions::time_values::parse_timevalue; +use crate::interpreter::ast::SelectableColumn; +use crate::interpreter::ast::SelectableStackElement; + +pub fn build_julian_day(args: &Vec) -> Result { + if args.is_empty() { + return Err("Invalid DateTime function: no arguments".to_string()); + } + let mut init_jdn = { + let arg = &args[0]; + match &arg.selectables[0] { + SelectableStackElement::Value(val) => parse_timevalue(val)?, + _ => { + return Err(format!( + "Invalid argument for datetime function: {:?}", + arg.selectables[0] + )); + } + } + }; + // ONLY SUPPORTS TIME MODIFIERS RN. + for arg in args[1..].iter() { + let arg = match &arg.selectables[0] { + SelectableStackElement::Value(Value::Text(val)) => val.to_string(), + _ => { + return Err(format!( + "Invalid argument for datetime function: {:?}", + arg.selectables[0] + )); + } + }; + let modifier = parse_modifier(&arg)?; + match modifier { + DateTimeModifier::JDNOffset(jd) => { + *init_jdn.value_mut() += jd.value(); + } + _ => { + return Err(format!("NOT SUPPORTED YET: '{}'", arg)); + } + } + } + Ok(init_jdn) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db::table::core::value::Value; + + #[test] + fn test_build_julian_day_as_date_time() { + let args = vec![SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Text( + "2025-12-12".to_string(), + ))], + column_name: "text".to_string(), + }]; + let result = build_julian_day(&args).unwrap().as_datetime(); + assert_eq!(result, "2025-12-12 00:00:00"); + let args = vec![SelectableColumn { + selectables: vec![SelectableStackElement::Value(Value::Real(2461022.6789))], + column_name: "real".to_string(), + }]; + let result = build_julian_day(&args).unwrap().as_datetime(); + assert_eq!(result, "2025-12-13 04:17:36"); + } +} diff --git a/src/db/table/operations/helpers/datetime_functions/modifiers.rs b/src/db/table/operations/helpers/datetime_functions/modifiers.rs new file mode 100644 index 0000000..962932e --- /dev/null +++ b/src/db/table/operations/helpers/datetime_functions/modifiers.rs @@ -0,0 +1,350 @@ +use crate::db::table::operations::helpers::datetime_functions::julian_day::JulianDay; + +#[derive(Debug, Clone, PartialEq)] +pub enum DateTimeModifier { + JDNOffset(JulianDay), + Ceiling, + Floor, + StartOfMonth, + StartOfYear, + StartOfDay, + Weekday(i64), + UnixEpoch, + JulianDay, + Auto, + Localtime, + Utc, + Subsecond, +} + +// Parsing here is done according to the SQLite documentation for date and time function modifiers. +// https://sqlite.org/lang_datefunc.html see section 3 +pub fn parse_modifier(modifier: &str) -> Result { + // Parse 'weekday N' format. + if let Some(value) = modifier.strip_prefix("weekday ") { + let value = value.trim(); + if value.is_empty() { + return Err("Weekday modifier requires a numeric argument".to_string()); + } + let weekday = value + .parse::() + .map_err(|_| format!("Invalid weekday value: '{}'", value))? + as i64; + if !(0..=6).contains(&weekday) { + return Err("Weekday modifier accepts values between 0 and 6".to_string()); + } + return Ok(DateTimeModifier::Weekday(weekday)); + } + + // Parse other modifiers. + match modifier { + "ceiling" => return Ok(DateTimeModifier::Ceiling), + "floor" => return Ok(DateTimeModifier::Floor), + "start of month" => return Ok(DateTimeModifier::StartOfMonth), + "start of year" => return Ok(DateTimeModifier::StartOfYear), + "start of day" => return Ok(DateTimeModifier::StartOfDay), + "unixepoch" => return Ok(DateTimeModifier::UnixEpoch), + "julianday" => return Ok(DateTimeModifier::JulianDay), + "auto" => return Ok(DateTimeModifier::Auto), + "localtime" => return Ok(DateTimeModifier::Localtime), + "utc" => return Ok(DateTimeModifier::Utc), + "subsec" | "subsecond" => return Ok(DateTimeModifier::Subsecond), + _ => {} + } + + // At this point we should have either a numeric modifier (1-13 in the SQLite documentation) or an error. + let original_modifier = modifier; + let has_sign = modifier.starts_with('+') || modifier.starts_with('-'); + let sign = if modifier.starts_with('-') { -1.0 } else { 1.0 }; + let modifier = modifier.trim_start_matches('+').trim_start_matches('-'); + + // Handle modifiers 1-6 + match modifier.split_once(' ').unwrap_or((modifier, "")) { + (value, "days") => { + return Ok(DateTimeModifier::JDNOffset( + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + value.parse::().unwrap() * sign, + 0.0, + 0.0, + 0.0, + 0.0, + ), + )); + } + (value, "hours") => { + return Ok(DateTimeModifier::JDNOffset( + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + value.parse::().unwrap() * sign, + 0.0, + 0.0, + 0.0, + ), + )); + } + (value, "minutes") => { + return Ok(DateTimeModifier::JDNOffset( + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + 0.0, + value.parse::().unwrap() * sign, + 0.0, + 0.0, + ), + )); + } + (value, "seconds") => { + return Ok(DateTimeModifier::JDNOffset( + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + value.parse::().unwrap() * sign, + 0.0, + ), + )); + } + (value, "months") => { + return Ok(DateTimeModifier::JDNOffset( + JulianDay::new_relative_from_datetime_vals( + 0.0, + value.parse::().unwrap() * sign, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ), + )); + } + (value, "years") => { + return Ok(DateTimeModifier::JDNOffset( + JulianDay::new_relative_from_datetime_vals( + value.parse::().unwrap() * sign, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ), + )); + } + // At this point all of the numeric modifiers have been parsed. The only remaining ones are 7-13 + (value, "") => { + if value.contains('-') { + if !has_sign { + return Err(format!("Invalid modifier: '{}'", original_modifier)); + } + let date = parse_date(value, sign)?; + return Ok(DateTimeModifier::JDNOffset(date)); + } else { + let time = parse_time(value, sign)?; + return Ok(DateTimeModifier::JDNOffset(time)); + } + } + (date, time) => { + if !has_sign { + return Err(format!("Invalid modifier: '{}'", original_modifier)); + } + let date = parse_date(date, sign)?; + let time = parse_time(time, sign)?; + return Ok(DateTimeModifier::JDNOffset(JulianDay::new( + date.value() + time.value(), + ))); + } + } +} + +fn parse_date(date: &str, sign: f64) -> Result { + if date.is_empty() + || date.len() != 10 + || date.chars().nth(4) != Some('-') + || date.chars().nth(7) != Some('-') + { + return Err(format!("Invalid date: '{}'.", date)); + } + let day = date[8..10] + .parse::() + .map_err(|_| format!("Invalid day: '{}'", &date[8..10]))?; + let year = date[0..4] + .parse::() + .map_err(|_| format!("Invalid year: '{}'", &date[0..4]))?; + let month = date[5..7] + .parse::() + .map_err(|_| format!("Invalid month: '{}'", &date[5..7]))?; + + if (month != 0 && (month < 1 || month > 12)) || (month == 0 && day != 0) { + // technically 2025-00-00 is valid + return Err(format!("Invalid date: '{}'.", date)); + } + + if month != 0 && day != 0 { + let max_days = match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { + 29 + } else { + 28 + } + } + _ => 0, + }; + if day < 1 || day > max_days { + return Err(format!("Invalid date: '{}'.", date)); + } + } + + Ok(JulianDay::new_relative_from_datetime_vals( + year as f64 * sign, + month as f64, + day as f64, + 0.0, + 0.0, + 0.0, + 0.0, + )) +} + +fn parse_in_range(s: &str, name: &str, min: i64, max: i64) -> Result { + let value = s + .parse::() + .map_err(|_| format!("Invalid {}: '{}'", name, s))?; + if !(min..=max).contains(&value) { + return Err(format!( + "{} out of range ({}-{}): {}", + name, min, max, value + )); + } + Ok(value) +} + +fn parse_time(time: &str, sign: f64) -> Result { + if time.is_empty() { + return Err(format!("Invalid time: '{}'.", time)); + } + + let mut parts = time.split(':'); + let hour = parse_in_range( + parts + .next() + .ok_or_else(|| format!("Invalid time: '{}'.", time))?, + "hour", + 0, + 23, + )?; + let minute = parse_in_range( + parts + .next() + .ok_or_else(|| format!("Invalid time: '{}'.", time))?, + "minute", + 0, + 59, + )?; + + let (second, subsecond) = if let Some(second_part) = parts.next() { + if parts.next().is_some() { + return Err(format!("Invalid time: '{}'.", time)); + } + if let Some(dot_pos) = second_part.find('.') { + if second_part[dot_pos + 1..].len() > 3 { + return Err(format!("Invalid time: '{}'.", time)); + } + ( + parse_in_range(&second_part[..dot_pos], "second", 0, 59)?, + parse_in_range(&second_part[dot_pos + 1..], "subsecond", 0, 999)? as f64 / 1000.0, + ) + } else { + (parse_in_range(second_part, "second", 0, 59)?, 0.0) + } + } else { + (0, 0.0) + }; + + Ok(JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + hour as f64 * sign, + minute as f64 * sign, + second as f64 * sign, + subsecond * sign, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + + impl DateTimeModifier { + fn jdnoffset(&self) -> Option<&JulianDay> { + match self { + DateTimeModifier::JDNOffset(jd) => Some(jd), + _ => None, + } + } + } + trait ModifierValue { + fn value(&self) -> f64; + } + + impl ModifierValue for Result { + fn value(&self) -> f64 { + self.as_ref().unwrap().jdnoffset().unwrap().value() + } + } + + #[test] + fn test_parse_modifier() { + assert!(parse_modifier("5 days").value() == 5.0); + assert!(parse_modifier("12 hours").value() == 0.5); + assert!((parse_modifier("30 minutes").value() - 0.020833333333333332).abs() < 0.000001); + assert!((parse_modifier("45 seconds").value() - 0.0005208333333333333).abs() < 0.000001); + assert!(parse_modifier("6 months").value() == 183.0); + assert!(parse_modifier("2 years").value() == 731.0); + assert!((parse_modifier("12:30").value() - 0.5208333333333333).abs() < 0.000001); + assert!((parse_modifier("+12:30:45").value() - 0.5213541666666666).abs() < 0.000001); + assert!((parse_modifier("-12:30:45.123").value() - (-0.5213555903173983)).abs() < 0.000001); + assert!(parse_modifier("+2025-12-25").value() == 740007.0); + assert!((parse_modifier("+2025-12-25 12:30").value() - 740007.5208333333).abs() < 0.000001); + assert!( + (parse_modifier("+2025-12-25 12:30:45").value() - 740007.5213541666).abs() < 0.000001 + ); + assert!( + (parse_modifier("+2025-12-25 12:30:45.123").value() - 740007.5213555903).abs() + < 0.000001 + ); + } + + #[test] + fn test_parse_modifier_date_requires_sign() { + assert!(parse_modifier("2025-12-25").is_err()); + assert!(parse_modifier("2025-12-25 12:30").is_err()); + assert!(parse_modifier("2025-12-25 12:30:45").is_err()); + assert!(parse_modifier("2025-12-25 12:30:45.123").is_err()); + assert!(parse_modifier("+2025-12-25").is_ok()); + } + + #[test] + fn test_parse_modifier_days_without_sign() { + assert!(parse_modifier("1 days").is_ok()); + assert_eq!( + parse_modifier("1 days") + .unwrap() + .jdnoffset() + .unwrap() + .value(), + 1.0 + ); + } +} diff --git a/src/db/table/operations/helpers/datetime_functions/time_values.rs b/src/db/table/operations/helpers/datetime_functions/time_values.rs index f6993b7..650d8b7 100644 --- a/src/db/table/operations/helpers/datetime_functions/time_values.rs +++ b/src/db/table/operations/helpers/datetime_functions/time_values.rs @@ -1,65 +1,13 @@ use crate::db::table::core::value::Value; +use crate::db::table::operations::helpers::datetime_functions::JulianDay; const UNIX_EPOCH_JULIAN_DAY: f64 = 2440587.5; const MILLISECONDS_PER_DAY: f64 = 86400000.0; -const JULIAN_DAY_EPOCH_OFFSET: i64 = 32045; -const JULIAN_DAY_NOON_OFFSET: f64 = 0.5; -const YEAR_OFFSET: i64 = 4800; - -fn days_in_month(year: i64, month: i64) -> i64 { - match month { - 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, - 4 | 6 | 9 | 11 => 30, - 2 => { - if is_leap_year(year) { - 29 - } else { - 28 - } - } - _ => 0, - } -} - -fn is_leap_year(year: i64) -> bool { - (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) -} - -// Parses a string to an i64 within a given range, i.e. parse hour and validate within 0-23. -fn parse_in_range(s: &str, name: &str, min: i64, max: i64, default: i64) -> Result { - if s.is_empty() { - return Ok(default); - } - let value = s - .parse::() - .map_err(|_| format!("Invalid {}: {:?}", name, s))?; - if !(min..=max).contains(&value) { - return Err(format!( - "{} out of range ({}-{}): {}", - name, min, max, value - )); - } - Ok(value) -} - -// https://en.wikipedia.org/wiki/Julian_day -fn calculate_julian_day(y: i64, m: i64, d: i64, h: i64, mi: i64, s: i64, fs: f64) -> f64 { - let a = (14 - m) / 12; - let y = y + YEAR_OFFSET - a; - let m = m + 12 * a - 3; - - let jdn_int = - d + (153 * m + 2) / 5 + 365 * y + y / 4 - y / 100 + y / 400 - JULIAN_DAY_EPOCH_OFFSET; - - let time_fraction = (h as f64) / 24.0 + (mi as f64) / 1440.0 + (s as f64 + fs) / 86400.0; - - (jdn_int as f64) + time_fraction - JULIAN_DAY_NOON_OFFSET -} // This is parsed according to the SQLite documentation for Time Values // https://sqlite.org/lang_datefunc.html -// This function takes a time value and returns the corresponding f64 julian day number -pub fn parse_timevalue(time_value: &Value) -> Result { +// This function takes a time value and returns the corresponding JDN (Julian Day Number) +pub fn parse_timevalue(time_value: &Value) -> Result { match time_value { Value::Text(text) if text == "now" => { let duration = std::time::SystemTime::now() @@ -68,7 +16,7 @@ pub fn parse_timevalue(time_value: &Value) -> Result { let unix_ms = duration.as_secs() as i64 * 1000 + duration.subsec_millis() as i64; // Convert to Julian Day Number let jdn = (unix_ms as f64 / MILLISECONDS_PER_DAY) + UNIX_EPOCH_JULIAN_DAY; - Ok(jdn) + Ok(JulianDay::new(jdn)) } Value::Text(txt) => { // Look at formats 1-10 in the SQLite documentation for Time Values @@ -152,8 +100,15 @@ pub fn parse_timevalue(time_value: &Value) -> Result { 0.0 }; - let mut jdn = - calculate_julian_day(year, month, day, hour, minute, second, frac_seconds); + let mut jdn = JulianDay::new_from_datetime_vals( + year as f64, + month as f64, + day as f64, + hour as f64, + minute as f64, + second as f64, + frac_seconds, + ); // timezone adjustment let (timezone_hour, timezone_minute) = if timezone_part.len() == 6 { @@ -169,20 +124,56 @@ pub fn parse_timevalue(time_value: &Value) -> Result { let tz_offset = tz_hour / 24.0 + tz_minute / 1440.0; if timezone_part.starts_with('-') { - jdn += tz_offset; + *jdn.value_mut() += tz_offset; } else if timezone_part.starts_with('+') { - jdn -= tz_offset; + *jdn.value_mut() -= tz_offset; } } Ok(jdn) } - Value::Integer(jdn_int) => Ok(*jdn_int as f64), - Value::Real(jdn_float) => Ok(*jdn_float), + Value::Integer(jdn_int) => Ok(JulianDay::new(*jdn_int as f64)), + Value::Real(jdn_float) => Ok(JulianDay::new(*jdn_float)), _ => Err(format!("Invalid time value: {:?}", time_value)), } } +fn days_in_month(year: i64, month: i64) -> i64 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + if is_leap_year(year) { + 29 + } else { + 28 + } + } + _ => 0, + } +} + +fn is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +// Parses a string to an i64 within a given range, i.e. parse hour and validate within 0-23. +fn parse_in_range(s: &str, name: &str, min: i64, max: i64, default: i64) -> Result { + if s.is_empty() { + return Ok(default); + } + let value = s + .parse::() + .map_err(|_| format!("Invalid {}: {:?}", name, s))?; + if !(min..=max).contains(&value) { + return Err(format!( + "{} out of range ({}-{}): {}", + name, min, max, value + )); + } + Ok(value) +} + #[cfg(test)] mod tests { use super::*; @@ -191,86 +182,98 @@ mod tests { fn test_parse_timevalue() { assert_eq!( parse_timevalue(&Value::Text("2025-12-12 12:00:00".to_string())), - Ok(2461022.0) + Ok(JulianDay::new(2461022.0)) ); - assert_eq!(parse_timevalue(&Value::Real(2461021.5)), Ok(2461021.5)); - assert_eq!(parse_timevalue(&Value::Integer(2461021)), Ok(2461021.0)); + assert_eq!( + parse_timevalue(&Value::Real(2461021.5)), + Ok(JulianDay::new(2461021.5)) + ); + assert_eq!( + parse_timevalue(&Value::Integer(2461021)), + Ok(JulianDay::new(2461021.0)) + ); assert_eq!( parse_timevalue(&Value::Text("2025-12-12".to_string())), - Ok(2461021.5) + Ok(JulianDay::new(2461021.5)) ); let result = parse_timevalue(&Value::Text("2025-12-12 12:30".to_string())).unwrap(); - assert!((result - 2461022.020833333).abs() < 0.000001); + assert!((result.value() - 2461022.020833333).abs() < 0.000001); let result = parse_timevalue(&Value::Text("2025-12-12 12:00:00.123".to_string())).unwrap(); - assert!((result - 2461022.0000014235).abs() < 0.0000001); + assert!((result.value() - 2461022.0000014235).abs() < 0.0000001); let result = parse_timevalue(&Value::Text("2025-12-12T12:30".to_string())).unwrap(); - assert!((result - 2461022.020833333).abs() < 0.000001); + assert!((result.value() - 2461022.020833333).abs() < 0.000001); assert_eq!( parse_timevalue(&Value::Text("2025-12-12T12:00:00".to_string())), - Ok(2461022.0) + Ok(JulianDay::new(2461022.0)) ); assert_eq!( parse_timevalue(&Value::Text("12:00:00".to_string())), - Ok(2451545.0) + Ok(JulianDay::new(2451545.0)) ); let result = parse_timevalue(&Value::Text("12:30".to_string())).unwrap(); - assert!((result - 2451545.020833333).abs() < 0.000001); + assert!((result.value() - 2451545.020833333).abs() < 0.000001); assert_eq!( parse_timevalue(&Value::Text("0000-01-01".to_string())), - Ok(1721059.5) + Ok(JulianDay::new(1721059.5)) ); assert_eq!( parse_timevalue(&Value::Text("2025-12-12 12:00:00.1234567890".to_string())), - Ok(2461022.0000014235) + Ok(JulianDay::new(2461022.0000014235)) ); // Timezone tests assert_eq!( parse_timevalue(&Value::Text("2025-12-12T12:00:00Z".to_string())), - Ok(2461022.0) + Ok(JulianDay::new(2461022.0)) ); assert_eq!( parse_timevalue(&Value::Text("2025-12-12 12:00:00Z".to_string())), - Ok(2461022.0) + Ok(JulianDay::new(2461022.0)) ); let result = parse_timevalue(&Value::Text("2025-12-12T12:00:00-04:00".to_string())).unwrap(); - assert!((result - 2461022.1666666665).abs() < 0.0000001); + assert!((result.value() - 2461022.1666666665).abs() < 0.0000001); let result = parse_timevalue(&Value::Text("2025-12-12T12:00:00+04:00".to_string())).unwrap(); - assert!((result - 2461021.8333333335).abs() < 0.0000001); + assert!((result.value() - 2461021.8333333335).abs() < 0.0000001); assert_eq!( parse_timevalue(&Value::Text("12:00:00Z".to_string())), - Ok(2451545.0) + Ok(JulianDay::new(2451545.0)) ); let result = parse_timevalue(&Value::Text("08:00:00-04:00".to_string())).unwrap(); - assert!((result - 2451545.0).abs() < 0.0000001); + assert!((result.value() - 2451545.0).abs() < 0.0000001); let result = parse_timevalue(&Value::Text("16:00:00+04:00".to_string())).unwrap(); - assert!((result - 2451545.0).abs() < 0.0000001); + assert!((result.value() - 2451545.0).abs() < 0.0000001); let now_result = parse_timevalue(&Value::Text("now".to_string())).unwrap(); - assert!(now_result > 2460000.0 && now_result < 2470000.0); + assert!(now_result.value() > 2460000.0 && now_result.value() < 2470000.0); // Trailing whitespace is ignored assert_eq!( parse_timevalue(&Value::Text("2025-12-12 12:00:00 ".to_string())), - Ok(2461022.0) + Ok(JulianDay::new(2461022.0)) ); // Leading whitespace should fail assert!(parse_timevalue(&Value::Text(" 2025-12-12 12:00:00".to_string())).is_err()); let result = parse_timevalue(&Value::Text("12:00:00.500".to_string())).unwrap(); - assert!((result - 2451545.0000057870).abs() < 0.0000001); + assert!((result.value() - 2451545.0000057870).abs() < 0.0000001); let result = parse_timevalue(&Value::Text("2025-12-12 12:00:00.1".to_string())).unwrap(); - assert!((result - 2461022.0000011574).abs() < 0.0000001); + assert!((result.value() - 2461022.0000011574).abs() < 0.0000001); let result = parse_timevalue(&Value::Text("2025-12-12 12:00:00.12".to_string())).unwrap(); - assert!((result - 2461022.0000013889).abs() < 0.0000001); + assert!((result.value() - 2461022.0000013889).abs() < 0.0000001); // Negative Julian day numbers - assert_eq!(parse_timevalue(&Value::Integer(-1)), Ok(-1.0)); - assert_eq!(parse_timevalue(&Value::Real(-100.5)), Ok(-100.5)); + assert_eq!( + parse_timevalue(&Value::Integer(-1)), + Ok(JulianDay::new(-1.0)) + ); + assert_eq!( + parse_timevalue(&Value::Real(-100.5)), + Ok(JulianDay::new(-100.5)) + ); } #[test] diff --git a/src/db/table/operations/helpers/datetime_functions/todo.md b/src/db/table/operations/helpers/datetime_functions/todo.md new file mode 100644 index 0000000..f6941ef --- /dev/null +++ b/src/db/table/operations/helpers/datetime_functions/todo.md @@ -0,0 +1,11 @@ +Currently the following needs to be completed: + - Fix the implementation of Modifiers, SQLite's implementation is very weird here + - Add support for multiple modifiers to work correctly, dependent on above + - Add support for the other modifiers that don't just directly add or subtract time + - Handle edge cases: Feb 29 in leap years, month-end dates + + - Improve abstraction between modifier.rs and time_value.rs they do sim things. + - strftime and timediff need to be done + + - Wayyyy more comprehesive testing here, maybe against sqlite smhow + \ No newline at end of file diff --git a/src/interpreter/ast/helpers/selectables/get_selectables.rs b/src/interpreter/ast/helpers/selectables/get_selectables.rs index 02d788f..ded5f35 100644 --- a/src/interpreter/ast/helpers/selectables/get_selectables.rs +++ b/src/interpreter/ast/helpers/selectables/get_selectables.rs @@ -20,7 +20,6 @@ fn token_to_function_name(token_type: &TokenTypes) -> Option { TokenTypes::DateTime => Some(FunctionName::DateTime), TokenTypes::JulianDay => Some(FunctionName::JulianDay), TokenTypes::UnixEpoch => Some(FunctionName::UnixEpoch), - TokenTypes::TimeDiff => Some(FunctionName::TimeDiff), _ => None, } } diff --git a/src/interpreter/ast/mod.rs b/src/interpreter/ast/mod.rs index bbad736..ace6829 100644 --- a/src/interpreter/ast/mod.rs +++ b/src/interpreter/ast/mod.rs @@ -246,7 +246,7 @@ pub enum FunctionName { JulianDay, UnixEpoch, // TODO: Support Strftime - TimeDiff, + // TODO: Support TimeDiff } impl FunctionName { @@ -261,8 +261,7 @@ impl FunctionName { | FunctionName::Time | FunctionName::DateTime | FunctionName::JulianDay - | FunctionName::UnixEpoch - | FunctionName::TimeDiff => false, + | FunctionName::UnixEpoch => false, } } } diff --git a/src/interpreter/tokenizer/scanner.rs b/src/interpreter/tokenizer/scanner.rs index 74755f5..cd35047 100644 --- a/src/interpreter/tokenizer/scanner.rs +++ b/src/interpreter/tokenizer/scanner.rs @@ -195,6 +195,11 @@ impl<'a> Scanner<'a> { slice if slice.eq_ignore_ascii_case("AVG") => TokenTypes::Avg, slice if slice.eq_ignore_ascii_case("MIN") => TokenTypes::Min, slice if slice.eq_ignore_ascii_case("MAX") => TokenTypes::Max, + slice if slice.eq_ignore_ascii_case("DATE") => TokenTypes::Date, + slice if slice.eq_ignore_ascii_case("TIME") => TokenTypes::Time, + slice if slice.eq_ignore_ascii_case("DATETIME") => TokenTypes::DateTime, + slice if slice.eq_ignore_ascii_case("JULIANDAY") => TokenTypes::JulianDay, + slice if slice.eq_ignore_ascii_case("UNIXEPOCH") => TokenTypes::UnixEpoch, slice if slice.eq_ignore_ascii_case("TRUE") => TokenTypes::TrueLiteral, slice if slice.eq_ignore_ascii_case("FALSE") => TokenTypes::FalseLiteral, _ => TokenTypes::Identifier, diff --git a/tests/main_tests.rs b/tests/main_tests.rs index 13205e4..715e587 100644 --- a/tests/main_tests.rs +++ b/tests/main_tests.rs @@ -1,6 +1,7 @@ mod common; mod suites { pub mod basic_crud; + pub mod datetime_operations; pub mod set_operators; pub mod transactions; } diff --git a/tests/suites/datetime_operations.rs b/tests/suites/datetime_operations.rs new file mode 100644 index 0000000..e584e8c --- /dev/null +++ b/tests/suites/datetime_operations.rs @@ -0,0 +1,71 @@ +use mollycache::db::database::Database; +use mollycache::db::table::core::{row::Row, value::Value}; +use mollycache::interpreter::run_sql; + +use crate::common::assert_eq_table_rows; + +#[test] +fn test_datetime_functions() { + let mut database = Database::new(); + let sql = " + CREATE TABLE users (id INTEGER); + INSERT INTO users (id) VALUES (1); + SELECT Date('2025-12-12 12:00:00') FROM users; + SELECT Time('2025-12-12 12:00:00') FROM users; + SELECT DateTime('2025-12-12 12:00:00') FROM users; + SELECT JulianDay('2025-12-12 12:00:00') FROM users; + SELECT UnixEpoch('2025-12-12 12:00:00') FROM users; + "; + let mut result = run_sql(&mut database, sql); + assert!(result.iter().all(|result| result.is_ok())); + + let unix_result = result.pop().unwrap().unwrap().unwrap(); + assert!(matches!(unix_result[0].0[0], Value::Real(epoch) if epoch > 0.0)); + + let jdn = match result.pop().unwrap().unwrap().unwrap()[0].0[0] { + Value::Real(j) => j, + _ => panic!("Expected Real value for JulianDay()"), + }; + assert!((jdn - 2461022.0).abs() < 0.0001); + + let expected_datetime = vec![Row(vec![Value::Text("2025-12-12 12:00:00".to_string())])]; + assert_eq_table_rows(result.pop().unwrap().unwrap().unwrap(), expected_datetime); + + let expected_time = vec![Row(vec![Value::Text("12:00:00".to_string())])]; + assert_eq_table_rows(result.pop().unwrap().unwrap().unwrap(), expected_time); + + let expected_date = vec![Row(vec![Value::Text("2025-12-12".to_string())])]; + assert_eq_table_rows(result.pop().unwrap().unwrap().unwrap(), expected_date); +} + +#[test] +fn test_datetime_functions_with_modifiers() { + let mut database = Database::new(); + let sql = " + CREATE TABLE users (id INTEGER); + INSERT INTO users (id) VALUES (1); + SELECT Date('2025-12-12 12:00:00', '1 days') FROM users; + SELECT DateTime('2025-12-12 12:00:00', '10 years') FROM users; + SELECT DateTime('2025-12-12 12:00:00', '+0000-00-01 00:00:01') FROM users; + "; + let result = run_sql(&mut database, sql); + assert_eq!(result.len(), 5); + assert!(result[0].is_ok()); + assert!(result[1].is_ok()); + assert!(result[2].is_ok()); + assert!(result[3].is_ok()); + assert!( + result[4].is_err(), + "Expected error for invalid modifier '+0000-00-01 00:00:01'" + ); // month is invalid + let expected_date = vec![Row(vec![Value::Text("2025-12-13".to_string())])]; + assert_eq_table_rows( + result[2].as_ref().unwrap().as_ref().unwrap().clone(), + expected_date, + ); + let expected_datetime = vec![Row(vec![Value::Text("2035-12-13 12:00:00".to_string())])]; // TECHNICALLY THIS BEHAVIOUR IS INCORRECT, SQLite does really funny stuff with years... + assert_eq_table_rows( + result[3].as_ref().unwrap().as_ref().unwrap().clone(), + expected_datetime, + ); +} From 522788c884a3932a1137b79e5fc916e6b4d4b577 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Sat, 13 Dec 2025 12:13:19 -0500 Subject: [PATCH 2/3] Fix AI reviewer feedback + small change to readme --- README.md | 3 +- .../helpers/datetime_functions/mod.rs | 2 +- .../helpers/datetime_functions/modifiers.rs | 78 ++++++------------- tests/suites/datetime_operations.rs | 10 +-- 4 files changed, 32 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 884c36b..a5d6424 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # MollyCache -[![Wakatime](https://wakatime.com/badge/user/9641004b-568b-4c27-99c5-a34ace36b886/project/2668a03d-d729-4e59-8fc8-bafe3d194ee1.svg)](https://wakatime.com/badge/user/9641004b-568b-4c27-99c5-a34ace36b886/project/2668a03d-d729-4e59-8fc8-bafe3d194ee1) +[![Fletcher](https://wakatime.com/badge/user/9641004b-568b-4c27-99c5-a34ace36b886/project/2668a03d-d729-4e59-8fc8-bafe3d194ee1.svg)](https://wakatime.com/badge/user/9641004b-568b-4c27-99c5-a34ace36b886/project/2668a03d-d729-4e59-8fc8-bafe3d194ee1) +[![Fletcher Pt. 2](https://wakatime.com/badge/user/9641004b-568b-4c27-99c5-a34ace36b886/project/b3cd9856-dee7-41a0-a31c-b3b8b68a0e80.svg)](https://wakatime.com/badge/user/9641004b-568b-4c27-99c5-a34ace36b886/project/b3cd9856-dee7-41a0-a31c-b3b8b68a0e80) ![GitHub last commit](https://img.shields.io/github/last-commit/MollyCache/mollycache) ![GitHub stars](https://img.shields.io/github/stars/MollyCache/mollycache?style=social) diff --git a/src/db/table/operations/helpers/datetime_functions/mod.rs b/src/db/table/operations/helpers/datetime_functions/mod.rs index 0251f87..4d4f703 100644 --- a/src/db/table/operations/helpers/datetime_functions/mod.rs +++ b/src/db/table/operations/helpers/datetime_functions/mod.rs @@ -40,7 +40,7 @@ pub fn build_julian_day(args: &Vec) -> Result { - *init_jdn.value_mut() += jd.value(); + init_jdn = JulianDay::new(init_jdn.value() + jd.value()); } _ => { return Err(format!("NOT SUPPORTED YET: '{}'", arg)); diff --git a/src/db/table/operations/helpers/datetime_functions/modifiers.rs b/src/db/table/operations/helpers/datetime_functions/modifiers.rs index 962932e..ddef305 100644 --- a/src/db/table/operations/helpers/datetime_functions/modifiers.rs +++ b/src/db/table/operations/helpers/datetime_functions/modifiers.rs @@ -61,81 +61,51 @@ pub fn parse_modifier(modifier: &str) -> Result { // Handle modifiers 1-6 match modifier.split_once(' ').unwrap_or((modifier, "")) { (value, "days") => { + let days = value + .parse::() + .map_err(|_| format!("Invalid days value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - value.parse::().unwrap() * sign, - 0.0, - 0.0, - 0.0, - 0.0, - ), + JulianDay::new_relative_from_datetime_vals(0.0, 0.0, days * sign, 0.0, 0.0, 0.0, 0.0), )); } (value, "hours") => { + let hours = value + .parse::() + .map_err(|_| format!("Invalid hours value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - value.parse::().unwrap() * sign, - 0.0, - 0.0, - 0.0, - ), + JulianDay::new_relative_from_datetime_vals(0.0, 0.0, 0.0, hours * sign, 0.0, 0.0, 0.0), )); } (value, "minutes") => { + let minutes = value + .parse::() + .map_err(|_| format!("Invalid minutes value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - 0.0, - value.parse::().unwrap() * sign, - 0.0, - 0.0, - ), + JulianDay::new_relative_from_datetime_vals(0.0, 0.0, 0.0, 0.0, minutes * sign, 0.0, 0.0), )); } (value, "seconds") => { + let seconds = value + .parse::() + .map_err(|_| format!("Invalid seconds value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - value.parse::().unwrap() * sign, - 0.0, - ), + JulianDay::new_relative_from_datetime_vals(0.0, 0.0, 0.0, 0.0, 0.0, seconds * sign, 0.0), )); } (value, "months") => { + let months = value + .parse::() + .map_err(|_| format!("Invalid months value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - 0.0, - value.parse::().unwrap() * sign, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ), + JulianDay::new_relative_from_datetime_vals(0.0, months * sign, 0.0, 0.0, 0.0, 0.0, 0.0), )); } (value, "years") => { + let years = value + .parse::() + .map_err(|_| format!("Invalid years value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals( - value.parse::().unwrap() * sign, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - 0.0, - ), + JulianDay::new_relative_from_datetime_vals(years * sign, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), )); } // At this point all of the numeric modifiers have been parsed. The only remaining ones are 7-13 diff --git a/tests/suites/datetime_operations.rs b/tests/suites/datetime_operations.rs index e584e8c..7a373a7 100644 --- a/tests/suites/datetime_operations.rs +++ b/tests/suites/datetime_operations.rs @@ -29,13 +29,13 @@ fn test_datetime_functions() { assert!((jdn - 2461022.0).abs() < 0.0001); let expected_datetime = vec![Row(vec![Value::Text("2025-12-12 12:00:00".to_string())])]; - assert_eq_table_rows(result.pop().unwrap().unwrap().unwrap(), expected_datetime); + assert_eq_table_rows(expected_datetime, result.pop().unwrap().unwrap().unwrap()); let expected_time = vec![Row(vec![Value::Text("12:00:00".to_string())])]; - assert_eq_table_rows(result.pop().unwrap().unwrap().unwrap(), expected_time); + assert_eq_table_rows(expected_time, result.pop().unwrap().unwrap().unwrap()); let expected_date = vec![Row(vec![Value::Text("2025-12-12".to_string())])]; - assert_eq_table_rows(result.pop().unwrap().unwrap().unwrap(), expected_date); + assert_eq_table_rows(expected_date, result.pop().unwrap().unwrap().unwrap()); } #[test] @@ -60,12 +60,12 @@ fn test_datetime_functions_with_modifiers() { ); // month is invalid let expected_date = vec![Row(vec![Value::Text("2025-12-13".to_string())])]; assert_eq_table_rows( - result[2].as_ref().unwrap().as_ref().unwrap().clone(), expected_date, + result[2].as_ref().unwrap().as_ref().unwrap().clone(), ); let expected_datetime = vec![Row(vec![Value::Text("2035-12-13 12:00:00".to_string())])]; // TECHNICALLY THIS BEHAVIOUR IS INCORRECT, SQLite does really funny stuff with years... assert_eq_table_rows( - result[3].as_ref().unwrap().as_ref().unwrap().clone(), expected_datetime, + result[3].as_ref().unwrap().as_ref().unwrap().clone(), ); } From c9e41c3cc7465368749fa9d0998c0c3963296e21 Mon Sep 17 00:00:00 2001 From: Fletcher555 Date: Sat, 13 Dec 2025 12:14:00 -0500 Subject: [PATCH 3/3] fix fmt --- .../helpers/datetime_functions/modifiers.rs | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/db/table/operations/helpers/datetime_functions/modifiers.rs b/src/db/table/operations/helpers/datetime_functions/modifiers.rs index ddef305..d9c7ef8 100644 --- a/src/db/table/operations/helpers/datetime_functions/modifiers.rs +++ b/src/db/table/operations/helpers/datetime_functions/modifiers.rs @@ -65,7 +65,15 @@ pub fn parse_modifier(modifier: &str) -> Result { .parse::() .map_err(|_| format!("Invalid days value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals(0.0, 0.0, days * sign, 0.0, 0.0, 0.0, 0.0), + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + days * sign, + 0.0, + 0.0, + 0.0, + 0.0, + ), )); } (value, "hours") => { @@ -73,7 +81,15 @@ pub fn parse_modifier(modifier: &str) -> Result { .parse::() .map_err(|_| format!("Invalid hours value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals(0.0, 0.0, 0.0, hours * sign, 0.0, 0.0, 0.0), + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + hours * sign, + 0.0, + 0.0, + 0.0, + ), )); } (value, "minutes") => { @@ -81,7 +97,15 @@ pub fn parse_modifier(modifier: &str) -> Result { .parse::() .map_err(|_| format!("Invalid minutes value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals(0.0, 0.0, 0.0, 0.0, minutes * sign, 0.0, 0.0), + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + 0.0, + minutes * sign, + 0.0, + 0.0, + ), )); } (value, "seconds") => { @@ -89,7 +113,15 @@ pub fn parse_modifier(modifier: &str) -> Result { .parse::() .map_err(|_| format!("Invalid seconds value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals(0.0, 0.0, 0.0, 0.0, 0.0, seconds * sign, 0.0), + JulianDay::new_relative_from_datetime_vals( + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + seconds * sign, + 0.0, + ), )); } (value, "months") => { @@ -97,7 +129,15 @@ pub fn parse_modifier(modifier: &str) -> Result { .parse::() .map_err(|_| format!("Invalid months value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals(0.0, months * sign, 0.0, 0.0, 0.0, 0.0, 0.0), + JulianDay::new_relative_from_datetime_vals( + 0.0, + months * sign, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ), )); } (value, "years") => { @@ -105,7 +145,15 @@ pub fn parse_modifier(modifier: &str) -> Result { .parse::() .map_err(|_| format!("Invalid years value: '{}'", value))?; return Ok(DateTimeModifier::JDNOffset( - JulianDay::new_relative_from_datetime_vals(years * sign, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), + JulianDay::new_relative_from_datetime_vals( + years * sign, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + ), )); } // At this point all of the numeric modifiers have been parsed. The only remaining ones are 7-13