Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5bec407
Started developing support for operators
liuzicheng1987 Jun 13, 2025
374e820
Merge branch 'main' into f/operations
liuzicheng1987 Jun 14, 2025
3833edb
Added operations to select_from
liuzicheng1987 Jun 14, 2025
29c81e6
Redesigned the operations
liuzicheng1987 Jun 15, 2025
82d695c
Redesigned aggregations
liuzicheng1987 Jun 15, 2025
8757c89
Added basic support for operations
liuzicheng1987 Jun 15, 2025
54f6e0a
Better type inference for operators
liuzicheng1987 Jun 15, 2025
3d95a37
Allow complex operations inside conditions
liuzicheng1987 Jun 15, 2025
c1f33cb
Made sure that nullable types are handled correctly
liuzicheng1987 Jun 16, 2025
42ccd20
Added the modulo operator
liuzicheng1987 Jun 17, 2025
4bb8fae
Added the abs operation
liuzicheng1987 Jun 17, 2025
ae4ae36
Added more operations
liuzicheng1987 Jun 17, 2025
fb06c15
Added cast operations
liuzicheng1987 Jun 18, 2025
471a0de
Added more compelling examples to the README
liuzicheng1987 Jun 18, 2025
64b904f
Added the round(...) operation
liuzicheng1987 Jun 19, 2025
3dd630e
Added string-based operators
liuzicheng1987 Jun 19, 2025
6e37904
Added more tests
liuzicheng1987 Jun 19, 2025
08f8c41
Added the string replace operator
liuzicheng1987 Jun 19, 2025
11b2ce8
Added coalesce(...)
liuzicheng1987 Jun 19, 2025
d4f6079
Make sure that cast respects NULL
liuzicheng1987 Jun 19, 2025
1dda22e
Added documentation for other operations and functions
liuzicheng1987 Jun 19, 2025
0622e48
Removed any references to std::format
liuzicheng1987 Jun 20, 2025
b445785
Some improvements to the documentation
liuzicheng1987 Jun 20, 2025
28ef8e5
Added operator NOT
liuzicheng1987 Jun 20, 2025
861cf14
Added GROUP BY checks for the operations
liuzicheng1987 Jun 22, 2025
24b6217
Make sure we can use operations inside of aggregations and vice versa
liuzicheng1987 Jun 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,17 @@ const auto query = read<std::vector<Person>> |
const auto query = read<std::vector<Person>> |
where("age"_c == "Homer");

// Compile-time error: "age" must be aggregated or included in GROUP BY
const auto query = select_from<Person>(
"last_name"_c,
"age"_c
) | group_by("last_name"_c);
Comment thread
liuzicheng1987 marked this conversation as resolved.

// Compile-time error: Cannot add string and int
const auto query = select_from<Person>(
"last_name"_c + "age"_c
);

// Runtime protection against SQL injection
std::vector<Person> get_people(const auto& conn,
const sqlgen::AlphaNumeric& first_name) {
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ 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

## Data Types and Validation

Expand Down
249 changes: 249 additions & 0 deletions docs/other_operations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# 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<double>("age"_c)), 2) | as<"exp_age">
round(sqrt(cast<double>("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<double>("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<T>`), the result will also be nullable if any operand is nullable. For example, adding two `std::optional<int>` columns will yield a `std::optional<int>`. 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<T>`), 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<ResultType>`).
- 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<T>`).
- 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<int>{}, std::optional<int>{}) // -> std::optional<int>

// At least one argument non-nullable: result is non-nullable
coalesce(std::optional<int>{}, 42) // -> int
coalesce(42, std::optional<int>{}) // -> int

// All arguments non-nullable: result is non-nullable
coalesce(1, 2) // -> int

// Mixed string example
coalesce(std::optional<std::string>{}, "default") // -> std::string

// Compile-time error: mismatched types
// coalesce(std::optional<int>{}, std::optional<double>{}) // 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<Person>(
("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<double>("age"_c)), 2) | as<"exp_age">,
round(sqrt(cast<double>("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<std::vector<Children>>;
```

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.

1 change: 1 addition & 0 deletions include/sqlgen.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
#include "sqlgen/insert.hpp"
#include "sqlgen/is_connection.hpp"
#include "sqlgen/limit.hpp"
#include "sqlgen/operations.hpp"
#include "sqlgen/order_by.hpp"
#include "sqlgen/patterns.hpp"
#include "sqlgen/read.hpp"
Expand Down
53 changes: 33 additions & 20 deletions include/sqlgen/aggregations.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,61 @@
#include <type_traits>

#include "col.hpp"
#include "transpilation/aggregations.hpp"
#include "transpilation/Aggregation.hpp"
#include "transpilation/AggregationOp.hpp"
#include "transpilation/to_transpilation_type.hpp"

namespace sqlgen {

template <rfl::internal::StringLiteral _name>
auto avg(const Col<_name>&) {
return transpilation::aggregations::Avg<transpilation::Col<_name>>{
.val = transpilation::Col<_name>{}};
template <class T>
auto avg(const T& _t) {
using Type =
typename transpilation::ToTranspilationType<std::remove_cvref_t<T>>::Type;
return transpilation::Aggregation<transpilation::AggregationOp::avg, Type>{
.val = transpilation::to_transpilation_type(_t)};
}

inline auto count() {
return transpilation::aggregations::Count<transpilation::aggregations::All>{};
return transpilation::Aggregation<transpilation::AggregationOp::count,
transpilation::All>{};
}

template <rfl::internal::StringLiteral _name>
auto count(const Col<_name>&) {
return transpilation::aggregations::Count<transpilation::Col<_name>>{
return transpilation::Aggregation<transpilation::AggregationOp::count,
transpilation::Col<_name>>{
.val = transpilation::Col<_name>{}};
}

template <rfl::internal::StringLiteral _name>
auto count_distinct(const Col<_name>&) {
return transpilation::aggregations::Count<transpilation::Col<_name>>{
return transpilation::Aggregation<transpilation::AggregationOp::count,
transpilation::Col<_name>>{
.val = transpilation::Col<_name>{}, .distinct = true};
}

template <rfl::internal::StringLiteral _name>
auto max(const Col<_name>&) {
return transpilation::aggregations::Max<transpilation::Col<_name>>{
.val = transpilation::Col<_name>{}};
template <class T>
auto max(const T& _t) {
using Type =
typename transpilation::ToTranspilationType<std::remove_cvref_t<T>>::Type;
return transpilation::Aggregation<transpilation::AggregationOp::max, Type>{
.val = transpilation::to_transpilation_type(_t)};
}

template <rfl::internal::StringLiteral _name>
auto min(const Col<_name>&) {
return transpilation::aggregations::Min<transpilation::Col<_name>>{
.val = transpilation::Col<_name>{}};
template <class T>
auto min(const T& _t) {
using Type =
typename transpilation::ToTranspilationType<std::remove_cvref_t<T>>::Type;
return transpilation::Aggregation<transpilation::AggregationOp::min, Type>{
.val = transpilation::to_transpilation_type(_t)};
}

template <rfl::internal::StringLiteral _name>
auto sum(const Col<_name>&) {
return transpilation::aggregations::Sum<transpilation::Col<_name>>{
.val = transpilation::Col<_name>{}};
template <class T>
auto sum(const T& _t) {
using Type =
typename transpilation::ToTranspilationType<std::remove_cvref_t<T>>::Type;
return transpilation::Aggregation<transpilation::AggregationOp::sum, Type>{
.val = transpilation::to_transpilation_type(_t)};
}

} // namespace sqlgen
Expand Down
Loading
Loading