ClickHouse provider
Use
nextorm.clickhousefor ClickHouse; it renders@nameparameters (rewritten to{name:Type}by the driver), backtick-quoted identifiers,concat(...)concatenation,limit/offsetpaging 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/PasteJoinandj => j.Global()— are not part of the commonnextormbuilder API. They ship with thenextorm.clickhousepackage as extension methods; addusing 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
UInt64columns andulong/ulong?properties/projections materialise through the row reader'sDbDataReader.GetFieldValue<ulong>accessor, so no SQL cast is needed (MySQL/MariaDBBIGINT UNSIGNEDbenefits from the same accessor); - string concatenation with the
concat(a, b, ...)function; coalesce(a, b),true/falseboolean literals andlengthUTF8(x)for string length;trimBoth/trimLeft/trimRightfor the three trim kinds;now()for local time andnow('UTC')for UTC;stdev/stdevp/var/varpasstddevSamp/stddevPop/varSamp/varPop;date_trunc(field, x)asdateTrunc('field', x)(the plural ANSI sub-second parts are singularised;decade/century/millenniumthrow), anddate_add/DateTime.Add*as the dedicatedaddYears/addQuarters/…/addSecondsfunctions (year/decade/centurypromote the operand toDateTime64(6)and scaleaddYears, so the SQL result type of those units isDateTime64(6)while the CLR API is unchanged;millenniumthrows 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_monthastoLastDayOfMonth(x);- the date conversion/part surface
SqlFunctions.ClickHouse.to_*(gated byDateConversion):to_date/to_date_time/to_date32astoDate/toDateTime/toDate32, theto_year/to_quarter/to_month/to_day_of_month/to_day_of_week/to_day_of_year/to_hour/to_minute/to_secondaccessors (and theDateTime.Year/Month/… projections) astoYear/toQuarter/… wrapped intoInt32(...)(the nativeUInt8/UInt16cannot be read back otherwise),to_start_of_*astoStartOfYear/toStartOfQuarter/toStartOfMonth/toStartOfWeek/toStartOfDay/toStartOfHour/toStartOfMinute/toStartOfSecond,to_mondayastoMonday(ISO Monday, whereastoStartOfWeekstarts on Sunday),to_yyyymm/to_yyyymmddastoInt32(toYYYYMM(...))/toInt32(toYYYYMMDD(...))andto_unix_timestampastoInt64(toUnixTimestamp(...)); string_agg(x, delimiter)asarrayStringConcat(groupArray(x), delimiter);bit_and/bit_or/bit_xorasgroupBitAnd/groupBitOr/groupBitXor,covar_pop/covar_sampascovarPop/covarSamp,corrascorr,arg_min/arg_maxasargMin/argMax, and an aggregate filter as the-Ifcombinator (countIf/sumIf/avgIf/minIf/maxIf, emitted for the sharedSqlFunctions.Sql.count/sum/avg/min/maxwhen they take a predicate); the distinct-count aggregatesuniq/uniq_exact/uniq_combined/uniq_hll12asuniq/uniqExact/uniqCombined/uniqHLL12wrapped intoInt64(...)to normalise the nativeUInt64to the declared CLRlong; the count aggregates (count/count_distinctand the filteredcountIf, pluscount_big/count_big_distinct) are cast the same way — totoInt32(...)for theint-returning variants andtoInt64(...)for the 64-bit ones; the parameterised quantile aggregatesquantile(level)(value)/quantileExact/quantileTimingandmedianwrapped intoFloat64(...)(so every variant materialises as a CLRdouble);medianis a plain aggregate (one row per group), and the genericSqlFunctions.ClickHouse.percentilehas no native spelling and throwsNotSupportedExceptionrather than aliasingquantile/median; the arbitrary-value aggregateany_aggasany(cross-provider:ANY_VALUE(x)on MySQL) and the last-row aggregateany_lastasanyLast; the array-returning aggregatesgroup_array/group_uniq_arrayasgroupArray/groupUniqArray(materialised as a CLRT[]); the sequence/funnel aggregateswindow_funnel/sequence_match/retentionaswindowFunnel/sequenceMatch/retention(windowFunnel/sequenceMatchare wrapped intoInt32(...);retentionis normalised server-side to the declaredint[], andtop_k,top_k_weighted,group_array,group_uniq_array,array_mapand all threerangeoverloads 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_typeasJSONExtractString/JSONExtractInt/JSONExtractFloat/JSONExtractBool/JSONExtractRaw/JSONHas/JSONType,json_lengthastoInt64(JSONLength(...)), the array-returningjson_extract_keys/json_extract_array_raw/json_extract_keys_and_valuesasJSONExtractKeys/JSONExtractArrayRaw/JSONExtractKeysAndValues(projecting asstring[]/Tuple<string, T>[]; the value type of the last is a non-nullable generic), and the flat-JSON fast pathvisit_param_extract_string/_int/_float/_bool/_rawasvisitParamExtractString/visitParamExtractInt/visitParamExtractFloat/visitParamExtractBool/visitParamExtractRaw; the JSONPath scalarsjson_value/json_query/json_existsasJSON_VALUE/JSON_QUERY/JSON_EXISTSand the native-JSON functionsjson_all_paths/json_all_paths_with_types/to_json_stringasJSONAllPaths/JSONAllPathsWithTypes/toJSONString(sameSupportsJsonExtractgate); the dictionary functionsdict_get/dict_get_or_default/dict_has/dict_get_hierarchy/dict_get_children/dict_is_inasdictGet/dictGetOrDefault/dictHas/dictGetHierarchy/dictGetChildren/dictIsIn; - the super-aggregate
GROUP BY ... WITH TOTALSmodifier (SupportsGroupByWithTotals) viaEntityBuilder.WithTotals(); - ClickHouse type names in casts (
Int32,Int64,Float64,Decimal(38, 10), …); limit n/limit n offset mpaging; an offset without a limit becomeslimit 18446744073709551615 offset m, because ClickHouse only acceptsoffsettogether withlimit;LIMIT n BY expr(LimitBy) viaEntityBuilder.LimitBy(...): at mostnrows per distinct key, emitted afterORDER BYand before the finalLIMIT;- the
numbers/numbers_mttable functions viaSqlFunctions.ClickHouse.numbers(...)(theUInt64numbercolumn is cast toInt64through a wrapping subquery so the row reader can materialise it) and thezeros/zeros_mtrow-count table functions viaSqlFunctions.ClickHouse.zeros(...)(IZerosRow, thezero UInt8column materialises directly asbyte); the server/cluster table functionsurl/s3/file/remote/remote_secure/cluster/cluster_all_replicasare also pre-declared (the row shape is declared by the caller's genericTRowinterface, which must match thestructureargument or the target table); thevaluestable function (SqlFunctions.ClickHouse.values<TRow>(tuples)) renders itsstructurefromTRow(SupportsResultSchema(TableFunctionSchema)) and takes the verbatim tuple list as its argument; - the query modifiers
FINAL/SAMPLE/PREWHERE/SETTINGSviaFinal(),Sample(ratio[, offset]),PreWhere(predicate)andSettings(("key", "value"), ...)(SupportsFinal/SupportsSample/SupportsPreWhere/SupportsSettings);FINAL/PREWHEREneed a table engine that supports them (theMemoryengine rejects both); - the portable conditional
iifasif(condition, a, b)(Iif,IIifRenderer.Render), the ClickHouse-only multi-branchmultiIfbuilt withwhen(...)/otherwise(...)(MultiIf,IMultiIfRenderer.Render), and thepercent_rank()/cume_dist(),nth_value(expr, n)and frame-respectinglagInFrame(value[, offset[, default]])/leadInFrame(...)window functions (SupportsPercentRankCumeDist,SupportsNthValue,SupportsInFrameWindowFunctions); - the distributed
GLOBAL INpredicate viaSqlFunctions.ClickHouse.global_in(over a subquery or a value list,SupportsGlobalPredicates); negate with C#!forGLOBAL NOT IN; - the join strictness/kind modifiers
ANY/ALL/ASOFviaJoinOptions.WithStrictnessin a join's trailing lambda (SupportsJoinStrictness,MakeJoinKeyword, enumJoinStrictness).LEFT ANY JOINkeeps a single right-hand row per left-hand row,ALLkeeps every match andASOFneeds one equi-join column plus a final inequality. TheSEMI/ANTI/PASTEkinds have dedicated builder methods:SemiJoin/AntiJoinreturn only the left-hand columns for left rows that do (respectively do not) have a match, andPasteJoinpairs the two sources by row position with noON(yielding as many rows as the shorter side). They are gated bySupportsSemiAntiJoin/SupportsPasteJoin. TheGLOBALvariant (resolved once and broadcast for distributed queries) is set withJoinOptions.Globaland 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 { ... })carriesMaxDegreeOfParallelismandUseSession:nullpreserves the configured value/session,truerequires an actual session id, andfalseexplicitly 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
DEFAULTcolumns are writable: a mapped ALIAS or MATERIALIZED column is refused by name (the catalog is read once per target).KeepIdentitywrites the explicit value. IgnoreDuplicates,Returning*,TableLock,FireTriggersandCheckConstraints=falseare refused;KeepNulls=falseneeds a default-equivalent shape.DatabaseNamerendersdb.table;ServerNameis refused (no rerouting).Timeoutis one whole-invocation budget with a linked cancellation; an expired budget is aTimeoutExceptionand 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
arrayExceptexists only from ClickHouse 25.9.SqlFunctions.ClickHouse.array_exceptstays in the public API: an undeclared context keeps the pinned 25.8 refusal (NotSupportedExceptionat translation time, no SQL), a context declaring 25.9 or later rendersarrayExcept(...), and a context declaring an older version refuses with a message naming the required25.9and the declared version. See Declaring the server version. GROUP BY ... WITH TOTALSrenders correctly, but the officialClickHouse.Driverreturns the totals row in a separate response block it does not expose throughIDataReader, 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; anINSERT ... VALUESstatement 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
nullparameter, 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) withBAD_ARGUMENTS. UseSqlFunctions.ClickHouse.lag_in_frame/lead_in_framewhen the calculation must respect the frame; theirlag/leadspelling is frame-agnostic. multi_ifreturns the common supertype ClickHouse infers for its branches. WhenTResultis 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 exampleUInt8for small integer literals), which would not match the declared CLRTResult; non-numeric results (string, date) are not cast, so choose aTResultmatching 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 bySupportsJsonExtract. TheJSONAllPathspair takes a nativeJSONvalue (cast aStringcolumn withCAST(col AS JSON));json_all_pathsprojects asstring[], andjson_all_paths_with_typessurfaces the nativeMap(String, String)asDictionary<string, string>(mapKeys/mapValuesbridge it to a collection). The nativeJSONcolumn type is object-rooted and backs a[JsonColumn]property (Auto/Native: object-root POCO/JsonObject/JsonDocument/JsonElement) as well as a bareJsonObject/JsonDocument/JsonElementprojection and parameter; project through an anonymous type or a DTO because a bare top-level scalarSelect(x => x.Doc)is not handled by the core projection classifier. An empty{}stays a non-null value and SQLNULLinNullable(JSON)stays distinct from it. Arrays/scalar roots fail closed withNotSupportedException; a barestringread over aJSONcolumn and the legacyObject('json')alias are not supported — the alias needs a connection-levelallow_experimental_object_type(which nextorm never sets) and even then reads materialize asTuple<SByte,String>, not through the nativeJsonObjecttransport (JSON support). hits_v1and similar wide tables have far more columns than an entity interface declares. Rather than mapping every column, project the extra ones by name withSqlFunctions.Column<T>(see Querying and projections); the name is a raw name, always safely quoted by the dialect (independently of theUseQuotedIdentifiersoption).- The LOB streaming terminals (
ToStream/ToTextReader) are rejected withNotSupportedException:ClickHouse.Driverexposes 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.