Expand fluent syntax docs with parser and join nuances
This commit is contained in:
@@ -1,122 +1,263 @@
|
||||
# Fluent Builder API
|
||||
|
||||
The fluent API is built around interfaces like:
|
||||
The fluent API is built around:
|
||||
|
||||
- `IDynamicSelectQueryBuilder`
|
||||
- `IDynamicInsertQueryBuilder`
|
||||
- `IDynamicUpdateQueryBuilder`
|
||||
- `IDynamicDeleteQueryBuilder`
|
||||
|
||||
This API is best when SQL structure must be explicit and composable.
|
||||
This page documents call semantics and parser nuances from:
|
||||
|
||||
## Core Select Flow
|
||||
- `DynamORM/Builders/Implementation/DynamicSelectQueryBuilder.cs`
|
||||
- `DynamORM/Builders/Implementation/DynamicQueryBuilder.cs`
|
||||
- `DynamORM/Builders/Extensions/DynamicWhereQueryExtensions.cs`
|
||||
- `DynamORM/Builders/Extensions/DynamicHavingQueryExtensions.cs`
|
||||
- parser tests in `DynamORM.Tests/Select/ParserTests.cs` and `LegacyParserTests.cs`
|
||||
|
||||
## Builder Lifecycle and Clause Output Order
|
||||
|
||||
Builder methods can be chained in many orders, but generated SQL is emitted in canonical order:
|
||||
|
||||
1. `SELECT`
|
||||
2. `FROM`
|
||||
3. `JOIN`
|
||||
4. `WHERE`
|
||||
5. `GROUP BY`
|
||||
6. `HAVING`
|
||||
7. `ORDER BY`
|
||||
8. paging (`TOP`/`FIRST SKIP` or `LIMIT OFFSET` depending on options)
|
||||
|
||||
Recommendation: start with `From(...)` first so alias registration is unambiguous for later clauses.
|
||||
|
||||
## Fast Baseline
|
||||
|
||||
```csharp
|
||||
using (var query = db.From("users", "u")
|
||||
.Where("u.id", 19)
|
||||
.SelectColumn("u.first", "u.last"))
|
||||
{
|
||||
var result = query.Execute().ToList();
|
||||
}
|
||||
```
|
||||
|
||||
## Typed Select
|
||||
|
||||
```csharp
|
||||
using (var query = db.From<User>("u")
|
||||
.Where(x => x.u.id == 19)
|
||||
.Select(x => x.u.All()))
|
||||
{
|
||||
var users = query.Execute<User>().ToList();
|
||||
}
|
||||
```
|
||||
|
||||
## Parser Lambda Patterns
|
||||
|
||||
The parser supports patterns tested in `DynamORM.Tests/Select/ParserTests.cs` and `DynamORM.Tests/Select/LegacyParserTests.cs`:
|
||||
|
||||
- `From(x => x.dbo.Users)`
|
||||
- `From(x => x.dbo.Users.As(x.u))`
|
||||
- `From(x => "dbo.Users AS u")`
|
||||
- `Join(x => x.Left().Accounts.As(x.a).On(x.a.userId == x.u.id))`
|
||||
- `Where(x => x.Or(x.u.id > 100))`
|
||||
- `Select(x => x.u.first.As(x.firstName))`
|
||||
- `GroupBy(x => x.u.last)`
|
||||
- `Having(x => x.Count() > 1)`
|
||||
- `OrderBy(x => x.u.id.Desc())`
|
||||
|
||||
## Joins and Projection Example
|
||||
|
||||
```csharp
|
||||
using (var query = db.From("users", "u")
|
||||
.Join(x => x.Left().profiles.As(x.p).On(x.p.user_id == x.u.id))
|
||||
.Select(
|
||||
x => x.u.id,
|
||||
x => x.u.first.As(x.firstName),
|
||||
x => x.p.city.As(x.city))
|
||||
.Where(x => x.u.id > 10)
|
||||
.OrderBy(x => x.u.id.Desc()))
|
||||
{
|
||||
var rows = query.Execute().ToList();
|
||||
}
|
||||
```
|
||||
|
||||
## `From(...)` Deep Dive
|
||||
|
||||
Supported source forms:
|
||||
|
||||
- string source:
|
||||
- `From(x => "dbo.Users")`
|
||||
- `From(x => "dbo.Users AS u")`
|
||||
- member source:
|
||||
- `From(x => x.dbo.Users)`
|
||||
- `From(x => x.dbo.Users.As(x.u))`
|
||||
- typed source:
|
||||
- `From(x => x(typeof(User)).As(x.u))`
|
||||
- `db.From<User>("u")`
|
||||
- subquery source:
|
||||
- `From(x => x(subQuery).As(x.u))`
|
||||
|
||||
Aliasing nuance:
|
||||
|
||||
- `As(...)` is optional for simple table/member source.
|
||||
- For generic invoke source (`x => x(expression)`), aliasing is effectively required when the source should be referenced later.
|
||||
|
||||
`NoLock()` nuance:
|
||||
|
||||
- `NoLock()` is parsed in `From`/`Join`, but SQL `WITH(NOLOCK)` is emitted only if database option `SupportNoLock` is enabled.
|
||||
|
||||
Examples from tests:
|
||||
|
||||
```csharp
|
||||
cmd.From(x => x.dbo.Users.As(x.u));
|
||||
cmd.From(x => x("\"dbo\".\"Users\"").As(x.u));
|
||||
cmd.From(x => x(cmd.SubQuery().From(y => y.dbo.Users)).As(x.u));
|
||||
cmd.From(x => x.dbo.Users.As("u").NoLock());
|
||||
```
|
||||
|
||||
## `Join(...)` Deep Dive
|
||||
|
||||
`Join(...)` supports table/member/string/invoke/subquery forms with `On(...)` conditions.
|
||||
|
||||
### Two-pass join processing
|
||||
|
||||
Implementation runs join parsing in two passes:
|
||||
|
||||
- pass 1: collect tables/aliases
|
||||
- pass 2: render SQL conditions
|
||||
|
||||
This enables robust alias resolution inside `On(...)` expressions.
|
||||
|
||||
### Join type resolution
|
||||
|
||||
Join type comes from a dynamic method near the root of the join expression.
|
||||
|
||||
Common forms:
|
||||
|
||||
- `Inner()` -> `INNER JOIN`
|
||||
- `Left()` -> `LEFT JOIN`
|
||||
- `LeftOuter()` -> `LEFT OUTER JOIN`
|
||||
- `Right()` -> `RIGHT JOIN`
|
||||
- `RightOuter()` -> `RIGHT OUTER JOIN`
|
||||
- no type method -> plain `JOIN`
|
||||
|
||||
Non-obvious option:
|
||||
|
||||
- the join-type method can accept arguments.
|
||||
- if first argument is `false`, auto-append/split of `JOIN` is disabled.
|
||||
- if any argument is a string, that string is used as explicit join type text.
|
||||
|
||||
This allows custom forms such as provider-specific joins.
|
||||
|
||||
Example patterns:
|
||||
|
||||
```csharp
|
||||
cmd.Join(x => x.Inner().dbo.UserClients.As(x.uc).On(x.u.Id == x.uc.UserId));
|
||||
|
||||
cmd.Join(x => x.Custom(false, "CROSS APPLY")
|
||||
.dbo.UserClients.As(x.uc)
|
||||
.On(x.u.Id == x.uc.UserId));
|
||||
```
|
||||
|
||||
### `On(...)` ordering rule
|
||||
|
||||
`On(...)` can appear with other join modifiers in the same lambda chain. Parsed order is normalized by node traversal.
|
||||
|
||||
Recommended readable order:
|
||||
|
||||
```csharp
|
||||
.Join(x => x.Inner().dbo.UserClients.As(x.uc).NoLock().On(x.u.Id == x.uc.UserId))
|
||||
```
|
||||
|
||||
Validated join variants are heavily covered in parser tests (`TestJoinClassic`, `TestInnerJoin*`, `TestLeftJoin`, `TestRightJoin`, `TestSubQueryJoin`).
|
||||
|
||||
## `Where(...)` and `Having(...)` Nuances
|
||||
|
||||
Both clauses share the same behavior model.
|
||||
|
||||
### Lambda conditions
|
||||
|
||||
```csharp
|
||||
.Where(x => x.u.UserName == "admin")
|
||||
.Where(x => x.Or(x.u.IsActive == true))
|
||||
```
|
||||
|
||||
Chaining behavior:
|
||||
|
||||
- each chained call defaults to `AND`
|
||||
- root wrapper `Or(...)` switches that clause append to `OR`
|
||||
|
||||
### `DynamicColumn` behavior
|
||||
|
||||
`DynamicColumn` supports:
|
||||
|
||||
- comparison operators (`Eq`, `Not`, `Like`, `NotLike`, `In`, `Between`, etc.)
|
||||
- `SetOr()` to force OR join with previous condition
|
||||
- `SetBeginBlock()` / `SetEndBlock()` to control parenthesis grouping
|
||||
- `SetVirtualColumn(...)` to influence null parameterization behavior
|
||||
|
||||
Example (legacy parser tests):
|
||||
|
||||
```csharp
|
||||
.Where(new DynamicColumn("u.Deleted").Eq(0).SetBeginBlock())
|
||||
.Where(new DynamicColumn("u.IsActive").Eq(1).SetOr().SetEndBlock())
|
||||
```
|
||||
|
||||
### Object conditions + `_table`
|
||||
|
||||
```csharp
|
||||
.Where(new { Deleted = 0, IsActive = 1, _table = "u" })
|
||||
```
|
||||
|
||||
Nuance:
|
||||
|
||||
- `_table` applies alias/table prefix during object-condition expansion.
|
||||
- when `schema: true`, only key-like mapped columns are included.
|
||||
|
||||
`Having(...)` mirrors these forms and semantics.
|
||||
|
||||
## `Select(...)` Nuances
|
||||
|
||||
Supported forms:
|
||||
|
||||
- simple member: `Select(x => x.u.UserName)`
|
||||
- aliasing: `Select(x => x.u.UserName.As(x.Name))`
|
||||
- all columns: `Select(x => x.u.All())`
|
||||
- aggregate functions: `Select(x => x.Sum(x.u.Score).As(x.Total))`
|
||||
- anonymous projection: `Select(x => new { Id = x.u.Id, Name = x.u.UserName })`
|
||||
- raw invoke concatenation: `Select(x => x("CASE WHEN ", x.u.Active == 1, " THEN ", 1, " ELSE ", 0, " END").As(x.Flag))`
|
||||
|
||||
Non-obvious behavior:
|
||||
|
||||
- anonymous projection property names become SQL aliases.
|
||||
- `All()` cannot be combined with alias emission for that same select item.
|
||||
- invoke syntax (`x("...", expr, "...")`) is the parser escape hatch for complex SQL expressions.
|
||||
|
||||
## `OrderBy(...)` Nuances
|
||||
|
||||
Default direction is ascending.
|
||||
|
||||
```csharp
|
||||
.OrderBy(x => x.u.UserName) // ASC
|
||||
.OrderBy(x => x.Desc(x.u.UserName)) // DESC
|
||||
.OrderBy(x => "1 DESC") // numeric column position
|
||||
.OrderBy(x => x.Desc(1)) // numeric column position
|
||||
```
|
||||
|
||||
`OrderByColumn(...)` also supports `DynamicColumn` parsing style, including numeric alias position patterns from legacy tests.
|
||||
|
||||
## `GroupBy(...)`
|
||||
|
||||
```csharp
|
||||
.GroupBy(x => x.u.UserName)
|
||||
// or
|
||||
.GroupByColumn("u.Name")
|
||||
```
|
||||
|
||||
Group-by supports multiple entries by repeated calls or params arrays.
|
||||
|
||||
## Paging and Distinct
|
||||
|
||||
- `Distinct(true)` toggles `SELECT DISTINCT`.
|
||||
- `Top(n)` delegates to `Limit(n)`.
|
||||
- `Limit(n)` and `Offset(n)` validate database capability flags.
|
||||
|
||||
Capability checks:
|
||||
|
||||
- `Limit` requires one of: `SupportLimitOffset`, `SupportFirstSkip`, `SupportTop`.
|
||||
- `Offset` requires one of: `SupportLimitOffset`, `SupportFirstSkip`.
|
||||
|
||||
Unsupported combinations throw `NotSupportedException`.
|
||||
|
||||
## Subqueries
|
||||
|
||||
```csharp
|
||||
var sub = new DynamicSelectQueryBuilder(db)
|
||||
.From(x => x.dbo.Users)
|
||||
.Where(x => x.id > 100);
|
||||
|
||||
using (var query = new DynamicSelectQueryBuilder(db)
|
||||
.From(x => x(sub).As("u"))
|
||||
.Select(x => x.u.All()))
|
||||
{
|
||||
var rows = query.Execute().ToList();
|
||||
}
|
||||
```
|
||||
|
||||
## Scalar Helpers
|
||||
Subqueries can be embedded in select/where/join contexts.
|
||||
|
||||
```csharp
|
||||
var count = db.From<User>()
|
||||
.Select(x => x.Count())
|
||||
.ScalarAs<int>();
|
||||
cmd.From(x => x.dbo.Users.As(x.u))
|
||||
.Select(x => x(cmd.SubQuery()
|
||||
.From(y => y.dbo.AccessRights.As(y.a))
|
||||
.Where(y => y.a.User_Id == y.u.Id)
|
||||
.Select(y => y.a.IsAdmin)).As(x.IsAdmin));
|
||||
```
|
||||
|
||||
## Modify Builders
|
||||
Supported tested placements:
|
||||
|
||||
- `SELECT (subquery) AS alias`
|
||||
- `WHERE column = (subquery)`
|
||||
- `WHERE column IN (subquery)`
|
||||
- `JOIN (subquery) AS alias ON ...`
|
||||
|
||||
## Typed Execution and Scalar
|
||||
|
||||
```csharp
|
||||
db.Insert("users")
|
||||
.Values("code", "301")
|
||||
.Values("first", "Ada")
|
||||
.Execute();
|
||||
|
||||
db.Update("users")
|
||||
.Values("first", "Alicia")
|
||||
.Where("id", 301)
|
||||
.Execute();
|
||||
|
||||
db.Delete("users")
|
||||
.Where("id", 301)
|
||||
.Execute();
|
||||
var list = db.From<User>("u").Where(x => x.u.Active == true).Execute<User>().ToList();
|
||||
var count = db.From<User>("u").Select(x => x.Count()).ScalarAs<int>();
|
||||
```
|
||||
|
||||
Typed variant:
|
||||
## Practical Patterns
|
||||
|
||||
```csharp
|
||||
db.Insert<User>()
|
||||
.Values(x => x.code, "302")
|
||||
.Values(x => x.first, "Grace")
|
||||
.Execute();
|
||||
```
|
||||
|
||||
## SQL Inspection
|
||||
|
||||
You can inspect generated SQL from builder objects:
|
||||
|
||||
```csharp
|
||||
var sql = query.CommandText();
|
||||
```
|
||||
|
||||
Command text assertions are heavily used in parser tests.
|
||||
- Register source aliases early (`From(...)` first).
|
||||
- Keep join lambdas readable with explicit `Inner/Left/Right` and `On(...)` at the end.
|
||||
- Use invoke syntax only for cases the parser cannot express directly.
|
||||
- Use legacy `DynamicColumn` API when you need explicit grouping flags (`SetBeginBlock`, `SetEndBlock`, `SetOr`).
|
||||
|
||||
Reference in New Issue
Block a user