Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
v0.3.1 (Unreleased)
---
### Query translation
* **`toStartOf*` date-time functions** via `EF.Functions`: `ToStartOfYear`, `ToStartOfQuarter`, `ToStartOfMonth`, `ToStartOfWeek` (with optional week `mode`), `ToStartOfDay`, `ToStartOfHour`, `ToStartOfMinute`, `ToStartOfSecond`, the fixed buckets `ToStartOfFiveMinutes` / `ToStartOfTenMinutes` / `ToStartOfFifteenMinutes`, and the general `ToStartOfInterval(source, value, unit)`. Each maps to the matching ClickHouse function and works in `GROUP BY`. Return types follow ClickHouse: the calendar buckets (`Year`/`Quarter`/`Month`/`Week`) return `Date`, the day/hour/minute buckets return `DateTime`, and `ToStartOfSecond` returns `DateTime64`. All accept `DateTime`/`DateTime64` columns, and the plain truncation functions also accept `DateOnly`; `ToStartOfInterval` requires a `DateTime`/`DateTime64` column on older ClickHouse, which rejects a `DateOnly` (Date/Date32) source with `Illegal type Date32 of 1st argument` for every unit; recent versions accept it. `ToStartOfInterval` takes a `ClickHouseInterval` unit (`Second`…`Year`) and emits `toStartOfInterval(source, toInterval<unit>(value))`; the unit must be a constant so it can be translated. The default `Date`/`DateTime` result types only span 1970–2149/2106, so ClickHouse narrows out-of-range values — enable `enable_extended_results_for_datetime_functions` (e.g. `set_enable_extended_results_for_datetime_functions=1` in the connection string) for range-preserving `Date32`/`DateTime64` results.

