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 .github/workflows/coverage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ jobs:
with:
dotnet-version: 10.x

- name: Install dotnet-ef
run: dotnet tool install --global dotnet-ef --version 10.0.5

- name: Test with Coverage
run: dotnet test test/EFCore.ClickHouse.Tests/EFCore.ClickHouse.Tests.csproj --configuration Release --verbosity normal --logger GitHubActions /clp:ErrorsOnly /p:CollectCoverage=true /p:CoverletOutputFormat=opencover /p:SkipAutoProps=true

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ jobs:
with:
dotnet-version: 10.0.x

- name: Install dotnet-ef
run: dotnet tool install --global dotnet-ef --version 10.0.5

- name: Restore
run: dotnet restore

Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
v0.2.0
---
* **Table engine configuration** via fluent API: MergeTree, ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree, CollapsingMergeTree, VersionedCollapsingMergeTree, GraphiteMergeTree, plus simple engines (Log, TinyLog, StripeLog, Memory).
* **Engine clauses**: ORDER BY, PARTITION BY, PRIMARY KEY, SAMPLE BY, TTL, SETTINGS — all configurable per-entity.
* **Column-level DDL features**: CODEC, TTL, COMMENT, DEFAULT values.
* **Data-skipping indexes**: configurable type, granularity, and parameters.
* **Migrations support**: `dotnet ef migrations add` / `database update` with full DDL generation (CREATE TABLE, ALTER TABLE ADD/DROP/MODIFY/RENAME COLUMN, RENAME TABLE, CREATE/DROP DATABASE).
* **Model validation**: engine parameter columns checked for existence and correct store types (Int8 for sign, UInt8 for isDeleted). Foreign key warnings.
* **Default engine convention**: MergeTree with ORDER BY derived from primary key when no explicit engine is configured.
* Lambda-based overloads for engine configuration (e.g. `HasReplacingMergeTreeEngine<T>(e => e.Version)`).
* `ListToArrayConverter` handles null → empty array for ClickHouse `Array(T)` columns.
* Nullable wrapping correctly skips container types (Array, Map, Tuple, Variant, Dynamic, Json).

v0.1.0
---
Initial preview release.

