7.9.4. SQL-04 — Reading Rows with _sql
Tutorials 1-3 covered opening a database, declaring a table, and writing
rows. This tutorial introduces _sql(...) — the call macro that
translates a daslib/linq-shaped chain into SQL at compile time — and
the select_from / _where / _select chain stages.
7.9.4.1. Setup
Three requires:
require daslib/sql
require sqlite/sqlite_boost
require sqlite/sqlite_linq
The first two are the standard pair from tutorial 1. The third,
sqlite/sqlite_linq, brings in the _sql and _sql_text call
macros plus the select_from runtime entry. The abstract layer
daslib/sql deliberately does not re-export sqlite-specific
symbols — each provider keeps its linq adapter in its own boost
module so a future postgres/postgres_linq lands cleanly alongside.
7.9.4.2. Reading every row
The minimal chain select_from(type<T>) reads every row of the table
into array<T>:
let cars <- _sql(db |> select_from(type<Car>))
for (car in cars) {
to_log(LOG_INFO, " {car.Name}: {car.Price}\n")
}
The macro emits the SQL SELECT "Id", "Name", "Price" FROM "Cars"
and the runtime materializes each row into a Car via the
per-field sqlite_read overloads [sql_table] generates. The
move-assign <- is required because array<Car> is non-copyable.
7.9.4.3. Filtering with _where (bind capture)
_where(predicate) pipes after the chain root and adds a WHERE
clause. Free variables in the predicate are captured and lowered to
? bind parameters — so the SQL is parameterized, not
string-interpolated:
let threshold = 200
let pricey <- _sql(
db |> select_from(type<Car>) |> _where(_.Price > threshold))
// emits: SELECT "Id", "Name", "Price" FROM "Cars" WHERE "Price" > ?
// binds: [threshold]
The _.Field shorthand inside the predicate refers to a column of
the row source. The macro classifies each name in the predicate as
either a column reference (_.X) or a captured value (everything
else), and routes them to the SQL identifier vs. bind list
accordingly. Multiple _where calls compose AND-wise.
Supported operators inside _where: ==, !=, <, <=,
>, >=, &&, ||, !. The captured side may be a
literal or a free variable from any enclosing scope.
7.9.4.4. Projecting a single column with _select
_select(_.Field) projects a single column, producing
array<FieldType> instead of array<T>:
let names <- _sql(db |> select_from(type<Car>) |> _select(_.Name))
// emits: SELECT "Name" FROM "Cars"
// result: array<string>
This chunk supports the single-column _.Field form. Multi-column
projections (struct constructors, named-tuple literals) ship in a
later chunk.
7.9.4.5. Inspecting the emitted SQL with _sql_text
_sql_text is the debug companion: same chain, but instead of
running it the macro returns the SQL string that _sql would have
prepared. ? placeholders stay literal — the bind values are not
substituted into the text:
let sql = _sql_text(
db |> select_from(type<Car>) |> _where(_.Id == 5))
to_log(LOG_INFO, "{sql}\n")
// SELECT "Id", "Name", "Price" FROM "Cars" WHERE "Id" = ?
Use this in tests to lock the emission shape, and during development to confirm the macro is translating what you expect.
7.9.4.6. Composability
_sql runs as a default [call_macro] (canVisitArgument =
true), so any user-defined call macro that expands to one of the
recognised chain stages composes transparently. A wrapper macro that
expands to _where(it, _.Price < threshold) slots into a _sql
chain at the same place a literal _where would, and the analyzer
walks the post-expansion shape uniformly. Tests in
tests/dasSQLITE/test_07_sql_composability.das pin the contract.
7.9.4.7. Quick reference
Helper |
Description |
|---|---|
|
Translate a linq-shaped chain to SQL and materialize |
|
Same translation, return the SQL string instead |
|
Chain root — iterate every row of table T |
|
Filter; captured variables become |
|
Single-column projection |
|
No-op terminal — materializes into |
See also
Full source: tutorials/sql/04-select_all.das
Previous tutorial: SQL-03 — Auto-Increment Primary Key
Next tutorial: SQL-05 — Parameter Binding