SQL Server-specific SQL
SQL Server contributes the
CHOOSEconditional function, the T-SQL-only scalar library (PATINDEX,QUOTENAME,SOUNDEX,DIFFERENCE,STRING_ESCAPE,UNICODE,NCHAR,FORMAT, the trigonometric functions,DATENAME,DATE_BUCKET,HASHBYTES,NEWSEQUENTIALID, the clock/offset/*FROMPARTSdate family, theCHECKSUM/COMPRESS/RAND/STUFFscalars, the metadata functions and the SQL/JSON constructors/aggregates), statement/table hints and theFOR JSON/FOR XMLresult shape, the XML data-type methods (value/query/existand thenodesrowset), the nativePIVOT/UNPIVOTsource constructs, plusstring_split/openjsontable functions.
Prerequisites: Querying and projections · SQL Server provider
CHOOSE
SqlFunctions.SqlServer.choose(index, v1, v2, ...) renders CHOOSE(index, v1, v2, ...), gated by
SupportsChoose (SQL Server only; every other provider
throws).
var rows = dataContext.From<IComplexEntity>()
.Select(c => new { Label = SqlFunctions.SqlServer.choose(2, "a", "b", "c") })
.ToList();
select choose(2, 'a', 'b', 'c') as [Label]
from complex_entity
See Scalar functions.
Hints
WithTableHint attaches a table hint to the query's FROM table
(SupportsTableHints), and a query hint renders the
trailing OPTION (...) clause, for example OPTION (RECOMPILE)
(SupportsQueryHints); the CTE maxRecursion
option maps to option (maxrecursion n). See Query hints.
Row locking
SQL Server has no trailing FOR UPDATE clause, so
ForUpdate and
ForShare attach the updlock/holdlock table hint to
the query's primary source instead
(ILockRenderer.UsesTableHints, ILockRenderer.Render):
var rows = dataContext.From<ISimpleEntity>().ForUpdate().Select(e => e.Id).ToList();
select id
from simple_entity with (updlock)
ForUpdate renders updlock (update lock held to the end of the transaction) and ForShare renders
holdlock (shared lock). The lock hint combines with
WithTableHint, so dataContext.From<ISimpleEntity>(o => o.WithTableHint("rowlock")).ForUpdate()
renders with (rowlock, updlock). See
Row locking.
FOR JSON and FOR XML
ForJson/ForXml are terminals that execute the query and return the whole result set as one JSON or
XML document
(SupportsForJson/SupportsForXml);
WithForJson/WithForXml attach the clause without executing; the two clause kinds are mutually
exclusive. See JSON support across providers.
XML data-type methods
SqlFunctions.SqlServer.xml_value<T>(xml, xpath, sqlType), xml_query(xml, xpath) and
xml_exist(xml, xpath) render the postfix T-SQL XML methods
(XmlFunctions); the XQuery
and the SQL type must be string literals, and xml_exist returns bit (compared with 1 in a
predicate context).
var rows = dataContext.From<IXmlEntity>()
.Select(x => new
{
Value = SqlFunctions.SqlServer.xml_value<string>(x.Payload, "(/root/item)[1]", "nvarchar(100)"),
Exists = SqlFunctions.SqlServer.xml_exist(x.Payload, "/root/item[2]")
})
.ToList();
select payload.value('(/root/item)[1]', 'nvarchar(100)') as [Value],
payload.exist('/root/item[2]') as [Exists]
from xml_entity
The rowset method .nodes is a correlated CrossApply/OuterApply source: xml_nodes(xml, xpath)
unfolds the XML value into one row per node and renders <xml>.nodes('xpath') as [alias]([value]),
after which the unfolded IXmlNodesRow.Value is projected with the scalar methods above. The operand
must be a column of the outer row and the XQuery a string literal.
var rows = dataContext.From<IXmlEntity>()
.CrossApply(x => SqlFunctions.SqlServer.xml_nodes(x.Payload, "/root/item"))
.Select(p => new { Id = SqlFunctions.SqlServer.xml_value<int>(p.Item2.Value, "(.)[1]/@id", "int") })
.ToList();
select t2.value.value('(.)[1]/@id', 'int') as [Id]
from xml_entity as [t1]
cross apply t1.payload.nodes('/root/item') as [t2](value)
See Scalar functions.
Table-valued functions
string_split(...) and openjson(...) are exposed through
From with a runtime-factory call. See
Table-valued functions and
JSON support across providers.
Temporal tables
ForSystemTime queries system-versioned (temporal) tables
(SupportsTemporalTable): AS OF,
BETWEEN ... AND ..., FROM ... TO ..., CONTAINED IN (...) and ALL are available through the
TemporalClause factory methods.
var rows = dataContext.From<ISimpleEntity>()
.ForSystemTime(TemporalClause.AsOf(new DateTime(2020, 1, 1)))
.Select(e => new { e.Id })
.ToList();
select id
from simple_entity for system_time as of '2020-01-01 00:00:00'
MariaDB supports the same clause on system-versioned tables, except CONTAINED IN.
PIVOT / UNPIVOT
EntityBuilder.Pivot reshapes a source into columns with
the native T-SQL PIVOT operator and
Unpivot stacks columns into rows with UNPIVOT
(Pivot /
Pivot). They are extension methods in the NextORM.SqlServer
namespace; add using NextORM.SqlServer; to use them. The result is an untyped source:
select the grouping columns and the pivoted columns by name through TableAlias. The operators apply to
a table expression, so the source may be a plain table/entity, a table-valued function or a derived
query (ctx.From(derivedQuery)); filters and other modifiers belong to the reshaped result (or, for a
derived source, inside the derived query).
var rows = ctx.From<Sales>()
.Pivot(PivotAggregate.Sum, s => s.Margin, s => s.Quarter, PivotValue.Create("1"), PivotValue.Create("2"))
.Select(t => new
{
Category = t.GetString("category"),
Q1 = SqlFunctions.Column<decimal?>(t, "1"),
Q2 = SqlFunctions.Column<decimal?>(t, "2")
})
.ToList();
var stacked = ctx.From<Quarterly>()
.Unpivot("value", "quarter", UnpivotColumn.Create("q1"), UnpivotColumn.Create("q2"))
.Select(t => new { Quarter = t.GetString("quarter"), Value = t.GetNullableDecimal("value") })
.ToList();
Raw names, always safely quoted. The identifier strings of these APIs are raw, unescaped names, not
SQL: PivotValue.Create carries the pivot data value, UnpivotColumn.Create a source column name,
and the pivoted/stacked result columns are read by name. Every emitted occurrence goes through exactly
one provider quoting pass, independently of the context's
UseQuotedIdentifiers option
— with quoting off a by-name accessor still emits [1] on SQL Server / "1" on
PostgreSQL and SQLite / `1` on MySQL and ClickHouse. A dot is a literal name character (never a
schema/alias qualifier), SQL-looking punctuation stays inside the one quoted identifier, and manual
pre-quoting is not supported: Column(t, "[1]") is the literal three-character name [1] (rendered
[[1]]] on SQL Server), so to reference the column named 1 write Column(t, "1") — which is why the
example above reads the pivoted columns through
SqlFunctions.Column<T> and not
GetNullableDecimal("[1]"). On SQL Server an embedded ] is doubled, so the pivot value a]b renders
[a]]b] in both the IN (...) list and the projection reference.
A derived query can be the pivoted source, which lets the PIVOT input carry joins and computed
aggregate/FOR columns (the equivalent of pivoting a CTE):
var orderMargins = ctx.From<OrderDetail>()
.Join<OrderHeader>((d, h) => d.OrderId == h.OrderId)
.Where(p => SqlFunctions.Sql.extract("year", p.Item2.OrderDate) == 2013)
.Select(p => new
{
CategoryName = p.Item3.Name,
QuarterNum = SqlFunctions.Sql.extract("quarter", p.Item2.OrderDate),
Margin = p.Item1.LineTotal - p.Item3.StandardCost * p.Item1.OrderQty
});
var byQuarter = ctx.From(orderMargins)
.Pivot(PivotAggregate.Sum, m => m.Margin, m => m.QuarterNum,
PivotValue.Create("1"), PivotValue.Create("2"), PivotValue.Create("3"), PivotValue.Create("4"))
.OrderBy(t => t.GetString("CategoryName"))
.Select(t => new
{
Category = t.GetString("CategoryName"),
Q1 = SqlFunctions.Column<decimal?>(t, "1"),
Q4 = SqlFunctions.Column<decimal?>(t, "4")
})
.ToList();
select CategoryName, [1], [4]
from (select name as [CategoryName], datepart(quarter, OrderDate) as [QuarterNum],
(LineTotal - StandardCost * OrderQty) as [Margin]
from sales_order_detail
join sales_order_header on sales_order_detail.OrderId = sales_order_header.OrderId
where datepart(year, OrderDate) = 2013) as [t1]
pivot (sum([Margin]) for [QuarterNum] in ([1], [2], [3], [4])) as [t2]
order by CategoryName
Typed result shape
The overloads above return an untyped TableAlias source — every
grouping column and every pivoted column is read by name. SQL Server also exposes typed overloads
that declare the result members and the CLR cell type of each pivoted column once, and derive the
IN (...) list from that projection:
var typed = ctx.From<Sales>()
.Pivot(PivotAggregate.Sum, s => s.Margin, s => s.Quarter,
(s, pivot) => new
{
s.Category, // a direct member of the inner source is a grouping column
Q1 = pivot.Value<decimal?>("1"), // the explicit CLR cell type of the pivoted column "1"
Q2 = pivot.Value<decimal?>("2")
});
var rows = typed
.OrderBy(x => x.Q1) // typed members are usable in Where/OrderBy/Select
.Select(x => new { x.Category, x.Q1, x.Q2 })
.ToList();
select category, [Q1], [Q2]
from (select category, [1] as [Q1], [2] as [Q2]
from sales pivot (sum([Margin]) for [Quarter] in ([1], [2])) as [t1]) as [t2]
order by t2.[Q1]
The typed members are aliases over the pivoted columns (Q1 = pivot.Value<decimal?>("1") emits
[1] as [Q1]), so the shape can rename cells and order them differently from the IN list. A
grouping member is any other direct member of the inner source (s.Category); inner members that the
projection does not mention remain grouping inputs. Value<T> requires the CLR type explicitly —
there is no inference and no automatic nullability: use decimal?/int?/string? when an absent value
or an excluded NULL cell is possible (see below). A subset of the grouping members is allowed; the
operator still groups by every remaining inner member. Because the projection is a derived source, it
composes further: Where/OrderBy/Select on the result, a further From(...), or AsCte to reuse it
as a CTE. As for every derived source, materializing it needs an explicit Select.
var byQuarter = ctx.From(orderMargins) // a derived query (join + computed Margin/FOR columns)
.Pivot(PivotAggregate.Sum, m => m.Margin, m => m.QuarterNum,
(m, pivot) => new { m.Category, Q4 = pivot.Value<decimal?>("4") })
.Select(m => new { m.Category, m.Q4 })
.ToList();
Unpivot mirrors this: the projection declares the two role columns
(unpivot.Value<decimal?>("val"), unpivot.Name<string>("qtr")) using the same identifiers the
UNPIVOT ([val] for [qtr] in (...)) clause takes, and any other direct member is a grouping column.
SQL Server excludes a source cell whose value is NULL, so the stacked result can have fewer rows
than columns × source rows; declare the value cell nullable to model that exclusion.
var stacked = ctx.From<Quarterly>()
.Unpivot(
(m, unpivot) => new
{
m.Category,
Quarter = unpivot.Name<string>("qtr"),
Value = unpivot.Value<decimal?>("val")
},
UnpivotColumn.Create("q1"), UnpivotColumn.Create("q2"))
.Select(x => new { x.Category, x.Quarter, x.Value })
.ToList();
The value and role strings are runtime identifiers and raw names: they are not checked against
the data or the inner mapping at compile time, and each is emitted as one safely quoted identifier
regardless of the context's
UseQuotedIdentifiers option
— a dot is a literal character, and embedded brackets are not a pre-quoted form, so Value("[1]") names
the column [1] (rendered [[1]]] on SQL Server), never a silent 1. An absent PIVOT value still
renders its column but yields SQL NULL, so
a non-nullable cell would fail materialization — declare it nullable; an invalid name or an incompatible
cell type can fail during query preparation or materialization instead of at compile time. A truly
dynamic PIVOT whose value list is only known at runtime cannot be expressed as a typed shape and stays
with the untyped TableAlias overload. The typed projection is fail-closed: a duplicate value, a
projection that declares no value, a non-constant value name, a grouping member that is also the
aggregate or FOR column, an empty UNPIVOT column list, a missing UNPIVOT role and conflicting role
identifiers all throw ArgumentException (or NotSupportedException for reading the inner source as a
whole) instead of emitting wrong SQL.
Variants and boundaries. The typed operators cover the same inner sources as the untyped ones — a
plain table/entity, a table-valued function or a derived query, so
ctx.From(() => Db.MyTvf()).Pivot(...) reshapes the function call exactly like a table.
The declared value strings stay runtime identifiers (raw names, always safely quoted as above): a
typo or an absent value is not validated at
compile time, so its column is still rendered and a cell that no source row matches materializes as SQL
NULL (declare the cell nullable). A genuinely dynamic PIVOT — a value list known only at runtime —
has no typed form and stays on the untyped TableAlias overload, which
is unchanged. Both operators keep the eager-load guard: calling them on a builder that carries
LoadWith/single-query state throws NotSupportedException (the composition cannot survive it), and
interleaved typed queries share no sticky command or plan-cache state.
TABLESAMPLE
FromOptions.TableSample adds a TABLESAMPLE modifier to the
query's primary table (TableSample); an
optional seed makes the sample repeatable. SQL Server renders tablesample (10 percent) /
tablesample (10 percent) repeatable (3)
(ITableSampleMethods.Render).
var rows = await dataContext.From<ISimpleEntity>(o => o.TableSample(10, TableSampleMethod.System, seed: 3))
.Select(x => x.Id)
.ToListAsync();
select id
from simple_entity tablesample (10 percent) repeatable (3)
SQL Server supports only the System method
(TableSample); Bernoulli
throws NotSupportedException. See
Table sampling.
Full-text search
The cross-provider boolean predicates contains/freetext render CONTAINS/FREETEXT. For ranking,
SqlFunctions.SqlServer.containstable/freetexttable expose the matched key and RANK score through
SqlFunctions.IKeyRankRow<TKey>; see
Table-valued functions.
Sequences
SqlFunctions.SqlServer.next_value_for(sequence) and its alias nextval(sequence) render the T-SQL
NEXT VALUE FOR expression and return CLR long. The name must be a constant string written as one
component (name) or two (schema.name); the components are bracket-quoted separately and an embedded
] is doubled, so the name text cannot escape the identifier:
var rows = dataContext.From<IOrderEntity>()
.Select(x => new { Next = SqlFunctions.SqlServer.next_value_for("dbo.order_seq") })
.ToList();
select next value for [dbo].[order_seq] as [Next]
from order_entity
Non-constant or captured names (a column, a captured local, null/default) throw
NotSupportedException before any SQL is emitted, and the name is never bound as a parameter. The
sequence itself is an ordinary database object created with DDL; only its next value is exposed (no
session currval/lastval, no value-returning setval). See
Sequences.
Version-gated functions (2022 and 2025)
The #282 names below are gated by the declared
server major version on this provider. The gate is version-only: unlike the native JSON storage type
(SupportsJson, #345), which also consults an
engine-edition allowlist because a few Azure/Fabric products have the type while reporting a lower major
version, function availability follows the engine major version itself. An edition declaration never
substitutes for a version here, and an unversioned context refuses them before SQL.
| Member | SQL | Prerequisite |
|---|---|---|
SqlFunctions.SqlServer.left_shift / right_shift / bit_count / get_bit / set_bit (one overload set per operand type byte/short/int/long) |
CASE WHEN … THEN CAST(NULL AS <operand type>) ELSE left_shift(cast(value as tinyint\|smallint\|int\|bigint), cast(shift as bigint)) END; bit_count → bigint, get_bit → bit |
major 16 (SQL Server 2022) |
SqlFunctions.SqlServer.regexp_count(input, pattern) |
regexp_count(input, pattern) |
major 17 (SQL Server 2025); no compatibility-level gate |
SqlFunctions.SqlServer.regexp_substr(input, pattern[, start[, occurrence[, match_type]]]) / regexp_instr(input, pattern[, start[, occurrence[, return_option[, match_type]]]]) |
the native call; 0 for a no-match regexp_instr; the match type is a varchar argument and renders as cast(… as varchar(4000)) (an nvarchar argument is Msg 8116) |
major 17; no compatibility-level gate |
SqlFunctions.SqlServer.regexp_matches(input, pattern[, match_type]) |
a FROM source; rows are match_id bigint, start_position/end_position int, match_value, substring_matches (full JSON capture text) |
major 17 and database compatibility level 170 |
SqlFunctions.SqlServer.edit_distance(input, pattern[, maximum_distance]) / edit_distance_similarity / jaro_winkler_distance / jaro_winkler_similarity |
the native call; the capped edit_distance returns maximum_distance + 1 when the distance exceeds the cap |
major 17 and compatibility level 170 and PREVIEW_FEATURES = ON and a Windows collation |
SqlFunctions.SqlServer.product(value) |
product(value) (aggregate context only — no OVER) |
major 17 |
SqlFunctions.SqlServer.regexp_split_to_table(input, pattern) |
a FROM source; rows are value plus the one-based ordinal bigint |
major 17 and database compatibility level 170 |
SqlFunctions.SqlServer.approx_count_distinct(value) |
approx_count_distinct(value) (aggregate context; 0 for an empty or all-NULL group) |
major 15 (SQL Server 2019) |
SqlFunctions.SqlServer.checksum_agg(value) / grouping_id(expressions...) |
checksum_agg(value); grouping_id(a, b, …) in the SELECT list, HAVING or ORDER BY of a grouped command |
no version floor; GROUPING_ID needs a GROUP BY context and every argument must be a grouping expression (both enforced before SQL) |
SqlFunctions.SqlServer.approx_percentile_cont(fraction, value) / approx_percentile_disc(fraction, value) |
approx_percentile_cont(f) within group (order by value); the _disc form returns the sort input type |
major 16 (SQL Server 2022); no OVER form |
var rows = dataContext.From<IComplexEntity>()
.Select(e => new
{
Count = SqlFunctions.SqlServer.regexp_count(e.String, "[0-9]"),
Shifted = SqlFunctions.SqlServer.left_shift(e.Int, 1),
Bits = SqlFunctions.SqlServer.bit_count(e.Int),
Bit = SqlFunctions.SqlServer.get_bit(e.Int, 1),
Set = SqlFunctions.SqlServer.set_bit(e.Int, 1, false)
})
.ToList();
var fragments = dataContext
.From(() => SqlFunctions.SqlServer.regexp_split_to_table("a,b,c", ","))
.Select(r => new { r.Value, r.Ordinal }) // value + ordinal bigint
.ToList();
A context declared for 15, 16 or 17 permits exactly the rows its major version lists; anything lower or unversioned is refused before SQL, for example:
The LEFT_SHIFT function requires SQL Server major version 16 or later; declared major version: 15.
The REGEXP_SPLIT_TO_TABLE function requires SQL Server major version 17 or later; declared major version: 16.
The bit family types every argument inside the call and guards a NULL operand, shift, offset or bit value
with a typed NULL, because a bare left_shift(NULL, …) is Msg 8116; the operand's own type selects
tinyint/smallint/int/bigint, bit_count is bigint (long?) and get_bit is bit (bool?).
right_shift is the engine's logical (zero-fill) shift — right_shift(-8, 1) is 2147483644. Because the
family is one overload set per operand type, an untyped constant binds to the smallest overload
(left_shift(1, 2) → byte?/tinyint, left_shift(-8, 1) → short?): cast the operand
(left_shift((int?)1, 2)) when a specific SQL type is required. regexp_split_to_table is a real gate on
two axes: at compatibility level 160 a major-17 engine reports the function as missing (Msg 208), so the 170
prerequisite is asserted live rather than assumed. product is offered in the aggregate context only.
The U3 regex scalars and the fuzzy family were executed on the tested engine baseline
17.0.5005.3: regexp_substr/regexp_instr work at compatibility levels 160 and 170, while
regexp_matches disappears at 160 (Msg 208) and the four fuzzy names need compatibility 170,
PREVIEW_FEATURES = ON (off by default) and a Windows collation (a SQL_* collation is Msg 9847, and a
missing preview flag is Msg 195). Those are runtime prerequisites the dialect cannot inspect, so they are
documented and asserted live; the version flag promises the major-version gate only, and no promise is
made for future builds.
The U4 aggregate family is aggregate-context only. approx_count_distinct is long (bigint) and
counts 0 — not NULL — for an empty or all-NULL group; checksum_agg is int? and returns NULL
there; the approximate percentiles are double? and the sort input type respectively and also return
NULL. A constant NULL operand renders as a typed CAST(NULL AS <type>) (a bare NULL is Msg 8117);
the percentile fraction must be a query-independent literal or captured scalar in [0, 1], a
row-dependent fraction is refused before SQL, and a literal or NULL WITHIN GROUP sort key is refused
before SQL because the engine answers Msg 5309. GROUPING_ID's grouping-context and grouping-list
obligations are enforced before execution with zero statements dispatched; neither approximate
percentile has an OVER overload (ordered-set, WITHIN GROUP only), and an OVER shape constructed
by other means is rejected by the engine with Msg 4113 (checksum_agg is the one name whose native
OVER the engine accepts, but this surface offers the aggregate form only). Both percentiles and the
distinct count are approximate: the vendor documents a
≈1.33 % rank error at 99 % confidence for the percentiles, which is a statistical bound, not a
deterministic tolerance.
T-SQL scalar functions
SqlFunctions.SqlServer exposes the T-SQL-only scalar functions through
ISqlDialect.SqlServerFunctions
(ISqlServerFunctions); SQL Server is the only provider that
implements it, so every other provider throws NotSupportedException for these members. The members
have no portable equivalent: the cross-provider ascii/char/translate live on
SqlFunctions.Sql instead, and
Math.Log10 already renders T-SQL LOG10.
The functions are usable as ordinary values (and as predicates where the T-SQL form is a condition):
var data = System.Text.Encoding.UTF8.GetBytes("abc");
var rows = dataContext.From<IComplexEntity>()
.Select(e => new
{
Pos = SqlFunctions.SqlServer.patindex("%a%", e.String),
Bracketed = SqlFunctions.SqlServer.quotename(e.String),
Code = SqlFunctions.SqlServer.soundex(e.String),
Similar = SqlFunctions.SqlServer.difference(e.String, "abc"),
Escaped = SqlFunctions.SqlServer.string_escape(e.String, "json"),
CodePoint = SqlFunctions.SqlServer.unicode(e.String),
Letter = SqlFunctions.SqlServer.nchar(65),
Number = SqlFunctions.SqlServer.format(e.Id, "D6"),
Angle = SqlFunctions.SqlServer.acos(0.5),
Bucket = SqlFunctions.SqlServer.date_bucket("day", 1, e.Datetime),
Month = SqlFunctions.SqlServer.datename("month", e.Datetime),
Digest = SqlFunctions.SqlServer.hashbytes("SHA2_256", data),
Arr = SqlFunctions.SqlServer.json_array("a", e.Id, "b"),
Obj = SqlFunctions.SqlServer.json_object("id", e.Id),
HasId = SqlFunctions.SqlServer.json_path_exists(e.String, "$.id")
})
.ToList();
select patindex('%a%', somestring) as [Pos],
quotename(somestring) as [Bracketed],
soundex(somestring) as [Code],
difference(somestring, 'abc') as [Similar],
string_escape(somestring, 'json') as [Escaped],
unicode(somestring) as [CodePoint],
nchar(65) as [Letter],
format(id, 'D6') as [Number],
acos(0.5) as [Angle],
date_bucket(day, 1, dt) as [Bucket],
datename(month, dt) as [Month],
hashbytes('SHA2_256', @p0) as [Digest],
json_array('a', id, 'b') as [Arr],
json_object('id' : id) as [Obj],
cast(case when json_path_exists(somestring, '$.id') = 1 then 1 else 0 end as bit) as [HasId]
from complex_entity
- String:
patindex(pattern, expression),quotename(value)/quotename(value, quote),soundex(value),difference(first, second),string_escape(value, type),unicode(value),nchar(code),format(value, format)/format(value, format, culture)andstuff(value, start, length, newValue)(string only — SQL Server'sSTUFFimplicitly converts binary arguments to character data, so nobyte[]overload is exposed).formatis the native T-SQLFORMAT, distinct from the CLRstring.Formattranslation;str(value[, length[, decimals]])is the fixed-width floating-point conversion. An apostrophe delimiter is an ordinary string argument:quotename("abc", "'")rendersquotename(N'abc', N'''')and the server returns'abc'. Text literals are UnicodeN'…'literals with inline apostrophes doubled (O'BrienbecomesN'O''Brien'), and a value containing a NUL character is rendered as a constructed Unicode expression overNCHAR(0)rather than as one literal, so the value is always passed as-is and is never pre-escaped by the caller. This paragraph describes SQL Server only and is not a cross-provider guarantee. - Numeric:
acos,asin,atan,atn2(y, x),square(cot/degrees/radians/piare portable — seeSqlFunctions.Sql) andrand()/rand(seed)(non-deterministic without a seed). - Date/time:
datename(datepart, date)(the part is a constant),date_bucket(datepart, width, date[, origin])(SQL Server 2022+), the server clockssysdatetime/sysdatetimeoffset/sysutcdatetime, the offset functionsswitchoffset(value, offset)/todatetimeoffset(value, offset)(a string such as"+02:00"or signed minutes) and the construction familytimefromparts/smalldatetimefromparts/datetimefromparts/datetime2fromparts/datetimeoffsetfromparts. The trailing*FROMPARTSprecision must be a constant0..7(a non-constant,nullor out-of-range value throws before any SQL is emitted). - Binary/system:
hashbytes(algorithm, data)(the algorithm is a constant such asSHA2_256),newsequentialid()(valid only as a columnDEFAULT, not in an ordinarySELECT),checksum(values...)/binary_checksum(values...)(at least one value; thechecksum(*)wildcard is not exposed) andcompress(value)/decompress(value)(GZIP overvarbinary(max)). - Metadata:
col_length,col_name,ident_incr/ident_seed,index_col,object_definition/object_id/object_name/object_schema_name,stats_date,db_id/db_name/original_db_name,schema_id/schema_name,type_id/type_name,filegroup_id/filegroup_name/file_id/file_idex/file_name,current_timezone/current_timezone_id,getansinull,parsenameandpublishingservername. A missing object/column/index/statistic yieldsnull, like the native function. - Live state:
current_request_id()(short?, NULL outside a request),current_transaction_id()(long?, NULL outside a transaction),xact_state()(nativesmallint0/1/-1),ident_current(table)(decimal?, the last identity of the table in any session — racy, not a last-insert API),min_active_rowversion()(nativebinary(8), not a counter),rowcount_big()(the previous command on the same connection) andscope_identity()(decimal?, only in the same batch as theINSERT). - Conversion/logical:
isdate(value)/isnumeric(value)return the native T-SQLint(1/0), not abit, so they stay integer values rather than materialised predicates, andformatmessage(messageOrId, args...)accepts at most 20 formatting arguments.try_cast<T>(value)/try_convert<T>(value[, style])(plus thestring-target overloads overnvarchar(4000)) render the T-SQLTRY_CAST/TRY_CONVERTthrough a closed CLR→SQL destination map and are not version-gated: an invalid value is SQL NULL, a NULL style is SQL NULL even for a convertible value, and a target outside the map is refused before SQL. See TRY_CAST and TRY_CONVERT.
The portable BASE64 and crypto members are SQL Server 2025-only on this provider: SqlFunctions.Sql.base64_encode renders BASE64_ENCODE(value) over varbinary and base64_decode renders BASE64_DECODE(value) over varchar (cast an nvarchar column), while crypt_gen_random renders the typed CASE WHEN length IS NULL THEN CAST(NULL AS varbinary(8000)) ELSE CRYPT_GEN_RANDOM(length) END wrapper so a NULL length stays a typed NULL. No compatibility level or preview flag is required.
- JSON:
json_array(value, ...)andjson_object(key, value, ...)(SQL Server 2022+),json_objectagg(key, value)andjson_arrayagg(value)(SQL Server 2025+),json_path_exists(json, path)(SQL Server 2022+) andjson_contains(json, searchValue, path)(SQL Server 2025+). The two predicates are materialised asbitvalues, mirroringisjson.
Live mutable state is not an exclusion criterion: the seven names that report live connection-,
transaction-, session- or statement-scope state are exposed as-is, with the native SQL Server semantics
and the caveats above. nextorm promises no per-row stability, no connection continuity between queries
and no identity-retrieval guarantee — a separate SCOPE_IDENTITY/IDENT_CURRENT call is not a
last-insert API, so use ReturningIdentity<TKey>() for the key. The stable configuration/identity
constants of the same scope (CURRENT_TIMEZONE/CURRENT_TIMEZONE_ID, SERVER_NAME, HOST_ID,
HOST_NAME, APP_NAME, SUSER_NAME, ORIGINAL_LOGIN) stay on the same surface.
Null-safe comparison
SQL Server has no IS [NOT] DISTINCT FROM on the supported version floor, so both helpers are emulated
over ordinary equality as an always-two-valued guarded predicate:
((a is not null and b is not null and a = b) or (a is null and b is null)), with is_distinct_from as
its not (...) complement. A boolean projection is legalised as
cast(case when ... then 1 else 0 end as bit). Because the operands are compared twice, a captured null
operand needs a SQL type derived from metadata; an unmapped one is rejected before execution with
Null-safe comparison requires a SQL Server type mapping for null operands. See
Null-safe comparison.
Refused functions (deterministic, before SQL)
#282 is closed for this slice:
22 delivered / 0 delivery names remaining / 11 tested refusals. The eleven refusals are part of the
surface — callable SqlFunctions.SqlServer members that throw NotSupportedException with one stable
message per member (SqlFunctions.SqlServer.<member> (<TOKEN>) is refused: <reason>) at the SQL-builder
command boundary, before SQL is rendered and before any command is created. They are not deferred names:
they are deterministic, tested refusals, and each keeps the supported siblings of the same surface
(rowcount_big, xact_state, scope_identity, db_name, current_user, …) rendering.
| Member | SQL token | Kind and reason |
|---|---|---|
session_context(key) |
SESSION_CONTEXT |
Scope/CLR: returns sql_variant, for which nextorm has no faithful CLR result contract; no lossy cast substitution is performed. |
session_id() |
SESSION_ID |
Engine rejection: not a SQL Server engine function (Msg 195); the documented function belongs to Azure Synapse. Session identity is @@SPID or sys.dm_exec_sessions.session_id; no substitution is performed. |
rowcount() |
@@ROWCOUNT |
Scope: describes the previous statement and is statement-scoped; a per-row query projection cannot expose that value faithfully. |
error() |
@@ERROR |
Scope: the same previous-statement scope as @@ROWCOUNT. |
error_number() |
ERROR_NUMBER |
Scope: requires a CATCH-block scope; outside CATCH it returns NULL, and nextorm does not establish that scope. |
error_severity() |
ERROR_SEVERITY |
Scope: the same CATCH-only scope. |
error_state() |
ERROR_STATE |
Scope: the same CATCH-only scope. |
error_line() |
ERROR_LINE |
Scope: the same CATCH-only scope. |
error_message() |
ERROR_MESSAGE |
Scope: the same CATCH-only scope. |
error_procedure() |
ERROR_PROCEDURE |
Scope: the same CATCH-only scope. |
any_value(value) |
ANY_VALUE |
Engine rejection: SQL Server build 17.0.5005.3 rejected it with Msg 195 in the aggregate and OVER () forms, at compatibility 160/170 and with PREVIEW_FEATURES = ON; no supported SQL Server execution contract is established. |
The two kinds stay distinct: the ten system names are scope/CLR limitations (a statement or CATCH
scope, or sql_variant without a faithful CLR carrier) except SESSION_ID, which — like ANY_VALUE — is
an observed engine rejection. ANY_VALUE is not a version gap a later nextorm release can close, and
the portable CommonFunctions.any_agg is unaffected on the providers that enable it. See
Limitations and out-of-scope features and
system functions.
See also
Source: src/nextorm.sqlserver/SqlServerDialect.cs, src/nextorm.core/Query/SqlFunctions/SqlFunctions.SqlServer.cs,
tests/nextorm.sqlserver.tests/SqlGeneration/SqlGenerationTests.cs.