### Bug fixes
* `Sum`/`SumAsync` over a `double` or `float` column no longer throws `InvalidCastException`. EF Core wraps a top-level aggregate so the empty case returns `0`, supplying that fallback as a boxed `Int32` carrying the `Float64`/`Float32` mapping; the literal generators now convert rather than unbox. The `Float32` read path also converts, since ClickHouse widens `sum(Float32)` to `Float64` (which the driver's `GetFloat()` refuses to downcast). ([#46](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore/issues/46))
* **SummingMergeTree with multiple sum columns**: `HasSummingMergeTreeEngine("A", "B")` now generates valid DDL (`SummingMergeTree((A, B))`). Previously it emitted a comma-separated argument list (`SummingMergeTree(A, B)`), which ClickHouse rejects with `NUMBER_OF_ARGUMENTS_DOESNT_MATCH`. Single-column and no-column usage are unaffected.
Expand Down
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,33 @@ This provider is in active development. It supports **LINQ queries**, **inserts*

`Math.Abs`, `Floor`, `Ceiling`, `Round`, `Truncate`, `Pow`, `Sqrt`, `Cbrt`, `Exp`, `Log`, `Log2`, `Log10`, `Sign`, `Sin`, `Cos`, `Tan`, `Asin`, `Acos`, `Atan`, `Atan2`, `RadiansToDegrees`, `DegreesToRadians`, `IsNaN`, `IsInfinity`, `IsFinite`, `IsPositiveInfinity`, `IsNegativeInfinity` — with both `Math` and `MathF` overloads.

### Date/Time Functions

The ClickHouse `toStartOf*` family is exposed through `EF.Functions`, so you can bucket and truncate timestamps directly in queries, including in `GROUP BY`:

`ToStartOfYear`, `ToStartOfQuarter`, `ToStartOfMonth`, `ToStartOfWeek` (with an optional ClickHouse week `mode`), `ToStartOfDay`, `ToStartOfHour`, `ToStartOfMinute`, `ToStartOfSecond`, `ToStartOfFiveMinutes`, `ToStartOfTenMinutes`, `ToStartOfFifteenMinutes`, and the general `ToStartOfInterval(source, value, unit)`.

```csharp
// Truncate to the start of the month
var monthly = await ctx.Events
.Select(e => EF.Functions.ToStartOfMonth(e.Timestamp))
.ToListAsync();

// Bucket into 15-minute intervals and count per bucket
var buckets = await ctx.Events
.GroupBy(e => EF.Functions.ToStartOfInterval(e.Timestamp, 15, ClickHouseInterval.Minute))
.Select(g => new { Bucket = g.Key, Count = g.Count() })
.ToListAsync();
```

`ToStartOfInterval` takes a `ClickHouseInterval` unit (`Second`, `Minute`, `Hour`, `Day`, `Week`, `Month`, `Quarter`, `Year`) — from the `ClickHouse.EntityFrameworkCore.Metadata` namespace — and emits `toStartOfInterval(source, toInterval<unit>(value))`. The unit must be a constant.

Input and return types follow ClickHouse. The calendar buckets (`ToStartOfYear`/`Quarter`/`Month`/`Week`) return `Date`; `ToStartOfDay` and the hour/minute buckets return `DateTime`; `ToStartOfSecond` returns `DateTime64`. They all accept `DateTime` and `DateTime64` columns, and the plain truncation functions also accept `DateOnly` (Date/Date32). `ToStartOfInterval` is the exception: older ClickHouse rejects a `DateOnly` (Date/Date32) source with `Illegal type Date32 of 1st argument` while recent versions accept it. Prefer a `DateTime`/`DateTime64` column for interval bucketing.

> **Date range:** those default result types (`Date`, `DateTime`) only span 1970–2149/2106, so ClickHouse **narrows values outside that window** — a pre-1970 date is clamped to the epoch (calendar buckets) or wraps around (sub-day/interval buckets). To preserve the full range, enable [`enable_extended_results_for_datetime_functions`](https://clickhouse.com/docs/operations/settings/settings#enable_extended_results_for_datetime_functions) for your session — e.g. add `set_enable_extended_results_for_datetime_functions=1` to the connection string — which makes ClickHouse return `Date32`/`DateTime64` instead.

`ToStartOfWeek` defaults to ClickHouse week mode `0` (Sunday-based); pass a `mode` to change it.

### INSERT via SaveChanges

`SaveChanges` supports INSERT operations using the driver's native `InsertBinaryAsync` API — RowBinary encoding with GZip compression, far more efficient than parameterized SQL.
Expand Down
3 changes: 3 additions & 0 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
v0.3.1 (Unreleased)
---
### Query translation
* **`toStartOf*` date-time functions** are now translatable through `EF.Functions`, covering the full family: `ToStartOfYear`, `ToStartOfQuarter`, `ToStartOfMonth`, `ToStartOfWeek` (with an optional ClickHouse week `mode`), `ToStartOfDay`, `ToStartOfHour`, `ToStartOfMinute`, `ToStartOfSecond`, the fixed buckets `ToStartOfFiveMinutes` / `ToStartOfTenMinutes` / `ToStartOfFifteenMinutes`, and the general-purpose `ToStartOfInterval(source, value, unit)`. They compose in `GROUP BY` for time bucketing. Input and return types follow ClickHouse: the calendar buckets (`ToStartOfYear`/`Quarter`/`Month`/`Week`) return `Date`, `ToStartOfDay` and the hour/minute buckets return `DateTime`, and `ToStartOfSecond` returns `DateTime64`. All accept `DateTime`/`DateTime64` columns, and the plain truncation functions also accept `DateOnly` (Date/Date32); `ToStartOfInterval` is the exception — older ClickHouse rejects a `DateOnly` (Date/Date32) source with `Illegal type Date32 of 1st argument`, while recent versions accept it, so prefer a `DateTime`/`DateTime64` column for interval bucketing. `ToStartOfInterval` uses a `ClickHouseInterval` enum for the unit and is emitted as `toStartOfInterval(source, toInterval<unit>(value))`. Because the default result types (`Date`/`DateTime`) only span 1970–2149/2106, ClickHouse narrows out-of-range values (pre-1970 clamps to the epoch or wraps around); enable `enable_extended_results_for_datetime_functions` in your session (e.g. `set_enable_extended_results_for_datetime_functions=1` in the connection string) to get range-preserving `Date32`/`DateTime64` results.

### Bug fixes
* Summing a `double` or `float` column (`.SumAsync(x => x.Value)`) no longer throws `InvalidCastException`. Two ClickHouse-specific mismatches were biting: EF Core hands the float literal generator a boxed `Int32` `0` as the empty-result fallback, and ClickHouse widens `sum(Float32)` to `Float64` so the driver couldn't read it back as a `float`. Both the literal generation and the `Float32` read path now convert instead of hard-casting. ([#46](https://github.com/ClickHouse/ClickHouse.EntityFrameworkCore/issues/46)) (Thanks to @HotTotem!)
* **SummingMergeTree with multiple sum columns** now produces valid DDL. Configuring more than one sum column (`HasSummingMergeTreeEngine("A", "B")`) previously emitted `SummingMergeTree(A, B)`, which ClickHouse rejects with `NUMBER_OF_ARGUMENTS_DOESNT_MATCH` — the engine takes a single optional parameter that must be a tuple of columns. Multiple columns are now wrapped in a tuple (`SummingMergeTree((A, B))`); single-column and no-column usage are unchanged.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using ClickHouse.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Diagnostics;

namespace Microsoft.EntityFrameworkCore;

/// <summary>
/// ClickHouse-specific <see cref="DbFunctions"/> extension methods for the <c>toStartOf*</c> family of
/// date-time functions. Each method is translated to the corresponding ClickHouse SQL function and cannot
/// be evaluated on the client.
/// </summary>
public static class ClickHouseDateTimeDbFunctionsExtensions
{
/// <summary>
/// Rounds a date or date with time down to the first day of the year.
/// Maps to ClickHouse: <c>toStartOfYear(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date or date-time value to truncate.</param>
[DbFunction("toStartOfYear")]
public static T ToStartOfYear<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfYear)));

