MySQL provider
Use
nextorm.mysqlfor MySQL 8.x; it renders@nameparameters, backtick-quoted identifiers,concat(...)concatenation,limit/offsetpaging and the standard-deviation/variance aggregate names.
Prerequisites: Provider overview · Quickstart
Overview
MySqlDataContext (src/nextorm.mysql/MySqlDataContext.cs) wraps MySqlConnector. It creates a
MySqlConnection from the connection string and returns Instance from its Dialect
property. MySqlDialect (src/nextorm.mysql/MySqlDialect.cs) is the dialect:
- parameter placeholder
@name; - identifiers and aliases are quoted with backticks;
- string concatenation uses the
concat(a, b, ...)function — MySQL's infix||is a logical OR unless thePIPES_AS_CONCATSQL mode is set, so the dialect never emits it; MakeCoalescerenderscoalesce(a, b);MakeStringLengthrenderschar_length(x)(MySQL'slength()counts bytes);MakeNowrendersnow()for local time andutc_timestamp()for UTC;stdev/stdevp/var/varpmap tostddev_samp/stddev_pop/var_samp/var_pop(stddevandvarianceare the population synonyms in MySQL);- a CLR conversion is rendered with a MySQL
CASTtarget (signed/unsignedfor the integer types,double,decimal,char,datetime) rather than the ANSIbigint/integer; - the integer ones-complement renders as
(-(x) - 1), because MySQL's~yields an unsigned 64-bit value that overflows the signed CLR integer; - the
ESCAPEcharacter of aLIKEpredicate is emitted as the mode-independent_utf8mb4 X'5c'token, soNO_BACKSLASH_ESCAPES,ANSI_QUOTESand the driver'sNoBackslashEscapescannot change its meaning; - paging is
limit n/limit n offset m; an offset without a limit becomeslimit 18446744073709551615 offset m, because MySQL only acceptsoffsettogether withlimit; - the session/information family (
SessionInfoFunctions) rendersSqlFunctions.Sql.current_user/session_user/current_database/versionascurrent_user()/session_user()/database()/version()andcurrent_schemaasschema(); - the arbitrary-value aggregate
SqlFunctions.Sql.any_aggrenders asANY_VALUE(x)(SupportsAnyValueAggregate, MySQL 5.7+); - the portable conditional
SqlFunctions.Sql.iifrenders asif(condition, a, b)(Iif,IIifRenderer.Render).
The variability aggregates follow the shared cardinality contract: Stdev/Var are null for zero or
one contributing value, Stdevp/Varp are null for zero and 0.0 for one. One provider limitation is
independent of the nullable result: MySQL 8.4 rejects DISTINCT inside
STDDEV_SAMP/STDDEV_POP/VAR_SAMP/VAR_POP (stddev_samp(distinct x) fails with a syntax error).
The renderer keeps emitting that spelling for the *_distinct forms, so only the SQL text is guaranteed
and a live distinct variance query is not claimed as supported on MySQL 8.4.
Registering the provider
Two overloads are available on DataContextBuilder
(src/nextorm.mysql/DI/MySqlDataContextOptionsBuilderExtensions.cs):
using NextORM.Core;
using NextORM.MySql;
// From a connection string.
var builder = new DataContextBuilder()
.UseMySql("Server=localhost;Port=3306;Database=app;User ID=app;Password=secret");
// From an existing, caller-owned connection.
using var connection = new MySqlConnector.MySqlConnection("Server=localhost;Database=app");
var byConnection = new DataContextBuilder().UseMySql(connection);
using var ctx = builder.CreateDataContext(); // IDataContext
You can also construct the context directly (this is what the provider tests do):
using NextORM.Core;
using NextORM.MySql;
using IDataContext ctx = new MySqlDataContext(
"Server=localhost;Database=app;User ID=app;Password=secret", new DataContextBuilder());
String concatenation
var query = ctx.From<ISimpleEntity>().Select(x => new { Label = "id:" + x.Id });
select concat('id:', id) as `Label`
from simple_entity
Paging
ctx.From<ISimpleEntity>().Page(5, 10).Select(x => x.Id); // limit 5 offset 10
ctx.From<ISimpleEntity>().Offset(10).Select(x => x.Id); // limit 18446744073709551615 offset 10
String literals and parameters
Every inline text value — including an operand in a position where the grammar accepts only a
literal — is rendered as the mode-independent _utf8mb4 X'…' form, an uppercase UTF-8 hex string,
so neither sql_mode nor ANSI_QUOTES, NO_BACKSLASH_ESCAPES or the driver's
NoBackslashEscapes can change its meaning. An ORM-owned text parameter slot in a read query
keeps its @name placeholder in the cached command and is lowered to the same _utf8mb4 X'…'
literal at execution time: the driver parameter is omitted for that execution, and the command is
restored to its canonical text and bindings before the next one. Mutations
(INSERT/UPDATE/DELETE) are never prepared, so their text values are lowered at render time and
no template is kept.
GROUP_CONCAT ... SEPARATOR is a grammar-token position where the engine accepts only a text
literal. A constant separator is rendered as a bare apostrophe-doubled literal from the accepted
set U+0020–U+007E excluding the backslash; a non-constant, null, backslash-containing,
NUL-containing or otherwise non-printable-ASCII separator fails closed with NotSupportedException
naming MySQL/MariaDB — no placeholder, no invalid SQL, and the value is never echoed.
var query = ctx.From<IComplexEntity>().Where(e => e.String == "O'Brien").Select(e => e.Id);
-- the text parameter slot is lowered to a literal for this execution
select `Id` as `Id`
from complex_entity
where `String` = _utf8mb4 X'4F27427269656E'
A constant SEPARATOR keeps the literal form:
var names = ctx.From<IComplexEntity>().Select(e => SqlFunctions.Sql.string_agg(e.String, ","));
select group_concat(`String` separator ',')
from complex_entity
Bulk insert
The native path is MySqlBulkCopy, a LOAD DATA LOCAL INFILE load, and it needs both prerequisites:
AllowLoadLocalInfile=true on the connection string (client side) and local_infile=1 as the server variable
@@GLOBAL.local_infile. The server value is read with a read-only preflight (select @@global.local_infile),
and nextorm never enables either one. Without them Auto falls back only when the portable path honours the
whole request and Native refuses by name; a runtime LOAD failure is never replayed through the SQL path.
- The source is read in bounded chunks (
MaxBatchSize, default 10 000 rows); each chunk is one load. KeepIdentitywrites the explicit identity column under a scopedNO_AUTO_VALUE_ON_ZEROguard, so an explicit0is not replaced by a generated value.IgnoreDuplicatesmaps toConflictOption.Ignoreon the native load andINSERT IGNOREon the portable statement.KeepNulls=falseneeds a default-equivalent shape;CheckConstraints=false,TableLockandFireTriggersare refused (MySQL always checks constraints), andReturning*is refused — read keys withCreateInsertBuilder.DatabaseNamerendersdb.table;ServerNameis refused (no rerouting).Timeoutis one whole-invocation budget; an expired budget is aTimeoutExceptionand cancellation wins.
See Bulk insert.
Provider differences
| Aspect | MySQL |
|---|---|
| Parameter placeholder | @name |
| Concat | concat(a, b) |
| Coalesce | coalesce |
| Boolean literal | 1 / 0 (aliases true / false) |
| Identifier quoting | backticks (as `t1`) |
| Derived table alias | required |
| TVF alias | required |
FULL JOIN |
not supported (right join is) |
*ALL |
not supported (MySQL 8.0.31 has INTERSECT/EXCEPT, but not the ALL variants) |
| Text JSON | json_value/json_query/json_modify/isjson (over JSON_EXTRACT/JSON_UNQUOTE/JSON_SET/JSON_VALID) |
| Session/info functions | current_user(), session_user(), schema(), database(), version() |
| Arbitrary-value aggregate | ANY_VALUE(x) |
| Conditional function | iif(cond, a, b) → if(cond, a, b) |
| Window percentiles | not supported: percentile_cont/percentile_disc on a window throw NotSupportedException (MySQL has neither the functions nor WITHIN GROUP; PERCENTILE_CONT is MariaDB-only) |
| Median (analytic) | not supported: SqlFunctions.MySql.median(v).Over(...) throws NotSupportedException("The median function is not supported by MySQL.") |
| Generic percentile | not supported: SqlFunctions.MySql.percentile(fraction, v) throws NotSupportedException("The percentile function is not supported by MySQL.") |
LOB streaming (ToStream/ToTextReader) |
NotSupportedException |
MySQL 8.0.31 and later support INTERSECT/EXCEPT; the dialect rejects the *ALL variants with a
NotSupportedException, matching the engine.
See also
- Provider overview
- MySQL and MariaDB-specific SQL
- MariaDB
- ClickHouse
- In-memory
- Limitations and out-of-scope features
Source: src/nextorm.mysql/MySqlDialect.cs, src/nextorm.mysql/MySqlDataContext.cs,
src/nextorm.mysql/DI/MySqlDataContextOptionsBuilderExtensions.cs,
tests/nextorm.mysql.tests/Dialect/MySqlDialectTests.cs, tests/nextorm.mysql.tests/SqlGeneration/SqlGenerationTests.cs.