Table of Contents

ClickHouse provider

Use nextorm.clickhouse for ClickHouse; it renders @name parameters (rewritten to {name:Type} by the driver), backtick-quoted identifiers, concat(...) concatenation, limit/offset paging and ClickHouse type names.

Prerequisites: Provider overview · Quickstart

Extension methods. The ClickHouse-only fluent members — Final, PreWhere, Settings, Sample, LimitBy, WithTotals, ArrayJoin/LeftArrayJoin/ArrayJoinElement/LeftArrayJoinElement, SemiJoin/AntiJoin/PasteJoin and j => j.Global() — are not part of the common nextorm builder API. They ship with the nextorm.clickhouse package as extension methods; add using NextORM.ClickHouse; and the fluent chains below compile unchanged.

Overview

ClickHouseDataContext (src/nextorm.clickhouse/ClickHouseDataContext.cs) wraps the official ClickHouse.Driver ADO.NET provider. It creates a ClickHouseConnection from the connection string and returns Instance from its Dialect property.

ClickHouseDialect (src/nextorm.clickhouse/ClickHouseDialect.cs) renders:

  • parameter placeholder @name; the driver rewrites these to ClickHouse's native {name:Type} form and infers the type from the .NET value;
  • backtick-quoted identifiers and aliases, escaping an embedded backslash (\ → \\) and an embedded backtick (` → ``). The correction is ClickHouse-specific: MySQL and MariaDB also quote with backticks but do not treat backslash as an identifier escape, so their quoting is unchanged;
  • native UInt64 columns and ulong/ulong? properties/projections materialise through the row reader's DbDataReader.GetFieldValue<ulong> accessor, so no SQL cast is needed (MySQL/MariaDB BIGINT UNSIGNED benefits from the same accessor);
  • string concatenation with the concat(a, b, ...) function;
  • coalesce(a, b), true/false boolean literals and lengthUTF8(x) for string length;
  • trimBoth/trimLeft/trimRight for the three trim kinds;
  • now() for local time and now('UTC') for UTC;
  • stdev/stdevp/var/varp as stddevSamp/stddevPop/varSamp/varPop;
  • date_trunc(field, x) as dateTrunc('field', x) (the plural ANSI sub-second parts are singularised; decade/century/millennium throw), and date_add/DateTime.Add* as the dedicated addYears/addQuarters/…/addSeconds functions (year/decade/century promote the operand to DateTime64(6) and scale addYears, so the SQL result type of those units is DateTime64(6) while the CLR API is unchanged; millennium throws before execution — on pinned ClickHouse 25.8 the supported year-family results stay within 1900–2299, arithmetic outside that range is unsupported, and inputs are normalized to microseconds, discarding finer precision), end_of_month as toLastDayOfMonth(x);
  • the date conversion/part surface SqlFunctions.ClickHouse.to_* (gated by DateConversion): to_date/to_date_time/to_date32 as toDate/toDateTime/toDate32, the to_year/to_quarter/ to_month/to_day_of_month/to_day_of_week/to_day_of_year/to_hour/to_minute/to_second accessors (and the DateTime.Year/Month/… projections) as toYear/toQuarter/… wrapped in toInt32(...) (the native UInt8/UInt16 cannot be read back otherwise), to_start_of_* as toStartOfYear/toStartOfQuarter/toStartOfMonth/toStartOfWeek/toStartOfDay/toStartOfHour/ toStartOfMinute/toStartOfSecond, to_monday as toMonday (ISO Monday, whereas toStartOfWeek starts on Sunday), to_yyyymm/to_yyyymmdd as toInt32(toYYYYMM(...))/toInt32(toYYYYMMDD(...)) and to_unix_timestamp as toInt64(toUnixTimestamp(...));
  • string_agg(x, delimiter) as arrayStringConcat(groupArray(x), delimiter);
  • bit_and/bit_or/bit_xor as groupBitAnd/groupBitOr/groupBitXor, covar_pop/covar_samp as covarPop/covarSamp, corr as corr, arg_min/arg_max as argMin/argMax, and an aggregate filter as the -If combinator (countIf/sumIf/avgIf/minIf/maxIf, emitted for the shared SqlFunctions.Sql.count/sum/avg/min/max when they take a predicate); the distinct-count aggregates uniq/uniq_exact/uniq_combined/uniq_hll12 as uniq/uniqExact/uniqCombined/uniqHLL12 wrapped in toInt64(...) to normalise the native UInt64 to the declared CLR long; the count aggregates (count/count_distinct and the filtered countIf, plus count_big/count_big_distinct) are cast the same way — to toInt32(...) for the int-returning variants and toInt64(...) for the 64-bit ones; the parameterised quantile aggregates quantile(level)(value)/quantileExact/quantileTiming and median wrapped in toFloat64(...) (so every variant materialises as a CLR double); median is a plain aggregate (one row per group), and the generic SqlFunctions.ClickHouse.percentile has no native spelling and throws NotSupportedException rather than aliasing quantile/median; the arbitrary-value aggregate any_agg as any (cross-provider: ANY_VALUE(x) on MySQL) and the last-row aggregate any_last as anyLast; the array-returning aggregates group_array/group_uniq_array as groupArray/groupUniqArray (materialised as a CLR T[]); the sequence/funnel aggregates window_funnel/sequence_match/retention as windowFunnel/sequenceMatch/retention (windowFunnel/sequenceMatch are wrapped in toInt32(...); retention is normalised server-side to the declared int[], and top_k, top_k_weighted, group_array, group_uniq_array, array_map and all three range overloads likewise return their declared CLR array type);
  • the string-JSON extractors json_extract_string/json_extract_int/json_extract_float/ json_extract_bool/json_extract_raw/json_has/json_type as JSONExtractString/JSONExtractInt/ JSONExtractFloat/JSONExtractBool/JSONExtractRaw/JSONHas/JSONType, json_length as toInt64(JSONLength(...)), the array-returning json_extract_keys/json_extract_array_raw/ json_extract_keys_and_values as JSONExtractKeys/JSONExtractArrayRaw/JSONExtractKeysAndValues (projecting as string[]/Tuple<string, T>[]; the value type of the last is a non-nullable generic), and the flat-JSON fast path visit_param_extract_string/_int/_float/ _bool/_raw as visitParamExtractString/visitParamExtractInt/visitParamExtractFloat/ visitParamExtractBool/visitParamExtractRaw; the JSONPath scalars json_value/json_query/ json_exists as JSON_VALUE/JSON_QUERY/JSON_EXISTS and the native-JSON functions json_all_paths/ json_all_paths_with_types/to_json_string as JSONAllPaths/JSONAllPathsWithTypes/toJSONString (same SupportsJsonExtract gate); the dictionary functions dict_get/dict_get_or_default/ dict_has/dict_get_hierarchy/dict_get_children/dict_is_in as dictGet/dictGetOrDefault/dictHas/dictGetHierarchy/dictGetChildren/dictIsIn;
  • the super-aggregate GROUP BY ... WITH TOTALS modifier (SupportsGroupByWithTotals) via EntityBuilder.WithTotals();
  • ClickHouse type names in casts (Int32, Int64, Float64, Decimal(38, 10), …);
  • limit n / limit n offset m paging; an offset without a limit becomes limit 18446744073709551615 offset m, because ClickHouse only accepts offset together with limit;
  • LIMIT n BY expr (LimitBy) via EntityBuilder.LimitBy(...): at most n rows per distinct key, emitted after ORDER BY and before the final LIMIT;
  • the numbers/numbers_mt table functions via SqlFunctions.ClickHouse.numbers(...) (the UInt64 number column is cast to Int64 through a wrapping subquery so the row reader can materialise it) and the zeros/zeros_mt row-count table functions via SqlFunctions.ClickHouse.zeros(...) (IZerosRow, the zero UInt8 column materialises directly as byte); the server/cluster table functions url/s3/file/remote/remote_secure/cluster/cluster_all_replicas are also pre-declared (the row shape is declared by the caller's generic TRow interface, which must match the structure argument or the target table); the values table function (SqlFunctions.ClickHouse.values<TRow>(tuples)) renders its structure from TRow (SupportsResultSchema(TableFunctionSchema)) and takes the verbatim tuple list as its argument;
  • the query modifiers FINAL/SAMPLE/PREWHERE/SETTINGS via Final(), Sample(ratio[, offset]), PreWhere(predicate) and Settings(("key", "value"), ...) (SupportsFinal/SupportsSample/SupportsPreWhere/SupportsSettings); FINAL/PREWHERE need a table engine that supports them (the Memory engine rejects both);
  • the portable conditional iif as if(condition, a, b) (Iif, IIifRenderer.Render), the ClickHouse-only multi-branch multiIf built with when(...)/otherwise(...) (MultiIf, IMultiIfRenderer.Render), and the percent_rank()/cume_dist(), nth_value(expr, n) and frame-respecting lagInFrame(value[, offset[, default]])/leadInFrame(...) window functions (SupportsPercentRankCumeDist, SupportsNthValue, SupportsInFrameWindowFunctions);
  • the distributed GLOBAL IN predicate via SqlFunctions.ClickHouse.global_in (over a subquery or a value list, SupportsGlobalPredicates); negate with C# ! for GLOBAL NOT IN;
  • the join strictness/kind modifiers ANY/ALL/ASOF via JoinOptions.WithStrictness in a join's trailing lambda (SupportsJoinStrictness, MakeJoinKeyword, enum JoinStrictness). LEFT ANY JOIN keeps a single right-hand row per left-hand row, ALL keeps every match and ASOF needs one equi-join column plus a final inequality. The SEMI/ANTI/PASTE kinds have dedicated builder methods: SemiJoin/AntiJoin return only the left-hand columns for left rows that do (respectively do not) have a match, and PasteJoin pairs the two sources by row position with no ON (yielding as many rows as the shorter side). They are gated by SupportsSemiAntiJoin/SupportsPasteJoin. The GLOBAL variant (resolved once and broadcast for distributed queries) is set with JoinOptions.Global and combines with strictness (global left any join, SupportsGlobalJoin).

The declared contract for the variability aggregates is the shared one: Stdev/Var are null for zero or one contributing value, Stdevp/Varp are null for zero and 0.0 for one. For this change the ClickHouse arm was verified at SQL generation only (stddevSamp/stddevPop/varSamp/varPop above), not against a live server: no live result is claimed and no NaN/Inf normalization is implied. The live arms of this change were SQLite and PostgreSQL.

The avg result follows the SQL-standard contract on ClickHouse: over an empty input or an input whose values are all NULL it is SQL NULL, not ClickHouse's native nan. The dialect restores that with a row-eligibility check around the aggregate — if(count(x)=0, cast(null as Nullable(Float64)), avg(x)), if(countIf(isNotNull(x) AND p)=0, ..., avgIf(x, p)) for a filtered AVG and the same count(x) over (...) check for a window AVG (with the identical partition/order/frame). No nan/isFinite test is involved: the check counts eligible rows, not the result, so a genuine NaN or ±infinity produced by non-empty data is returned unchanged. This applies to ordinary, DISTINCT, filtered and window AVG alike.

ClickHouse has no recursive CTE support, so the dialect declares every CTE with plain with.

Registering the provider

Two overloads are available on DataContextBuilder (src/nextorm.clickhouse/DI/ClickHouseDataContextOptionsBuilderExtensions.cs):

using NextORM.Core;
using NextORM.ClickHouse;

var builder = new DataContextBuilder()
    .UseClickHouse("Host=localhost;Port=8123;Username=default;Password=secret;Database=app");

using var ctx = builder.CreateDataContext();   // IDataContext

You can also construct the context directly:

using NextORM.Core;
using NextORM.ClickHouse;

using IDataContext ctx = new ClickHouseDataContext(
    "Host=localhost;Username=default;Database=app", new DataContextBuilder());

Declaring the server version

The server version is a caller declaration, never probed: without one the provider keeps the pinned 25.8 baseline and refuses every function introduced later. When the deployment provably runs a newer server, declare it through the versioned constructor (the connection-string and caller-connection overloads both accept it):

using var ctx = new ClickHouseDataContext(
    new Version(25, 9),
    "Host=localhost;Username=default;Database=app",
    new DataContextBuilder());

A declared version of 25.9 or later enables the native arrayExcept behind SqlFunctions.ClickHouse.array_except; the two-array call renders as arrayExcept(...) (the native function takes exactly two arrays, so any other arity is refused before SQL). An undeclared context keeps the historical refusal — the shared ClickHouseDialect.Instance never enables a 25.9 function — and a context declaring an older version (for example 25.8) still refuses the call, but the NotSupportedException names both the required 25.9 and the declared version and is thrown before any SQL is emitted.

A concrete context type carries one immutable declaration, because the plan cache is keyed by the context type. Declare each version through its own subclass:

// One context type = one immutable declaration.
public sealed class Declared25Context : ClickHouseDataContext
{
    public Declared25Context(string connectionString, DataContextBuilder optionsBuilder)
        : base(new Version(25, 9), connectionString, optionsBuilder)
    {
    }
}

var builder = new DataContextBuilder();
builder.Factory = b => new Declared25Context(connectionString, b);   // the DI path

A second, different declaration for the same type throws InvalidOperationException before any query is prepared. The client and data-source constructors are unversioned: they always create the pinned-baseline dialect.

String concatenation

var query = ctx.From<ISimpleEntity>().Select(x => new { Label = "id:" + x.Id });
select concat('id:', id) as `Label`
 from simple_entity

Table-valued parameters

ClickHouse emulates a table-valued parameter with a native array bound through ProcedureParameter.Table<T> (SupportsTableValuedParameters is true): a scalar row type binds as Array(T) and an entity row type as Array(Tuple(col1, col2, ...)), and the server expands the array with arrayJoin. The provider sets the driver's explicit ClickHouseType (wrapping a nullable column in Nullable(...)) because the driver infers a non-nullable type from a CLR value and would otherwise fail to serialize a null element or an empty array. A decimal column binds as the declared Decimal(p, s) (from [DecimalPrecision(p, s)] or the fluent mapping) or Decimal(38, 10) when none is declared.

using var ctx = new ClickHouseDataContext(
    "Host=localhost;Username=default;Database=app", new DataContextBuilder());

// scalar set -> select arrayJoin(@ids)
using var scalar = ctx.ExecuteRaw(
    "select arrayJoin(@ids) as value order by value",
    [ProcedureParameter.Table("ids", new[] { 3, 1, 2 })]);
var values = scalar.Read<int>();   // [1, 2, 3]

// entity set -> select t.1, t.2 from (select arrayJoin(@rows) as t)
using var entity = ctx.ExecuteRaw(
    "select t.1 as Id, t.2 as Name from (select arrayJoin(@rows) as t) order by Id",
    [ProcedureParameter.Table("rows", new[]
    {
        new TvpRow { Id = 2, Name = "beta" },
        new TvpRow { Id = 1, Name = null },
    })]);
var rows = entity.Read<TvpRow>();

public sealed class TvpRow
{
    public int Id { get; set; }
    public string? Name { get; set; }
}

The format(JSONEachRow, ...), values() and input() forms are not used for a bound parameter — format/values require a literal structure and input is valid only in INSERT ... SELECT, so the array form is the supported emulation. ClickHouse has no stored procedures (SupportsStoredProcedures is false, and ExecuteProcedure throws NotSupportedException), so the parameter is consumed by ExecuteRaw/ExecuteRawAsync SQL that calls arrayJoin itself. TypeName is SQL Server only and is rejected with ArgumentException. An empty set binds an empty array, and null elements, nullable columns and empty sets are covered by the provider tests. Only CLR types with a ClickHouse mapping are supported — the scalar column set (the integer types, float/double, decimal as the declared Decimal(p, s) or Decimal(38, 10), bool, string/char, Guid, DateTime/DateTimeOffset/DateOnly, an enum as its underlying number; TimeOnly as String formatted invariantly as HH:mm:ss.fffffff, TimeSpan as Int64 in its mapped duration unit (ticks by default), and byte[] as String); any other column type throws NotSupportedException. A byte[] column is bound as String, so the row reader returns a string, not a byte[] — do not rely on a byte[] round-trip. The set is sent as one array parameter, not streamed or copied, so use the driver's binary insert API for very large sets. See Raw SQL: table-valued parameters.

Bulk insert

The native path is ClickHouseClient.InsertBinaryAsync through the context's own client, so it needs a ClickHouse.Driver client (ClickHouseClient/ClickHouseDataSource — the UseClickHouse overloads). Client settings are copied for the insert; the caller's settings object and the connection's settings are never mutated.

  • ClickHouse(new ClickHouseBulkInsertOptions { ... }) carries MaxDegreeOfParallelism and UseSession: null preserves the configured value/session, true requires an actual session id, and false explicitly requests a sessionless non-temporary insert. A parallelism above one cannot be combined with a session (ClickHouse allows one concurrent query per session) and is refused by name; a non-positive parallelism is an argument error.
  • The adapter reads the source in bounded chunks (MaxBatchSize, default 10 000 rows), awaits every driver call before pulling the next chunk, and reports progress only after a chunk completes.
  • Only ordinary and DEFAULT columns are writable: a mapped ALIAS or MATERIALIZED column is refused by name (the catalog is read once per target). KeepIdentity writes the explicit value.
  • IgnoreDuplicates, Returning*, TableLock, FireTriggers and CheckConstraints=false are refused; KeepNulls=false needs a default-equivalent shape.
  • DatabaseName renders db.table; ServerName is refused (no rerouting). Timeout is one whole-invocation budget with a linked cancellation; an expired budget is a TimeoutException and cancellation wins.

ClickHouse.Driver 1.4.0's ClickHouseBulkCopy class is not obsolete — only its InitAsync method is — so the class is not why nextorm uses InsertBinaryAsync: the binary insert API is what carries the per-insert settings copy and the session/parallelism contract. See Bulk insert.

Provider differences

Aspect ClickHouse
Parameter placeholder @name (driver rewrites to {name:Type})
Table-valued parameters bound Array(T) (scalar) / Array(Tuple(...)) (entity) expanded with arrayJoin(@p); TypeName rejected, no stored procedures
Concat concat(a, b)
Coalesce coalesce
Boolean literal true / false
Identifier quoting backticks (as `t1`); embedded \ and backtick escaped
Derived table alias required
TVF alias required
*ALL supported
Recursive CTE not supported (the recursive modifier is omitted)
date_trunc dateTrunc('field', x)
Date arithmetic addDays(x, n) … addYears(x, (n) * 10); toLastDayOfMonth(x)
Date conversion / parts toDate/toDateTime/toDate32, toYear/… (as toInt32(...)), toStartOf*, toMonday, toInt32(toYYYYMM(...))/toInt32(toYYYYMMDD(...)), toInt64(toUnixTimestamp(...))
string_agg arrayStringConcat(groupArray(x), delimiter) (no array_agg)
Bit / statistical aggregates groupBitAnd/groupBitOr/groupBitXor; corr/covarPop/covarSamp
Regression aggregates not supported (regr_* is PostgreSQL-only)
Boolean aggregates not supported (bool_and/bool_or/every are PostgreSQL-only)
Filtered aggregate the -If combinator countIf/sumIf/avgIf/minIf/maxIf, emitted from the shared filter API (no ANSI filter (where ...))
ArgMin / ArgMax argMin/argMax
quantile / median quantile(0.5)(x), quantileExact(0.9)(x), quantileTiming(0.5)(x), median(x) (as toFloat64(...), aggregate: one row per group); generic percentile and the window percentile_cont/percentile_disc throw NotSupportedException
any_agg (arbitrary value) any(x) (cross-provider; ANY_VALUE(x) on MySQL)
any_last (last row) anyLast(x)
Conditional function iif(cond, a, b) → if(cond, a, b); multi_if(when(c1, v1), ..., otherwise(v)) → multiIf(c1, v1, ..., v)
Window functions percent_rank(), cume_dist(), nth_value(expr, n) supported; lag_in_frame/lead_in_frame → lagInFrame/leadInFrame (frame-respecting; the plain lag/lead reject an explicit frame on ClickHouse)
String JSON JSONExtractString, JSONExtractInt, JSONExtractFloat, JSONExtractBool, JSONExtractRaw, JSONHas, toInt64(JSONLength(...)), JSONType, JSONExtractKeys/JSONExtractArrayRaw (string[]), JSONExtractKeysAndValues (Tuple<string, T>[]), visitParamExtract*, JSON_VALUE/JSON_QUERY/JSON_EXISTS (JSONPath)
Native JSON functions json_all_paths → JSONAllPaths (projects as string[]), json_all_paths_with_types → JSONAllPathsWithTypes (native Map(String, String) surfaced as Dictionary<string, string>; mapKeys/mapValues bridge to a collection), to_json_string → toJSONString; all take a native JSON value (CAST(col AS JSON) for a String column), while a native JSON column type maps only as a bare JsonObject
Dictionaries dictGet, dictGetOrDefault, dictHas, dictGetHierarchy, dictGetChildren, dictIsIn (needs a configured CREATE DICTIONARY)
Session/info functions currentUser(), currentDatabase(), version() (session_user/current_schema are not available)
GROUP BY ... WITH TOTALS with totals (the extra totals row is not surfaced by ClickHouse.Driver)
LIMIT n BY expr limit [offset, ]n by col1, col2 (before the final LIMIT)
Query modifiers final, sample r [offset o], prewhere, settings k = v (FINAL/PREWHERE need a supporting table engine)
Table functions numbers/numbers_mt (the UInt64 number column is cast to Int64), zeros/zeros_mt (zero UInt8), generateRandom (the built-in generate_random()/generate_random(seed) fix the structure id UInt64, value Float64, name String and cast id to Int64), and the server/cluster functions url(url, format, structure), s3(url, format, structure), file(path, format, structure), remote(addresses, db, table), remote_secure(...), cluster(cluster, db, table), cluster_all_replicas(...) (the row shape is the caller's TRow interface)
Array functions over Array(T) columns/expressions: length, has, indexOf, hasAny, hasAll, startsWith, endsWith, hasSubstr, arrayStringConcat, splitByChar, arraySort, arrayReverse, arrayDistinct, range, arrayEnumerate, arrayCumSum, arraySlice, arrayPushBack; the CLR string.Split renders as splitByChar(separator, value) under StringSplit (one-character separator only); arrayJoin(array) expands one row per element, and EntityBuilder.ArrayJoin/LeftArrayJoin render the [left ]array join expr, ... clause. EntityBuilder.ArrayJoinElement/LeftArrayJoinElement additionally bind the expanded element to ArrayJoinProjection<TEntity, TElement>.Element (with the original entity at .Item1); the clause expression is aliased and p.Element references that alias (see ClickHouseFunctions, ArrayJoinKind, ArrayJoinProjection)
Tuple surface a native Tuple(...) column/expression projects as System.Tuple<...> (arity 1–7); Tuple.Create(a, b, ...) renders tuple(a, b, ...) and System.Tuple<...>.ItemN renders tupleElement(t, n), both under SupportsTupleFunctions; untuple is not supported (it changes the result column set)
Native JSON column type a native object-rooted JSON type: a [JsonColumn] property (Auto/Native: object-root POCO/JsonObject/JsonDocument/JsonElement) and a bare JsonObject/JsonDocument/JsonElement property (read and parameter) map to it; arrays/scalar roots fail closed. A bare top-level scalar Select(x => x.Doc) is not handled by the core projection classifier — use an anonymous type or a DTO. Text keeps a String column; a bare string and the legacy Object('json') alias are not supported. See JSON support
LOB streaming (ToStream/ToTextReader) NotSupportedException (the driver exposes no streaming getters)

String literals and LIKE escapes

An inline string literal is single-quoted with apostrophes and backslashes doubled and the other C0 characters written as \xNN. An ESCAPE clause is never emitted: the engine rejects every spelling of it, and the LIKE pattern literal already carries \%/\_. A custom LIKE escape character (the third argument of SqlFunctions.Sql.like) therefore fails closed with NotSupportedException naming ClickHouse.

select `Id` as `Id`
 from simple_entity
 where `Name` like 'a\%b'

Notes and limitations

  • The native arrayExcept exists only from ClickHouse 25.9. SqlFunctions.ClickHouse.array_except stays in the public API: an undeclared context keeps the pinned 25.8 refusal (NotSupportedException at translation time, no SQL), a context declaring 25.9 or later renders arrayExcept(...), and a context declaring an older version refuses with a message naming the required 25.9 and the declared version. See Declaring the server version.
  • GROUP BY ... WITH TOTALS renders correctly, but the official ClickHouse.Driver returns the totals row in a separate response block it does not expose through IDataReader, so only the group rows are materialised. Use it if your driver surfaces totals; nextorm itself only emits the modifier.
  • ClickHouse parameters are sent as HTTP query parameters. A bulk insert should use the provider's native binary path (see Bulk insert), which the query builder exposes through CreateBulkInsertBuilder; an INSERT ... VALUES statement remains the portable fallback (Mode.Portable).
  • Null parameter values cannot have their ClickHouse type inferred from the CLR value alone. When a query binds a null parameter, set an explicit parameter type at the driver level (for example with a custom resolver) or cast the placeholder in SQL.
  • ClickHouse rejects an explicit window frame on the standard lag/lead (and other non-frame-aware window functions) with BAD_ARGUMENTS. Use SqlFunctions.ClickHouse.lag_in_frame/lead_in_frame when the calculation must respect the frame; their lag/lead spelling is frame-agnostic.
  • multi_if returns the common supertype ClickHouse infers for its branches. When TResult is a numeric CLR type the whole call is cast to it (cast(multiIf(...) as Int64), Float64, ...), because ClickHouse would otherwise materialise the common type (for example UInt8 for small integer literals), which would not match the declared CLR TResult; non-numeric results (string, date) are not cast, so choose a TResult matching the branches for those.
  • The native-JSON functions (json_value/json_query/json_exists, json_all_paths/ json_all_paths_with_types/to_json_string) are gated by SupportsJsonExtract. The JSONAllPaths pair takes a native JSON value (cast a String column with CAST(col AS JSON)); json_all_paths projects as string[], and json_all_paths_with_types surfaces the native Map(String, String) as Dictionary<string, string> (mapKeys/mapValues bridge it to a collection). The native JSON column type is object-rooted and backs a [JsonColumn] property (Auto/Native: object-root POCO/JsonObject/ JsonDocument/JsonElement) as well as a bare JsonObject/JsonDocument/JsonElement projection and parameter; project through an anonymous type or a DTO because a bare top-level scalar Select(x => x.Doc) is not handled by the core projection classifier. An empty {} stays a non-null value and SQL NULL in Nullable(JSON) stays distinct from it. Arrays/scalar roots fail closed with NotSupportedException; a bare string read over a JSON column and the legacy Object('json') alias are not supported — the alias needs a connection-level allow_experimental_object_type (which nextorm never sets) and even then reads materialize as Tuple<SByte,String>, not through the native JsonObject transport (JSON support).
  • hits_v1 and similar wide tables have far more columns than an entity interface declares. Rather than mapping every column, project the extra ones by name with SqlFunctions.Column<T> (see Querying and projections); the name is a raw name, always safely quoted by the dialect (independently of the UseQuotedIdentifiers option).
  • The LOB streaming terminals (ToStream/ToTextReader) are rejected with NotSupportedException: ClickHouse.Driver exposes no streaming getters (see Streaming large objects).

See also


Source: src/nextorm.clickhouse/ClickHouseDialect.cs, src/nextorm.clickhouse/ClickHouseDataContext.cs, src/nextorm.clickhouse/DI/ClickHouseDataContextOptionsBuilderExtensions.cs, tests/nextorm.clickhouse.tests/Dialect/ClickHouseDialectTests.cs, tests/nextorm.clickhouse.tests/SqlGeneration/SqlGenerationTests.cs.