/// <summary>
/// Rounds a date or date with time down to the first day of the quarter.
/// Maps to ClickHouse: <c>toStartOfQuarter(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date or date-time value to truncate.</param>
[DbFunction("toStartOfQuarter")]
public static T ToStartOfQuarter<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfQuarter)));

/// <summary>
/// Rounds a date or date with time down to the first day of the month.
/// Maps to ClickHouse: <c>toStartOfMonth(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date or date-time value to truncate.</param>
[DbFunction("toStartOfMonth")]
public static T ToStartOfMonth<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfMonth)));

/// <summary>
/// Rounds a date or date with time down to the start of the week. The default ClickHouse week mode is
/// <c>0</c>, which treats Sunday as the first day of the week; use the <c>mode</c> overload to change this.
/// Maps to ClickHouse: <c>toStartOfWeek(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date or date-time value to truncate.</param>
[DbFunction("toStartOfWeek")]
public static T ToStartOfWeek<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfWeek)));

/// <summary>
/// Rounds a date or date with time down to the start of the week using the specified week mode.
/// Maps to ClickHouse: <c>toStartOfWeek(source, mode)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date or date-time value to truncate.</param>
/// <param name="mode">The ClickHouse week mode (0-9) that determines the first day of the week.</param>
[DbFunction("toStartOfWeek")]
public static T ToStartOfWeek<T>(this DbFunctions _, T source, byte mode) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfWeek)));

/// <summary>
/// Rounds a date with time down to the start of the day.
/// Maps to ClickHouse: <c>toStartOfDay(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate.</param>
[DbFunction("toStartOfDay")]
public static T ToStartOfDay<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfDay)));

/// <summary>
/// Rounds a date with time down to the start of the hour.
/// Maps to ClickHouse: <c>toStartOfHour(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate.</param>
[DbFunction("toStartOfHour")]
public static T ToStartOfHour<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfHour)));

/// <summary>
/// Rounds a date with time down to the start of the minute.
/// Maps to ClickHouse: <c>toStartOfMinute(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate.</param>
[DbFunction("toStartOfMinute")]
public static T ToStartOfMinute<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfMinute)));

