diff --git a/docs/fluent-builder-api.md b/docs/fluent-builder-api.md index 49c8e82..55449eb 100644 --- a/docs/fluent-builder-api.md +++ b/docs/fluent-builder-api.md @@ -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("u") - .Where(x => x.u.id == 19) - .Select(x => x.u.All())) -{ - var users = query.Execute().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("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() - .Select(x => x.Count()) - .ScalarAs(); +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("u").Where(x => x.u.Active == true).Execute().ToList(); +var count = db.From("u").Select(x => x.Count()).ScalarAs(); ``` -Typed variant: +## Practical Patterns -```csharp -db.Insert() - .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`).