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
//! Where a schema's `Decimal` values sit, for reading a row's text with their digits kept.
//!
//! A `Decimal` travels as a JSON string carrying its stored digits. A read that reaches one
//! takes the row as text and quotes each number at a `Decimal` position before parsing it
//! ([`quote_decimals`](crate::db::traits::quote_decimals)); [`DecimalIndex`] says where
//! those positions are, for a document of any type.
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
use crate::{
db::traits::{DecimalFields, DecimalNode, DecimalShape, WriteDecimals},
runtime::projection::stored_key_candidates,
schema::{CompiledSchema, FieldType},
};
#[cfg(test)]
mod tests;
/// Every type in a schema from which a `Decimal` is reachable, and the shape of a document
/// read as each, built once per schema.
#[derive(Debug, Clone, Default)]
pub struct DecimalIndex {
/// The shape of a document read as each object type, union or interface.
shapes: HashMap<String, Arc<DecimalShape>>,
/// The shape of the documents each query reads.
reads: HashMap<String, Arc<DecimalShape>>,
/// The fields of every type from which a `Decimal` is reachable.
types: Arc<HashMap<String, DecimalFields>>,
}
impl DecimalIndex {
/// Index `schema`: each type that holds a `Decimal`, or a field leading to a type that
/// does, with every key a document may hold such a field by.
#[must_use]
pub fn new(schema: &CompiledSchema) -> Self {
let reaching = reaching_types(schema);
let types: HashMap<String, DecimalFields> = schema
.types
.iter()
.filter(|t| reaching.contains(t.name.as_str()))
.map(|t| {
let mut fields = DecimalFields::new();
for field in &t.fields {
let Some(node) = node_for(schema, &reaching, &field.field_type) else {
continue;
};
let (stored, camel) = stored_key_candidates(field.name.as_str());
let keys = [
Some(field.name.to_string()),
field.alias.clone(),
Some(stored),
camel,
];
for key in keys.into_iter().flatten() {
fields.insert(key, node.clone());
}
}
(t.name.to_string(), fields)
})
.collect();
let types = Arc::new(types);
let abstracts = schema
.unions
.iter()
.map(|u| u.name.as_str())
.chain(schema.interfaces.iter().map(|i| i.name.as_str()));
let shapes = types
.keys()
.map(|name| (name.clone(), DecimalNode::Object(name.clone())))
.chain(
abstracts
.filter(|name| reaches_abstract(schema, &reaching, name))
.map(|name| (name.to_string(), DecimalNode::Abstract(None))),
)
.map(|(name, root)| (name, Arc::new(DecimalShape::new(root, Arc::clone(&types)))))
.collect::<HashMap<_, _>>();
let reads = schema
.queries
.iter()
.filter_map(|query| {
let read_as = query.return_type.as_str();
let shape = shapes.get(read_as)?;
let stored = schema.possible_types(read_as).and_then(|possible| {
schema.type_stored_in(&possible, query.sql_source.as_deref())
});
let shape = match stored {
Some(fallback) => Arc::new(DecimalShape::new(
DecimalNode::Abstract(Some(fallback.to_string())),
Arc::clone(&types),
)),
None => Arc::clone(shape),
};
Some((query.name.clone(), shape))
})
.collect();
Self {
shapes,
reads,
types,
}
}
/// The shape of a document read as `type_name`, an object type, a union or an
/// interface; `None` when no `Decimal` is reachable from it.
#[must_use]
pub fn shape(&self, type_name: &str) -> Option<&Arc<DecimalShape>> {
self.shapes.get(type_name)
}
/// The shape of the documents the query `query_name` reads: its return type's, an
/// abstract one reading a document that names no `__typename` as the one possible type
/// stored in the query's view, as the read itself types such a row.
#[must_use]
pub fn read_by(&self, query_name: &str) -> Option<&Arc<DecimalShape>> {
self.reads.get(query_name)
}
/// Whether a value of `field_type` can hold a `Decimal`, at any depth: one is reachable
/// from it exactly where a shape built from this index has a position for one.
#[must_use]
pub fn holds_decimal(&self, field_type: &FieldType) -> bool {
match field_type {
FieldType::Decimal => true,
FieldType::List { item, .. } => self.holds_decimal(item),
FieldType::Object(name) | FieldType::Union(name) | FieldType::Interface(name) => {
self.shapes.contains_key(name)
},
_ => false,
}
}
/// A mutation response's documents, each read as the type its row stamps; `None` when
/// the schema holds no `Decimal`.
#[must_use]
pub fn write(&self) -> Option<WriteDecimals> {
(!self.types.is_empty()).then(|| WriteDecimals::new(Arc::clone(&self.types)))
}
}
/// Whether `text` is a `Decimal` as one travels: optionally signed digits, with a fractional
/// part after a point when it has one. No exponent, no leading point, no padding.
#[must_use]
pub fn is_decimal_digits(text: &str) -> bool {
let unsigned = text.strip_prefix('-').unwrap_or(text);
let (whole, fraction) =
unsigned.split_once('.').map_or((unsigned, None), |(w, f)| (w, Some(f)));
let digits = |part: &str| !part.is_empty() && part.bytes().all(|b| b.is_ascii_digit());
digits(whole) && fraction.is_none_or(digits)
}
/// Whether a union or an interface named `name` holds a type from which a `Decimal` is
/// reachable.
fn reaches_abstract(schema: &CompiledSchema, reaching: &HashSet<String>, name: &str) -> bool {
schema
.possible_types(name)
.is_some_and(|members| members.iter().any(|m| reaching.contains(*m)))
}
/// Every object type from which a `Decimal` is reachable, through any depth of fields.
fn reaching_types(schema: &CompiledSchema) -> HashSet<String> {
let mut reaching = HashSet::new();
loop {
let before = reaching.len();
for t in &schema.types {
if !reaching.contains(t.name.as_str())
&& t.fields.iter().any(|f| node_for(schema, &reaching, &f.field_type).is_some())
{
reaching.insert(t.name.to_string());
}
}
if reaching.len() == before {
return reaching;
}
}
}
/// Where a value of `field_type` leads, given the types known to reach a `Decimal`.
fn node_for(
schema: &CompiledSchema,
reaching: &HashSet<String>,
field_type: &FieldType,
) -> Option<DecimalNode> {
match field_type {
FieldType::Decimal => Some(DecimalNode::Decimal),
FieldType::List { item, .. } => node_for(schema, reaching, item),
FieldType::Object(name) => {
reaching.contains(name.as_str()).then(|| DecimalNode::Object(name.clone()))
},
FieldType::Union(name) | FieldType::Interface(name) => {
reaches_abstract(schema, reaching, name).then_some(DecimalNode::Abstract(None))
},
_ => None,
}
}