7.9.32. SQL-31 — Views
A view is a saved SELECT: a virtual table whose rows are produced
on demand by re-running its body. dasSQLITE represents a view with the
same struct-driven story as a table — declare a struct describing the
projected columns, attach [sql_view], and the same read-side helpers
work without any view-specific dispatch.
7.9.32.1. The contract
[sql_view(name = "...")]on a struct registers it as a view shape. Field annotations are the read-only subset of[sql_table]:@sql_column = "..."(rename) and@sql_json/@sql_blob(non-scalar storage). DDL-affecting annotations on the underlying table — keys, uniqueness, computed columns, defaults, foreign keys — are rejected at compile time._create_view(type<V>, chain)builds theCREATE VIEWfrom a LINQ-shaped chain. The macro walks it, validates the projection againstV’s fields (count + per-position type), and emits adb |> execofCREATE VIEW name(c1, ...) AS SELECT ....Reading the view uses
select_from(type<V>)— the same call site as a table.column_info(type<V>)works too (noteis_pkis alwaysfalsefor views).drop_view_if_exists(type<V>)is the idempotent teardown; thetry_form returnsSqlErrorinstead of panicking.
7.9.32.2. Mutation paths
Views are read-only. Any mutation attempt is blocked:
Compile-time rejection.
_sql_update(type<V>, ...),_sql_delete(type<V>, ...), and_sql_upsert(viewRow, ...)fail with: “cannot mutate [sql_view] ‘<name>’ — views are read-only. Mutate the underlying table(s) and re-query the view.”Runtime rejection. Row-form mutations (
insert(viewRow)/update(...)/delete_(...)) panic with the same message.
Real mutating workflows update the underlying tables and re-query the view to see the new state.
7.9.32.3. End-to-end
[sql_table(name = "Customers")]
struct Customer {
@sql_primary_key Id : int
Name : string
City : string
}
[sql_table(name = "Orders")]
struct Order {
@sql_primary_key Id : int
@sql_references = "Customer" CustomerId : int
Amount : int
Status : string
}
[sql_view(name = "BigOrders")]
struct BigOrder {
Id : int
CustomerId : int
Amount : int
Status : string
}
[export]
def main {
with_sqlite(":memory:") $(db) {
db |> create_table(type<Customer>)
db |> create_table(type<Order>)
// ... insert rows ...
// CREATE VIEW BigOrders(Id, CustomerId, Amount, Status) AS
// SELECT Id, CustomerId, Amount, Status FROM Orders
// WHERE Amount >= 100
db |> _create_view(type<BigOrder>,
db |> select_from(type<Order>) |> _where(_.Amount >= 100))
for (b in db |> select_from(type<BigOrder>)) {
to_log(LOG_INFO, "BigOrder #{b.Id}: amt={b.Amount}\n")
}
}
}
A second view with a join + named-tuple projection follows the same
shape; the view’s struct supplies the column names and the
_select((... = ...)) aliases just have to match by position and
type:
[sql_view(name = "OrderSummary")]
struct OrderSummary {
OrderId : int
Customer : string
Amount : int
}
db |> _create_view(type<OrderSummary>,
db |> select_from(type<Order>)
|> _join(db |> select_from(type<Customer>),
$(o : Order, c : Customer) => o.CustomerId == c.Id,
$(o : Order, c : Customer) =>
(OrderId = o.Id, Customer = c.Name, Amount = o.Amount)))
7.9.32.4. Bound parameters are rejected
SQLite stores view bodies as text in sqlite_schema, so ?
placeholders are not allowed inside a view definition. _create_view
rejects chains that reference captured locals at compile time:
let cutoff = 100
db |> _create_view(type<BigOrder>, // compile error
db |> select_from(type<Order>) |> _where(_.Amount >= cutoff))
The fix is to inline literals into the view body and apply runtime filtering at the query site:
db |> _create_view(type<BigOrder>,
db |> select_from(type<Order>) |> _where(_.Amount >= 100))
let recent <- _sql(db |> select_from(type<BigOrder>)
|> _where(_.Amount >= cutoff)) // bind here
7.9.32.5. Raw-SQL escape hatch
_create_view accepts the same chain shapes as _sql (single
source or join, FullRow / NamedTuple / GroupedNamedTuple /
SingleColumn / Aggregate). Anything outside that — window functions,
recursive CTEs, custom SQL functions — needs raw
db |> exec("CREATE VIEW ... AS ..."). The view is queryable
through the same select_from(type<V>) rail as long as a matching
[sql_view] struct is declared:
db |> exec(
"CREATE VIEW IF NOT EXISTS CustomerRank AS " +
"SELECT CustomerId, " +
" ROW_NUMBER() OVER (ORDER BY SUM(Amount) DESC) AS Rank, " +
" SUM(Amount) AS Total " +
"FROM Orders GROUP BY CustomerId")
7.9.32.6. Production tip
In production, view DDL belongs in a migration body so the schema
lives alongside table DDL. The migration story ships in a future
chunk; for now the inline _create_view form is fine for app setup
and tests.
See also
Full source: tutorials/sql/31-views.das
Previous tutorial: SQL-30 — Listing tables
Next tutorial: SQL-32 — User-defined SQL scalar functions