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
//! The expression indexes a filter or sort on a localized field reads (#1513).
//!
//! `compile` reports them and `doctor` checks them against a live database. Each is built by
//! `fraiseql_db::projection_generator::localized_index` over the key the `where` generator and
//! the `ORDER BY` renderer build, so the reported index is the one the planner can match.
use fraiseql_db::projection_generator::{LocalizedIndex, localized_index};
use super::CompiledSchema;
/// One (type, localized field, allowed locale) the schema serves, and the index for it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocalizedIndexAdvice {
/// The type declaring the field.
pub type_name: String,
/// The field, as declared.
pub field: String,
/// The allowed locale the index serves.
pub locale: String,
/// The type's source.
pub table: String,
/// The index, when the source is a table (`tv_`/`tb_`). `None` for a view: an index
/// belongs on its base table, which the schema does not name.
pub index: Option<LocalizedIndex>,
}
impl CompiledSchema {
/// Whether any argument or input field is localized (#1513): the schema then publishes
/// `LocalizedInput` and `LocalizedStringInput`.
#[must_use]
pub fn has_localized_inputs(&self) -> bool {
let arguments = self
.queries
.iter()
.flat_map(|q| &q.arguments)
.chain(self.mutations.iter().flat_map(|m| &m.arguments))
.chain(self.subscriptions.iter().flat_map(|s| &s.arguments));
arguments.into_iter().any(|a| a.localized)
|| self.input_types.iter().any(|i| i.fields.iter().any(|f| f.localized))
}
/// Every index a filter or sort on a localized field reads: one per (type, field,
/// allowed locale), in declaration order.
#[must_use]
pub fn localized_index_report(&self) -> Vec<LocalizedIndexAdvice> {
let Some(locale) = self.locale.as_ref() else {
return Vec::new();
};
let mut report = Vec::new();
for type_def in &self.types {
let table = type_def.sql_source.to_string();
let relation = table.rsplit('.').next().unwrap_or(&table);
let table_backed = relation.starts_with("tv_") || relation.starts_with("tb_");
for field in type_def.fields.iter().filter(|f| f.localized) {
let key = crate::utils::to_snake_case(field.name.as_str());
for tag in &locale.allowed {
// Derived here rather than read from the load-time cache: `compile`
// reports from a schema that has not been loaded.
let index = table_backed
.then(|| {
let chain = locale.derive_chain(tag);
let collation = locale.collation(tag);
localized_index(&table, &key, tag, &chain, collation.as_deref()).ok()
})
.flatten();
report.push(LocalizedIndexAdvice {
type_name: type_def.name.to_string(),
field: field.name.to_string(),
locale: tag.clone(),
table: table.clone(),
index,
});
}
}
}
report
}
}