From f95156df772b388055555d0f22a2c012deed6703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dr=2E=20Patrick=20Urbanke=20=28=E5=8A=89=E8=87=AA=E6=88=90?= =?UTF-8?q?=29?= Date: Wed, 2 Jul 2025 20:04:50 +0200 Subject: [PATCH] Added operations on time stamps (#24) --- docs/README.md | 9 +- docs/mathematical_operations.md | 72 +++++ docs/null_handling_operations.md | 81 ++++++ docs/other_operations.md | 249 ------------------ docs/string_operations.md | 65 +++++ docs/timestamp_operations.md | 127 +++++++++ docs/type_conversion_operations.md | 25 ++ include/sqlgen/Timestamp.hpp | 3 + include/sqlgen/col.hpp | 44 +++- include/sqlgen/dynamic/Operation.hpp | 51 +++- include/sqlgen/dynamic/TimeUnit.hpp | 19 ++ include/sqlgen/dynamic/Value.hpp | 14 +- include/sqlgen/operations.hpp | 76 ++++++ include/sqlgen/transpilation/Operation.hpp | 54 ++-- include/sqlgen/transpilation/Operator.hpp | 12 +- .../sqlgen/transpilation/OperatorCategory.hpp | 2 +- .../transpilation/dynamic_operator_t.hpp | 70 +++++ include/sqlgen/transpilation/is_duration.hpp | 23 ++ include/sqlgen/transpilation/is_timestamp.hpp | 25 ++ include/sqlgen/transpilation/make_field.hpp | 219 ++++++--------- .../transpilation/remove_reflection_t.hpp | 17 +- include/sqlgen/transpilation/to_condition.hpp | 49 +++- include/sqlgen/transpilation/to_duration.hpp | 61 +++++ include/sqlgen/transpilation/underlying_t.hpp | 57 +++- src/sqlgen/postgres/to_sql.cpp | 47 ++++ src/sqlgen/sqlite/to_sql.cpp | 83 ++++++ .../test_select_from_with_timestamps.cpp | 89 +++++++ tests/postgres/test_where_with_timestamps.cpp | 66 +++++ .../test_select_from_with_timestamps.cpp | 78 ++++++ tests/sqlite/test_where_with_timestamps.cpp | 59 +++++ 30 files changed, 1410 insertions(+), 436 deletions(-) create mode 100644 docs/mathematical_operations.md create mode 100644 docs/null_handling_operations.md delete mode 100644 docs/other_operations.md create mode 100644 docs/string_operations.md create mode 100644 docs/timestamp_operations.md create mode 100644 docs/type_conversion_operations.md create mode 100644 include/sqlgen/dynamic/TimeUnit.hpp create mode 100644 include/sqlgen/transpilation/is_duration.hpp create mode 100644 include/sqlgen/transpilation/is_timestamp.hpp create mode 100644 include/sqlgen/transpilation/to_duration.hpp create mode 100644 tests/postgres/test_select_from_with_timestamps.cpp create mode 100644 tests/postgres/test_where_with_timestamps.cpp create mode 100644 tests/sqlite/test_select_from_with_timestamps.cpp create mode 100644 tests/sqlite/test_where_with_timestamps.cpp diff --git a/docs/README.md b/docs/README.md index de41ca5..3f99bab 100644 --- a/docs/README.md +++ b/docs/README.md @@ -26,7 +26,14 @@ Welcome to the sqlgen documentation. This guide provides detailed information ab - [sqlgen::group_by and Aggregations](group_by_and_aggregations.md) - How generate GROUP BY queries and aggregate data - [sqlgen::insert](insert.md) - How to insert data within transactions - [sqlgen::update](update.md) - How to update data in a table -- [Other Operations and Functions](other_operations.md) - How to use SQL functions like `coalesce`, `concat`, `abs`, `cast`, and more + +## Other Operations + +- [Mathematical Operations](mathematical_operations.md) - How to use mathematical functions in queries (e.g., abs, ceil, floor, exp, trigonometric functions, round). +- [String Operations](string_operations.md) - How to manipulate and transform strings in queries (e.g., length, lower, upper, trim, replace, concat). +- [Type Conversion Operations](type_conversion_operations.md) - How to convert between types safely in queries (e.g., cast int to double). +- [Null Handling Operations](null_handling_operations.md) - How to handle nullable values and propagate nullability correctly (e.g., with coalesce and nullability rules). +- [Timestamp and Date/Time Functions](timestamp_operations.md) - How to work with timestamps, dates, and times (e.g., extract parts, perform arithmetic, convert formats). ## Data Types and Validation diff --git a/docs/mathematical_operations.md b/docs/mathematical_operations.md new file mode 100644 index 0000000..d0352be --- /dev/null +++ b/docs/mathematical_operations.md @@ -0,0 +1,72 @@ +# Mathematical Operations + +The `sqlgen` library provides a set of mathematical functions for use in queries. These functions are type-safe and map to the appropriate SQL operations for the target database. + +## Motivating Example +Suppose you want to analyze the ages of children in a family, grouped by their last name, and you want to compute the average, minimum, and maximum age (with some mathematical operations applied): + +```cpp +struct Children { + std::string last_name; + double avg_age; + double max_age_plus_one; + double min_age_plus_one; +}; + +const auto get_children = + select_from( + "last_name"_c, + round(avg(cast("age"_c))).as<"avg_age">(), + max(cast("age"_c) + 1.0).as<"max_age_plus_one">(), + (min(cast("age"_c)) + 1.0).as<"min_age_plus_one">() + ) + | where("age"_c < 18) + | group_by("last_name"_c) + | to>; +``` + +This query groups people by last name, filters for those under 18, and computes the average age (rounded), the maximum age plus one, and the minimum age plus one. + +## Mathematical Functions + +### `abs` +Returns the absolute value of a numeric expression. + +```cpp +abs("age"_c * (-1)) | as<"abs_age"> +``` + +### `ceil` / `floor` +Rounds a numeric value up (`ceil`) or down (`floor`) to the nearest integer. + +```cpp +ceil("salary"_c) | as<"salary_ceiled"> +floor("salary"_c) | as<"salary_floored"> +``` + +### `exp`, `ln`, `log2`, `sqrt` +- `exp(x)`: Exponential function (e^x) +- `ln(x)`: Natural logarithm +- `log2(x)`: Base-2 logarithm +- `sqrt(x)`: Square root + +```cpp +round(exp(cast("age"_c)), 2) | as<"exp_age"> +round(sqrt(cast("age"_c)), 2) | as<"sqrt_age"> +``` + +### `sin`, `cos`, `tan` +Trigonometric functions. + +```cpp +sin("angle"_c) | as<"sin_angle"> +cos("angle"_c) | as<"cos_angle"> +tan("angle"_c) | as<"tan_angle"> +``` + +### `round` +Rounds a numeric value to a specified number of decimal places. + +```cpp +round("price"_c, 2) | as<"rounded_price"> +``` \ No newline at end of file diff --git a/docs/null_handling_operations.md b/docs/null_handling_operations.md new file mode 100644 index 0000000..1863c74 --- /dev/null +++ b/docs/null_handling_operations.md @@ -0,0 +1,81 @@ +# Null Handling Operations + +The `sqlgen` library provides functions for handling null values in a type-safe way. These functions allow you to work with nullable columns and propagate nullability correctly in your queries. + +## Null Handling + +### `coalesce` +Returns the first non-null value in the argument list. + +```cpp +coalesce("last_name"_c, "none") | as<"last_name_or_none"> +coalesce(upper("last_name"_c), "none") | as<"last_name_or_none"> +``` + +--- + +## Nullable Values + +When using these operations on nullable columns (e.g., `std::optional`), the result will also be nullable if any operand is nullable. For example, adding two `std::optional` columns will yield a `std::optional`. The `coalesce` function is especially useful for providing default values for nullable columns. + +--- + +## Nullability Propagation and `coalesce` Semantics + +### General Nullability Rules + +- **Unary operations** (e.g., `abs`, `upper`, `sqrt`): + - If the operand is nullable (`std::optional`), the result is also nullable. + - If the operand is not nullable, the result is not nullable. +- **Binary or ternary operations** (e.g., `+`, `concat`, `replace`, etc.): + - If *any* operand is nullable, the result is nullable (`std::optional`). + - If *all* operands are non-nullable, the result is non-nullable. +- **Type conversion (`cast`)**: + - If the source is nullable, the result is nullable of the target type. + - If the source is not nullable, the result is not nullable. +- **String operations** (e.g., `concat`, `replace`, `ltrim`, `rtrim`, `trim`): + - If any input is nullable, the result is nullable. + - All string operands must have the same underlying type (checked at compile time). + +### `coalesce` Nullability Semantics + +The `coalesce` function returns the first non-null value from its arguments. Its nullability is determined as follows: + +- If **all** arguments are nullable, the result is nullable (`std::optional`). +- If **any** argument is non-nullable, the result is non-nullable (`T`). +- All arguments must have the same underlying type (ignoring nullability), enforced at compile time. + +#### Examples + +```cpp +// All arguments nullable: result is nullable +coalesce(std::optional{}, std::optional{}) // -> std::optional + +// At least one argument non-nullable: result is non-nullable +coalesce(std::optional{}, 42) // -> int +coalesce(42, std::optional{}) // -> int + +// All arguments non-nullable: result is non-nullable +coalesce(1, 2) // -> int + +// Mixed string example +coalesce(std::optional{}, "default") // -> std::string + +// Compile-time error: mismatched types +// coalesce(std::optional{}, std::optional{}) // Error +``` + +#### Practical Usage + +```cpp +// Provide a default for a nullable column +coalesce("last_name"_c, "none") | as<"last_name_or_none"> // Result is std::string +coalesce("middle_name"_c, "nickname"_c) | as<"any_name"> +``` + +### Advanced: How sqlgen Enforces Nullability + +The nullability rules are enforced at compile time using template metaprogramming (see `underlying_t.hpp`). This ensures that: +- You cannot accidentally assign a nullable result to a non-nullable field. +- All arguments to `coalesce` must have the same base type (e.g., all `int` or all `std::string`). +- The result type of any operation is always correct and safe to use in your result structs. \ No newline at end of file diff --git a/docs/other_operations.md b/docs/other_operations.md deleted file mode 100644 index 5b993cb..0000000 --- a/docs/other_operations.md +++ /dev/null @@ -1,249 +0,0 @@ -# Other Operations and Functions - -The `sqlgen` library provides a rich set of SQL operations and functions that can be used in a type-safe and composable way within C++ queries. These operations cover mathematical, string, type conversion, and null-handling functions, and are designed to closely mirror SQL's expressive power. - -## Usage - -You can use these functions in your `select_from` queries, often in combination with column expressions, literals, and other operations. All functions are available in the `sqlgen` namespace. - ---- - -## Mathematical Functions - -### `abs` -Returns the absolute value of a numeric expression. - -```cpp -abs("age"_c * (-1)) | as<"abs_age"> -``` - -### `ceil` / `floor` -Rounds a numeric value up (`ceil`) or down (`floor`) to the nearest integer. - -```cpp -ceil("salary"_c) | as<"salary_ceiled"> -floor("salary"_c) | as<"salary_floored"> -``` - -### `exp`, `ln`, `log2`, `sqrt` -- `exp(x)`: Exponential function (e^x) -- `ln(x)`: Natural logarithm -- `log2(x)`: Base-2 logarithm -- `sqrt(x)`: Square root - -```cpp -round(exp(cast("age"_c)), 2) | as<"exp_age"> -round(sqrt(cast("age"_c)), 2) | as<"sqrt_age"> -``` - -### `sin`, `cos`, `tan` -Trigonometric functions. - -```cpp -sin("angle"_c) | as<"sin_angle"> -cos("angle"_c) | as<"cos_angle"> -tan("angle"_c) | as<"tan_angle"> -``` - -### `round` -Rounds a numeric value to a specified number of decimal places. - -```cpp -round("price"_c, 2) | as<"rounded_price"> -``` - ---- - -## String Functions - -### `length` -Returns the length of a string. - -```cpp -length(trim("first_name"_c)) | as<"length_first_name"> -``` - -### `lower` / `upper` -Converts a string to lowercase or uppercase. - -```cpp -lower("first_name"_c) | as<"first_name_lower"> -upper("first_name"_c) | as<"first_name_upper"> -``` - -### `ltrim`, `rtrim`, `trim` -Removes whitespace (or a specified character) from the left, right, or both sides of a string. - -```cpp -ltrim("first_name"_c) | as<"ltrimmed_name"> -rtrim("last_name"_c) | as<"rtrimmed_name"> -trim("nickname"_c) | as<"trimmed_nickname"> -// With custom characters: -ltrim("field"_c, "_ ") | as<"ltrimmed_field"> -``` - -### `replace` -Replaces all occurrences of a substring with another substring. - -```cpp -replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> -``` - -### `concat` -Concatenates multiple strings or expressions. - -```cpp -concat("first_name"_c, " ", "last_name"_c) | as<"full_name"> -concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name"> -``` - ---- - -## Type Conversion - -### `cast` -Casts a value to a different type (e.g., int to double). - -```cpp -cast("age"_c) | as<"age_as_double"> -``` - ---- - -## Null Handling - -### `coalesce` -Returns the first non-null value in the argument list. - -```cpp -coalesce("last_name"_c, "none") | as<"last_name_or_none"> -coalesce(upper("last_name"_c), "none") | as<"last_name_or_none"> -``` - ---- - -## Nullable Values - -When using these operations on nullable columns (e.g., `std::optional`), the result will also be nullable if any operand is nullable. For example, adding two `std::optional` columns will yield a `std::optional`. The `coalesce` function is especially useful for providing default values for nullable columns. - ---- - -## Nullability Propagation and `coalesce` Semantics - -### General Nullability Rules - -- **Unary operations** (e.g., `abs`, `upper`, `sqrt`): - - If the operand is nullable (`std::optional`), the result is also nullable. - - If the operand is not nullable, the result is not nullable. -- **Binary or ternary operations** (e.g., `+`, `concat`, `replace`, etc.): - - If *any* operand is nullable, the result is nullable (`std::optional`). - - If *all* operands are non-nullable, the result is non-nullable. -- **Type conversion (`cast`)**: - - If the source is nullable, the result is nullable of the target type. - - If the source is not nullable, the result is not nullable. -- **String operations** (e.g., `concat`, `replace`, `ltrim`, `rtrim`, `trim`): - - If any input is nullable, the result is nullable. - - All string operands must have the same underlying type (checked at compile time). - -### `coalesce` Nullability Semantics - -The `coalesce` function returns the first non-null value from its arguments. Its nullability is determined as follows: - -- If **all** arguments are nullable, the result is nullable (`std::optional`). -- If **any** argument is non-nullable, the result is non-nullable (`T`). -- All arguments must have the same underlying type (ignoring nullability), enforced at compile time. - -#### Examples - -```cpp -// All arguments nullable: result is nullable -coalesce(std::optional{}, std::optional{}) // -> std::optional - -// At least one argument non-nullable: result is non-nullable -coalesce(std::optional{}, 42) // -> int -coalesce(42, std::optional{}) // -> int - -// All arguments non-nullable: result is non-nullable -coalesce(1, 2) // -> int - -// Mixed string example -coalesce(std::optional{}, "default") // -> std::string - -// Compile-time error: mismatched types -// coalesce(std::optional{}, std::optional{}) // Error -``` - -#### Practical Usage - -```cpp -// Provide a default for a nullable column -coalesce("last_name"_c, "none") | as<"last_name_or_none"> // Result is std::string -coalesce("middle_name"_c, "nickname"_c) | as<"any_name"> -``` - -### Advanced: How sqlgen Enforces Nullability - -The nullability rules are enforced at compile time using template metaprogramming (see `underlying_t.hpp`). This ensures that: -- You cannot accidentally assign a nullable result to a non-nullable field. -- All arguments to `coalesce` must have the same base type (e.g., all `int` or all `std::string`). -- The result type of any operation is always correct and safe to use in your result structs. - ---- - -## Example: Combining Operations - -```cpp -struct Children { - int id_plus_age; - int age_times_2; - int id_plus_2_minus_age; - int abs_age; - double exp_age; - double sqrt_age; - size_t length_first_name; - std::string full_name; - std::string first_name_lower; - std::string first_name_upper; - std::string first_name_replaced; -}; - -const auto get_children = select_from( - ("id"_c + "age"_c) | as<"id_plus_age">, - ("age"_c * 2) | as<"age_times_2">, - abs("age"_c * (-1)) | as<"abs_age">, - round(exp(cast("age"_c)), 2) | as<"exp_age">, - round(sqrt(cast("age"_c)), 2) | as<"sqrt_age">, - length(trim("first_name"_c)) | as<"length_first_name">, - concat("first_name"_c, " ", "last_name"_c) | as<"full_name">, - lower("first_name"_c) | as<"first_name_lower">, - upper("first_name"_c, " ") | as<"first_name_upper">, - replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> -) | where("age"_c < 18) | to>; -``` - -This generates the following SQL: - -```sql -SELECT - ("id" + "age") AS "id_plus_age", - ("age" * 2) AS "age_times_2", - ABS(("age" * -1)) AS "abs_age", - ROUND(EXP(CAST("age" AS NUMERIC)), 2) AS "exp_age", - ROUND(SQRT(CAST("age" AS NUMERIC)), 2) AS "sqrt_age", - LENGTH(TRIM("first_name")) AS "length_first_name", - ("first_name" || ' ' || "last_name") AS "full_name", - LOWER("first_name") AS "first_name_lower", - UPPER("first_name") AS "first_name_upper", - REPLACE("first_name", 'Bart', 'Hugo') AS "first_name_replaced" -FROM "Person" -WHERE "age" < 18; -``` - ---- - -## Notes - -- All functions are type-safe and map to the appropriate SQL operations for the target database. -- You can chain and nest operations as needed. -- Use the `as<"alias">(...)` or `| as<"alias">` syntax to alias expressions for mapping to struct fields. - diff --git a/docs/string_operations.md b/docs/string_operations.md new file mode 100644 index 0000000..54c5f55 --- /dev/null +++ b/docs/string_operations.md @@ -0,0 +1,65 @@ +# String Operations + +The `sqlgen` library provides a set of string functions for use in queries. These functions are type-safe and map to the appropriate SQL operations for the target database. + +## Motivating Example +Suppose you want to create a report of people with their full names in uppercase, and also want to trim any whitespace from their last names: + +```cpp +struct PersonReport { + std::string full_name; + std::string last_name_trimmed; +}; + +const auto get_reports = + select_from( + concat(upper("last_name"_c), ", ", "first_name"_c).as<"full_name">(), + trim("last_name"_c).as<"last_name_trimmed">() + ) + | to>; +``` + +This query produces a list of people with their full names in the format "LASTNAME, Firstname" (with the last name in uppercase) and ensures the last name has no leading or trailing whitespace. + +## String Functions + +### `length` +Returns the length of a string. + +```cpp +length(trim("first_name"_c)) | as<"length_first_name"> +``` + +### `lower` / `upper` +Converts a string to lowercase or uppercase. + +```cpp +lower("first_name"_c) | as<"first_name_lower"> +upper("first_name"_c) | as<"first_name_upper"> +``` + +### `ltrim`, `rtrim`, `trim` +Removes whitespace (or a specified character) from the left, right, or both sides of a string. + +```cpp +ltrim("first_name"_c) | as<"ltrimmed_name"> +rtrim("last_name"_c) | as<"rtrimmed_name"> +trim("nickname"_c) | as<"trimmed_nickname"> +// With custom characters: +ltrim("field"_c, "_ ") | as<"ltrimmed_field"> +``` + +### `replace` +Replaces all occurrences of a substring with another substring. + +```cpp +replace("first_name"_c, "Bart", "Hugo") | as<"first_name_replaced"> +``` + +### `concat` +Concatenates multiple strings or expressions. + +```cpp +concat("first_name"_c, " ", "last_name"_c) | as<"full_name"> +concat(upper("last_name"_c), ", ", "first_name"_c) | as<"full_name"> +``` \ No newline at end of file diff --git a/docs/timestamp_operations.md b/docs/timestamp_operations.md new file mode 100644 index 0000000..f4b19a7 --- /dev/null +++ b/docs/timestamp_operations.md @@ -0,0 +1,127 @@ +# Timestamp and Date/Time Functions + +The `sqlgen` library provides a comprehensive set of operations for working with timestamps, dates, and times. These functions allow you to extract date/time parts, perform arithmetic, and convert between types in a type-safe and composable way. All timestamp operations work with the `sqlgen::Timestamp<...>` type (including the provided aliases `Date` and `DateTime`). + +### Timestamp Types + +- `Timestamp`: Represents a timestamp with a custom format (see `sqlgen::Timestamp`). +- `Date`: Alias for `Timestamp<"%Y-%m-%d">` (date only). +- `DateTime`: Alias for `Timestamp<"%Y-%m-%d %H:%M:%S">` (date and time). + +--- + +### Date/Time Extraction Functions + +These functions extract parts of a timestamp or date: + +- `year(ts)`: Extracts the year as an integer. +- `month(ts)`: Extracts the month (1-12). +- `day(ts)`: Extracts the day of the month (1-31). +- `hour(ts)`: Extracts the hour (0-23). +- `minute(ts)`: Extracts the minute (0-59). +- `second(ts)`: Extracts the second (0-59). +- `weekday(ts)`: Extracts the day of the week (0 = Sunday for SQLite, 0 = Monday for Postgres). + +```cpp +year("birthday"_c) | as<"year"> +month("birthday"_c) | as<"month"> +day("birthday"_c) | as<"day"> +hour("birthday"_c) | as<"hour"> +minute("birthday"_c) | as<"minute"> +second("birthday"_c) | as<"second"> +weekday("birthday"_c) | as<"weekday"> +``` + +--- + +### Timestamp Arithmetic + +You can add or subtract durations (e.g., days, years, weeks, milliseconds) to/from timestamp columns using standard C++ chrono types: + +```cpp +("birthday"_c + std::chrono::days(10)) | as<"birthday_plus_10"> +("birthday"_c + std::chrono::years(1) - std::chrono::weeks(2)) | as<"birthday_shifted"> +``` + +This is translated to the appropriate SQL for the backend (e.g., `datetime(..., '+10 days')` for SQLite, `+ INTERVAL '10 days'` for Postgres). + +--- + +### Days Between + +Calculates the number of days between two timestamps or dates: + +```cpp +days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days"> +``` + +--- + +### Unix Epoch Conversion + +Converts a timestamp to the number of seconds since the Unix epoch: + +```cpp +unixepoch("birthday"_c) | as<"birthday_unixepoch"> +unixepoch("birthday"_c + std::chrono::days(10)) | as<"birthday_unixepoch_plus_10"> +``` + +--- + +### Timestamp Construction and Conversion + +You can construct or cast timestamps from strings or from extracted parts: + +```cpp +cast(concat(cast(year("birthday"_c)), "-", cast(month("birthday"_c)), "-", cast(day("birthday"_c)))) | as<"birthday_recreated"> +``` + +--- + +### Example: Working with Timestamps + +```cpp +struct Birthday { + Date birthday; + Date birthday_recreated; + time_t birthday_unixepoch; + double age_in_days; + int hour; + int minute; + int second; + int weekday; +}; + +const auto get_birthdays = select_from( + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + (cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c))))) | as<"birthday_recreated">, + days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, + unixepoch("birthday"_c + std::chrono::days(10)) | as<"birthday_unixepoch">, + hour("birthday"_c) | as<"hour">, + minute("birthday"_c) | as<"minute">, + second("birthday"_c) | as<"second">, + weekday("birthday"_c) | as<"weekday"> +) | order_by("id"_c) | to>; +``` + +This generates SQL like: + +**SQLite:** +```sql +SELECT datetime("birthday", '+10 days') AS "birthday", ... +``` +**Postgres:** +```sql +SELECT "birthday" + INTERVAL '10 days' AS "birthday", ... +``` + +--- + +### Notes + +- All timestamp operations are type-safe and propagate nullability as described above. +- You can chain and nest timestamp operations with other operations. +- Duration arithmetic supports `std::chrono::days`, `std::chrono::years`, `std::chrono::weeks`, `std::chrono::milliseconds`, etc. +- The result types and SQL translation may differ slightly between SQLite and Postgres, but the C++ interface is unified. \ No newline at end of file diff --git a/docs/type_conversion_operations.md b/docs/type_conversion_operations.md new file mode 100644 index 0000000..0ea486d --- /dev/null +++ b/docs/type_conversion_operations.md @@ -0,0 +1,25 @@ +# Type Conversion + +Casts a value to a different type (e.g., int to double). + +## Motivating Example +Suppose you want to store birthdays as strings in your database, but you need to convert them to date types for calculations, or you want to cast an integer column to double for mathematical operations: + +```cpp +struct BirthdayInfo { + Date birthday; + double age_in_days; +}; + +const auto get_birthdays = + select_from( + cast("birthday"_c).as<"birthday">(), + days_between("birthday"_c, Date("2011-01-01")).as<"age_in_days">() + ) + | to>; +``` + +This query casts the `birthday` column to a `Date` type and calculates the number of days between each birthday and a reference date. + +```cpp +cast("age"_c) | as<"age_as_double"> diff --git a/include/sqlgen/Timestamp.hpp b/include/sqlgen/Timestamp.hpp index e21ed0d..e44be83 100644 --- a/include/sqlgen/Timestamp.hpp +++ b/include/sqlgen/Timestamp.hpp @@ -8,6 +8,9 @@ namespace sqlgen { template using Timestamp = rfl::Timestamp<_format>; +using Date = Timestamp<"%Y-%m-%d">; +using DateTime = Timestamp<"%Y-%m-%d %H:%M:%S">; + }; // namespace sqlgen #endif diff --git a/include/sqlgen/col.hpp b/include/sqlgen/col.hpp index 5be2e4c..e2ad471 100644 --- a/include/sqlgen/col.hpp +++ b/include/sqlgen/col.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_COL_HPP_ #define SQLGEN_COL_HPP_ +#include #include #include @@ -13,6 +14,7 @@ #include "transpilation/Set.hpp" #include "transpilation/Value.hpp" #include "transpilation/conditions.hpp" +#include "transpilation/is_duration.hpp" #include "transpilation/to_transpilation_type.hpp" namespace sqlgen { @@ -135,14 +137,20 @@ struct Col { } template - friend auto operator-(const Col&, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; + friend auto operator-(const Col& _op1, const T& _op2) noexcept { + if constexpr (transpilation::is_duration_v) { + using DurationType = std::remove_cvref_t; + return _op1 + DurationType(_op2.count() * (-1)); - return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, - .operand2 = transpilation::to_transpilation_type(_op2)}; + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return transpilation::Operation, OtherType>{ + .operand1 = transpilation::Col<_name>{}, + .operand2 = transpilation::to_transpilation_type(_op2)}; + } } template @@ -169,13 +177,23 @@ struct Col { template friend auto operator+(const Col&, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; + if constexpr (transpilation::is_duration_v) { + using DurationType = std::remove_cvref_t; + return transpilation::Operation< + transpilation::Operator::date_plus_duration, + transpilation::Col<_name>, rfl::Tuple>{ + .operand1 = transpilation::Col<_name>{}, + .operand2 = rfl::Tuple(_op2)}; - return transpilation::Operation, OtherType>{ - .operand1 = transpilation::Col<_name>{}, - .operand2 = transpilation::to_transpilation_type(_op2)}; + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return transpilation::Operation, OtherType>{ + .operand1 = transpilation::Col<_name>{}, + .operand2 = transpilation::to_transpilation_type(_op2)}; + } } }; diff --git a/include/sqlgen/dynamic/Operation.hpp b/include/sqlgen/dynamic/Operation.hpp index 7360521..281f270 100644 --- a/include/sqlgen/dynamic/Operation.hpp +++ b/include/sqlgen/dynamic/Operation.hpp @@ -8,6 +8,7 @@ #include "../Ref.hpp" #include "Column.hpp" +#include "ColumnOrValue.hpp" #include "Type.hpp" #include "Value.hpp" @@ -68,6 +69,20 @@ struct Operation { Ref op1; }; + struct DatePlusDuration { + Ref date; + std::vector durations; + }; + + struct Day { + Ref op1; + }; + + struct DaysBetween { + Ref op1; + Ref op2; + }; + struct Divides { Ref op1; Ref op2; @@ -81,6 +96,10 @@ struct Operation { Ref op1; }; + struct Hour { + Ref op1; + }; + struct Length { Ref op1; }; @@ -107,11 +126,19 @@ struct Operation { Ref op2; }; + struct Minute { + Ref op1; + }; + struct Mod { Ref op1; Ref op2; }; + struct Month { + Ref op1; + }; + struct Multiplies { Ref op1; Ref op2; @@ -138,6 +165,10 @@ struct Operation { Ref op2; }; + struct Second { + Ref op1; + }; + struct Sin { Ref op1; }; @@ -155,15 +186,29 @@ struct Operation { Ref op2; }; + struct Unixepoch { + Ref op1; + }; + struct Upper { Ref op1; }; + struct Weekday { + Ref op1; + }; + + struct Year { + Ref op1; + }; + using ReflectionType = rfl::TaggedUnion<"what", Abs, Aggregation, Cast, Ceil, Column, Coalesce, - Concat, Cos, Divides, Exp, Floor, Length, Ln, Log2, - Lower, LTrim, Minus, Mod, Multiplies, Plus, Replace, - Round, RTrim, Sin, Sqrt, Tan, Trim, Upper, Value>; + Concat, Cos, DatePlusDuration, Day, DaysBetween, Divides, + Exp, Floor, Hour, Length, Ln, Log2, Lower, LTrim, Month, + Minus, Minute, Mod, Multiplies, Plus, Replace, Round, + RTrim, Second, Sin, Sqrt, Tan, Trim, Unixepoch, Upper, + Value, Weekday, Year>; const ReflectionType& reflection() const { return val; } diff --git a/include/sqlgen/dynamic/TimeUnit.hpp b/include/sqlgen/dynamic/TimeUnit.hpp new file mode 100644 index 0000000..ecaa571 --- /dev/null +++ b/include/sqlgen/dynamic/TimeUnit.hpp @@ -0,0 +1,19 @@ +#ifndef SQLGEN_DYNAMIC_TIMEUNIT_HPP_ +#define SQLGEN_DYNAMIC_TIMEUNIT_HPP_ + +namespace sqlgen::dynamic { + +enum class TimeUnit { + milliseconds, + seconds, + minutes, + hours, + days, + weeks, + months, + years +}; + +} + +#endif diff --git a/include/sqlgen/dynamic/Value.hpp b/include/sqlgen/dynamic/Value.hpp index c6b3259..f5cc9c0 100644 --- a/include/sqlgen/dynamic/Value.hpp +++ b/include/sqlgen/dynamic/Value.hpp @@ -4,8 +4,15 @@ #include #include +#include "TimeUnit.hpp" + namespace sqlgen::dynamic { +struct Duration { + TimeUnit unit; + int64_t val; +}; + struct Float { double val; }; @@ -18,8 +25,13 @@ struct String { std::string val; }; +struct Timestamp { + int64_t seconds_since_unix; +}; + struct Value { - using ReflectionType = rfl::TaggedUnion<"type", Float, Integer, String>; + using ReflectionType = + rfl::TaggedUnion<"type", Duration, Float, Integer, String, Timestamp>; const auto& reflection() const { return val; } ReflectionType val; }; diff --git a/include/sqlgen/operations.hpp b/include/sqlgen/operations.hpp index ca8c01a..c4081ed 100644 --- a/include/sqlgen/operations.hpp +++ b/include/sqlgen/operations.hpp @@ -65,6 +65,26 @@ auto cos(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto day(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto days_between(const T& _t, const U& _u) { + using Type1 = + typename transpilation::ToTranspilationType>::Type; + using Type2 = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t), + .operand2 = transpilation::to_transpilation_type(_u)}; +} + template auto exp(const T& _t) { using Type = @@ -81,6 +101,14 @@ auto floor(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto hour(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto length(const T& _t) { using Type = @@ -129,6 +157,22 @@ auto ltrim(const T& _t) { return ltrim(_t, std::string(" ")); } +template +auto minute(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto month(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto replace(const StringType& _str, const FromType& _from, const ToType& _to) { using Type1 = typename transpilation::ToTranspilationType< @@ -176,6 +220,14 @@ auto rtrim(const T& _t) { return rtrim(_t, std::string(" ")); } +template +auto second(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto sin(const T& _t) { using Type = @@ -216,6 +268,14 @@ auto trim(const T& _t) { return trim(_t, std::string(" ")); } +template +auto unixepoch(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + template auto upper(const T& _t) { using Type = @@ -224,6 +284,22 @@ auto upper(const T& _t) { .operand1 = transpilation::to_transpilation_type(_t)}; } +template +auto weekday(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + +template +auto year(const T& _t) { + using Type = + typename transpilation::ToTranspilationType>::Type; + return transpilation::Operation{ + .operand1 = transpilation::to_transpilation_type(_t)}; +} + } // namespace sqlgen #endif diff --git a/include/sqlgen/transpilation/Operation.hpp b/include/sqlgen/transpilation/Operation.hpp index 887b9e9..46c5145 100644 --- a/include/sqlgen/transpilation/Operation.hpp +++ b/include/sqlgen/transpilation/Operation.hpp @@ -9,6 +9,8 @@ #include "Condition.hpp" #include "Operator.hpp" #include "conditions.hpp" +#include "is_duration.hpp" +#include "to_duration.hpp" #include "to_transpilation_type.hpp" namespace sqlgen::transpilation { @@ -95,19 +97,23 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } template friend auto operator-(const Operation& _op1, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; + if constexpr (is_duration_v) { + using DurationType = std::remove_cvref_t; + return _op1 + DurationType(_op2.count() * (-1)); - return Operation, OtherType>{ - .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } } template @@ -115,8 +121,7 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } @@ -125,19 +130,36 @@ struct Operation { using OtherType = typename transpilation::ToTranspilationType< std::remove_cvref_t>::Type; - return Operation, OtherType>{ + return Operation{ .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; } template friend auto operator+(const Operation& _op1, const T& _op2) noexcept { - using OtherType = typename transpilation::ToTranspilationType< - std::remove_cvref_t>::Type; + if constexpr (is_duration_v) { + if constexpr (Operation::op == Operator::date_plus_duration) { + using DurationType = std::remove_cvref_t; + const auto op2 = + rfl::tuple_cat(_op1.operand2, rfl::Tuple(_op2)); + using Op2Type = std::remove_cvref_t; + return transpilation::Operation< + transpilation::Operator::date_plus_duration, Operand1Type, Op2Type>{ + .operand1 = _op1.operand1, .operand2 = op2}; - return Operation, OtherType>{ - .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } else { + using DurationType = std::remove_cvref_t; + return Operation>{ + .operand1 = _op1, .operand2 = rfl::Tuple(_op2)}; + } + + } else { + using OtherType = typename transpilation::ToTranspilationType< + std::remove_cvref_t>::Type; + + return Operation{ + .operand1 = _op1, .operand2 = to_transpilation_type(_op2)}; + } } }; diff --git a/include/sqlgen/transpilation/Operator.hpp b/include/sqlgen/transpilation/Operator.hpp index b6a037e..9bcc6d9 100644 --- a/include/sqlgen/transpilation/Operator.hpp +++ b/include/sqlgen/transpilation/Operator.hpp @@ -10,26 +10,36 @@ enum class Operator { coalesce, concat, cos, + date_plus_duration, + day, + days_between, divides, exp, floor, + hour, length, ln, log2, lower, ltrim, minus, + minute, mod, + month, multiplies, plus, replace, round, rtrim, + second, sin, sqrt, tan, trim, - upper + unixepoch, + upper, + weekday, + year }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/OperatorCategory.hpp b/include/sqlgen/transpilation/OperatorCategory.hpp index 3092d82..a3f1af4 100644 --- a/include/sqlgen/transpilation/OperatorCategory.hpp +++ b/include/sqlgen/transpilation/OperatorCategory.hpp @@ -3,7 +3,7 @@ namespace sqlgen::transpilation { -enum class OperatorCategory { numerical, string, other }; +enum class OperatorCategory { date_part, numerical, string, other }; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/dynamic_operator_t.hpp b/include/sqlgen/transpilation/dynamic_operator_t.hpp index 95c696e..8b4f763 100644 --- a/include/sqlgen/transpilation/dynamic_operator_t.hpp +++ b/include/sqlgen/transpilation/dynamic_operator_t.hpp @@ -54,6 +54,27 @@ struct DynamicOperator { using Type = dynamic::Operation::Cos; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = std::numeric_limits::max(); + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::DatePlusDuration; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Day; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 2; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::DaysBetween; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -75,6 +96,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Floor; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Hour; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -117,6 +145,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Minus; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Minute; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -124,6 +159,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Mod; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Month; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 2; @@ -159,6 +201,13 @@ struct DynamicOperator { using Type = dynamic::Operation::RTrim; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Second; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -187,6 +236,13 @@ struct DynamicOperator { using Type = dynamic::Operation::Trim; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::other; + using Type = dynamic::Operation::Unixepoch; +}; + template <> struct DynamicOperator { static constexpr size_t num_operands = 1; @@ -194,6 +250,20 @@ struct DynamicOperator { using Type = dynamic::Operation::Upper; }; +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Weekday; +}; + +template <> +struct DynamicOperator { + static constexpr size_t num_operands = 1; + static constexpr auto category = OperatorCategory::date_part; + using Type = dynamic::Operation::Year; +}; + template using dynamic_operator_t = typename DynamicOperator::Type; diff --git a/include/sqlgen/transpilation/is_duration.hpp b/include/sqlgen/transpilation/is_duration.hpp new file mode 100644 index 0000000..0837e4d --- /dev/null +++ b/include/sqlgen/transpilation/is_duration.hpp @@ -0,0 +1,23 @@ +#ifndef SQLGEN_TRANSPILATION_IS_DURATION_HPP_ +#define SQLGEN_TRANSPILATION_IS_DURATION_HPP_ + +#include + +namespace sqlgen::transpilation { + +template +class is_duration; + +template +class is_duration : public std::false_type {}; + +template +class is_duration> : public std::true_type { +}; + +template +constexpr bool is_duration_v = is_duration>(); + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/is_timestamp.hpp b/include/sqlgen/transpilation/is_timestamp.hpp new file mode 100644 index 0000000..0112092 --- /dev/null +++ b/include/sqlgen/transpilation/is_timestamp.hpp @@ -0,0 +1,25 @@ +#ifndef SQLGEN_TRANSPILATION_IS_TIMESTAMP_HPP_ +#define SQLGEN_TRANSPILATION_IS_TIMESTAMP_HPP_ + +#include +#include + +#include "../PrimaryKey.hpp" + +namespace sqlgen::transpilation { + +template +class is_timestamp; + +template +class is_timestamp : public std::false_type {}; + +template +class is_timestamp> : public std::true_type {}; + +template +constexpr bool is_timestamp_v = is_timestamp>::value; + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/make_field.hpp b/include/sqlgen/transpilation/make_field.hpp index c1bef43..a7d5dbe 100644 --- a/include/sqlgen/transpilation/make_field.hpp +++ b/include/sqlgen/transpilation/make_field.hpp @@ -1,6 +1,7 @@ #ifndef SQLGEN_TRANSPILATION_MAKE_FIELD_HPP_ #define SQLGEN_TRANSPILATION_MAKE_FIELD_HPP_ +#include #include #include @@ -19,8 +20,10 @@ #include "all_columns_exist.hpp" #include "dynamic_aggregation_t.hpp" #include "dynamic_operator_t.hpp" +#include "is_timestamp.hpp" #include "remove_as_t.hpp" #include "remove_nullable_t.hpp" +#include "to_duration.hpp" #include "to_value.hpp" #include "underlying_t.hpp" @@ -220,37 +223,89 @@ struct MakeField - requires((_op == Operator::coalesce) || (_op == Operator::concat)) -struct MakeField>> { +template +struct MakeField>> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + static constexpr bool is_operation = true; + + using Name = Nothing; + using Type = underlying_t>; + using Operands = rfl::Tuple; + + static_assert(is_timestamp_v>, + "Must be a timestamp."); + + dynamic::SelectFrom::Field operator()(const auto& _o) const { + return dynamic::SelectFrom::Field{ + dynamic::Operation{dynamic::Operation::DatePlusDuration{ + .date = Ref::make( + MakeField>{}( + _o.operand1) + .val), + .durations = rfl::apply( + [](const auto&... _ops) { + return std::vector({to_duration(_ops)...}); + }, + _o.operand2)}}}; + } +}; + +template + requires((num_operands_v<_op>) == 1) +struct MakeField> { + static constexpr bool is_aggregation = false; + static constexpr bool is_column = false; + static constexpr bool is_operation = true; + + using Name = Nothing; + using Type = underlying_t>; + using Operands = rfl::Tuple; + + dynamic::SelectFrom::Field operator()(const auto& _o) const { + using DynamicOperatorType = dynamic_operator_t<_op>; + return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ + .op1 = Ref::make( + MakeField>{}( + _o.operand1) + .val)}}}; + } +}; + +template + requires((num_operands_v<_op>) == 2) +struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; using Name = Nothing; using Type = - underlying_t>>; - using Operands = rfl::Tuple; + underlying_t>; + using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .ops = rfl::apply( - [](const auto&... _ops) { - return std::vector>( - {Ref::make( - MakeField>{}(_ops) - .val)...}); - }, - _o.operand1)}}}; + .op1 = Ref::make( + MakeField>{}( + _o.operand1) + .val), + .op2 = Ref::make( + MakeField>{}( + _o.operand2) + .val)}}}; } }; -template -struct MakeField> { +template + requires((num_operands_v<_op>) == 3) +struct MakeField> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; @@ -279,130 +334,30 @@ struct MakeField -struct MakeField> { +template + requires((num_operands_v<_op>) == std::numeric_limits::max()) +struct MakeField>> { static constexpr bool is_aggregation = false; static constexpr bool is_column = false; static constexpr bool is_operation = true; using Name = Nothing; using Type = - underlying_t>; - using Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - return dynamic::SelectFrom::Field{ - dynamic::Operation{dynamic::Operation::Round{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val)}}}; - } -}; - -template - requires((num_operands_v<_op>) == 1 && - (operator_category_v<_op>) == OperatorCategory::string) -struct MakeField> { - static constexpr bool is_aggregation = false; - static constexpr bool is_column = false; - static constexpr bool is_operation = true; - - using Name = Nothing; - using Type = underlying_t>; - using Operands = rfl::Tuple; + underlying_t>>; + using Operands = rfl::Tuple; dynamic::SelectFrom::Field operator()(const auto& _o) const { using DynamicOperatorType = dynamic_operator_t<_op>; return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val)}}}; - } -}; - -template - requires((num_operands_v<_op>) == 2 && - (operator_category_v<_op>) == OperatorCategory::string) -struct MakeField> { - static constexpr bool is_aggregation = false; - static constexpr bool is_column = false; - static constexpr bool is_operation = true; - - using Name = Nothing; - using Type = - underlying_t>; - using Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - using DynamicOperatorType = dynamic_operator_t<_op>; - return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val)}}}; - } -}; - -template - requires((num_operands_v<_op>) == 1 && - (operator_category_v<_op>) == OperatorCategory::numerical) -struct MakeField> { - static constexpr bool is_aggregation = false; - static constexpr bool is_column = false; - static constexpr bool is_operation = true; - - using Name = Nothing; - using Type = underlying_t>; - using Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - using DynamicOperatorType = dynamic_operator_t<_op>; - return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val)}}}; - } -}; - -template - requires((num_operands_v<_op>) == 2 && - (operator_category_v<_op>) == OperatorCategory::numerical) -struct MakeField> { - static constexpr bool is_aggregation = false; - static constexpr bool is_column = false; - static constexpr bool is_operation = true; - - using Name = Nothing; - using Type = - underlying_t>; - using Operands = rfl::Tuple; - - dynamic::SelectFrom::Field operator()(const auto& _o) const { - using DynamicOperatorType = dynamic_operator_t<_op>; - return dynamic::SelectFrom::Field{dynamic::Operation{DynamicOperatorType{ - .op1 = Ref::make( - MakeField>{}( - _o.operand1) - .val), - .op2 = Ref::make( - MakeField>{}( - _o.operand2) - .val)}}}; + .ops = rfl::apply( + [](const auto&... _ops) { + return std::vector>( + {Ref::make( + MakeField>{}(_ops) + .val)...}); + }, + _o.operand1)}}}; } }; diff --git a/include/sqlgen/transpilation/remove_reflection_t.hpp b/include/sqlgen/transpilation/remove_reflection_t.hpp index 1c37da5..7fcb585 100644 --- a/include/sqlgen/transpilation/remove_reflection_t.hpp +++ b/include/sqlgen/transpilation/remove_reflection_t.hpp @@ -13,15 +13,20 @@ struct RemoveReflection { using Type = T; }; -template - requires has_reflection_method> -struct RemoveReflection { - using Type = typename RemoveReflection< - typename std::remove_cvref_t::ReflectionType>::Type; +template +struct RemoveReflection> { + using Type = rfl::Timestamp<_format>; }; template -using remove_reflection_t = typename RemoveReflection::Type; + requires has_reflection_method +struct RemoveReflection { + using Type = typename RemoveReflection::Type; +}; + +template +using remove_reflection_t = + typename RemoveReflection>::Type; } // namespace sqlgen::transpilation diff --git a/include/sqlgen/transpilation/to_condition.hpp b/include/sqlgen/transpilation/to_condition.hpp index 113b77c..3b6ed7a 100644 --- a/include/sqlgen/transpilation/to_condition.hpp +++ b/include/sqlgen/transpilation/to_condition.hpp @@ -12,6 +12,7 @@ #include "Condition.hpp" #include "all_columns_exist.hpp" #include "conditions.hpp" +#include "is_timestamp.hpp" #include "make_field.hpp" #include "to_transpilation_type.hpp" #include "underlying_t.hpp" @@ -43,8 +44,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::equality_comparable_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::equality_comparable_with || + (is_timestamp_v && + is_timestamp_v), "Must be equality comparable."); dynamic::Condition operator()(const auto& _cond) const { @@ -56,8 +61,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -69,8 +78,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -82,8 +95,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -95,8 +112,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::totally_ordered_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::totally_ordered_with || + (is_timestamp_v && + is_timestamp_v), "Must be totally ordered."); dynamic::Condition operator()(const auto& _cond) const { @@ -148,8 +169,12 @@ struct ToCondition> { template struct ToCondition> { - static_assert(std::equality_comparable_with, - underlying_t>, + using Underlying1 = underlying_t; + using Underlying2 = underlying_t; + + static_assert(std::equality_comparable_with || + (is_timestamp_v && + is_timestamp_v), "Must be equality comparable."); dynamic::Condition operator()(const auto& _cond) const { diff --git a/include/sqlgen/transpilation/to_duration.hpp b/include/sqlgen/transpilation/to_duration.hpp new file mode 100644 index 0000000..631f758 --- /dev/null +++ b/include/sqlgen/transpilation/to_duration.hpp @@ -0,0 +1,61 @@ +#ifndef SQLGEN_TRANSPILATION_TO_DURATION_HPP_ +#define SQLGEN_TRANSPILATION_TO_DURATION_HPP_ + +#include +#include +#include + +#include "../dynamic/TimeUnit.hpp" +#include "../dynamic/Value.hpp" + +namespace sqlgen::transpilation { + +template +struct ToDuration { + dynamic::Duration operator()(const DurationType& _t) { + if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::milliseconds, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::seconds, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::minutes, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::hours, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::days, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::weeks, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::months, + .val = static_cast(_t.count())}; + + } else if constexpr (std::is_same_v) { + return dynamic::Duration{.unit = dynamic::TimeUnit::years, + .val = static_cast(_t.count())}; + + } else { + static_assert(rfl::always_false_v, "Unsupported type."); + } + } +}; + +template +auto to_duration(const T& _t) { + return ToDuration>{}(_t); +} + +} // namespace sqlgen::transpilation + +#endif diff --git a/include/sqlgen/transpilation/underlying_t.hpp b/include/sqlgen/transpilation/underlying_t.hpp index 1afa3ff..213c3e9 100644 --- a/include/sqlgen/transpilation/underlying_t.hpp +++ b/include/sqlgen/transpilation/underlying_t.hpp @@ -14,6 +14,7 @@ #include "all_columns_exist.hpp" #include "dynamic_operator_t.hpp" #include "is_nullable.hpp" +#include "is_timestamp.hpp" #include "remove_nullable_t.hpp" #include "remove_reflection_t.hpp" @@ -81,6 +82,35 @@ struct Underlying>> { std::optional, std::string>; }; +template +struct Underlying>> { + using Underlying1 = typename Underlying::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = std::conditional_t, + std::optional>, + Underlying1>; +}; + +template +struct Underlying< + T, Operation> { + using Underlying1 = typename Underlying::Type; + using Underlying2 = typename Underlying::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = std::conditional_t || + is_nullable_v, + std::optional, double>; +}; + template struct Underlying< T, Operation> { @@ -119,6 +149,31 @@ struct Underlying> { using Type = Underlying1; }; +template +struct Underlying> { + using Underlying1 = typename Underlying::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = std::conditional_t, + std::optional, time_t>; +}; + +template + requires((num_operands_v<_op>) == 1 && + (operator_category_v<_op>) == OperatorCategory::date_part) +struct Underlying> { + using Underlying1 = + typename Underlying>::Type; + + static_assert(is_timestamp_v>, + "Must be a timestamp"); + + using Type = + std::conditional_t, std::optional, int>; +}; + template requires((num_operands_v<_op>) == 1 && (operator_category_v<_op>) == OperatorCategory::string) @@ -196,7 +251,7 @@ struct Underlying> { template struct Underlying> { - using Type = _Type; + using Type = remove_reflection_t<_Type>; }; template diff --git a/src/sqlgen/postgres/to_sql.cpp b/src/sqlgen/postgres/to_sql.cpp index 7cf790d..156634e 100644 --- a/src/sqlgen/postgres/to_sql.cpp +++ b/src/sqlgen/postgres/to_sql.cpp @@ -99,6 +99,14 @@ std::string column_or_value_to_sql( using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { return "'" + escape_single_quote(_v.val) + "'"; + + } else if constexpr (std::is_same_v) { + return "INTERVAL '" + std::to_string(_v.val) + " " + + rfl::enum_to_string(_v.unit) + "'"; + + } else if constexpr (std::is_same_v) { + return "to_timestamp(" + std::to_string(_v.seconds_since_unix) + ")"; + } else { return std::to_string(_v.val); } @@ -402,6 +410,24 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << operation_to_sql(*_s.date) << " + " + << internal::strings::join( + " + ", + internal::collect::vector( + _s.durations | transform([](const auto& _d) { + return column_or_value_to_sql(dynamic::Value{_d}); + }))); + + } else if constexpr (std::is_same_v) { + stream << "extract(DAY from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "cast(" << operation_to_sql(*_s.op2) << " as DATE) - cast(" + << operation_to_sql(*_s.op1) << " as DATE)"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") / (" << operation_to_sql(*_s.op2) << ")"; @@ -412,6 +438,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "floor(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(HOUR from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "length(" << operation_to_sql(*_s.op1) << ")"; @@ -432,10 +461,16 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "(" << operation_to_sql(*_s.op1) << ") - (" << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(MINUTE from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "mod(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(MONTH from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") * (" << operation_to_sql(*_s.op2) << ")"; @@ -457,6 +492,9 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(SECOND from " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "sin(" << operation_to_sql(*_s.op1) << ")"; @@ -470,12 +508,21 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "trim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "extract(EPOCH FROM " << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { stream << "upper(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { stream << column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + stream << "extract(DOW from " << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "extract(YEAR from " << operation_to_sql(*_s.op1) << ")"; + } else { static_assert(rfl::always_false_v, "Unsupported type."); } diff --git a/src/sqlgen/sqlite/to_sql.cpp b/src/sqlgen/sqlite/to_sql.cpp index befdf72..3a76fca 100644 --- a/src/sqlgen/sqlite/to_sql.cpp +++ b/src/sqlgen/sqlite/to_sql.cpp @@ -77,12 +77,48 @@ std::string aggregation_to_sql( }); } +std::string pad_with_zeros(const std::string& _str, + const size_t _expected_length) { + if (_str.size() < _expected_length) { + return std::string(_expected_length - _str.size(), '0') + _str; + } else { + return _str; + } +} + std::string column_or_value_to_sql( const dynamic::ColumnOrValue& _col) noexcept { const auto handle_value = [](const auto& _v) -> std::string { using Type = std::remove_cvref_t; if constexpr (std::is_same_v) { return "'" + escape_single_quote(_v.val) + "'"; + + } else if constexpr (std::is_same_v) { + const auto prefix = std::string("'") + (_v.val >= 0 ? "+" : "-"); + const auto val = std::abs(_v.val); + switch (_v.unit) { + case dynamic::TimeUnit::milliseconds: { + const auto h = (val / 3600000); + const auto m = (val / 60000) % 60; + const auto s = (val / 1000) % 60; + const auto ms = val % 1000; + return prefix + pad_with_zeros(std::to_string(h), 2) + ":" + + pad_with_zeros(std::to_string(m), 2) + ":" + + pad_with_zeros(std::to_string(s), 2) + "." + + pad_with_zeros(std::to_string(ms), 3) + "'"; + } + + case dynamic::TimeUnit::weeks: + return prefix + std::to_string(val * 7) + " days'"; + + default: + return prefix + std::to_string(val) + " " + + rfl::enum_to_string(_v.unit) + "'"; + } + + } else if constexpr (std::is_same_v) { + return std::to_string(_v.seconds_since_unix); + } else { return std::to_string(_v.val); } @@ -368,6 +404,26 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "cos(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%d', " << operation_to_sql(*_s.op1) + << ") as INT)"; + + } else if constexpr (std::is_same_v) { + stream << "julianday(" << operation_to_sql(*_s.op2) << ") - julianday(" + << operation_to_sql(*_s.op1) << ")"; + + } else if constexpr (std::is_same_v) { + stream << "datetime(" << operation_to_sql(*_s.date) << ", " + << internal::strings::join( + ", ", + internal::collect::vector( + _s.durations | transform([](const auto& _d) { + return column_or_value_to_sql(dynamic::Value{_d}); + }))) + << ")"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") / (" << operation_to_sql(*_s.op2) << ")"; @@ -378,6 +434,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { } else if constexpr (std::is_same_v) { stream << "floor(" << operation_to_sql(*_s.op1) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%H', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "length(" << operation_to_sql(*_s.op1) << ")"; @@ -398,10 +458,18 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "(" << operation_to_sql(*_s.op1) << ") - (" << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%M', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "mod(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%m', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "(" << operation_to_sql(*_s.op1) << ") * (" << operation_to_sql(*_s.op2) << ")"; @@ -423,6 +491,10 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "rtrim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%S', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else if constexpr (std::is_same_v) { stream << "sin(" << operation_to_sql(*_s.op1) << ")"; @@ -436,12 +508,23 @@ std::string operation_to_sql(const dynamic::Operation& _stmt) noexcept { stream << "trim(" << operation_to_sql(*_s.op1) << ", " << operation_to_sql(*_s.op2) << ")"; + } else if constexpr (std::is_same_v) { + stream << "unixepoch(" << operation_to_sql(*_s.op1) << ", 'subsec')"; + } else if constexpr (std::is_same_v) { stream << "upper(" << operation_to_sql(*_s.op1) << ")"; } else if constexpr (std::is_same_v) { stream << column_or_value_to_sql(_s); + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%w', " << operation_to_sql(*_s.op1) + << ") as INT)"; + + } else if constexpr (std::is_same_v) { + stream << "cast(strftime('%Y', " << operation_to_sql(*_s.op1) + << ") as INT)"; + } else { static_assert(rfl::always_false_v, "Unsupported type."); } diff --git a/tests/postgres/test_select_from_with_timestamps.cpp b/tests/postgres/test_select_from_with_timestamps.cpp new file mode 100644 index 0000000..c0e168c --- /dev/null +++ b/tests/postgres/test_select_from_with_timestamps.cpp @@ -0,0 +1,89 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Timestamp<"%Y-%m-%d"> birthday; +}; + +TEST(postgres, test_range_select_from_with_timestamps) { + const auto people1 = std::vector( + {Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Timestamp<"%Y-%m-%d">("2010-01-01")}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + + struct Birthday { + Date birthday; + Date birthday_recreated; + time_t birthday_unixepoch; + double age_in_days; + int hour; + int minute; + int second; + int weekday; + }; + + const auto get_birthdays = + select_from( + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + ((cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c))))) + + std::chrono::days(10)) | + as<"birthday_recreated">, + days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, + unixepoch("birthday"_c + std::chrono::days(10)) | + as<"birthday_unixepoch">, + hour("birthday"_c) | as<"hour">, minute("birthday"_c) | as<"minute">, + second("birthday"_c) | as<"second">, + weekday("birthday"_c) | as<"weekday">) | + order_by("id"_c) | to>; + + const auto birthdays = postgres::connect(credentials) + .and_then(drop | if_exists) + .and_then(write(std::ref(people1))) + .and_then(get_birthdays) + .value(); + + const std::string expected_query = + R"(SELECT "birthday" + INTERVAL '10 days' AS "birthday", cast((cast(extract(YEAR from "birthday") as TEXT) || '-' || cast(extract(MONTH from "birthday") as TEXT) || '-' || cast(extract(DAY from "birthday") as TEXT)) as TIMESTAMP) + INTERVAL '10 days' AS "birthday_recreated", cast('2011-01-01' as DATE) - cast("birthday" as DATE) AS "age_in_days", extract(EPOCH FROM "birthday" + INTERVAL '10 days') AS "birthday_unixepoch", extract(HOUR from "birthday") AS "hour", extract(MINUTE from "birthday") AS "minute", extract(SECOND from "birthday") AS "second", extract(DOW from "birthday") AS "weekday" FROM "Person" ORDER BY "id";)"; + + const std::string expected = + R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-11","birthday_unixepoch":864000,"age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":4},{"birthday":"2000-01-11","birthday_recreated":"2000-01-11","birthday_unixepoch":947548800,"age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":6},{"birthday":"2002-01-11","birthday_recreated":"2002-01-11","birthday_unixepoch":1010707200,"age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":2},{"birthday":"2010-01-11","birthday_recreated":"2010-01-11","birthday_unixepoch":1263168000,"age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":5}])"; + + EXPECT_EQ(postgres::to_sql(get_birthdays), expected_query); + EXPECT_EQ(rfl::json::write(birthdays), expected); +} + +} // namespace test_range_select_from_with_timestamps + +#endif diff --git a/tests/postgres/test_where_with_timestamps.cpp b/tests/postgres/test_where_with_timestamps.cpp new file mode 100644 index 0000000..9875884 --- /dev/null +++ b/tests/postgres/test_where_with_timestamps.cpp @@ -0,0 +1,66 @@ +#ifndef SQLGEN_BUILD_DRY_TESTS_ONLY + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Date birthday; +}; + +TEST(postgres, test_where_with_timestamps) { + const auto people1 = + std::vector({Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Date("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Date("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Date("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Date("2010-01-01")}}); + + const auto credentials = sqlgen::postgres::Credentials{.user = "postgres", + .password = "password", + .host = "localhost", + .dbname = "postgres"}; + + using namespace sqlgen; + using namespace std::literals::chrono_literals; + + const auto conn = + sqlgen::postgres::connect(credentials).and_then(drop | if_exists); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | + where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + + std::chrono::milliseconds(4000000) > + Date("2010-01-01")) | + order_by("id"_c); + + const auto people2 = query(conn).value(); + + const std::string expected = + R"([{"id":2,"first_name":"Bart","last_name":"Simpson","birthday":"2000-01-01"},{"id":3,"first_name":"Lisa","last_name":"Simpson","birthday":"2002-01-01"},{"id":4,"first_name":"Maggie","last_name":"Simpson","birthday":"2010-01-01"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_timestamps + +#endif diff --git a/tests/sqlite/test_select_from_with_timestamps.cpp b/tests/sqlite/test_select_from_with_timestamps.cpp new file mode 100644 index 0000000..c1d92cb --- /dev/null +++ b/tests/sqlite/test_select_from_with_timestamps.cpp @@ -0,0 +1,78 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace test_range_select_from_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Date birthday; +}; + +TEST(sqlite, test_range_select_from_with_timestamps) { + const auto people1 = + std::vector({Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Date("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Date("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Date("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Date("2010-01-01")}}); + + using namespace sqlgen; + + struct Birthday { + Date birthday; + Date birthday_recreated; + time_t birthday_unixepoch; + double age_in_days; + int hour; + int minute; + int second; + int weekday; + }; + + const auto get_birthdays = + select_from( + ("birthday"_c + std::chrono::days(10)) | as<"birthday">, + ((cast(concat(cast(year("birthday"_c)), "-", + cast(month("birthday"_c)), "-", + cast(day("birthday"_c)))))) | + as<"birthday_recreated">, + days_between("birthday"_c, Date("2011-01-01")) | as<"age_in_days">, + unixepoch("birthday"_c + std::chrono::days(10)) | + as<"birthday_unixepoch">, + hour("birthday"_c) | as<"hour">, minute("birthday"_c) | as<"minute">, + second("birthday"_c) | as<"second">, + weekday("birthday"_c) | as<"weekday">) | + order_by("id"_c) | to>; + + const auto birthdays = sqlite::connect() + .and_then(write(std::ref(people1))) + .and_then(get_birthdays) + .value(); + + const std::string expected_query = + R"(SELECT datetime("birthday", '+10 days') AS "birthday", cast((cast(cast(strftime('%Y', "birthday") as INT) as TEXT) || '-' || cast(cast(strftime('%m', "birthday") as INT) as TEXT) || '-' || cast(cast(strftime('%d', "birthday") as INT) as TEXT)) as TEXT) AS "birthday_recreated", julianday('2011-01-01') - julianday("birthday") AS "age_in_days", unixepoch(datetime("birthday", '+10 days'), 'subsec') AS "birthday_unixepoch", cast(strftime('%H', "birthday") as INT) AS "hour", cast(strftime('%M', "birthday") as INT) AS "minute", cast(strftime('%S', "birthday") as INT) AS "second", cast(strftime('%w', "birthday") as INT) AS "weekday" FROM "Person" ORDER BY "id";)"; + const std::string expected = + R"([{"birthday":"1970-01-11","birthday_recreated":"1970-01-01","birthday_unixepoch":864000,"age_in_days":14975.0,"hour":0,"minute":0,"second":0,"weekday":4},{"birthday":"2000-01-11","birthday_recreated":"2000-01-01","birthday_unixepoch":947548800,"age_in_days":4018.0,"hour":0,"minute":0,"second":0,"weekday":6},{"birthday":"2002-01-11","birthday_recreated":"2002-01-01","birthday_unixepoch":1010707200,"age_in_days":3287.0,"hour":0,"minute":0,"second":0,"weekday":2},{"birthday":"2010-01-11","birthday_recreated":"2010-01-01","birthday_unixepoch":1263168000,"age_in_days":365.0,"hour":0,"minute":0,"second":0,"weekday":5}])"; + + EXPECT_EQ(sqlite::to_sql(get_birthdays), expected_query); + EXPECT_EQ(rfl::json::write(birthdays), expected); +} + +} // namespace test_range_select_from_with_timestamps + diff --git a/tests/sqlite/test_where_with_timestamps.cpp b/tests/sqlite/test_where_with_timestamps.cpp new file mode 100644 index 0000000..066492d --- /dev/null +++ b/tests/sqlite/test_where_with_timestamps.cpp @@ -0,0 +1,59 @@ + +#include + +#include +#include +#include +#include +#include +#include + +namespace test_where_with_timestamps { + +struct Person { + sqlgen::PrimaryKey id; + std::string first_name; + std::string last_name; + sqlgen::Date birthday; +}; + +TEST(sqlite, test_where_with_timestamps) { + const auto people1 = + std::vector({Person{.first_name = "Homer", + .last_name = "Simpson", + .birthday = sqlgen::Date("1970-01-01")}, + Person{.first_name = "Bart", + .last_name = "Simpson", + .birthday = sqlgen::Date("2000-01-01")}, + Person{.first_name = "Lisa", + .last_name = "Simpson", + .birthday = sqlgen::Date("2002-01-01")}, + Person{.first_name = "Maggie", + .last_name = "Simpson", + .birthday = sqlgen::Date("2010-01-01")}}); + + using namespace sqlgen; + + const auto conn = sqlgen::sqlite::connect(); + + sqlgen::write(conn, people1).value(); + + const auto query = + sqlgen::read> | + where("birthday"_c + std::chrono::years(11) - std::chrono::weeks(10) + + std::chrono::milliseconds(4000005) > + Date("2010-01-01")) | + order_by("id"_c); + + const auto people2 = query(conn).value(); + + const std::string expected_query = + R"(SELECT "id", "first_name", "last_name", "birthday" FROM "Person" WHERE datetime("birthday", '+11 years', '-70 days', '+01:06:40.005') > '2010-01-01' ORDER BY "id";)"; + const std::string expected = + R"([{"id":2,"first_name":"Bart","last_name":"Simpson","birthday":"2000-01-01"},{"id":3,"first_name":"Lisa","last_name":"Simpson","birthday":"2002-01-01"},{"id":4,"first_name":"Maggie","last_name":"Simpson","birthday":"2010-01-01"}])"; + + EXPECT_EQ(rfl::json::write(people2), expected); +} + +} // namespace test_where_with_timestamps +