Arrays (PostgreSQL)
PostgreSQL has native array types. An array operand is always passed as a single parameter (the
whole array), never expanded into a value list, so the SQL text does not depend on the number of
elements and the plan stays cacheable. An array can be a runtime parameter (Parameter), a
captured local/field or an inline new[]. Only a dialect that opts in with SupportsArrays
(PostgreSQL) can render the array surface; every other provider throws NotSupportedException.
SqlFunctions.Postgres.any / SqlFunctions.Postgres.all accept an array, either as a complete predicate (column = any(@array))
or as the right-hand side of a comparison:
var ids = new long[] { 1, 2, 3 };
var rows = dataContext.From<IComplexEntity>()
.Where(e => SqlFunctions.Postgres.any(e.Id, ids)) // (id = any(@p0))
.Select(e => new { e.Id })
.ToList();
var same = dataContext.From<IComplexEntity>()
.Where(e => e.Id == SqlFunctions.Postgres.any(ids)) // id = any(@p0)
.Select(e => new { e.Id })
.ToList();
select id
from complex_entity where (id = any(@p0))
A runtime array parameter uses the same Parameter mechanism, so the array never has to be known when
the query is prepared:
var prepared = dataContext.From<IComplexEntity>()
.Where(e => e.Id == SqlFunctions.Postgres.any(SqlFunctions.Parameter<long[]>(0)))
.Select(e => new { e.Id })
.Prepare();
var rows = prepared.ToList(new long[] { 1, 2, 3 });
select id
from complex_entity where id = any(@norm_p0)
The array functions and operators map to their PostgreSQL names:
| C# | SQL |
|---|---|
SqlFunctions.Postgres.cardinality(a) |
cardinality(a) |
SqlFunctions.Postgres.array_length(a, dim) |
array_length(a, dim) |
SqlFunctions.Postgres.array_ndims(a) |
array_ndims(a) |
SqlFunctions.Postgres.array_lower(a, dim) |
array_lower(a, dim) |
SqlFunctions.Postgres.array_upper(a, dim) |
array_upper(a, dim) |
SqlFunctions.Postgres.array_position(a, element) |
array_position(a, element) |
SqlFunctions.Postgres.array_contains(a, b) |
a @> b |
SqlFunctions.Postgres.array_contained_by(a, b) |
a <@ b |
SqlFunctions.Postgres.array_overlaps(a, b) |
a && b |
SqlFunctions.Postgres.array_concat(a, b) |
a \|\| b |
SqlFunctions.Postgres.array_cat(a, b) |
array_cat(a, b) |
SqlFunctions.Postgres.array_append(a, element) |
array_append(a, element) |
SqlFunctions.Postgres.array_prepend(element, a) |
array_prepend(element, a) |
SqlFunctions.Postgres.array_remove(a, element) |
array_remove(a, element) |
SqlFunctions.Postgres.array_replace(a, from, to) |
array_replace(a, from, to) |
SqlFunctions.Postgres.array_fill(value, dims) |
array_fill(value, dims) |
SqlFunctions.Postgres.array_dims(a) |
array_dims(a) |
SqlFunctions.Postgres.array_positions(a, element) |
array_positions(a, element) |
SqlFunctions.Postgres.array_reverse(a) |
— (no PostgreSQL release provides array_reverse; refused at preparation) |
SqlFunctions.Postgres.array_sort(a) |
array_sort(a) (PostgreSQL 18+, explicit version required) |
SqlFunctions.Postgres.array_shuffle(a) |
array_shuffle(a) (PostgreSQL 16+) |
SqlFunctions.Postgres.array_sample(a, n) |
array_sample(a, n) (PostgreSQL 16+) |
SqlFunctions.Postgres.array_to_string(a, delimiter) |
array_to_string(a, delimiter) |
SqlFunctions.Postgres.string_to_array(s, delimiter) |
string_to_array(s, delimiter) |
array_reverseandarray_sortare gated per name bySupportsArrayFunction: the PostgreSQL provider refuses both at preparation, before any SQL is sent, with a plainNotSupportedException.array_reverseis refused on every PostgreSQL version — no release provides it — withThe PostgreSQL function 'array_reverse' is not supported by this provider.;array_sortis refused below PostgreSQL 18 and when the server version is unset, withThe PostgreSQL function 'array_sort' is not supported by this provider.The refusal is never aNULL, an unchanged array or a driver42883.
The functions that return an array (
array_append,array_cat,string_to_array, ...) can be used inside a query (a predicate,havingor a nested expression) or projected directly: the row reader materialises anArray(T)result as a CLRT[].
var rows = dataContext.From<IComplexEntity>()
.Where(e => SqlFunctions.Postgres.array_length(SqlFunctions.Parameter<long[]>(0), 1) == 3)
.Select(e => new { N = SqlFunctions.Postgres.cardinality(SqlFunctions.Parameter<long[]>(1)) })
.ToList();
select cardinality(@norm_p1) as "N"
from complex_entity where array_length(@norm_p0, 1) = 3
Shaping and expanding arrays (#281)
trim_array returns a shortened copy of a one-dimensional array, and the multi-array unnest, the
WITH ORDINALITY and the generate_subscripts forms expand arrays into rows. The six set-returning
members are FROM sources: call them inside ctx.From(() => ...) and project the members of their row
interface. The single-array unnest(array) source (IUnnestRow<T>, the unnest column) is described in
Table-valued functions.
The scalar member:
| C# | SQL |
|---|---|
SqlFunctions.Postgres.trim_array(array, n) |
trim_array(@p0, n) |
The set-returning members — the FROM fragment each one renders:
| C# | SQL | Row interface | Columns (C# ← SQL: CLR) |
|---|---|---|---|
SqlFunctions.Postgres.unnest(first, second) |
rows from (unnest(@p0), unnest(@p1)) as u(value1, value2) |
IUnnestMultiRow<T1, T2> |
Value1 T1 ← value1; Value2 T2 ← value2 |
SqlFunctions.Postgres.unnest(first, second, third) |
rows from (unnest(@p0), unnest(@p1), unnest(@p2)) as u(value1, value2, value3) |
IUnnestMultiRow<T1, T2, T3> |
Value1 T1 ← value1; Value2 T2 ← value2; Value3 T3 ← value3 |
SqlFunctions.Postgres.unnest_with_ordinality(array) |
rows from (unnest(@p0)) with ordinality as u(value, ordinality) |
IUnnestOrdinalityRow<T> |
Value T ← value; Ordinality long ← ordinality |
SqlFunctions.Postgres.unnest_with_ordinality(first, second) |
rows from (unnest(@p0), unnest(@p1)) with ordinality as u(value1, value2, ordinality) |
IUnnestOrdinalityMultiRow<T1, T2> |
Value1 T1 ← value1; Value2 T2 ← value2; Ordinality long ← ordinality |
SqlFunctions.Postgres.generate_subscripts(array, dimension) |
generate_subscripts(@p0, 1) |
IGenerateSubscriptsRow |
Value int ← generate_subscripts |
SqlFunctions.Postgres.generate_subscripts(array, dimension, reverse) |
generate_subscripts(@p0, 1, true) |
IGenerateSubscriptsRow |
Value int ← generate_subscripts |
Measured behaviour on PostgreSQL 17.11:
- Unequal arrays are padded with
NULL. One row is produced per position of the longest array, and every shorter array contributesNULLfrom that position on:unnest({1,2}, {a})yields(1, 'a'),(2, NULL). Two empty arrays yield no rows. - The ordinality column is
bigint, so it islongon the CLR side. It is one-based and shared by every admitted array of the call, sounnest_with_ordinality({10,20})yields(10, 1),(20, 2), and the two-array form returns the shared position alongside both arrays. - The admitted multi-array arity is one, two or three arrays. Four or more arrays are refused while
the source is composed —
NotSupportedException, no SQL emitted — withThe unnest function is refused for four or more arrays: multi-array unnest is FROM-clause grammar with no variadic arity, and this library admits exactly one, two and three arrays as typed overloads.PostgreSQL's multi-arrayunnestisFROM-clause grammar rather than apg_procfunction, so it has no variadic arity; the provider renders the admitted arities throughROWS FROM (...). generate_subscriptsnumbers dimensions from one and yields one row per valid subscript as anint;reverse: truegenerates them in descending order, and an empty (or SQL-NULL) array yields no rows.trim_arrayremoves the lastnelements and keeps the element type;nequal to the length yields an empty array,n = 0returns the array unchanged, andngreater than the length raises the server's error. Only one-dimensional arrays are accepted.- An array operand travels as one parameter of its own type —
SqlFunctions.Parameter<T[]>(idx)or a captured array — including the array of aFROMsource:generate_subscripts(SqlFunctions.Parameter<long[]>(0), 1).
var rows = dataContext
.From(() => SqlFunctions.Postgres.unnest_with_ordinality(SqlFunctions.Parameter<long[]>(0)))
.Select(r => new { r.Value, r.Ordinality })
.ToList(new long[] { 10, 20 });
select value, ordinality
from (select value, ordinality
from rows from (unnest(@norm_p0)) with ordinality as u(value, ordinality)) as "t1"
Arrays (ClickHouse)
ClickHouse has a native Array(T) type. The array functions operate on array columns (or on nested
array expressions) and are gated by ISqlDialect.SupportsArrayFunctions; arrayJoin additionally
requires SupportsArrayJoin. arrayJoin(array) expands the array into one row per element, so its
result can be projected like a scalar column.
| Function | SQL |
|---|---|
SqlFunctions.ClickHouse.length(a) |
length(a) |
SqlFunctions.ClickHouse.has(a, element) |
has(a, element) |
SqlFunctions.ClickHouse.index_of(a, element) |
indexOf(a, element) |
SqlFunctions.ClickHouse.has_any(a, b) |
hasAny(a, b) |
SqlFunctions.ClickHouse.has_all(a, b) |
hasAll(a, b) |
SqlFunctions.ClickHouse.starts_with(a, prefix) |
startsWith(a, prefix) |
SqlFunctions.ClickHouse.ends_with(a, suffix) |
endsWith(a, suffix) |
SqlFunctions.ClickHouse.has_substr(a, other) |
hasSubstr(a, other) |
SqlFunctions.ClickHouse.array_string_concat(a, delimiter) |
arrayStringConcat(a, delimiter) |
SqlFunctions.ClickHouse.split_by_char(separator, s) |
splitByChar(separator, s) |
SqlFunctions.ClickHouse.array_sort(a) |
arraySort(a) |
SqlFunctions.ClickHouse.array_reverse(a) |
arrayReverse(a) |
SqlFunctions.ClickHouse.array_distinct(a) |
arrayDistinct(a) |
SqlFunctions.ClickHouse.range(start, end) |
range(start, end) |
SqlFunctions.ClickHouse.array_enumerate(a) |
arrayEnumerate(a) |
SqlFunctions.ClickHouse.array_cum_sum(a) |
arrayCumSum(a) |
SqlFunctions.ClickHouse.array_slice(a, offset, length) |
arraySlice(a, offset, length) |
SqlFunctions.ClickHouse.array_push_back(a, element) |
arrayPushBack(a, element) |
SqlFunctions.ClickHouse.array_join(a) |
arrayJoin(a) |
SqlFunctions.ClickHouse.group_array(a) |
groupArray(a) |
SqlFunctions.ClickHouse.group_uniq_array(a) |
groupUniqArray(a) |
SqlFunctions.ClickHouse.array_map(f, a) |
arrayMap(f, a) |
SqlFunctions.ClickHouse.array_filter(f, a) |
arrayFilter(f, a) |
SqlFunctions.ClickHouse.array_exists(f, a) |
arrayExists(f, a) |
SqlFunctions.ClickHouse.array_all(f, a) |
arrayAll(f, a) |
SqlFunctions.ClickHouse.array_count(f, a) |
arrayCount(f, a) |
SqlFunctions.ClickHouse.array_first(f, a) |
arrayFirst(f, a) |
SqlFunctions.ClickHouse.array_first_index(f, a) |
arrayFirstIndex(f, a) |
SqlFunctions.ClickHouse.array_last(f, a) |
arrayLast(f, a) |
SqlFunctions.ClickHouse.array_last_index(f, a) |
arrayLastIndex(f, a) |
Delivered array, tuple and map functions (#286, T286b)
The members below complete the array catalogue on the pinned 25.8.33.6 server. Result shapes are
declared explicitly: element-wise predicates return bool, aggregations return the scalar/array CLR
type they declare, and the sort/split/fill families return T[].
| Function | SQL |
|---|---|
SqlFunctions.ClickHouse.array_element(a, i) |
arrayElement(a, i) |
SqlFunctions.ClickHouse.array_enumerate_dense(a) |
arrayEnumerateDense(a) |
SqlFunctions.ClickHouse.array_zip(a, b) |
arrayZip(a, b) |
SqlFunctions.ClickHouse.array_difference(a) |
arrayDifference(a) |
SqlFunctions.ClickHouse.array_avg(a) |
arrayAvg(a) |
SqlFunctions.ClickHouse.array_sum(a) |
arraySum(a) |
SqlFunctions.ClickHouse.array_min(a) |
arrayMin(a) |
SqlFunctions.ClickHouse.array_max(a) |
arrayMax(a) |
SqlFunctions.ClickHouse.array_product(a) |
arrayProduct(a) |
SqlFunctions.ClickHouse.array_top_k(a, k) |
arrayTopK(a, k) (version-gated: 26.6) |
SqlFunctions.ClickHouse.array_bottom_k(a, k) |
arrayBottomK(a, k) (version-gated: 26.6) |
SqlFunctions.ClickHouse.array_random_sample(a, n) |
arrayRandomSample(a, n) |
SqlFunctions.ClickHouse.array_compact(a) |
arrayCompact(a) |
SqlFunctions.ClickHouse.array_pop_back(a) |
arrayPopBack(a) |
SqlFunctions.ClickHouse.array_pop_front(a) |
arrayPopFront(a) |
SqlFunctions.ClickHouse.array_push_front(a, element) |
arrayPushFront(a, element) |
SqlFunctions.ClickHouse.array_resize(a, size[, extender]) |
arrayResize(a, size[, extender]) |
SqlFunctions.ClickHouse.array_remove(a, f) |
arrayRemove(a, f) (version-gated: 25.11) |
SqlFunctions.ClickHouse.array_rotate_left(a, n) / array_rotate_right(a, n) |
arrayRotateLeft / arrayRotateRight |
SqlFunctions.ClickHouse.array_shift_left(a, n) / array_shift_right(a, n) |
arrayShiftLeft / arrayShiftRight |
SqlFunctions.ClickHouse.array_fold(f, a, init) |
arrayFold(f, a, init) |
SqlFunctions.ClickHouse.array_fill(f, a) / array_reverse_fill(f, a) |
arrayFill / arrayReverseFill |
SqlFunctions.ClickHouse.array_split(f, a) / array_reverse_split(f, a) |
arraySplit / arrayReverseSplit |
SqlFunctions.ClickHouse.array_reverse_sort(a) / array_partial_sort(a, k) |
arrayReverseSort / arrayPartialSort |
SqlFunctions.ClickHouse.array_sort(a) |
arraySort(a) |
SqlFunctions.ClickHouse.array_shingles(a, n) |
arrayShingles(a, n) |
SqlFunctions.ClickHouse.array_transpose(a) |
arrayTranspose(a) (version-gated: 26.4) |
SqlFunctions.ClickHouse.array_cum_sum_non_negative(a) |
arrayCumSumNonNegative(a) |
SqlFunctions.ClickHouse.count_equal(a, b) |
countEqual(a, b) |
SqlFunctions.ClickHouse.array_with_constant(n, value) |
arrayWithConstant(n, value) |
SqlFunctions.ClickHouse.array_jaccard_index(a, b) / array_similarity(a, b) |
arrayJaccardIndex / arraySimilarity |
SqlFunctions.ClickHouse.tuple_concat(a, b) |
tupleConcat(a, b) |
SqlFunctions.ClickHouse.tuple_names(t) |
tupleNames(t) |
SqlFunctions.ClickHouse.flatten_tuple(t) |
flattenTuple(t) |
SqlFunctions.ClickHouse.tuple_to_name_value_pairs(t) |
tupleToNameValuePairs(t) |
SqlFunctions.ClickHouse.tuple_plus / tuple_minus / tuple_multiply / tuple_divide |
tuplePlus / tupleMinus / tupleMultiply / tupleDivide |
SqlFunctions.ClickHouse.tuple_hamming_distance(a, b) |
tupleHammingDistance(a, b) |
SqlFunctions.ClickHouse.dot_product(a, b) |
dotProduct(a, b) |
SqlFunctions.ClickHouse.map_subtract(a, b) |
mapSubtract(a, b) |
SqlFunctions.ClickHouse.map_update(a, b) |
mapUpdate(a, b) |
SqlFunctions.ClickHouse.map_reverse_sort(m[, f]) / map_partial_sort(m, k[, f]) |
mapReverseSort / mapPartialSort |
SqlFunctions.ClickHouse.map_from_arrays(keys, values) |
mapFromArrays(keys, values) |
SqlFunctions.ClickHouse.map_populate_series(m, start, end[, step]) |
mapPopulateSeries(...) |
SqlFunctions.ClickHouse.extract_key_value_pairs(s) |
extractKeyValuePairs(s) |
arrayReduce/arrayReduceInRanges stay refused: the aggregate is named by a runtime SQL string, so no
typed contract can close it (see
ClickHouse-specific SQL › Not yet supported).
untuple is refused for the same structural reason: one projection expression maps to exactly one
result column.
length/indexOfreturnUInt64natively, so the dialect casts them withtoInt64(...). Functions that return an array (split_by_char,array_sort,array_reverse,array_distinct,range,array_enumerate,array_cum_sum,array_slice,array_push_back,group_array,group_uniq_array) can be projected directly — the row reader materialises anArray(T)result as a CLRT[]— or used as the operand of another array function (for examplelength(...)orarray_string_concat(...)). The facades whose declared CLR array type can differ from the element type ClickHouse infers (top_k,top_k_weighted,group_array,group_uniq_array,retention,array_map, and the threerangeoverloads) are normalised server-side: the dialect wraps the completed call in exactly onecast(... as Array(...))built from the closed declared CLR type (int[]→Array(Int32),int?[]→Array(Nullable(Int32)),long[]→Array(Int64),string[]/string?[]→Array(Nullable(String))). A reference-type element is always declaredNullable(...), because its CLR nullable annotation is erased at runtime, so no caller-side cast is required andretentionmaterialises asint[], notbyte[]. The same reader materialises a nativeTuple(...)column (or aTuple(...)-returning expression) as aSystem.Tuple<...>of arity 1–7.
SqlFunctions.ClickHouse.array_exceptis retained for API compatibility. An undeclared context — the pinned 25.8 baseline — has no nativearrayExcept(it exists only from ClickHouse 25.9), so the call is refused withNotSupportedExceptionat translation time instead of being emitted as SQL. A context that declares 25.9 or later through the versionedClickHouseDataContextconstructor renders the nativearrayExcept(...)for the two-array form only (the native function takes exactly two arrays; any other arity is refused before SQL); a declared older version still refuses, naming the required25.9and the declared version. See Declaring the server version.
The array relation predicates return
bool:starts_with(array, prefix)/ends_with(array, suffix)test a prefix/suffix andhas_substr(array, other)tests thatotheroccurs inarraycontiguously and in order (an emptyotheris always contained). They require a provider that supports the array functions.
The row-value (tuple) surface is cross-provider and built from
System.Tuple.Create/new Tuple<...>/System.Tuple<...>.ItemN: a constructor renders through the dialect's row constructor —tuple(a, b)on ClickHouse,ROW(a, b)on PostgreSQL, and the flat ANSI(a, b)on MySQL/MariaDB/SQLite — and access to an element of a server-side row renders through the dialect's positional access (tupleElement(pair, 1)on ClickHouse,(pair).f1on PostgreSQL). MySQL/MariaDB/SQLite have no server-side element access, soSystem.Tuple<...>.ItemNon a server row throwsNotSupportedExceptionthere. Access to an element of an inline constructor (Tuple.Create(a, b).Item1,new ValueTuple<...>(a, b).Item2) folds to the argument and works on every dialect that can express the constructor.System.Tuple<,> ==(reference equality in C#) is reinterpreted as a SQL row-value comparison (Tuple.Create(x.A, x.B) == Tuple.Create(1, 'a')→ROW(a, b) = ROW(1, 'a'));ValueTuple==cannot appear in an expression tree. On PostgreSQL and ClickHouse the surface is the native row type (ITupleRenderer/ISqlDialect.Tuple); on MySQL/MariaDB/SQLite the flat(a, b)constructor is accepted only as a direct comparison operand inWHERE/HAVING/JOIN ON— inSelect/ORDER BY/GROUP BYor as a function argument it throwsNotSupportedExceptionat preparation. SQL Server and the in-memory provider reject that row-constructor/row-comparison surface; the in-memory provider still evaluates a tupleIN/Containsvalue list in process (it is not rejected). TupleIN/Containsover a value list of flatSystem.Tuple/System.ValueTupleentries of arity 1..7 is shipped (#193); the exact provider forms and semantics are documented in the tupleINblock below. Materialising a rawROW(...)is shipped on PostgreSQL (#194): a singleROW(...)(arity 1..7) or a caller-registered named composite result column materialises into the matchingSystem.Tuple<...>/named type (see Raw SQL);ValueTuple, arity ≥8, a nested/emptyROW, several or mixed record columns, an unregistered named composite and a composite declared asSystem.Tupleremain guarded withNotSupportedException.untupleis not supported because it changes the result column set rather than producing a scalar.
Tuple
IN/Containstranslates a value list of flatSystem.Tuple<...>/System.ValueTuple<...>entries of arity 1..7 (SqlFunctions.Sql.@in,SqlFunctions.ClickHouse.global_inorEnumerable.Contains) into a row membership test built from the dialect's row constructor, with the ordered components as separate parameters (never one opaque tuple parameter). The exact provider forms:
Provider Tuple INSQLSQLite (a, b) IN (VALUES (@p0, @p1), (@p2, @p3))MySQL / MariaDB (a, b) IN ((@p0, @p1), (@p2, @p3))PostgreSQL ROW(a, b) IN (ROW(@p0, @p1), ROW(@p2, @p3))ClickHouse tuple(a, b) IN (tuple(@p0, @p1), tuple(@p2, @p3))(global_inrendersGLOBAL IN)SQL Server not supported — every tuple form throws NotSupportedExceptionwithSQL Server does not support tuple IN/Contains translation., raised before the collection is evaluated (a null collection, a null reference-tuple entry, arity ≥8 or a nested tuple all surface this message, never an evaluation error)In-memory evaluated in process (no SQL is rendered; Enumerable.Containsruns against the in-memory rows)An empty (non-null) collection renders
1 = 0. On the providers that support the surface, a null collection throwsArgumentNullException, adefaultvalue tuple is an ordinary row, and a nullable tuple element (List<(int, int)?>) throwsNotSupportedException. Membership is two-valued, so a null component matches another null component: rows whose cells are all non-null use the provider's native row list, guarded withIS NOT NULLon every nullable left-hand component, while a row that contains a null is rendered as an explicit OR-of-AND arm (c1 IS NULL/c1 IS NOT NULL AND c1 = @p), because a plain rowINcan never match SQLNULL. A null reference-tuple entry (nullinside the collection) throwsNotSupportedExceptionrather than degrading into an all-null row, and arity ≥8, theRestform and nested tuple components throwNotSupportedException; on SQL Server all of these surface the provider rejection above instead. Deferred in the current milestone: a tuple-typedQueryCommand(subquery) right-hand side andRest/nested tuples are not supported yet; no future milestone is promised.
The higher-order (lambda) functions take an inline C# lambda whose parameter is the array element, for example
array_map(v => -v, e.Nums)rendersarrayMap(v -> -(v), nums).array_exists/array_allreturnbool;array_count/array_first_index/array_last_indexreturnlong(the dialect casts the nativeUInt32withtoInt64(...));array_first/array_lastreturn the element or its default value when nothing matches. They require a provider that supports the higher-order array functions (seeSupportsHigherOrderArrayFunctions; ClickHouse). ClickHouse promotes the arithmetic result type independently of C# (anInt32element multiplied by an integer literal becomesArray(Int64)); the dialect normalises the completedarrayMapresult to the closed declared CLR element type, so the lambda operand stays exactly as written and no(long)v-style cast inside the lambda is needed (arrayFilteralready returns its input element type).
Nullable elements in a bound array parameter: a rank-1 CLR array whose declared element type is nullable (
Nullable<T>orstring) is bound asArray(Nullable(T))derived from that declared CLR type (int?[]→Array(Nullable(Int32)),double?[]→Array(Nullable(Float64)),DateTime?[]→Array(Nullable(DateTime)),decimal?[]→Array(Nullable(Decimal(38,10))),string?[]→Array(Nullable(String))), sonullelements and their positions reach the server —array_map(v => v, new int?[] { 1, null, 3 })yields{ 1, null, 3 }. Empty and all-nullarrays carry the same typed element, and the caller's array is not mutated. Non-nullable element arrays (int[],double[],DateTime[],decimal[],byte[]) are unchanged; nested/jagged and multidimensional arrays and anullarray object are not supported. Array results follow the same element rule: a reference-type element result is declaredNullable(...)unconditionally (astring[]/string?[]result rendersArray(Nullable(String)), a jagged oneArray(Array(Nullable(String)))) and materialises as a CLRstring[]; the element declaration affects only the rendered SQL text, not the data, the CLR type or the public API, while value-type elements (int[],int?[],byte[]) are unaffected. Decimal array results are materialised in both shapes:Array(Nullable(Decimal(38,10)))→decimal?[]andArray(Decimal(38,10))→decimal[], through the direct, member-bound, multi-column and raw read routes, with every representable value preserved exactly (no rounding, truncation or culture conversion). ADecimal(38,10)element whose scaled mantissa exceeds 96 bits — for example1e19, whose mantissa at scale 10 is1e29while CLRdecimal's mantissa is2^96 − 1— is not representable in CLRdecimal: the materialisation fails closed with the driver'sOverflowException(Value cannot be represented as System.Decimal), and because the pinned driver does not trim trailing zeros a mathematically smaller representation is not substituted. The remaining array restrictions are listed under Limitations.
The CLR string.Split is rendered as splitByChar(separator, value) (gated by
StringSplit); only a single-character
separator is supported (the multi-character splitByString is not exposed), the result is a string[]
that can be projected directly or used inside another array function, and the count overload, multiple separators and
StringSplitOptions other than None throw NotSupportedException:
var parts = dataContext.From<IComplexEntity>()
.Select(e => SqlFunctions.ClickHouse.length(e.String!.Split(',')))
.First();
var tags = dataContext.From<IArrayEntity>()
.Where(e => e.Id == 1)
.Select(e => new { e.Id, Tag = SqlFunctions.ClickHouse.array_join(e.Tags) })
.ToList();
select id, arrayJoin(tags) as `Tag`
from array_entity where id = 1
EntityBuilder.ArrayJoin/LeftArrayJoin render the [LEFT] ARRAY JOIN clause, which expands the rows
before WHERE/GROUP BY; LEFT ARRAY JOIN keeps a row whose array is empty. The expanded element is
not bound to a CLR member, so use the scalar array_join above when the value must be projected or
filtered.
var ids = dataContext.From<IArrayEntity>()
.LeftArrayJoin(e => e.Tags)
.Select(e => e.Id)
.ToList();
select id
from array_entity
left array join tags
EntityBuilder.ArrayJoinElement/LeftArrayJoinElement add the same clause but return
EntityBuilder<ArrayJoinProjection<TEntity, TElement>>, so both the original entity (p.Item1) and the
expanded element (p.Element) can be referenced. The clause expression is aliased and p.Element
translates to that alias:
var rows = dataContext.From<IArrayEntity>()
.ArrayJoinElement(e => e.Tags)
.Where(p => p.Element == "b")
.Select(p => new { p.Item1.Id, Tag = p.Element })
.ToList();
select id, __nextorm_aj_element as `Tag`
from array_entity
array join tags as __nextorm_aj_element
where __nextorm_aj_element = 'b'
A bound array join is only supported on a single, un-joined source; Where/Having must be applied
after it (their parameter type is the array-join projection). Use ArrayJoin/LeftArrayJoin with the
scalar array_join when you need multiple arrays or a joined query.