7.9.7. SQL-07 — Anatomy of _sql

_sql(chain) is a compile-time macro. It walks a daslib/linq-shaped chain, classifies each operator, and emits a SQL string plus a list of bind expressions. By the time the program runs, the chain is gone — only the SQL and the binds remain. There is no runtime LINQ-to-SQL inspection.

7.9.7.1. Three moving parts

Source shape

Compile-time translation

_.Field

quoted column identifier ("Field")

free variable, literal

? bind parameter

recognized operator / function call

SQL operator (=, <, AND, LIKE, COALESCE, …)

Anything outside that surface raises a compile-time macro_error pointing at the offending node.

7.9.7.2. Inspecting the SQL with _sql_text

_sql_text shares the analyzer with _sql and returns the SQL string instead of running it. The ? placeholders show where each bind goes:

let sql1 = _sql_text(db |> select_from(type<Car>)
                       |> _where(_.Price > 100))
// SELECT "Id", "Name", "Price" FROM "Cars" WHERE "Price" > ?

This is the primary debugging tool for _sql chains — when the chain grows, _sql_text makes the macro’s view of it visible without a database round-trip.

7.9.7.3. Captured-vs-literal binding

A captured local becomes a bind. A literal also becomes a bind — the analyzer doesn’t bother distinguishing, because keeping the SQL parameterized either way is the safe-by-default behavior:

let cutoff = 150
let sql2 = _sql_text(db |> select_from(type<Car>)
                       |> _where(_.Price > cutoff))
// SELECT "Id", "Name", "Price" FROM "Cars" WHERE "Price" > ?

Column-side detection is symmetric — the analyzer recognizes _.Field as a column on whichever side of an operator it appears:

_where(_.Price > cutoff)    // WHERE "Price" > ?
_where(cutoff < _.Price)    // WHERE ? < "Price"

7.9.7.4. Composing _where

Each _where adds another AND conjunct:

_sql(db |> select_from(type<Car>)
       |> _where(_.Price > 100)
       |> _where(_.Name |> starts_with("T")))
// ... WHERE ("Name" LIKE ? || '%') AND ("Price" > ?)

The full _where translation surface lives in SQL-08 — _where Predicates: the Full Surface.

7.9.7.5. The raw-SQL escape hatch

For SQL the macro can’t or shouldn’t translate (DDL, vendor-specific statements, dynamic SQL), drop down to the raw-SQL helpers covered in SQL-05 — Parameter Binding:

Helper

Purpose

db |> exec(sql)

run a statement, no result

db |> query_scalar(sql, type<T>)

SELECT one column from one row

db |> query_one(sql, type<T>, args...)

SELECT one row, build a struct

db |> query_one_opt(sql, type<T>, args...)

same, but Option<T> if no row

Bind parameters are positional ? — pass values as trailing arguments.

7.9.7.6. The translation surface in one glance

Tutorials 7-18 cover the full read-side translation. Each one shows its operator’s recognized shapes and the SQL the macro emits:

Tutorial

Surface

SQL-08 — _where Predicates: the Full Surface

predicates: ==, <, &&, starts_with, …

SQL-09 — _select Projections

projections: _.Field, named tuples

SQL-10 — _order_by and _order_by_descending

_order_by, _order_by_descending

SQL-11 — take and skip: Paging

take / skip (LIMIT / OFFSET)

SQL-12 — distinct

distinct

SQL-13 — Aggregates: sum/avg/…

count / sum / average / min / max

SQL-14 — _group_by and _having

_group_by + _having

SQL-18 — NULL Handling: Option<T> Everywhere

Option<T> columns + is_some / is_none / unwrap_or

7.9.7.7. What _sql refuses

If you write a shape outside the translation table — an arbitrary user-defined function in _where, an unsupported math op, a regex — compile fails with macro_error pointing at the offending node. Two responses:

  • Add the rule to the analyzer (fork-and-extend) if the pattern is reusable across queries.

  • Use the raw-SQL escape hatch above for one-offs.