/// <summary>
/// Rounds a date with time down to the start of the second.
/// Maps to ClickHouse: <c>toStartOfSecond(source)</c>.
/// </summary>
/// <remarks>ClickHouse <c>toStartOfSecond</c> requires a <c>DateTime64</c> argument.</remarks>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate. Must map to a ClickHouse <c>DateTime64</c> column.</param>
[DbFunction("toStartOfSecond")]
public static T ToStartOfSecond<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfSecond)));

/// <summary>
/// Rounds a date with time down to the start of the five-minute interval.
/// Maps to ClickHouse: <c>toStartOfFiveMinutes(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate.</param>
[DbFunction("toStartOfFiveMinutes")]
public static T ToStartOfFiveMinutes<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfFiveMinutes)));

/// <summary>
/// Rounds a date with time down to the start of the ten-minute interval.
/// Maps to ClickHouse: <c>toStartOfTenMinutes(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate.</param>
[DbFunction("toStartOfTenMinutes")]
public static T ToStartOfTenMinutes<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfTenMinutes)));

/// <summary>
/// Rounds a date with time down to the start of the fifteen-minute interval.
/// Maps to ClickHouse: <c>toStartOfFifteenMinutes(source)</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">The date-time value to truncate.</param>
[DbFunction("toStartOfFifteenMinutes")]
public static T ToStartOfFifteenMinutes<T>(this DbFunctions _, T source) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfFifteenMinutes)));

/// <summary>
/// Rounds a date or date with time down to the start of the specified interval.
/// Maps to ClickHouse: <c>toStartOfInterval(source, INTERVAL value unit)</c>, emitted as
/// <c>toStartOfInterval(source, toInterval&lt;unit&gt;(value))</c>.
/// </summary>
/// <param name="_">The <see cref="DbFunctions"/> instance.</param>
/// <param name="source">
/// The value to truncate. Prefer a date-time source: older ClickHouse rejects a date-only
/// (<c>Date</c>/<c>Date32</c>) source for <c>toStartOfInterval</c> with
/// <c>Illegal type Date32 of 1st argument</c> while recent versions accept it.
/// </param>
/// <param name="value">The number of interval units in each bucket.</param>
/// <param name="unit">The interval unit. Must be a constant so it can be translated to SQL.</param>
[DbFunction("toStartOfInterval")]
public static T ToStartOfInterval<T>(this DbFunctions _, T source, int value, ClickHouseInterval unit) =>
throw new InvalidOperationException(CoreStrings.FunctionOnClient(nameof(ToStartOfInterval)));
}
33 changes: 33 additions & 0 deletions src/EFCore.ClickHouse/Metadata/ClickHouseInterval.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace ClickHouse.EntityFrameworkCore.Metadata;

/// <summary>
/// Identifies the interval unit used by <c>EF.Functions.ToStartOfInterval</c>, which maps to the
/// ClickHouse <c>toStartOfInterval(t, INTERVAL n unit)</c> function. Each value corresponds to a
/// ClickHouse <c>toInterval*</c> helper (for example <see cref="Minute"/> emits <c>toIntervalMinute</c>).
/// </summary>
public enum ClickHouseInterval
{
/// <summary>Second interval (<c>toIntervalSecond</c>).</summary>
Second,

/// <summary>Minute interval (<c>toIntervalMinute</c>).</summary>
Minute,

/// <summary>Hour interval (<c>toIntervalHour</c>).</summary>
Hour,

/// <summary>Day interval (<c>toIntervalDay</c>).</summary>
Day,

/// <summary>Week interval (<c>toIntervalWeek</c>).</summary>
Week,

/// <summary>Month interval (<c>toIntervalMonth</c>).</summary>
Month,

/// <summary>Quarter interval (<c>toIntervalQuarter</c>).</summary>
Quarter,

/// <summary>Year interval (<c>toIntervalYear</c>).</summary>
Year
}
Loading
Loading