Expand fluent syntax docs with parser and join nuances

This commit is contained in:
root
2026-02-26 08:19:38 +01:00
parent c1a35eec7a
commit cf15a898be

View File

@@ -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`).