on master help sign in
Remove the `full` read marker. A bare `*rel` read is always allowed (no opt-in gate); `rows <- *rel` in an IO do-block binds the whole relation `[T]`, while a pure comprehension `i <- *rel` iterates per-row — the difference is the block monad, not a marker. `full` now only marks a whole-table WRITE (`full *rel = ...`). Removed the SourceRef.full flag, the codegen full-read gate + unnecessary-full lint, and the full_read_spans/full_read_in_memory tracking.
commit9d49da11d32ec37b0ea0a82a42041b61a7eccc90 authoriko <mail@iko.soy> dateSat 15 Aug 2026, 06:09 (2 months ago) parent59b3f04
11 files changed+76−161
README.md+1−1 crates/knot-compiler/src/codegen.rs+2−56 crates/knot-compiler/src/desugar.rs+1−1 crates/knot-compiler/src/infer.rs+9−9 crates/knot-jit/tests/migration.rs+1−1 crates/knot-jit/tests/persistence.rs+3−3 crates/knot-jit/tests/views.rs+3−3 crates/knot/src/ast.rs+2−5 crates/knot/src/format.rs+1−3 crates/knot/src/parser.rs+9−31 knot.md+44−48
README.md+1−1
@@ -336,7 +336,7 @@ type Todo = {title: Text, done: Int 1}
336336
337337 -- update: bind the rows to a local, then map, marking one done
338338 -- (base.unify t {done 1} merges the row with the new field value)
339− todos <- full *todos
339+ todos <- *todos
340340 *todos = (do
341341 t <- todos
342342 yield (case t.title == "write guide" of
crates/knot-compiler/src/codegen.rs+2−56
@@ -227,16 +227,6 @@ pub struct Codegen<M: cranelift_module::Module = ObjectModule> {
227227 // relation being written — hence the span set.
228228 relational_do_spans: HashSet<ast::Span>,
229229
230− // Spans of `SourceRef` reads written `full *rel`. A `full` marker is only
231− // *needed* when the read loads the whole relation into memory; if codegen
232− // pushes the read down to SQL instead, the marker is unnecessary and is
233− // reported as an error. Populated during expression compilation.
234− full_read_spans: HashSet<ast::Span>,
235− // Spans of `SourceRef` reads that actually reached the in-memory full-load
236− // fallback (`knot_source_read`). The difference `full_read_spans -
237− // full_read_in_memory` is the set of unnecessary `full` markers.
238− full_read_in_memory: HashSet<ast::Span>,
239−
240230 // Resolved monad types for desugared do-blocks (from type inference)
241231 monad_info: MonadInfo,
242232
@@ -927,14 +917,6 @@ fn compile_inner<M: cranelift_module::Module>(
927917 cg.trace_types = trace_types.clone();
928918 cg.trace_bindings = trace_bindings.clone();
929919 cg.source_text = source.to_string();
930− // Collect every `SourceRef` read the user marked `full`, so that any which
931− // codegen pushes down to SQL (and therefore never reaches the in-memory
932− // full-load fallback) can be reported as unnecessary at the end.
933− crate::infer::walk_exprs_read(program, &mut |e| {
934− if let ast::ExprKind::SourceRef { full: true, .. } = &e.node {
935− cg.full_read_spans.insert(e.span);
936− }
937− });
938920 cg.source_name = source_file.to_string();
939921 // Seed the relation-typed top-level constant set from inference's type
940922 // strings (`[T]` / `[T; units]` display forms). Used by
@@ -1180,25 +1162,6 @@ fn compile_inner<M: cranelift_module::Module>(
11801162 if !cg.diagnostics.is_empty() {
11811163 return Err(cg.diagnostics);
11821164 }
1183−
1184− // Any `full`-marked read that never reached the in-memory full-load
1185− // fallback was pushed down to SQL — its `full` marker is unnecessary.
1186− let unnecessary: Vec<ast::Span> = cg
1187− .full_read_spans
1188− .difference(&cg.full_read_in_memory)
1189− .copied()
1190− .collect();
1191− for span in unnecessary {
1192− cg.diagnostics.push(
1193− knot::diagnostic::Diagnostic::error(
1194− "unnecessary `full`: this read is pushed down to SQL, so it does not load the whole relation into memory",
1195− )
1196− .label(span, "here"),
1197− );
1198− }
1199− if !cg.diagnostics.is_empty() {
1200− return Err(cg.diagnostics);
1201− }
12021165 Ok(cg)
12031166}
12041167
@@ -1536,8 +1499,6 @@ impl<M: cranelift_module::Module> Codegen<M> {
15361499 type_aliases: HashMap::new(),
15371500 user_fn_trampolines: HashMap::new(),
15381501 relational_do_spans: HashSet::new(),
1539− full_read_spans: HashSet::new(),
1540− full_read_in_memory: HashSet::new(),
15411502 monad_info: HashMap::new(),
15421503 nullable_ctors: HashMap::new(),
15431504 io_functions: HashSet::new(),
@@ -4885,25 +4846,10 @@ impl<M: cranelift_module::Module> Codegen<M> {
48854846 self.emit_ctor_as_function_value(builder, &name)
48864847 }
48874848
4888− ast::ExprKind::SourceRef { name, full } => {
4849+ ast::ExprKind::SourceRef { name, .. } => {
48894850 // A bare `*rel` read that reaches here was not consumed by any
48904851 // SQL-pushdown matcher — it loads the whole relation into
4891− // memory via `knot_source_read`. Require the user to opt in
4892− // with `full *rel` — EXCEPT inside a fixpoint body, where the
4893− // read is necessarily in-memory (the recursive query is
4894− // iterated by `knot_relation_fixpoint`, not pushed down).
4895− if !full && !self.inside_fixpoint_body {
4896− return self.push_codegen_error(
4897− builder,
4898− expr.span,
4899− format!(
4900− "reading `*{name}` loads the whole relation into memory (no SQL pushdown); write `full *{name}` to allow this",
4901− ),
4902− );
4903− }
4904− // The read IS in-memory: this `full` marker is necessary, so it
4905− // is not reported as unnecessary at the end of codegen.
4906− self.full_read_in_memory.insert(expr.span);
4852+ // memory via `knot_source_read`.
49074853 // Check if this is a view reference
49084854 let view_info = self.views.get(name).cloned();
49094855 if let Some(view) = view_info {
crates/knot-compiler/src/desugar.rs+1−1
@@ -220,7 +220,7 @@ fn rewrite_source_refs_in_expr(
220220 && names.iter().any(|(n, k)| n == bare && *k == kind)
221221 {
222222 expr.node = match kind {
223− RecordRelKind::Source => ExprKind::SourceRef { name: bare.to_string(), full: false },
223+ RecordRelKind::Source => ExprKind::SourceRef { name: bare.to_string() },
224224 };
225225 return;
226226 }
crates/knot-compiler/src/infer.rs+9−9
@@ -8989,17 +8989,17 @@ impl Infer {
89898989 let is_ctor_pat =
89908990 matches!(&pat.node, ast::PatKind::Constructor { .. });
89918991
8992− // `rows <- full *rel`: a `full`-marked source read loads the
8993− // WHOLE relation into memory and binds it as a relation
8994− // value `[T]` — it does NOT iterate per-row. (`full` is a
8995− // viewer-facing tag for the intentional whole-table load.)
8996− let is_full_read_bind = matches!(
8997− &expr.node,
8998− ast::ExprKind::SourceRef { full: true, .. }
8999− );
8992+ // `rows <- *rel` in an IO do-block loads the WHOLE relation
8993+ // into memory and binds it as a relation value `[T]` — it
8994+ // does NOT iterate per-row. (In a pure comprehension the
8995+ // same `i <- *rel` bind iterates per-row; the difference is
8996+ // the block's monad: `in_io_do` is set by the prescan when
8997+ // the block performs IO.)
8998+ let is_full_read_bind = self.in_io_do
8999+ && matches!(&expr.node, ast::ExprKind::SourceRef { .. });
90009000 if is_full_read_bind {
90019001 self.check_pattern(pat, &resolved);
9002− // Track `rows <- full *foo` for `set` detection too.
9002+ // Track `rows <- *foo` for `set` detection too.
90039003 if let ast::PatKind::Var(var_name) = &pat.node
90049004 && let ast::ExprKind::SourceRef { name: source_name, .. } = &expr.node {
90059005 self.source_var_binds
crates/knot-jit/tests/migration.rs+1−1
@@ -89,7 +89,7 @@ type PersonV2 = {name: Text, active: Active}
8989 migrate from PersonV1 to PersonV2 using \p -> {name p.name active (Active.Yes {})}
9090}
9191(do
92− people <- full *people
92+ people <- *people
9393 base.println (base.show people)
9494 yield {})"#,
9595 );
crates/knot-jit/tests/persistence.rs+3−3
@@ -38,7 +38,7 @@ type C = {n: Int 1}
3838}
3939(do
4040 full *cs = [{n 1} {n 2} {n 3}]
41− rows <- full *cs
41+ rows <- *cs
4242 base.println (base.show (base.count rows))
4343 base.println (base.show (base.sum (base.map (\c -> c.n) rows)))
4444 yield {})"#,
@@ -138,7 +138,7 @@ type Account = {name: Text, balance: Int 1}
138138(do
139139 full *accounts = [{name "from" balance 100} {name "to" balance 0}]
140140 _ <- atomic do
141− rows <- full *accounts
141+ rows <- *accounts
142142 *accounts = base.map (\a ->
143143 case a.name == "from" of
144144 Bool.True {} -> (base.unify a {balance (a.balance - 40)})
@@ -146,7 +146,7 @@ type Account = {name: Text, balance: Int 1}
146146 Bool.True {} -> (base.unify a {balance (a.balance + 40)})
147147 Bool.False {} -> a)) rows
148148 yield {}
149− base.println (base.show (full *accounts))
149+ base.println (base.show *accounts)
150150 yield {})"#,
151151 "\"[{balance: 60, name: from}, {balance: 40, name: to}]\"\n{}",
152152 );
crates/knot-jit/tests/views.rs+3−3
@@ -51,7 +51,7 @@ doubled (do
5151#[test]
5252fn view_read_filters_constant_column() {
5353 // A filtered view `*openTodos = do t <- *todos; where …; yield …` read via
54− // `full *openTodos` resolves to the source `_knot_todos` with the
54+ // `*openTodos` resolves to the source `_knot_todos` with the
5555 // constant-column filter — only the Open todo is returned.
5656 assert_stdout(
5757 "view_read",
@@ -65,7 +65,7 @@ data Status = Open {} | Closed {}
6565}
6666(do
6767 full *todos = [{title "a" status (Status.Open {})} {title "b" status (Status.Closed {})}]
68− rows <- full *openTodos
68+ rows <- *openTodos
6969 base.println (base.show (base.count rows))
7070 yield {})"#,
7171 "\"1\"\n{}",
@@ -88,7 +88,7 @@ data Status = Open {} | Closed {}
8888}
8989(do
9090 full *openTodos = [{title "task"}]
91− all <- full *todos
91+ all <- *todos
9292 base.println (base.show (base.count all))
9393 base.println (base.show all)
9494 yield {})"#,
crates/knot/src/ast.rs+2−5
@@ -73,11 +73,8 @@ pub enum ExprKind {
7373 /// `Circle`, `Open` — PascalCase constructor reference.
7474 Constructor(Name),
7575
76− /// `*people` — reference to a source relation. `full` is set when the
77− /// user wrote `full *people`, acknowledging that the read loads the whole
78− /// relation into memory (no SQL pushdown). A read that *is* pushed down
79− /// ignores the flag.
80− SourceRef { name: Name, full: bool },
76+ /// `*people` — reference to a source relation.
77+ SourceRef { name: Name },
8178
8279 /// `^name` — implicit field projection: DFS the fields of in-scope record
8380 /// bindings (innermost scope first, shallowest, earliest field) for a
crates/knot/src/format.rs+1−3
@@ -584,9 +584,7 @@ fn render_expr_inline(e: &Expr, parent: Prec) -> String {
584584 ExprKind::Var(n) if n == "yield" && parent == Prec::Atom => format!("({})", n),
585585 ExprKind::Var(n) => n.clone(),
586586 ExprKind::Constructor(n) => n.clone(),
587− ExprKind::SourceRef { name: n, full } => {
588− if *full { format!("full *{}", n) } else { format!("*{}", n) }
589− }
587+ ExprKind::SourceRef { name: n } => format!("*{}", n),
590588 ExprKind::ImplicitRef(n) => format!("^{}", n),
591589 ExprKind::CollectFold(n) => format!("<>{}", n),
592590 ExprKind::TypeHole => "_".to_string(),
crates/knot/src/parser.rs+9−31
@@ -1355,9 +1355,8 @@ impl Parser {
13551355 TokenKind::Full => {
13561356 // `full *rel = expr` is a replace-set expression. So is
13571357 // `full db.*rel = expr` (a source-field on a record-var:
1358− // `Lower` `.` `StarIdent`). A bare `full *rel` (no `=`) is a
1359− // full-read marker: the user acknowledges the read loads the
1360− // whole relation into memory. Otherwise `full` is a regular
1358+ // `Lower` `.` `StarIdent`). `full` only marks a WRITE; a bare
1359+ // `*rel` read needs no marker. Otherwise `full` is a regular
13611360 // identifier.
13621361 let mut offset = 1;
13631362 while self.peek_ahead(offset) == &TokenKind::Newline {
@@ -1374,8 +1373,9 @@ impl Parser {
13741373 self.skip_newlines();
13751374 self.parse_set_with_start(true, replace_start)
13761375 } else if next == &TokenKind::Star || matches!(next, TokenKind::StarIdent(_)) {
1377− // Bare `*rel` target: read vs write decided by a following `=`.
1378− // Find the token just past the source reference.
1376+ // `full` before a source is only valid as a write
1377+ // (`full *rel = expr`). A bare `full *rel` read-marker was
1378+ // removed — reads are written `*rel`.
13791379 let past = if matches!(next, TokenKind::StarIdent(_)) {
13801380 offset + 1 // `*rel` is one token
13811381 } else {
@@ -1389,30 +1389,8 @@ impl Parser {
13891389 self.skip_newlines();
13901390 self.parse_set_with_start(true, replace_start)
13911391 } else {
1392− // Read: `full *rel` — mark the SourceRef as full.
1393− let start = self.span();
1394− self.advance(); // consume `full`
1395− self.skip_newlines();
1396− let tok = self.advance();
1397− let name = match tok.kind {
1398− TokenKind::StarIdent(n) => n.trim_start_matches('*').to_string(),
1399− TokenKind::Star => {
1400− let id = self.advance();
1401− match id.kind {
1402− TokenKind::Lower(n) => n,
1403− _ => {
1404− self.error("expected identifier after '*' for source reference");
1405− return None;
1406− }
1407− }
1408− }
1409− _ => unreachable!(),
1410− };
1411− let end = self.prev_span();
1412− Some(Spanned::new(
1413− ExprKind::SourceRef { name, full: true },
1414− Span::new(start.start, end.end),
1415− ))
1392+ self.error("`full` only marks a write (`full *rel = ...`); a read is just `*rel`");
1393+ None
14161394 }
14171395 } else {
14181396 // `full` used as a regular identifier — fall through to
@@ -2093,7 +2071,7 @@ impl Parser {
20932071 let tok = self.advance();
20942072 let TokenKind::StarIdent(n) = tok.kind else { unreachable!() };
20952073 Some(Spanned::new(
2096− ExprKind::SourceRef { name: n.trim_start_matches('*').to_string(), full: false },
2074+ ExprKind::SourceRef { name: n.trim_start_matches('*').to_string() },
20972075 Span::new(start.start, tok.span.end),
20982076 ))
20992077 }
@@ -2105,7 +2083,7 @@ impl Parser {
21052083 let tok = self.advance();
21062084 let TokenKind::Lower(name) = tok.kind else { unreachable!() };
21072085 Some(Spanned::new(
2108− ExprKind::SourceRef { name, full: false },
2086+ ExprKind::SourceRef { name },
21092087 Span::new(start.start, tok.span.end),
21102088 ))
21112089 }
knot.md+44−48
@@ -11,7 +11,7 @@ type Person = {name: Text, age: Int 1}
1111
1212do
1313 full *people = [{name "Alice" age 30} {name "Bob" age 25}]
14− people <- full *people
14+ people <- *people
1515 with {result do
1616 p <- people
1717 where p.age > 27
@@ -156,7 +156,7 @@ data Status = Open {} | Closed {}
156156
157157-- Derived: read-only computed relation
158158&seniors = do
159− people <- full *people
159+ people <- *people
160160 yield (base.filter (\p -> p.age > 65) people)
161161
162162-- Constant
@@ -321,27 +321,23 @@ The join columns (`employees.dept`, `departments.name`) and filter columns
321321planner cannot translate falls back to reading the sources and joining in
322322memory — results are identical either way, only the strategy differs.
323323
324−### `full` reads
324+### Whole-relation reads
325325
326−When a relation read is **not** pushed down to SQL, the whole relation is
327−loaded into memory. Knot requires you to mark such a read with `full` before
328−the relation name, so the cost is explicit:
326+A relation read that is **not** pushed down to SQL loads the whole relation
327+into memory. This happens at a `base.run` boundary or a whole-relation bind
328+(`rows <- *rel` in an IO do-block), both of which materialize the query:
329329
330330```knot
331331*people : [{name: Text, age: Int 1}]
332332(do
333− people <- full *people -- whole relation loaded into memory
333+ people <- *people -- whole relation loaded into memory
334334 base.println (base.show (base.count people))
335335 yield {})
336336```
337337
338338A read that *is* pushed down — a comprehension the planner translates, or a
339−recognized aggregate such as `base.count *people` — needs no marker. The
340−compiler reports an error (`reading `*rel` loads the whole relation into
341−memory … write `full *rel``) at any read it cannot push down, so you always
342−know where a full table load happens. Writing `full` on a read that turns out
343−to be pushed down is itself an error (`unnecessary `full``), so the marker is
344−present exactly where a full load occurs — no more, no less.
339+recognized aggregate such as `base.count *people` — runs in SQL and loads
340+nothing. The strategy is chosen at the use site, after the query is inlined.
345341
346342Statements in a `do` block:
347343
@@ -369,7 +365,7 @@ do
369365-- IO do block with DB operations
370366*people : [{name: Text, age: Int 1}]
371367addPerson \name age -> do
372− people <- full *people
368+ people <- *people
373369 *people = base.union people [{name name age age}]
374370```
375371
@@ -385,7 +381,7 @@ data Status = InProgress {assignee: Text} | Done {}
385381*shapes : [Shape]
386382*tickets : [{title: Text, status: Status}]
387383&circles = do
388− shapes <- full *shapes
384+ shapes <- *shapes
389385 with {result do
390386 Shape.Circle c <- shapes
391387 yield c}
@@ -393,7 +389,7 @@ data Status = InProgress {assignee: Text} | Done {}
393389 yield result)
394390
395391&inProgress = do
396− tickets <- full *tickets
392+ tickets <- *tickets
397393 with {result do
398394 t <- tickets
399395 Status.InProgress ip <- t.status
@@ -413,12 +409,12 @@ Mutation is written `*rel = expr`, which makes the source relation equal to `exp
413409
414410-- Insert (union with singleton)
415411addPerson do
416− people <- full *people
412+ people <- *people
417413 *people = base.union people [{name "Alice" age 30}]
418414
419415-- Update (map with conditional)
420416birthday \name -> do
421− people <- full *people
417+ people <- *people
422418 *people = do
423419 p <- people
424420 yield (case p.name == name of
@@ -427,7 +423,7 @@ birthday \name -> do
427423
428424-- Delete (filter to keep)
429425removePerson \name -> do
430− people <- full *people
426+ people <- *people
431427 *people = do
432428 p <- people
433429 where p.name != name
@@ -581,7 +577,7 @@ Fields can hold `[T]` — sets nested inside rows:
581577
582578-- Query into nested relations
583579&allMembers = do
584− teams <- full *teams
580+ teams <- *teams
585581 with {result do
586582 t <- teams
587583 m <- t.members
@@ -591,7 +587,7 @@ Fields can hold `[T]` — sets nested inside rows:
591587
592588-- Update nested relations
593589updateTeams do
594− teams <- full *teams
590+ teams <- *teams
595591 *teams = do
596592 t <- teams
597593 yield (base.unify t {members do
@@ -631,7 +627,7 @@ data Status = Open {} | Closed {}
631627
632628-- Insert through view — status auto-filled
633629addOpenTodo do
634− openTodos <- full *openTodos
630+ openTodos <- *openTodos
635631 *openTodos = base.union openTodos [{title "New task" owner "Alice" priority Priority.High {}}]
636632```
637633
@@ -645,12 +641,12 @@ Read-only computed relations, prefixed with `&`:
645641*people : [{name: Text, age: Int 1}]
646642*todos : [{title: Text, owner: Text, done: Int 1}]
647643&seniors = do
648− people <- full *people
644+ people <- *people
649645 yield (base.filter (\p -> p.age > 65) people)
650646
651647&stats = do
652648 with {result do
653− t <- full *todos
649+ t <- *todos
654650 groupBy {owner t.owner}
655651 yield {owner t.owner total (base.count t)}}
656652 (do
@@ -666,7 +662,7 @@ Datalog-style fixpoint iteration for transitive closure:
666662
667663-- Base case: direct management edges, renamed to ancestor/descendant
668664&direct = (do
669− manages <- full *manages
665+ manages <- *manages
670666 yield (do
671667 m <- manages
672668 yield {ancestor m.manager descendant m.report}))
@@ -674,7 +670,7 @@ Datalog-style fixpoint iteration for transitive closure:
674670-- Recursive: ancestor/descendant via fixpoint over the base case
675671&reportsTo = do
676672 reportsTo <- &reportsTo -- self-reference (IO bind)
677− manages <- full *manages
673+ manages <- *manages
678674 direct <- &direct
679675 with {step do
680676 r <- reportsTo
@@ -745,7 +741,7 @@ type Person = {name: Text, age: Int 1}
745741
746742-- All relation operations are IO:
747743birthday \name -> do
748− people <- full *people -- IO [Person]
744+ people <- *people -- IO [Person]
749745 *people = do -- IO {}
750746 p <- people
751747 yield (case p.name == name of
@@ -763,9 +759,9 @@ birthday \name -> do
763759*orders : [{item: Text, qty: Int 1}]
764760handleOrder \item -> do
765761 orderId <- atomic do
766− orders <- full *orders
762+ orders <- *orders
767763 *orders = base.union orders [{item item qty 1}]
768− newOrders <- full *orders
764+ newOrders <- *orders
769765 yield (base.count newOrders)
770766 base.println ("Order #" ++ base.show orderId)
771767 yield {orderId orderId}
@@ -1038,11 +1034,11 @@ route AdminApi where
10381034route Api = TodoApi | AdminApi
10391035
10401036addTodo (\title owner -> do
1041− todos <- full *todos
1037+ todos <- *todos
10421038 *todos = base.union todos [{title title owner owner priority (Priority.Low {})}])
10431039
10441040getTodos (\owner -> do
1045− todos <- full *todos
1041+ todos <- *todos
10461042 with {result (do t <- todos; where t.owner == owner; yield t)} yield result)
10471043
10481044-- Handler
@@ -1054,7 +1050,7 @@ api (serve Api where
10541050 addTodo title owner
10551051 yield (Result.Ok {value {title title owner owner priority (Priority.Low {})}})
10561052 GetCount = \{} -> do
1057− todos <- full *todos
1053+ todos <- *todos
10581054 yield (Result.Ok {value (base.count todos)}))
10591055```
10601056
@@ -1070,7 +1066,7 @@ route Api where
10701066 GET /users/{id: Int 1} -> {id: Int 1, name: Text} = GetUser
10711067api (serve Api where
10721068 GetUser = \{id id} -> do
1073− users <- full *people
1069+ users <- *people
10741070 case base.filter (\u -> u.id == id) users of
10751071 [] -> yield (Result.Err {error {status 404 message "user not found"}})
10761072 Cons u rest -> yield (Result.Ok {value u}))
@@ -1104,7 +1100,7 @@ route Api where
11041100
11051101api (serve Api where
11061102 GetTodos = \{authorization authorization} -> do
1107− allTodos <- full *todos
1103+ allTodos <- *todos
11081104 yield (Result.Ok {value {body allTodos headers {xTotalCount (base.count allTodos)}}}))
11091105```
11101106
@@ -1338,11 +1334,11 @@ type Todo = {title: Text, owner: Text, priority: Priority, status: Status}
13381334*todos : [Todo]
13391335
13401336add \title owner priority -> do
1341− todos <- full *todos
1337+ todos <- *todos
13421338 *todos = base.union todos [{title title owner owner priority priority status (Status.Open {})}]
13431339
13441340complete \title -> do
1345− todos <- full *todos
1341+ todos <- *todos
13461342 *todos = do
13471343 t <- todos
13481344 yield (case t.title == title of
@@ -1350,7 +1346,7 @@ complete \title -> do
13501346 Bool.False {} -> t)
13511347
13521348assign \title person -> do
1353− todos <- full *todos
1349+ todos <- *todos
13541350 *todos = do
13551351 t <- todos
13561352 yield (case t.title == title of
@@ -1358,7 +1354,7 @@ assign \title person -> do
13581354 Bool.False {} -> t)
13591355
13601356pending \owner -> do
1361− todos <- full *todos
1357+ todos <- *todos
13621358 with {result do
13631359 t <- todos
13641360 where t.owner == owner
@@ -1369,7 +1365,7 @@ pending \owner -> do
13691365
13701366&workload = do
13711367 with {result do
1372− t <- full *todos
1368+ t <- *todos
13731369 where t.status == Status.Open {}
13741370 groupBy {owner t.owner}
13751371 yield {owner t.owner count (base.count t)}}
@@ -1400,7 +1396,7 @@ do
14001396```knot
14011397*rel : [{value: Int 1}]
14021398addRow \newRow -> do
1403− rel <- full *rel
1399+ rel <- *rel
14041400 *rel = base.union rel [newRow]
14051401```
14061402
@@ -1409,7 +1405,7 @@ addRow \newRow -> do
14091405```knot
14101406*rel : [{id: Int 1, field: Int 1}]
14111407deleteWhere \valueToDelete -> do
1412− rel <- full *rel
1408+ rel <- *rel
14131409 *rel = do
14141410 r <- rel
14151411 where r.field != valueToDelete
@@ -1421,7 +1417,7 @@ deleteWhere \valueToDelete -> do
14211417```knot
14221418*rel : [{id: Int 1, field: Int 1}]
14231419updateWhere \target newValue -> do
1424− rel <- full *rel
1420+ rel <- *rel
14251421 *rel = do
14261422 r <- rel
14271423 yield (case r.id == target of
@@ -1435,8 +1431,8 @@ updateWhere \target newValue -> do
14351431*employees : [{name: Text, dept: Text}]
14361432*departments : [{name: Text, budget: Int 1}]
14371433&joined = do
1438− employees <- full *employees
1439− departments <- full *departments
1434+ employees <- *employees
1435+ departments <- *departments
14401436 with {result do
14411437 e <- employees
14421438 d <- departments
@@ -1452,11 +1448,11 @@ updateWhere \target newValue -> do
14521448*orders : [{amount: Int 1}]
14531449*people : [{name: Text, age: Int 1}]
14541450getTotal do
1455− orders <- full *orders
1451+ orders <- *orders
14561452 yield (base.fold (\acc x -> acc + x.amount) 0 orders)
14571453
14581454getCount do
1459− people <- full *people
1455+ people <- *people
14601456 yield (base.count people)
14611457```
14621458
@@ -1468,12 +1464,12 @@ data Shape = Circle {radius: Float 1} | Rect {width: Float 1, height: Float 1}
14681464
14691465-- Using match
14701466&circles = do
1471− shapes <- full *shapes
1467+ shapes <- *shapes
14721468 yield (base.match Shape.Circle shapes)
14731469
14741470-- Using pattern bind in do
14751471&circles2 = do
1476− shapes <- full *shapes
1472+ shapes <- *shapes
14771473 with {result do
14781474 Shape.Circle c <- shapes
14791475 yield c}