* **LINQ query translation**: Where, OrderBy, Take, Skip, Select, First, Single, Any, Count, Sum, Min, Max, Average, Distinct, GroupBy (with DISTINCT and predicate overloads), LongCount.
* **60+ Math/MathF method translations**: Abs, Floor, Ceiling, Round, Truncate, Pow, Sqrt, Exp, Log, trig functions, etc.
* **String method translations**: Contains, StartsWith, EndsWith, IndexOf, Replace, Substring, Trim, ToLower, ToUpper, Length.
* **INSERT support**: `SaveChanges()` / `SaveChangesAsync()` via the driver's native `InsertBinaryAsync` (RowBinary with GZip compression). `BulkInsertAsync<T>()` for high-throughput bulk loads. UPDATE/DELETE throw `NotSupportedException`.
* **Type support**: `String`, `Bool`, `Int8`–`Int64`, `UInt8`–`UInt64`, `Float32`/`Float64`, `Decimal(P,S)` (32/64/128/256), `Date`/`Date32`, `DateTime`, `DateTime64`, `FixedString(N)`, `UUID`, `BFloat16`, Nullable(T)/LowCardinality(T) unwrapping, Enum8/Enum16, IPv4/IPv6, BigInteger (Int128/Int256/UInt128/UInt256), Array(T), Map(K,V), Tuple(T1,...), Time/Time64, Variant(T1,...,TN), Dynamic, Json (JsonNode + string), geographic types (Point, Ring, Polygon, MultiPolygon, Geometry).
15 changes: 15 additions & 0 deletions ClickHouse.EntityFrameworkCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.ClickHouse.Tests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.ClickHouse.FunctionalTests", "test\EFCore.ClickHouse.FunctionalTests\EFCore.ClickHouse.FunctionalTests.csproj", "{FC3416A6-643A-43C1-8D6F-E0308181979E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCore.ClickHouse.DesignSmoke", "test\EFCore.ClickHouse.DesignSmoke\EFCore.ClickHouse.DesignSmoke.csproj", "{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -59,6 +61,18 @@ Global
{FC3416A6-643A-43C1-8D6F-E0308181979E}.Release|x64.Build.0 = Release|Any CPU
{FC3416A6-643A-43C1-8D6F-E0308181979E}.Release|x86.ActiveCfg = Release|Any CPU
{FC3416A6-643A-43C1-8D6F-E0308181979E}.Release|x86.Build.0 = Release|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Debug|x64.ActiveCfg = Debug|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Debug|x64.Build.0 = Debug|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Debug|x86.ActiveCfg = Debug|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Debug|x86.Build.0 = Debug|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Release|Any CPU.Build.0 = Release|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Release|x64.ActiveCfg = Release|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Release|x64.Build.0 = Release|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Release|x86.ActiveCfg = Release|Any CPU
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -67,5 +81,6 @@ Global
{A3E072A2-61A5-4274-9720-316FE98B876B} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{79990B71-0CA9-495D-9988-1FE3BC5EB9A8} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
{FC3416A6-643A-43C1-8D6F-E0308181979E} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
{4A1A592D-DB71-4CD4-A4F2-3BFBAEED0BB4} = {0C88DD14-F956-CE84-757C-A364CCF449FC}
EndGlobalSection
EndGlobal
62 changes: 60 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public class PageView

## Current Status

This provider is in early development. It supports **read-only queries** and **inserts** — you can map entities to existing ClickHouse tables, query them with LINQ, and write data via `SaveChanges`.
This provider is in active development. It supports **LINQ queries**, **inserts**, **table engine configuration**, and **migrations** — you can define ClickHouse tables with engine-specific settings, create them via `dotnet ef migrations` or `EnsureCreated`, query with LINQ, and write data via `SaveChanges`.

### LINQ Queries

Expand Down Expand Up @@ -188,11 +188,69 @@ entity.Property(e => e.Payload).HasColumnType("Json");
- **NULL semantics** — ClickHouse's JSON type returns `{}` (empty object) for NULL values rather than SQL NULL. A row inserted with `Data = null` will read back as an empty `JsonNode`, not `null`.
- **Integer precision** — ClickHouse JSON stores all integers as `Int64` unless the path is typed otherwise. When reading via `JsonNode`, use `GetValue<long>()` rather than `GetValue<int>()`.

### Table Engine Configuration

Configure ClickHouse table engines, ordering, partitioning, and more via EF Core's fluent API:

```csharp
modelBuilder.Entity<SensorReading>(b =>
{
b.HasKey(e => e.Id);
b.Property(e => e.Temperature).HasCodec("Delta, ZSTD");
b.Property(e => e.Location).HasColumnComment("Installation site");
b.HasIndex(e => e.Timestamp)
.HasSkippingIndexType("minmax")
.HasGranularity(4);
b.ToTable("sensor_readings", t => t
.HasReplacingMergeTreeEngine("Version")
.WithOrderBy("Id", "Timestamp")
.WithPartitionBy("toYYYYMM(Timestamp)")
.WithPrimaryKey("Id")
.WithTtl("Timestamp + INTERVAL 1 YEAR")
.WithSetting("index_granularity", "4096"));
});
```

**Supported engines:** `MergeTree`, `ReplacingMergeTree`, `SummingMergeTree`, `AggregatingMergeTree`, `CollapsingMergeTree`, `VersionedCollapsingMergeTree`, `GraphiteMergeTree`, `Log`, `TinyLog`, `StripeLog`, `Memory`

**Column-level DDL:** `.HasCodec("Delta, ZSTD")`, `.HasColumnTtl("expr")`, `.HasColumnComment("text")`

**Data-skipping indices:** `.HasSkippingIndexType("minmax")`, `.HasGranularity(4)`, `.HasSkippingIndexParams("100")`

**Engine settings:** `.WithSetting("index_granularity", "4096")` — any ClickHouse setting as a key-value pair

**Default behavior:** If no engine is configured, the provider defaults to `MergeTree` with the EF primary key as `ORDER BY`.

### Migrations

The provider supports `dotnet ef migrations` for creating and applying migrations:

```bash
dotnet ef migrations add InitialCreate
dotnet ef database update
```

`EnsureCreated()` / `EnsureDeleted()` also work for quick setup without migrations.

**Supported migration operations:**
- CREATE TABLE with full ENGINE clause (all engine types, ORDER BY, PARTITION BY, PRIMARY KEY, SAMPLE BY, TTL, SETTINGS, codecs, comments, data-skipping indices)
- ADD COLUMN, DROP COLUMN, MODIFY COLUMN, RENAME COLUMN, RENAME TABLE
- DROP TABLE, CREATE/DROP INDEX (data-skipping)
- Custom `ClickHouseCreateDatabaseOperation` / `ClickHouseDropDatabaseOperation`

**ClickHouse limitations reflected in migrations:**
- ALTER TABLE cannot change engine, ORDER BY, PARTITION BY, or other structural metadata — the provider throws `NotSupportedException` with a clear message
- Foreign keys, unique constraints, and sequences throw `NotSupportedException`
- Primary key add/drop is a no-op (ClickHouse PK is structural, not a constraint)
- Idempotent scripts (`--idempotent`) are not supported (ClickHouse has no conditional SQL blocks)
- Transactions are suppressed (ClickHouse does not support them)

### Not Yet Implemented

- UPDATE / DELETE (ClickHouse mutations are async, not OLTP-compatible)
- Migrations
- JOINs, subqueries, set operations
- Reverse engineering / scaffolding (`dotnet ef dbcontext scaffold`)
- JSON path query translation

## Building

Expand Down
18 changes: 11 additions & 7 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
v0.1.0
v0.2.0
---
Initial preview release.

* Read-only functionality.
* Limited type support: `String`, `Bool`, `Int8`/`Int16`/`Int32`/`Int64`, `UInt8`/`UInt16`/`UInt32`/`UInt64`, `Float32`/`Float64`, `Decimal(P, S)`, `Date`/`Date32`, `DateTime`, `DateTime64(P, 'TZ')`, `FixedString(N)`, `UUID`.
* Basic aggregations: `Where`, `OrderBy`, `Take`, `Skip`, `Select`, `First`, `Single`, `Any`, `Count`, `Sum`, `Min`, `Max`, `Average`, `Distinct`, `GroupBy`.
* String methods: `Contains`, `StartsWith`, `EndsWith`, `IndexOf`, `Replace`, `Substring`, `Trim`, `ToLower`, `ToUpper`, `Length`.
* **Table engine configuration** via fluent API: MergeTree, ReplacingMergeTree, SummingMergeTree, AggregatingMergeTree, CollapsingMergeTree, VersionedCollapsingMergeTree, GraphiteMergeTree, plus simple engines (Log, TinyLog, StripeLog, Memory).
* **Engine clauses**: ORDER BY, PARTITION BY, PRIMARY KEY, SAMPLE BY, TTL, SETTINGS — all configurable per-entity.
* **Column-level DDL features**: CODEC, TTL, COMMENT, DEFAULT values.
* **Data-skipping indexes**: configurable type, granularity, and parameters.
* **Migrations support**: `dotnet ef migrations add` / `database update` with full DDL generation (CREATE TABLE, ALTER TABLE ADD/DROP/MODIFY/RENAME COLUMN, RENAME TABLE, CREATE/DROP DATABASE).
* **Model validation**: engine parameter columns checked for existence and correct store types (Int8 for sign, UInt8 for isDeleted). Foreign key warnings.
* **Default engine convention**: MergeTree with ORDER BY derived from primary key when no explicit engine is configured.
* Lambda-based overloads for engine configuration (e.g. `HasReplacingMergeTreeEngine<T>(e => e.Version)`).
* `ListToArrayConverter` handles null → empty array for ClickHouse `Array(T)` columns.
* Nullable wrapping correctly skips container types (Array, Map, Tuple, Variant, Dynamic, Json).
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using ClickHouse.EntityFrameworkCore.Metadata.Internal;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;

namespace ClickHouse.EntityFrameworkCore.Design.Internal;

public class ClickHouseAnnotationCodeGenerator : AnnotationCodeGenerator
{
public ClickHouseAnnotationCodeGenerator(AnnotationCodeGeneratorDependencies dependencies)
: base(dependencies)
{
}

protected override bool IsHandledByConvention(IModel model, IAnnotation annotation)
{
if (annotation.Name.StartsWith(ClickHouseAnnotationNames.Prefix, StringComparison.Ordinal))
return false;

return base.IsHandledByConvention(model, annotation);
}

protected override bool IsHandledByConvention(IEntityType entityType, IAnnotation annotation)
{
if (annotation.Name.StartsWith(ClickHouseAnnotationNames.Prefix, StringComparison.Ordinal))
return false;

return base.IsHandledByConvention(entityType, annotation);
}

protected override bool IsHandledByConvention(IProperty property, IAnnotation annotation)
{
if (annotation.Name.StartsWith(ClickHouseAnnotationNames.Prefix, StringComparison.Ordinal))
return false;

return base.IsHandledByConvention(property, annotation);
}

protected override bool IsHandledByConvention(IIndex index, IAnnotation annotation)
{
if (annotation.Name.StartsWith(ClickHouseAnnotationNames.Prefix, StringComparison.Ordinal))
return false;

return base.IsHandledByConvention(index, annotation);
}
}
30 changes: 30 additions & 0 deletions src/EFCore.ClickHouse/Design/Internal/ClickHouseCodeGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using ClickHouse.EntityFrameworkCore.Extensions;
using ClickHouse.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Scaffolding;
using System.Reflection;

namespace ClickHouse.EntityFrameworkCore.Design.Internal;

public class ClickHouseCodeGenerator : ProviderCodeGenerator
{
private static readonly MethodInfo UseClickHouseMethodInfo
= typeof(ClickHouseDbContextOptionsBuilderExtensions).GetRuntimeMethod(
nameof(ClickHouseDbContextOptionsBuilderExtensions.UseClickHouse),
[typeof(DbContextOptionsBuilder), typeof(string), typeof(Action<ClickHouseDbContextOptionsBuilder>)])!;

public ClickHouseCodeGenerator(ProviderCodeGeneratorDependencies dependencies)
: base(dependencies)
{
}

public override MethodCallCodeFragment GenerateUseProvider(
string connectionString,
MethodCallCodeFragment? providerOptions)
=> new(
UseClickHouseMethodInfo,
providerOptions is null
? [connectionString]
: [connectionString, new NestedClosureCodeFragment("x", providerOptions)]);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using ClickHouse.EntityFrameworkCore.Extensions;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Scaffolding;
using Microsoft.Extensions.DependencyInjection;

[assembly: DesignTimeProviderServices(
"ClickHouse.EntityFrameworkCore.Design.Internal.ClickHouseDesignTimeServices")]

namespace ClickHouse.EntityFrameworkCore.Design.Internal;

public class ClickHouseDesignTimeServices : IDesignTimeServices
{
public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
{
serviceCollection.AddEntityFrameworkClickHouse();

new EntityFrameworkRelationalDesignServicesBuilder(serviceCollection)
.TryAdd<IAnnotationCodeGenerator, ClickHouseAnnotationCodeGenerator>()
.TryAdd<IProviderConfigurationCodeGenerator, ClickHouseCodeGenerator>()
.TryAddCoreServices();
}
}
1 change: 1 addition & 0 deletions src/EFCore.ClickHouse/EFCore.ClickHouse.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2" PrivateAssets="all" IncludeAssets="runtime; build; native; contentfiles; analyzers; buildtransitive" />
<PackageReference Include="ClickHouse.Driver" Version="1.1.0" />
</ItemGroup>

Expand Down
Loading
Loading