Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 0
Dapper Generated SQL
Exactly what SqlQueryCompiler emits, statement by statement. Everything here is engine-independent
structure; the parts that vary are supplied by the dialect and are marked.
SqlQueryCompiler has six public compile methods. Which ones run depends on the query shape.
| Method | Purpose | Used by |
|---|---|---|
CompileSchemaProbe | discover the target's columns and types | once per object, ever |
CompileRowCount | Meta.Total for a flat query | every flat query |
CompileRows | the page of rows | every flat query |
CompileGroupCount | Meta.Total for a grouped query | every grouped query |
CompileGroupKeys | the page of outermost group keys | every grouped query |
CompileGroupRows | every row under those keys | every grouped query |
Each returns:
publicsealedrecordCompiledSql(stringText,IReadOnlyDictionary<string,object?>Parameters);Text contains only placeholders — never inlined literals. Parameters is keyed without the
dialect's prefix (p0, not @p0).
SELECT*FROM<source>WHERE1=0Returns no rows. The reader's FieldCount, GetName(i) and GetFieldType(i) give the column
whitelist and the CLR type of each column. Cached per provider | schema | name | type for the
process lifetime.
If a driver cannot describe a column's type, the name is still recorded and the type is left unknown — in which case no coercion is applied to values compared against it.
SELECTCOUNT(*) FROM<source>[ WHERE<predicate>]No ORDER BY, no paging. This is Meta.Total.Rows; Meta.Total.Pages is ceil(rows / size)
computed in the application.
SELECT<projection>FROM<source>[ WHERE<predicate>][ ORDER BY<terms>] <paging><projection>is*whenSelectColumnsis empty or names nothing real, otherwise the quoted, comma-separated list of the recognised columns.<paging>is always emitted.- When there is no usable sort column and the dialect requires an
ORDER BYfor paging (SQL Server), a placeholder is emitted first:ORDER BY (SELECT NULL).
SELECTCOUNT(*) FROM (SELECT DISTINCT<key>FROM<source>[ WHERE<predicate>]) qf_groupsThe alias is written without AS — Oracle rejects AS before a table alias, and every other
engine accepts the bare form, so one spelling works everywhere.
This is the count of distinct outermost keys, which becomes Meta.Total.Rows for a grouped result.
SELECT DISTINCT<key>FROM<source>[ WHERE<predicate>] ORDER BY<key term><paging>Ordered by the outermost GroupByDescriptor.SortOrder, with its null-ordering clause. This is the
statement paging actually slices.
SELECT<projection + grouping columns>FROM<source>WHERE (<predicate>) AND<key predicate>[ ORDER BY<sort terms>]No paging clause — every row under the paged keys is fetched, because a node's Count is the
number of leaf rows beneath it and that cannot be known from a partial set. See
Grouping and Hierarchies.
When there is no filter, the WHERE is just the key predicate.
IN never matches NULL, so nulls are handled explicitly:
| Keys on the page | Emitted |
|---|---|
| all non-null | <key> IN (@p0, @p1, …) |
| some null | (<key> IN (@p0, …) OR <key> IS NULL) |
| only null | <key> IS NULL |
| none | 1 = 0 |
DapperObjectType | SQL Server / PostgreSQL | MySQL / SQLite | Oracle |
|---|---|---|---|
Auto, Table, View | [schema].[name] | `name` (schema only if given) | "NAME" |
TVF | [schema].[fn](@p0, @p1) | not supported | TABLE("FN"(:p0, :p1)) |
SP | not compiled — see below |
TVF arguments are positional, taken in the order of the Parameters dictionary, and each becomes
a parameter.
SP is never composed into a SELECT. Calling CompileSchemaProbe or any other compile method with
an SP target throws InvalidOperationException — the executor routes procedures down a separate
path that calls them and filters in memory. See
Dapper Provider.
Built group by group. Only conditions that are usableand name a whitelisted column contribute.
(<condition> <AND|OR> <condition> …) one group
NOT (<condition> …) a negated group (AndNot / OrNot)
<group> <AND|OR> <group> … groups joined by criteria.Logic
A group producing no fragments is skipped entirely — it does not emit () or 1=1. If no group
produces anything, no WHERE clause is emitted at all.
col is the quoted column, @pN a parameter reference in the dialect's form.
| Operator | Emitted | Notes |
|---|---|---|
Equals, value set | col = @p0 | |
Equals, value null | col IS NULL | no parameter |
NotEquals, value set | col <> @p0 | |
NotEquals, value null | col IS NOT NULL | no parameter |
LessThan | col < @p0 | |
GreaterThan | col > @p0 | |
LessThanOrEqualTo | col <= @p0 | |
GreaterThanOrEqualTo | col >= @p0 | |
Between | col BETWEEN @p0 AND @p1 | two parameters |
Contains | col LIKE @p0 ESCAPE '\' | pattern %value% |
NotContains | col NOT LIKE @p0 ESCAPE '\' | pattern %value% |
StartsWith | col LIKE @p0 ESCAPE '\' | pattern value% |
EndsWith | col LIKE @p0 ESCAPE '\' | pattern %value |
The ESCAPE clause is the dialect's; MySQL emits ESCAPE '\\' because it processes backslash escapes
inside string literals.
The pattern is built after escaping the caller's value, so wildcards inside it match literally. A
search for 50% becomes the parameter %50\%% with ESCAPE '\'.
Values are coerced to the column's real type before binding — see Query Semantics.
One term per usable sort column, in list order:
<quoted column> <ASC|DESC><null ordering>
<null ordering> is the dialect's, and includes a leading space or is empty:
| Engine | Ascending | Descending |
|---|---|---|
| PostgreSQL, Oracle | NULLS FIRST | NULLS LAST |
| SQL Server, MySQL, SQLite | (empty — default already matches) | (empty) |
The standard is nulls first ascending, nulls last descending, everywhere. See Query Semantics.
| Engine | Emitted |
|---|---|
| SQL Server, Oracle | OFFSET <n> ROWS FETCH NEXT <m> ROWS ONLY |
| PostgreSQL, MySQL, SQLite | LIMIT <m> OFFSET <n> |
with m = Size > 0 ? Size : 12, n = (max(Number,1) - 1) * m.
The offset and size are inlined as integers, not parameters. They are computed from validated integers, never from caller text, and inlining lets the optimizer see the actual window.
- Named
p0,p1, … in the order the compiler encounters them. - Numbered per statement — the count query and the row query each start at
p0, and identical filters produce identical names in both. - Rendered with the dialect's prefix:
@p0everywhere,:p0on Oracle. - Never derived from column names, which avoids reserved-word collisions such as Oracle's ORA-01745.
- Bound by name;
BindByNameis set reflectively on drivers that expose it.
Model:
publicclassUser{publicintUserId{get;set;}publicstringFirstName{get;set;}publicstringCountry{get;set;}publicstring?Department{get;set;}publicdecimalScore{get;set;}publicboolIsActive{get;set;}}vardq=DapperQueryBuilder.Where(newQueryCriteria([newConditionGroup([newCondition("Country",ConditionOperator.Equals,"Germany"),newCondition("Score",ConditionOperator.GreaterThan,50)])])).Select("UserId","FirstName","Score").Sort(newSortDescriptor("Score",SortOrder.Descending)).Page(20,2).ForObject("Users","dbo").Build();SQL Server
-- countSELECTCOUNT(*) FROM [dbo].[Users] WHERE ([Country] = @p0 AND [Score] > @p1)
-- rowsSELECT [UserId], [FirstName], [Score] FROM [dbo].[Users]
WHERE ([Country] = @p0 AND [Score] > @p1)
ORDER BY [Score] DESC
OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLYPostgreSQL
SELECT"UserId", "FirstName", "Score"FROM"public"."Users"WHERE ("Country"= @p0 AND"Score"> @p1)
ORDER BY"Score"DESC NULLS LAST
LIMIT20 OFFSET 20MySQL
SELECT`UserId`, `FirstName`, `Score`FROM`Users`WHERE (`Country`= @p0 AND`Score`> @p1)
ORDER BY`Score`DESCLIMIT20 OFFSET 20Oracle
SELECT"UserId", "FirstName", "Score"FROM"Users"WHERE ("Country"= :p0 AND"Score"> :p1)
ORDER BY"Score"DESC NULLS LAST
OFFSET 20 ROWS FETCH NEXT 20 ROWS ONLYParameters in all four: p0 = "Germany", p1 = 50m — coerced to decimal because that is the
column's type, even if the client sent "50".
{ "criteria": { "logic": 0, "groups": [
{ "logic": 1, "conditions": [
{ "columnName": "Country", "operator": 0, "value": "Germany" },
{ "columnName": "Country", "operator": 0, "value": "Canada" } ] },
{ "logic": 2, "conditions": [
{ "columnName": "Department", "operator": 0, "value": "HR" } ] } ] } }WHERE ([Country] = @p0 OR [Country] = @p1) AND NOT ([Department] = @p2)newCondition("FirstName",ConditionOperator.Contains,"50%")WHERE ([FirstName] LIKE @p0 ESCAPE '\')-- p0 = "%50\%%"newCondition("Department",ConditionOperator.Equals,null)// → [Department] IS NULL
new Condition("Department",ConditionOperator.NotEquals,null)// → [Department] IS NOT NULLNeither emits a parameter.
vardq=DapperQueryBuilder.Where(newQueryCriteria([newConditionGroup([newCondition("IsActive",ConditionOperator.Equals,true)])])).Select("UserId","FirstName","Score").Sort(newSortDescriptor("Score",SortOrder.Descending)).GroupBy(newGroupByDescriptor("Country"),newGroupByDescriptor("Department")).Page(5,1).ForObject("Users","dbo").Build();Three statements, on SQL Server:
-- 1. how many distinct countries matchSELECTCOUNT(*) FROM (
SELECT DISTINCT [Country] FROM [dbo].[Users] WHERE ([IsActive] = @p0)
) qf_groups
-- 2. the first five of themSELECT DISTINCT [Country] FROM [dbo].[Users] WHERE ([IsActive] = @p0)
ORDER BY [Country] ASC
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
-- 3. every row in those five countries.-- Country and Department are appended to the projection because the tree is rebuilt from them.SELECT [UserId], [FirstName], [Score], [Country], [Department] FROM [dbo].[Users]
WHERE ([IsActive] = @p0) AND [Country] IN (@p1, @p2, @p3, @p4, @p5)
ORDER BY [Score] DESCThe hierarchy is then assembled from statement 3's rows by HierarchyBuilder.
.ForObject("tvf_GetUsersByTenant","dbo",DapperObjectType.TVF,newDictionary<string,object?>{["TenantId"]=1})-- SQL ServerSELECT*FROM [dbo].[tvf_GetUsersByTenant](@p0) WHERE …
-- PostgreSQLSELECT*FROM"public"."tvf_GetUsersByTenant"(@p0) WHERE …
-- OracleSELECT*FROM TABLE("TVF_GETUSERSBYTENANT"(:p0)) WHERE …The compiler is public, so you can inspect what a query produces without a database:
usingPepperX.QueryForge.Dapper.Compiler;usingPepperX.QueryForge.Dapper.Dialects;varcompiler=newSqlQueryCompiler(newPostgreSqlDialect());// Supply the whitelist yourself instead of probing a database.varcolumns=newColumnWhitelist(newDictionary<string,Type?>{["UserId"]=typeof(int),["Country"]=typeof(string),["Score"]=typeof(decimal)});varsql=compiler.CompileRows(dapperQuery,columns);Console.WriteLine(sql.Text);foreach(var(name,value)insql.Parameters)Console.WriteLine($" {name} = {value}");This is exactly how the repository's own dialect and compiler tests work — see Testing.
For the EF Core provider the equivalent is ToQueryString():
db.Users.ApplyQuery(query).ToQueryString();QueryForge · part of the PepperX Ecosystem · MIT licensed · packages 2.0.0, .NET 10