ClickHouse-specific SQL
ClickHouse has the largest provider-exclusive surface in nextorm: native arrays and
ARRAY JOIN, theLIMIT n BY exprandGROUP BY ... WITH TOTALSmodifiers, theFINAL/SAMPLE/PREWHERE/SETTINGSquery modifiers, join strictness andGLOBAL JOIN, the distributedGLOBAL INpredicate, its own aggregate, array, JSON and dictionary functions, and thenumbers/zerostable functions.
Prerequisites: Querying and projections · ClickHouse provider
Extension methods. Every ClickHouse-only member on this page (
ArrayJoin/LeftArrayJoin/ArrayJoinElement/LeftArrayJoinElement,LimitBy,WithTotals,Final,PreWhere,Settings,Sample,SemiJoin/AntiJoin/PasteJoinandj => j.Global()) is an extension method from thenextorm.clickhousepackage, not part of the commonnextormAPI. Addusing NextORM.ClickHouse;and the examples compile unchanged.
Arrays and ARRAY JOIN
ClickHouse has a native Array(T) type. Array functions operate on array columns or nested array
expressions, and the array-join clause expands one row per element:
var rows = dataContext.From<IArrayEntity>()
.LeftArrayJoin(e => e.Tags)
.Where(e => e.Id > 0)
.Select(e => new { e.Id })
.ToList();
select id
from array_entity
left array join tags where id > 0
The scalar array_join (one row per element, projectable) and the clause methods
ArrayJoin/LeftArrayJoin/ArrayJoinElement/LeftArrayJoinElement are covered in
Arrays (ClickHouse); the array functions are gated by
SupportsArrayFunctions, the clause by ArrayJoinClause.
LIMIT n BY expr
LimitBy returns the first n rows per distinct key, emitted
after ORDER BY and before the final LIMIT:
var rows = dataContext.From<IComplexEntity>()
.OrderBy(x => x.Id)
.LimitBy(2, x => x.Int)
.Select(x => new { x.Id, x.Int })
.ToList();
select id, nullableint
from complex_entity order by id limit 2 by nullableint
See Sorting and paging
(LimitBy).
GROUP BY ... WITH TOTALS
WithTotals appends the ClickHouse with totals modifier to a
grouping, adding a totals row for the whole result set. It is orthogonal to ROLLUP/CUBE and cannot
be combined with GROUPING SETS:
var rows = dataContext.From<IComplexEntity>()
.GroupBy(x => x.Int)
.Select(x => new { x.Int, Count = SqlFunctions.Sql.count() })
.WithTotals()
.ToList();
select nullableint, count(*)
from complex_entity group by nullableint with totals
See Grouping and aggregates
(SupportsGroupByWithTotals). The official
ClickHouse.Driver returns the totals block separately and does not surface it through IDataReader, so
only the group rows are materialised.
Query modifiers: FINAL, SAMPLE, PREWHERE, SETTINGS
Four builder methods render ClickHouse query-level modifiers: Final(), Sample(ratio[, offset]),
PreWhere(predicate) and Settings(("key", "value"), ...):
var rows = dataContext.From<IComplexEntity>()
.Final()
.PreWhere(c => c.Int > 0)
.Settings(("max_threads", "2"))
.Select(c => new { c.Id })
.ToList();
select id
from complex_entity final prewhere (nullableint > 0) settings max_threads = 2
The SETTINGS clause is always emitted last among the clauses the builder appends: after LIMIT n BY
and after LIMIT/OFFSET (the tail order is ORDER BY → LIMIT ... BY → LIMIT/OFFSET →
SETTINGS). ClickHouse parses a LIMIT that follows SETTINGS as a syntax error (server Code 62),
so this order is what makes a scalar terminal usable on a settings-bearing query: First,
FirstOrDefault, Single and SingleOrDefault add paging (limit 1 or limit 2), and the settings
clause follows it.
var count = dataContext.From<IComplexEntity>()
.Settings(("max_threads", "2"))
.Select(c => SqlFunctions.Sql.count())
.First();
select toInt32(count(*))
from complex_entity
limit 1
settings max_threads = 2
See Query hints
(SupportsFinal/SupportsSample/SupportsPreWhere/SupportsSettings).
FINAL/PREWHERE need a table engine that supports them — the Memory engine rejects both.
Join strictness and GLOBAL JOIN
ClickHouse join modifiers are passed to the join through its trailing lambda:
j => j.WithStrictness(JoinStrictness.Any | All | Asof) and j => j.Global():
var rows = dataContext.From<ISimpleEntity>()
.LeftJoin<IComplexEntity>((s, c) => s.Id == c.Id,
j => j.Global().WithStrictness(JoinStrictness.Any))
.Select(p => new { p.Item1.Id, p.Item2.String })
.ToList();
select t1.id, t2.somestring
from simple_entity as `t1`
global left any join complex_entity as `t2` on cast(t1.id as bigint) = t2.id
See Joins
(SupportsJoinStrictness/SupportsGlobalJoin).
ANY keeps one right-hand row per left-hand row, ALL keeps every match and ASOF needs one equi-join
column plus a final inequality; GLOBAL broadcasts the right side for distributed queries.
SEMI/ANTI/PASTE have dedicated builders: 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 (SupportsSemiAntiJoin/SupportsPasteJoin).
GLOBAL IN
SqlFunctions.ClickHouse.global_in renders the distributed
predicate over a subquery or a value list; negate with ! for GLOBAL NOT IN:
var values = new int[] { 1, 3 };
var ids = dataContext.From<ISimpleEntity>()
.Where(x => SqlFunctions.ClickHouse.global_in(x.Id, values))
.Select(x => new { x.Id })
.ToList();
select id
from simple_entity where global in (@p0, @p1)
See Filtering
(SupportsGlobalPredicates).
Aggregates
ClickHouse-exclusive aggregates live on ClickHouseFunctions
and are rejected by every other dialect. Their native result types (UInt64, Float32, the -If
integer types) do not always match the declared CLR return type, so the dialect wraps them in
toInt64(...)/toInt32(...)/toFloat64(...) casts that normalise the value to the type the method
declares:
- distinct-count aggregates
uniq/uniq_exact/uniq_combined/uniq_hll12(toInt64); - parameterised
quantile(level)(value)/quantile_exact/quantile_timingandmedian(toFloat64), multi-levelquantiles(level1, level2, ...)(value)(returnsArray(Float64), materialised as a CLRdouble[]; the levels must be an inline array); medianis the nativequantile-family aggregate (approximate; one row per group) and the only percentile-shaped name that renders: the genericSqlFunctions.ClickHouse.percentileand the windowpercentile_cont/percentile_discthrowNotSupportedException, and noquantile*alias is created for those portable spellings;- the most-frequent
topK(k)(value)/topKWeighted(k)(value, weight)aggregates (returnArray(T), materialised as a CLRT[]); - the last-row arbitrary-value aggregate
any_last(anyLast); - the array-returning aggregates
group_array/group_uniq_array(groupArray/groupUniqArray, materialised as a CLRT[];groupArrayof an array column yields a nestedT[][]); - the sequence/funnel aggregates
window_funnel/sequence_match/retention(windowFunnel/sequenceMatchwithtoInt32(...);retentionis normalised to the declaredint[]); - the shared filtered-aggregate API rendered as the
-IfcombinatorscountIf/sumIf/avgIf/minIf/maxIf; arg_min/arg_max;- the bitmap aggregates
group_bitmap/group_bitmap_and/group_bitmap_or/group_bitmap_xor(groupBitmap/groupBitmapAnd/groupBitmapOr/groupBitmapXor; the opaqueUInt64state) and the per-keysum_map/sum_map_filtered(sumMap/sumMapFiltered(keys)(key, value), surfaced as aMap).
Array-returning facades declare a closed CLR array type and the dialect normalises the server result to it
with exactly one outer cast(... as Array(...)): top_k, top_k_weighted, group_array,
group_uniq_array, retention (int[] → Array(Int32), never byte[]), array_map and all
three range overloads (long[] → Array(Int64)). The declared element type follows two rules: a
value-type element is spelled as declared (int → Array(Int32), int? →
Array(Nullable(Int32)); byte stays UInt8), while a reference-type element is declared
Nullable(...) unconditionally — a CLR nullable annotation on an element is erased at runtime, so it is
never consulted: string[] and string?[] both render Array(Nullable(String)), and a jagged
string[][] renders Array(Array(Nullable(String))) (the array arm is applied first, never
Nullable(Array(...))). A string[] result renders Array(Nullable(String)); the data, the materialised CLR
result type (string[]) and the public API are the same, and value-type elements (int[],
int?[], byte[]) are unaffected. Otherwise it is a type correction of the rendered call only: it
changes neither the lambda translation nor the operand rendering, so the higher-order lambda limitations
described below still apply. Binding is symmetric for input: a bound rank-1 array whose declared
element type is nullable is sent as Array(Nullable(T)) derived from the declared CLR type, so null
elements and their positions are preserved (see
Arrays (ClickHouse)).
Portable aggregates — including count, the arbitrary-value any_agg (rendered ANY_VALUE on MySQL and
MariaDB) and corr/covar* — are documented with the concept page, not here. A plain UInt64 column
(or any ulong/ulong? projection) materialises directly through the row reader's
DbDataReader.GetFieldValue<ulong> accessor, so only the function results above need the normalising
cast.
Native scalar functions, maps and hashes
Beyond the cross-provider scalar surface, ClickHouseFunctions exposes the native ClickHouse
functions that have no portable spelling. They render the exact camel-case name and are rejected by
every other dialect:
- UTF-8 case, trim and regexp/search strings:
lower_utf8/upper_utf8(lowerUTF8/upperUTF8),trim_left/trim_right/trim_both(trimLeft/trimRight/trimBoth, with an optional character set),replace_regexp_one/replace_regexp_all,match,extract,extract_all(projects asstring[]) andsplit_by_string/split_by_regexp/split_by_whitespace; - date and time:
format_date_time(formatDateTime, optional time zone),parse_date_time/parse_date_time_best_effort(parseDateTime/parseDateTimeBestEffort) and the current-date helpersnow/today/yesterday; - array set operations:
array_concat,array_flatten,array_uniq(arrayUniq, wrapped intoInt64),array_intersect,array_union,array_symmetric_difference, andarray_except(arrayExcept, available only when the context declares ClickHouse 25.9 or later — see Declaring the server version); - the
Mapfamily: themapconstructor (inlineTuple.Createpairs),map_keys/map_values,map_contains_key/map_contains_value,map_add/map_concat, and the public higher-order helpersmap_filter/map_apply/map_sort(tuple and array results, lowered to array forms) plusmap_all/map_exists(booleans, native predicates); - hashes: the six string-returning digests
md5/sha1/sha256/sha512/sip_hash128/murmur_hash3_128(MD5/SHA1/SHA256/SHA512/sipHash128/murmurHash3_128, lowercase hexadecimal) plus the numericxx_hash32/xx_hash64/xxh3,city_hash64,sip_hash64andmurmur_hash2_32/murmur_hash2_64/murmur_hash3_32/murmur_hash3_64; - math:
log2,cbrt,exp2andexp10(renderedlog2(x)/cbrt(x)/exp2(x)/exp10(x)), each with adoubleand adouble?overload returningFloat64/Nullable(Float64)(aNULLargument staysNULL); the other providers and the in-memory provider reject them; generate_ulid(generateULID).
Delivered families (#286, T286b)
The tables below complete the native surface. Every member is a typed ClickHouseFunctions method that
renders the exact vendor spelling, declares its CLR result shape explicitly and is rejected with
NotSupportedException by every other dialect (the shared IScalarFunctions/capability gate). The
pinned server is ClickHouse 25.8.33.6 (clickhouse/clickhouse-server:25.8-alpine); names introduced
after that version, and the CLR shapes that cannot be materialised, are listed under
Not yet supported.
| Family | Members |
|---|---|
| String | overlay, initcap, soundex, base64_encode, base64_decode, hex, unhex, edit_distance, jaro_similarity, byte_hamming_distance, substring_index, count_substrings, extract_groups, normalize_utf8_nfc, normalize_utf8_nfd, normalize_utf8_nfkc, normalize_utf8_nfkd |
| Numeric and rounding | erf, erfc, lgamma, tgamma, gcd, lcm, is_finite, is_infinite, is_nan, hypot, int_div, int_exp2, int_exp10, sigmoid, sqr; rounding round_age, round_bankers, round_down, round_duration, round_to_exp2 |
| Date and time | make_date, make_date32, make_date_time, now64, now_in_block, time_slot, time_slots, to_start_of_fifteen_minutes, to_start_of_five_minutes, to_start_of_ten_minutes, to_start_of_interval, to_iso_year, to_week, to_year_week, to_last_day_of_week, to_days_in_month, to_modified_julian_day, age, date_name, server_timezone, timezone_of, timezone_offset, to_time, timestamp, from_unix_timestamp, from_unix_timestamp64_milli, from_unix_timestamp64_micro |
| Array | array_element, array_enumerate_dense, array_zip, array_difference, array_avg, array_sum, array_min, array_max, array_product, array_top_k, array_bottom_k, array_random_sample, array_compact, array_pop_back, array_pop_front, array_push_front, array_resize, array_remove, array_rotate_left, array_rotate_right, array_shift_left, array_shift_right, array_fold, array_fill, array_reverse_fill, array_split, array_reverse_split, array_reverse_sort, array_partial_sort, array_sort, array_shingles, array_transpose, array_cum_sum_non_negative, count_equal, array_with_constant, array_jaccard_index, array_similarity |
| Tuple | tuple_concat, tuple_names, flatten_tuple, tuple_to_name_value_pairs, tuple_plus, tuple_minus, tuple_multiply, tuple_divide, tuple_hamming_distance, dot_product |
| Map | map_subtract, map_update, map_reverse_sort, map_partial_sort, map_from_arrays, map_populate_series, extract_key_value_pairs |
| JSON | json_merge_patch, json_array_length, json_dynamic_paths, json_shared_data_paths (each with its _with_types sibling), json_key, json_extract_keys_and_values_raw, json_extract_uint, is_valid_json, simple_json_extract_raw, simple_json_extract_int, simple_json_extract_uint, simple_json_extract_float, simple_json_extract_bool, simple_json_extract_string |
| Hash | sha224, sha384, sha512_256, blake3, md4, ripemd160, keccak256, farm_hash64, metro_hash64, java_hash, hive_hash, half_md5, url_hash, wy_hash64, int_hash32, int_hash64, ngram_min_hash, ngram_sim_hash, ngram_min_hash_arg, word_shingle_min_hash, word_shingle_sim_hash, word_shingle_min_hash_arg |
| Encoding | bin, unbin, bitmask_to_array, bitmask_to_list, bit_positions_to_array, sqid_encode, sqid_decode, hilbert_encode, hilbert_decode, morton_encode, morton_decode, base32_encode, base32_decode, base58_encode, base58_decode, base64_url_encode, base64_url_decode, bech32_encode, bech32_decode |
| UUID/ULID | uuid_num_to_string, uuid_string_to_num, uuid_to_num, uuid_v7_to_datetime, to_uuid, to_uuid_or_default, to_uuid_or_null, ulid_string_to_datetime, generate_snowflake_id, date_time_to_snowflake_id, date_time64_to_snowflake_id, date_time_to_uuid_v7, snowflake_id_to_date_time, snowflake_id_to_date_time64 |
| URL/IP | domain, protocol, path, query_string, fragment, port, cut_url_parameter, top_level_domain, encode_url_component, ipv4_num_to_string, ipv6_num_to_string, to_ipv4, to_ipv6, is_ipv4_string, is_ipv6_string, is_ip_address_in_range, ipv4_cidr_to_range, ipv6_cidr_to_range, extract_url_parameter, extract_url_parameter_names, cut_to_first_significant_subdomain, cut_to_first_significant_subdomain_with_www, cut_to_first_significant_subdomain_custom, cut_to_first_significant_subdomain_custom_with_www, url_hierarchy, url_path_hierarchy, domain_without_www, first_significant_subdomain, first_significant_subdomain_custom, path_full, query_string_and_fragment, cut_www, cut_query_string, cut_fragment, cut_query_string_and_fragment, decode_url_component, encode_url_form_component, decode_url_form_component, ipv4_string_to_num, ipv4_string_to_num_or_default, ipv4_string_to_num_or_null, ipv4_num_to_string_class_c, ipv6_string_to_num, ipv6_string_to_num_or_default, ipv6_string_to_num_or_null, ipv4_to_ipv6, cut_ipv6 |
| Bit | bitAnd, bitOr, bitXor, bitNot, bitShiftLeft, bitShiftRight, bitRotateLeft, bitRotateRight, bitCount, bitTest, bitTestAll, bitTestAny, bitHammingDistance |
| Type conversion | to_string, to_bool, to_fixed_string, to_low_cardinality, to_int8…to_int64, to_uint8…to_uint64, to_float32, to_float64, to_decimal32, to_decimal64, accurate_cast_* with the _or_null/_or_default variants, reinterpret_as_uint8…reinterpret_as_uuid |
| Aggregate | entropy, first_value, last_value, max_intersections (+max_intersections_position), simple_linear_regression, delta_sum, delta_sum_timestamp, gini, skew_pop, skew_samp, kurt_pop, kurt_samp, rank_corr, contingency, cramers_v, cramers_v_bias_corrected, theils_u, welch_t_test, mann_whitney_u_test, kolmogorov_smirnov_test, mean_z_test, analysis_of_variance, quantile_tdigest, quantile_gk, quantile_dd, quantile_bfloat16, quantile_prometheus_histogram, approx_top_k, approx_top_sum, any_heavy, max_map, min_map, sum_count, sum_kahan, group_array_moving_avg, group_array_moving_sum, group_array_sample, group_array_sorted, group_array_last, group_array_insert_at, group_array_intersect, group_concat, group_format, histogram, sparkbar, uniq_up_to, sequence_count, sequence_match_events, sequence_next_node, exponential_time_decayed_sum, exponential_time_decayed_max, exponential_time_decayed_count, exponential_time_decayed_avg, student_t_test, student_t_test_one_sample, quantiles, quantiles_exact, quantiles_exact_low, quantiles_exact_high, quantiles_exact_weighted, quantiles_timing, quantiles_tdigest, quantiles_bfloat16, quantiles_dd, quantiles_gk, quantiles_prometheus_histogram, time_series_group_sum, time_series_group_rate_sum, time_series_instant_rate_to_grid, time_series_rate_to_grid, time_series_resample_to_grid_with_staleness, group_array |
| Window | non_negative_derivative (returns WindowFunction<double>, complete it with Over(...)) |
| Dictionary | dict_get_or_null, dict_get_all, dict_get_descendants, region_to_name, region_to_city, region_to_area, region_to_district, region_to_country, region_to_continent, region_to_top_continent, region_to_population, region_hierarchy, region_in (plus the already shipped dict_get, dict_get_or_default, dict_get_hierarchy, dict_get_children, dict_has, dict_is_in) |
The declared CLR shapes are exact rather than inferred: UInt64/UInt32/UInt16 results are surfaced as
ulong/uint/int, UInt8 as byte or bool, FixedString dial results as hex text (binary UUID,
IPv6 and encoding results), Tuple/Array results as System.Tuple<...>/T[] with the component types
spelled out, and every nullable vendor argument propagates NULL instead of a default. Version-gated
members (array_top_k, array_bottom_k, array_remove, array_transpose, gini,
quantile_prometheus_histogram, group_format, the time_series_* grids) keep their native arm and pin
the server gap with a live UNKNOWN_FUNCTION/BAD_ARGUMENTS negative; sequence_next_node and the
time-series grids need allow_experimental_funnel_functions / allow_experimental_time_series_aggregate_functions.
The six string-returning digests are encoded server-side as lowercase hexadecimal, so a string?
projection receives the digest text rather than the raw FixedString bytes (which the driver would
decode lossily as UTF-8):
| Function | Rendered SQL | Characters |
|---|---|---|
md5 |
lower(hex(MD5(value))) |
32 |
sha1 |
lower(hex(SHA1(value))) |
40 |
sha256 |
lower(hex(SHA256(value))) |
64 |
sha512 |
lower(hex(SHA512(value))) |
128 |
sip_hash128 |
lower(hex(sipHash128(value))) |
32 |
murmur_hash3_128 |
lower(hex(murmurHash3_128(value))) |
32 |
Every character is in [0-9a-f]. A NULL argument propagates as NULL — it is never replaced by an
empty string or a default digest — and the value survives ordinary string materialisation unchanged
through an anonymous projection, a typed projection and Scalar<string> (a plain string, not a byte
array).
array_except keeps its place in SqlFunctions.ClickHouse for API compatibility and is gated by the
declared server version, because the native arrayExcept was introduced in ClickHouse 25.9:
| Declaration | Behaviour |
|---|---|
| none (the pinned 25.8 baseline, the default) | NotSupportedException with the historical The array_except function is not supported by this provider. — nothing is emitted |
earlier than 25.9 (for example 25.8) |
NotSupportedException naming the required 25.9 and the declared version, thrown before any SQL |
| 25.9 or later | the native arrayExcept(...), which takes exactly two arrays; any other arity is refused before SQL |
The declaration is per context, so two versions need distinct context subclasses; see Declaring the server version. The other array set operations are unaffected and never consult the version.
A native Map result (the map constructor, map_add, map_concat) is surfaced as
IDictionary<TKey, TValue>; combine it with map_keys/map_values or one of the contains
predicates when it is projected, because the row mapper does not materialise a bare IDictionary
column.
var rows = dataContext.From<Event>()
.Select(e => new
{
HasMatch = SqlFunctions.ClickHouse.match(e.Name, "error"),
Keys = SqlFunctions.ClickHouse.map_keys(SqlFunctions.ClickHouse.map(Tuple.Create("a", 1L))),
NewId = SqlFunctions.ClickHouse.generate_ulid()
})
.ToList();
The five higher-order helpers are public LINQ helpers whose declared CLR result type is part of the
contract; only map_all and map_exists keep a native server function:
map_filter— the entries matching the predicate, asTuple<TKey, TValue>[]. Lowered toarrayFilter(_t -> predicate, arrayZip(mapKeys(map), mapValues(map))), with the lambda's key and value rebound to the pair fields_t.1/_t.2;map_apply— one lambda result per entry, in map order, asTResult[]. Lowered toarrayMap((k, v) -> expression, mapKeys(map), mapValues(map));map_sort— the entries ordered by key, asTuple<TKey, TValue>[]. Lowered toarraySort(_t -> _t.1, arrayZip(mapKeys(map), mapValues(map)));map_all/map_exists—bool. Kept native and rendered asmapAll((k, v) -> predicate, map)/mapExists((k, v) -> predicate, map), lambda first.
The lowered filter, apply and sort expression is wrapped in a cast of the declared result type, not
of the type the server would infer from the operand — Tuple<string, int>[] becomes
cast(... as Array(Tuple(String, Int32))), a long apply result becomes
cast(... as Array(Int64)) — so the server reports exactly the element type the declaration promises
instead of a narrower one. Two rules decide that element:
- a value-type apply result stays bare —
int→Array(Int32); - a reference-type apply result is spelled
Nullable(...)—string→Array(Nullable(String))— because the declared element admitsnull(thedefault(string)branch of a projection is a realNULL) while ClickHouseStringdoes not; the materialised CLR type staysstring[].
The key/value tuple components of map_filter/map_sort stay bare unless the CLR component itself is
Nullable<T>, because the map keys and values are non-nullable.
-- map_filter: arrayFilter over the zipped pairs, then the declared-type cast
cast(arrayFilter(_t -> (_t.2 > 0),
arrayZip(mapKeys(map('a', 1, 'b', -1)), mapValues(map('a', 1, 'b', -1))))
as Array(Tuple(String, Int64)))
-- map_sort: arraySort by key over the same pairs, then the declared-type cast
cast(arraySort(_t -> _t.1,
arrayZip(mapKeys(map('b', 2, 'a', 1)), mapValues(map('b', 2, 'a', 1))))
as Array(Tuple(String, Int64)))
-- map_apply: one result per entry, cast as the declared TResult — bare for a value type,
-- Nullable(...) for a reference type that admits null
cast(arrayMap((k, v) -> (v * 2),
mapKeys(map('a', 1, 'b', -1)), mapValues(map('a', 1, 'b', -1)))
as Array(Int64))
cast(arrayMap((k, v) -> k,
mapKeys(map('a', 1, 'b', -1)), mapValues(map('a', 1, 'b', -1)))
as Array(Nullable(String)))
map_filter returns Tuple<TKey, TValue>[], because a map cannot be materialised from a projected
expression. map_sort returns a tuple array and map_apply returns TResult[].
No public path turns a filtered map back into a server-side Map: mapFilter is only reached through
the map_* lowering, map_keys/map_values require an IDictionary and therefore reject the tuple
array at compile time, and the higher-order array lambdas refuse member access on their parameter, so
arrayMap(t => t.Item2, ...) cannot be expressed. Read the returned tuples directly — plain LINQ
over the array once it is materialised: filtered.Select(t => t.Item2) for the values, or
filtered.ToDictionary(t => t.Item1, t => t.Item2) for a dictionary on the client.
The native Map surface — map_keys/map_values, map_contains_key/map_contains_value,
map_add/map_concat and the map constructor — stays IDictionary<TKey, TValue>-based and is
separate from these five helpers, which take a map operand and return arrays or booleans.
See Scalar functions.
JSON, dictionaries and array functions
- string-JSON extractors
JSONExtractString/JSONExtractInt/JSONExtractFloat/JSONExtractBool/JSONExtractRaw/JSONHas/JSONType,json_length, and the flat-JSONvisitParamExtract*; - array-returning
JSONExtractKeys/JSONExtractArrayRaw(json_extract_keys/json_extract_array_raw, projecting asstring[]) andJSONExtractKeysAndValues(json_extract_keys_and_values<T>, projecting asTuple<string, T>[];Tmust be a non-nullable ClickHouse type); - JSONPath scalars
JSON_VALUE/JSON_QUERY/JSON_EXISTS(over string JSON); - native-JSON functions
json_all_paths/json_all_paths_with_types(JSONAllPaths/JSONAllPathsWithTypes; the former projects asstring[], the latter's nativeMap(String, String)asDictionary<string, string>; both take a nativeJSONvalue) andto_json_string(toJSONString); - dictionary lookups
dict_get/dict_get_or_default/dict_hasand the hierarchydict_get_hierarchy/dict_get_children/dict_is_in(need a configuredCREATE DICTIONARY); - array functions
length/has/index_of/has_any/has_all/starts_with/ends_with/has_substr/array_string_concat/split_by_char/array_sort/array_reverse/array_distinct/range/array_enumerate/array_cum_sum/array_slice/array_push_back; - tuple constructor and element access:
System.Tuple.Create(a, b)renderstuple(a, b)andSystem.Tuple<...>.ItemNrenderstupleElement(t, n)(a wholeTuple(...)projects asSystem.Tuple<...>);untupleis not supported.
See Scalar functions and JSON support
(SupportsJsonExtract/SupportsDictionaries).
Table functions
numbers/numbers_mt, the row-count generators zeros/zeros_mt, generate_random and values
are available through
From with a runtime-factory call,
as are the server/cluster sources url, s3, file, remote, remote_secure, cluster and
cluster_all_replicas. Exactly these thirteen names are what ISqlDialect.SupportsTableFunction
accepts. The number UInt64 column is cast to Int64 inside a wrapping subquery so it materialises
as a CLR long; zeros materialises directly as byte:
var rows = dataContext.From(() => SqlFunctions.ClickHouse.zeros(3))
.Select(r => new { r.Value })
.ToList();
The external-engine table functions (mysql, postgresql, sqlite, jdbc, odbc, mongodb,
redis, hdfs, s3Cluster, azureBlobStorage, deltaLake, iceberg, hudi, paimon, bigquery,
gcs, ytsaurus, arrowFlight, prometheusQuery, input, view, format, null, executable,
primes, merge, dictionary, eval, loop, fuzzJSON, fuzzQuery) are not modelled: they
belong to the external-I/O boundary documented as §4 of the coverage catalog and are reachable only
through raw SQL or a [SqlFunction] wrapper. The dialect rejects every one of them, and a live
boundary test sweeps every function the server exposes in system.table_functions to prove that only
the thirteen names above are accepted.
Native JSON columns
ClickHouse's native JSON column is object-rooted (arrays and scalar roots are rejected). A
[JsonColumn] property (Auto or Native storage) maps an object-root POCO, JsonObject,
JsonDocument or JsonElement to it for both read and write; Text keeps a String column, and a
model that does not serialize to an object fails closed with NotSupportedException. Bare object-root
JsonObject/JsonDocument/JsonElement values also map for projection and parameter (the driver
surfaces JSON as JsonObject, and JsonDocument/JsonElement are adapted from it). Project through
a named shape (Select(x => new { x.Doc }) or a DTO): a bare top-level scalar Select(x => x.Doc) is
not handled by the core projection classifier. An empty {} stays a non-null value, while SQL NULL
in a Nullable(JSON) column stays distinct from it (null, or default/Undefined for the value-type
JsonElement). A bare string read over a JSON column and the legacy Object('json') alias are not
supported; the alias can be created only with a connection-level allow_experimental_object_type, which
nextorm never sets, and even then reads materialize as Tuple<SByte,String> rather than through the
native JsonObject transport. See
JSON support.
Null-safe comparison
ClickHouse has no usable scalar native form: isNotDistinctFrom(...) and the <=> operator are accepted
only in a JOIN ON section and fail elsewhere on 25.8 with server error Code 48 NOT_IMPLEMENTED
("Function isNotDistinctFrom can be used only in the JOIN ON section"), so both helpers are emulated as a
guarded predicate: ((a is null and b is null) or (a is not null and b is not null and a = b)), with
is_distinct_from as its not (...) complement. No sentinel normalisation is used, so NULL never
compares equal to a real value (including 0), and the scalar result is already UInt8. See
Null-safe comparison.
Not yet supported
The catalog closes the residual §3 set on the pinned 25.8.33.6 server: the members below exist as
typed refusal methods on ClickHouseFunctions and fail closed with a precise NotSupportedException
before any SQL reaches the provider — they are never silently rewritten, defaulted or truncated.
| Family | Refused member | Tested reason |
|---|---|---|
| Date/time | from_utc_timestamp |
The vendor result carries no zone in its type while its wall clock is the target-zone time, so the wall-clock/zone pair is not representable in System.DateTime. |
| Date/time | to_time64 |
Time64(scale) has no CLR carrier: TimeSpan maps to Int64 duration storage and the driver cannot materialise it. |
| Date/time | from_unix_timestamp64_nano |
DateTime64(9) exceeds DateTime's 100 ns tick resolution and would truncate silently. |
| Array | array_reduce, array_reduce_in_ranges |
The aggregate is named by a runtime SQL string, so no typed aggregate contract can close it and the string would reach the server as an opaque fragment. |
| Tuple | untuple |
It substitutes a dynamic, implementation-named column set into the call location, while nextorm maps one projection expression to exactly one result column. |
| JSON | pretty_print_json |
Introduced in ClickHouse 26.4; absent from the pinned 25.8.33.6 server (UNKNOWN_FUNCTION) and no documented equivalent composition exists. |
| JSON | json_extract_uint_or_default |
No such function exists; json_extract_uint already returns the vendor default 0 for a missing path/wrong type/malformed JSON and NULL for a NULL input. |
| Hash | ngram_sim_hash_arg, word_shingle_sim_hash_arg |
No such functions: only the MinHashArg forms return arguments; absent from system.functions on 25.8.33.6 and from the vendor reference. |
| Type conversion | to_int128, to_int256, to_uint128, to_uint256, reinterpret_as_int128, reinterpret_as_int256, reinterpret_as_uint128, reinterpret_as_uint256 |
The driver surfaces the 128/256-bit types as System.Numerics.BigInteger; the shared row materialiser has no BigInteger accessor, so the value or bit pattern cannot be materialised into a fixed-width CLR integer. |
| Type conversion | to_decimal128, to_decimal256 |
38/76 significant digits versus System.Decimal's 96-bit mantissa (28–29 digits): a value above that range would lose precision silently or throw OverflowException. |
| Type conversion | the eleven to_interval_* units |
The ClickHouse-only Interval* result types have no CLR representation and the driver itself cannot materialise them (Unknown type: Interval* on 25.8.33.6). |
| Aggregate | stochastic_linear_regression, stochastic_logistic_regression, exponential_moving_average |
The aggregate returns an opaque binary AggregateFunction state with no typed CLR result; only the -Merge combinator yields the value and it needs an AggregateFunction-typed column the provider cannot read. |
| Dictionary | dict_get_keys, dict_get_root |
Introduced in 25.12 and 26.7 respectively; absent from the pinned server. Use dict_get_hierarchy (last element for the root). |
| Dictionary | region_to_parent, region_to_parents |
No such functions in 25.8.33.6; the documented composition is region_hierarchy without the first element. |
Exercising the server/cluster table functions (url, s3, file, remote, remote_secure, cluster,
cluster_all_replicas) end-to-end against a real cluster is not covered and stays out of scope. See
Limitations and out-of-scope features.
See also
Source: src/nextorm.clickhouse/ClickHouseDialect.cs, src/nextorm.core/Query/SqlFunctions/SqlFunctions.ClickHouse.cs,
tests/nextorm.clickhouse.tests/SqlGeneration/SqlGenerationTests.cs.