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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use reblessive::tree::Stk;
use surrealdb_types::ToSql;
use crate::catalog::FieldDefinition;
use crate::ctx::FrozenContext;
use crate::dbs::Options;
use crate::doc::{CursorDoc, Document};
use crate::err::Error;
use crate::expr::FlowResultExt as _;
use crate::val::RecordId;
/// Identifies which of the four `CursorDoc` views on a [`Document`] to
/// evaluate computed fields against.
///
/// Reduction and computed-field evaluation are orthogonal: a SELECT against
/// a permission-restricted table needs reduction applied first, then
/// computed fields populated on the reduced view (so a permission predicate
/// `WHERE flag` sees `flag` after both filtering and computation).
#[derive(Clone, Copy, Debug)]
pub(super) enum DocKind {
Initial,
Current,
InitialReduced,
CurrentReduced,
}
impl Document {
/// Returns true when this document's table has at least one field
/// with a `COMPUTED` clause. Used to short-circuit the computed-field
/// pipeline on tables that don't have any.
pub(super) fn has_computed_fields(&self) -> bool {
match self.doc_ctx.fd() {
Ok(fields) => fields.iter().any(|fd| fd.computed.is_some()),
Err(_) => false,
}
}
/// Evaluate the closure of computed fields required to satisfy
/// `needed_roots` against the chosen [`DocKind`] view of this
/// document, populating the results into that view's `CursorDoc`.
///
/// - `needed_roots = None` means "every computed field is potentially referenced" — evaluate
/// them all. This is the conservative choice used by the write paths that return the full new
/// record (CREATE / UPSERT / UPDATE / RELATE / INSERT default output) and by `Output::After`
/// / `Output::Before` / `Output::Diff`.
/// - `needed_roots = Some(roots)` restricts evaluation to the transitive closure of those root
/// field names. SELECT extracts `roots` from its projection / WHERE / ORDER / GROUP / SPLIT.
///
/// When any field's dependency set is `is_complete = false`
/// (opaque sub-expressions, parameters, graph traversals), we fall
/// back to evaluating every computed field. This is the same
/// safety net main carries.
pub(super) async fn compute_fields(
&mut self,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc_kind: DocKind,
needed_roots: Option<&HashSet<String>>,
) -> anyhow::Result<()> {
// Skip when the table has no computed fields at all.
if !self.has_computed_fields() {
return Ok(());
}
// Computed field evaluation needs a record id — temporary
// documents (no id) have no schema to project against.
let Ok(rid) = self.id() else {
return Ok(());
};
let fields = Arc::clone(self.doc_ctx.fd()?);
// Resolve the doc reference up front so the &mut self borrow
// is dropped before the async call below.
let doc: &mut CursorDoc = match doc_kind {
DocKind::Initial => &mut self.initial,
DocKind::Current => &mut self.current,
DocKind::InitialReduced => match self.initial_reduced.as_mut() {
Some(d) => d,
None => return Ok(()),
},
DocKind::CurrentReduced => match self.current_reduced.as_mut() {
Some(d) => d,
None => return Ok(()),
},
};
let Some(needed_roots) = needed_roots else {
return Document::computed_fields_inner(
stk,
ctx,
opt,
rid.as_ref(),
&fields,
doc,
None,
)
.await;
};
// Build dependency metadata for computed fields only.
let mut dep_map: HashMap<String, crate::expr::computed_deps::ComputedDeps> = HashMap::new();
for fd in fields.iter() {
if fd.computed.is_none() {
continue;
}
let field_name = fd.name.to_raw_string();
let deps = if let Some(cd) = &fd.computed_deps {
crate::expr::computed_deps::ComputedDeps {
fields: cd.fields.clone(),
is_complete: cd.is_complete,
}
} else if let Some(expr) = &fd.computed {
crate::expr::computed_deps::extract_computed_deps(expr)
} else {
crate::expr::computed_deps::ComputedDeps::default()
};
dep_map.insert(field_name, deps);
}
// Resolve transitive computed-field requirements from the selected
// roots. Opaque dependencies trigger a safe full-compute fallback.
let required = match crate::expr::computed_deps::resolve_required_computed_fields(
needed_roots,
&dep_map,
) {
Some(required) => required,
None => {
return Document::computed_fields_inner(
stk,
ctx,
opt,
rid.as_ref(),
&fields,
doc,
None,
)
.await;
}
};
// If the projection doesn't reach any computed field, leave the
// cursor untouched. Keep `fields_computed = false` so a later
// full evaluation (e.g. live-query notification) can still run.
let has_required_computed = required.iter().any(|name| dep_map.contains_key(name));
if !has_required_computed {
return Ok(());
}
Document::computed_fields_inner(stk, ctx, opt, rid.as_ref(), &fields, doc, Some(&required))
.await
}
pub(super) async fn computed_fields_inner(
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
rid: &RecordId,
fields: &[FieldDefinition],
doc: &mut CursorDoc,
required: Option<&HashSet<String>>,
) -> anyhow::Result<()> {
// Skip when the full set has already been materialized — the
// flag only reflects "every computed field has run", so we
// must not honour it for partial (selective) computations.
if required.is_none() && doc.fields_computed {
return Ok(());
}
// Compute the fields
for fd in fields.iter() {
let Some(computed) = &fd.computed else {
continue;
};
// Restrict to the resolved closure when in selective mode.
if let Some(required) = required {
let field_name = fd.name.to_raw_string();
if !required.contains(&field_name) {
continue;
}
}
let mut val = computed.compute(stk, ctx, opt, Some(doc)).await.catch_return()?;
if let Some(kind) = fd.field_kind.as_ref() {
val = val.coerce_to_kind(kind).map_err(|e| Error::FieldCoerce {
record: rid.to_sql(),
field_name: fd.name.to_sql(),
error: Box::new(e),
})?;
}
doc.doc.to_mut().put(&fd.name, val);
}
// Only flag as fully computed for full evaluations. Selective
// runs leave the flag alone so a later non-selective pass can
// still fill in the missing fields.
if required.is_none() {
doc.fields_computed = true;
}
Ok(())
}
}