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 |
|---|---|
|
quoted column identifier ( |
free variable, literal |
|
recognized operator / function call |
SQL operator ( |
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 |
|---|---|
|
run a statement, no result |
|
SELECT one column from one row |
|
SELECT one row, build a struct |
|
same, but |
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 |
|---|---|
predicates: |
|
projections: |
|
|
|
|
|
|
|
|
|
|
|
|
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.
See also
Full source: tutorials/sql/07-anatomy.das
Previous tutorial: SQL-06 — Error Handling
Next tutorial: SQL-08 — _where Predicates: the Full Surface