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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
use imbl::HashMap;
use logisheets_base::{BlockCellId, BlockFieldId, BlockId, SheetId};
use crate::navigator::BlockPlace;
use super::field_type::FieldType;
use super::schema::{BlockCellRole, Field, FieldEntry, RenderId, Schema, SchemaTrait};
/// One field's declaration plus the rule its author wrote, borrowed.
#[derive(Debug, Clone, Copy)]
pub struct BlockFieldView<'a> {
pub name: &'a str,
pub field_type: &'a FieldType,
pub required: bool,
pub unique: bool,
/// The author's own rule. The rules the declaration implies are derived, not
/// stored — see `block_manager::derived_rules`.
pub validation_formula: Option<&'a str>,
/// How this field aggregates the block its own block analyses, if it does.
pub aggregate: Option<&'a super::field_type::FieldAggregate>,
/// When the block is a pivot and this column was hand-declared.
pub pivot_column: Option<&'a super::field_type::PivotColumn>,
}
#[derive(Debug, Clone, Default)]
pub struct SchemaManager {
pub schemas: HashMap<(SheetId, BlockId), Schema>,
pub refs: HashMap<String, (SheetId, BlockId)>,
}
impl SchemaManager {
pub fn new() -> Self {
SchemaManager {
schemas: HashMap::new(),
refs: HashMap::new(),
}
}
// Make sure you know what you are doing.
// Given the key cell id, return the key-field cell id.
pub fn partially_resolve(
&self,
ref_name: &str,
key: BlockCellId,
field: &String,
) -> Option<BlockCellId> {
let (sheet_id, block_id) = self.refs.get(ref_name)?;
let schema = self.schemas.get(&(*sheet_id, *block_id))?;
schema.partially_resolve(key, field)
}
/// Which block, if any, already answers to this ref name.
///
/// Ref names are the workbook's public addressing surface — `BLOCKREF`'s
/// first argument — so two blocks cannot share one. Rebinding the SAME
/// block under the name it already has is not a clash, which is why this
/// returns the owner rather than a bool.
pub fn ref_name_owner(&self, name: &str) -> Option<(SheetId, BlockId)> {
self.refs.get(name).copied()
}
pub fn get_all_key_cell_ids<'a, F>(
&'a self,
ref_name: &str,
f: &'a F,
) -> Option<(SheetId, Vec<BlockCellId>)>
where
F: Fn(&'a SheetId, &'a BlockId) -> Option<&'a BlockPlace>,
{
let (sheet_id, block_id) = self.refs.get(ref_name)?;
let schema = self.schemas.get(&(*sheet_id, *block_id))?;
let bp = f(sheet_id, block_id)?;
Some((*sheet_id, schema.get_all_key_cell_ids(*block_id, bp)))
}
#[inline]
pub fn get_all_fields(&self, ref_name: &str) -> Option<Vec<Field>> {
let (sheet_id, block_id) = self.refs.get(ref_name)?;
self.get_all_fields_by_block(*sheet_id, *block_id)
}
#[inline]
pub fn get_all_fields_by_block(
&self,
sheet_id: SheetId,
block_id: BlockId,
) -> Option<Vec<Field>> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
Some(schema.get_all_fields())
}
pub fn get_all_key_cell_ids_by_block<'a>(
&'a self,
sheet_id: SheetId,
block_id: BlockId,
bp: &'a BlockPlace,
) -> Option<Vec<BlockCellId>> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
Some(schema.get_all_key_cell_ids(block_id, bp))
}
pub fn partially_resolve_by_block(
&self,
sheet_id: SheetId,
block_id: BlockId,
key: BlockCellId,
field: &String,
) -> Option<BlockCellId> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
schema.partially_resolve(key, field)
}
pub fn partially_resolve_by_field_id(
&self,
sheet_id: SheetId,
block_id: BlockId,
key: BlockCellId,
field_id: BlockFieldId,
) -> Option<BlockCellId> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
schema.partially_resolve_by_field_id(key, field_id)
}
#[inline]
pub fn get_render_id(&self, sheet_id: &SheetId, cell_id: &BlockCellId) -> Option<RenderId> {
let schema = self.schemas.get(&(*sheet_id, cell_id.block_id))?;
schema.get_render_id(cell_id.row, cell_id.col)
}
/// Resolve `ref_name` to its current `(sheet_id, block_id)`. Used by the
/// parser to substitute the textual ref-name with stable ids.
pub fn resolve_block_ref_name(&self, ref_name: &str) -> Option<(SheetId, BlockId)> {
self.refs.get(ref_name).copied()
}
pub fn resolve_field_id(
&self,
sheet_id: SheetId,
block_id: BlockId,
field: &str,
) -> Option<BlockFieldId> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
schema.resolve_field_id(field)
}
pub fn fetch_field_name(
&self,
sheet_id: SheetId,
block_id: BlockId,
field_id: BlockFieldId,
) -> Option<String> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
schema.fetch_field_name(field_id)
}
pub fn fetch_block_ref_name(&self, sheet_id: SheetId, block_id: BlockId) -> Option<String> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
Some(schema.get_ref_name())
}
pub fn get_all_field_ids_by_block(
&self,
sheet_id: SheetId,
block_id: BlockId,
) -> Vec<(Field, BlockFieldId)> {
match self.schemas.get(&(sheet_id, block_id)) {
Some(s) => s.get_all_field_ids(),
None => Vec::new(),
}
}
/// Classify a block-cell so callers can dirty the right virtual node.
pub fn cell_role(&self, sheet_id: SheetId, cell: &BlockCellId) -> BlockCellRole {
match self.schemas.get(&(sheet_id, cell.block_id)) {
Some(schema) => schema.cell_role(cell),
None => BlockCellRole::None,
}
}
/// Look up the value-formula template attached to a cell's field, if
/// any. Returns the raw template string (still includes the leading
/// `=` if the schema author wrote one). Callers do substitution on
/// the body.
/// The value-formula template governing a cell.
///
/// `analysis` is what this cell's block analyses and how, when it is an
/// analysis block. It is passed in rather than looked up because it lives
/// on the navigator's `BlockPlace` alongside the block's other metadata,
/// not on the schema — and every caller already holds the navigator.
///
/// An analysis field's formula is GENERATED from its declaration rather
/// than stored (see `block_manager::analysis`), and it wins over any
/// authored template: a field cannot both aggregate a source and compute
/// something else per record.
///
/// Two kinds of generation, and a block is only ever one of them (the
/// combination is refused by `controller::pivot_guard`):
///
/// - a **pivot** generates every cell from the block-level recipe, using
/// the cell's own field name as the column filter and `#KEY` as the row
/// filter, so its key column alone stays an ordinary cell;
/// - a **total row** generates from each field's own `aggregate`.
pub fn formula_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
analysis: Option<crate::block_manager::analysis::AnalysisTarget<'_>>,
) -> Option<String> {
// A header cell holds a field NAME. It is not a record's value, so
// nothing generates it and no template applies — which is also what
// keeps the re-materialization walks from installing an aggregate into
// it: they ask this, get `None`, and (in an analysis block) clear
// whatever was there.
if self.is_header_cell(sheet_id, cell) {
return None;
}
if let Some(target) = analysis {
if let Some(pivot) = target.pivot {
// A pivot's cells are generated wholesale from the recipe, so
// an authored template on one of them cannot apply — the
// column means what the recipe says it means.
if self.is_key_cell(sheet_id, cell) {
return None;
}
let field = self.field_name_for_block_cell(sheet_id, cell)?;
let column = self
.field_view_for_block_cell(sheet_id, cell)
.and_then(|v| v.pivot_column);
return Some(crate::block_manager::analysis::pivot_formula(
sheet_id,
target.source,
pivot,
&field,
column,
));
}
if let Some(view) = self.field_view_for_block_cell(sheet_id, cell) {
if let Some(generated) = crate::block_manager::analysis::aggregate_formula(
sheet_id,
target.source,
view.aggregate,
) {
return Some(generated);
}
}
}
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
match schema {
Schema::RowSchema(s) => s.formula_for_field_axis(cell.col).map(String::from),
Schema::ColSchema(s) => s.formula_for_field_axis(cell.row).map(String::from),
// RandomSchema doesn't carry templates in v1.
Schema::RandomSchema(_) => None,
}
}
/// A read-only view of one field's declaration and its author-written
/// rule, resolved from a cell.
///
/// Exists so the rule derivation can see a field without knowing whether it
/// came from a `RowSchema` or a `ColSchema` — `FieldEntry` is generic over
/// its axis id, so it cannot be handed out directly across both.
pub fn field_view_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
) -> Option<BlockFieldView<'_>> {
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
fn view<'a, F>(name: &'a str, e: &'a FieldEntry<F>) -> BlockFieldView<'a> {
BlockFieldView {
name,
field_type: &e.field_type,
required: e.required,
unique: e.unique,
validation_formula: e.validation_formula.as_deref(),
aggregate: e.aggregate.as_ref(),
pivot_column: e.pivot_column.as_ref(),
}
}
match schema {
Schema::RowSchema(s) => s
.fields
.iter()
.find(|(_, e)| e.field_axis_id == cell.col)
.map(|(n, e)| view(n, e)),
Schema::ColSchema(s) => s
.fields
.iter()
.find(|(_, e)| e.field_axis_id == cell.row)
.map(|(n, e)| view(n, e)),
// RandomSchema carries no field declarations.
Schema::RandomSchema(_) => None,
}
}
/// Look up the validation-formula template attached to a cell's field,
/// if any.
pub fn validation_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
) -> Option<String> {
// A field name is not one of the values the field allows, so no rule
// reaches the header. Without this, the pivot key column's derived
// rule ("must name a group that occurs in the source") flagged the
// header cell that names the DIMENSION — a red marker on the one cell
// that is definitionally right.
if self.is_header_cell(sheet_id, cell) {
return None;
}
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
match schema {
Schema::RowSchema(s) => s.validation_for_field_axis(cell.col).map(String::from),
Schema::ColSchema(s) => s.validation_for_field_axis(cell.row).map(String::from),
Schema::RandomSchema(_) => None,
}
}
/// Look up the editability-formula template attached to a cell's field,
/// if any.
/// Whether this cell sits on the line its schema declares as the header.
///
/// Pure schema knowledge — the schema stores the header as a line id, so
/// this needs nothing from the navigator.
pub fn is_header_cell(&self, sheet_id: SheetId, cell: &BlockCellId) -> bool {
self.schemas
.get(&(sheet_id, cell.block_id))
.is_some_and(|s| matches!(s.cell_role(cell), BlockCellRole::Header))
}
/// Whether this block's schema declares a header AND it is the FIRST line
/// of the block. `None` when the block has no schema.
///
/// Needs the navigator, because "first" is a question about where lines
/// are — which is the block's business, not the schema's. The schema knows
/// WHICH line; only the two together know whether it leads.
pub fn header_is_first_line(
&self,
sheet_id: SheetId,
block_id: BlockId,
bp: &BlockPlace,
) -> Option<bool> {
let schema = self.schemas.get(&(sheet_id, block_id))?;
let header = schema.header_line();
// Which axis holds the records is the schema's own kind, so callers do
// not get to guess it.
let is_row = matches!(schema, Schema::RowSchema(_));
let first = if is_row {
bp.rows.get(0).copied()
} else {
bp.cols.get(0).copied()
};
Some(header.is_some() && header == first)
}
pub fn editability_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
) -> Option<String> {
// A header cell holds a field NAME, and typing over it would not
// rename the field — it would leave a heading that disagrees with what
// the column means. So the header declares itself NOT editable, which
// is enforced: the editability rule becomes this cell's `UserEditable`
// shadow, and the host permission layer refuses an edit whose shadow
// reads false. (The field's WRITE POLICY is only a declaration the
// host may interpret; the per-record rule is the half that bites.)
//
// Not simply "no rule": no rule means editable, which is how the
// heading could be typed over in the first place.
if self.is_header_cell(sheet_id, cell) {
return Some("FALSE()".to_string());
}
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
match schema {
Schema::RowSchema(s) => s.editability_for_field_axis(cell.col).map(String::from),
Schema::ColSchema(s) => s.editability_for_field_axis(cell.row).map(String::from),
Schema::RandomSchema(_) => None,
}
}
/// For a templated block cell, return the per-field sibling
/// `BlockCellId`s in the same row, keyed by field name. Used to
/// build the `#FIELD("name") → Reference` substitution map at
/// parse time. Returns `None` if the schema isn't a Row/Col schema.
pub fn siblings_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
) -> Option<Vec<(String, BlockCellId)>> {
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
Some(match schema {
Schema::RowSchema(s) => s
.fields
.iter()
.map(|(name, entry)| {
(
name.clone(),
BlockCellId {
block_id: cell.block_id,
row: cell.row,
col: entry.field_axis_id,
},
)
})
.collect(),
Schema::ColSchema(s) => s
.fields
.iter()
.map(|(name, entry)| {
(
name.clone(),
BlockCellId {
block_id: cell.block_id,
row: entry.field_axis_id,
col: cell.col,
},
)
})
.collect(),
Schema::RandomSchema(_) => return None,
})
}
/// Whether this cell sits on the schema's KEY axis — the column of a row
/// schema (or the row of a column schema) that holds each record's key.
///
/// By axis id rather than by field name, so a schema that declares no
/// field at the key axis still answers correctly.
pub fn is_key_cell(&self, sheet_id: SheetId, cell: &BlockCellId) -> bool {
match self.schemas.get(&(sheet_id, cell.block_id)) {
Some(Schema::RowSchema(s)) => s.key == cell.col,
Some(Schema::ColSchema(s)) => s.key == cell.row,
_ => false,
}
}
/// The name of the field this cell belongs to. For a pivot that is also
/// the column-dimension value the cell filters on.
pub fn field_name_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
) -> Option<String> {
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
match schema {
Schema::RowSchema(s) => s
.fields
.iter()
.find(|(_, e)| e.field_axis_id == cell.col)
.map(|(n, _)| n.clone()),
Schema::ColSchema(s) => s
.fields
.iter()
.find(|(_, e)| e.field_axis_id == cell.row)
.map(|(n, _)| n.clone()),
Schema::RandomSchema(_) => None,
}
}
/// `BlockCellId` of the key cell that shares this cell's row (Row
/// schema) or column (Col schema). Used to resolve `#KEY`.
pub fn key_cell_for_block_cell(
&self,
sheet_id: SheetId,
cell: &BlockCellId,
) -> Option<BlockCellId> {
let schema = self.schemas.get(&(sheet_id, cell.block_id))?;
Some(match schema {
Schema::RowSchema(s) => BlockCellId {
block_id: cell.block_id,
row: cell.row,
col: s.key,
},
Schema::ColSchema(s) => BlockCellId {
block_id: cell.block_id,
row: s.key,
col: cell.col,
},
Schema::RandomSchema(_) => return None,
})
}
}