1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
use reblessive::tree::Stk;
use super::IgnoreError;
use crate::ctx::FrozenContext;
use crate::dbs::{Options, Statement};
use crate::doc::Document;
use crate::val::Value;
impl Document {
pub(crate) async fn update(
&mut self,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
stm: &Statement<'_>,
) -> Result<Value, IgnoreError> {
// SECURITY (GHSA-2v9j): confine record users to their own tenant. The
// read path enforces this at the scan operators, but writes perform no
// scan, so the gate must be applied explicitly here.
self.check_record_user_access(opt)?;
// Ensure the record actually exists
self.check_record_exists()?;
// SECURITY: evaluate the table-level update permission BEFORE any
// user-supplied expression in the WHERE clause or data clause.
// Otherwise a `WHERE THROW ...` / `SET x = THROW ...` could exfiltrate
// field values before the permission check rejects the operation.
self.check_update_permissions(stk, ctx, opt, &self.current).await?;
// Reject writes to read-only view tables (after the permission gate)
self.check_table_not_view(opt)?;
// A lightweight edge is immutable. Gated after the permission check —
// like the view gate — so an actor without update permission gets the
// normal permission outcome rather than an error disclosing the
// table's storage mode; still before the WHERE condition and data
// clause, so no user expression runs against an edge that can never
// be updated.
self.check_table_not_lightweight("updated")?;
// Check if the WHERE condition is truthy BEFORE evaluating the data
// clause, so a data clause with side effects runs only for records the
// condition accepts. This also matches the index-backed plan, where
// records rejected by the condition never enter this pipeline at all.
self.check_where_condition(stk, ctx, opt, stm.cond()).await?;
// Ensure any input data is computed
self.compute_input_data(stk, ctx, opt, stm).await?;
// Ensure all special fields are valid
self.check_data_fields()?;
// Set the specified record content
self.process_record_data(stk, ctx, opt).await?;
// Set the default record field values
self.default_record_data()?;
// Process the field schema for the table
self.process_table_fields(stk, ctx, opt, stm).await?;
// Clean up table fields and NONE values
self.cleanup_table_fields()?;
// Check table permissions after update. The post-mutation document is
// gated too; a predicate naming a COMPUTED field sees it recomputed
// against the mutated record, on a scratch copy that never reaches
// storage.
self.recheck_update_permissions(stk, ctx, opt, &self.current).await?;
// Store the document and index data
self.store_record_data(ctx, stm).await?;
self.store_inline_adjacency_data(ctx, opt).await?;
self.store_index_data(stk, ctx, opt).await?;
// Materialise the computed fields the record's observers read: they are
// stripped before storage, so events, live queries, changefeeds and the
// output projection would otherwise see the record without them.
self.materialise_observed_fields(stk, ctx, opt).await?;
// Process additional table operations
self.process_table_references(stk, ctx, opt).await?;
self.process_table_views(stk, ctx, opt, super::Action::Update).await?;
self.process_table_events(stk, ctx, opt, super::Action::Update).await?;
self.process_table_lives(stk, ctx, opt, super::Action::Update).await?;
self.process_changefeeds(ctx, opt).await?;
// Check table permissions for output
self.check_select_permissions(stk, ctx, opt, &self.current).await?;
// Process the projected output document
self.output_write(stk, ctx, opt, stm.output(), stm).await
}
}