sofie_db/
read_versions.rs1use std::cell::RefCell;
2use std::hash::{DefaultHasher, Hash, Hasher};
3
4use sofie_core::{ColumnReader, Date, Result, SqlType, SqlValue, Timestamp, Uuid};
5
6use crate::RowSink;
7
8tokio::task_local! {
11 static READS: RefCell<Reads>;
12}
13
14struct Reads {
15 hasher: DefaultHasher,
16 untracked: bool,
17}
18
19pub async fn track_reads<F: Future>(seed: u64, future: F) -> (F::Output, Option<u64>) {
21 let mut hasher = DefaultHasher::new();
22 seed.hash(&mut hasher);
23 let reads = Reads {
24 hasher,
25 untracked: false,
26 };
27 READS
28 .scope(RefCell::new(reads), async {
29 let output = future.await;
30 (output, reads_so_far())
31 })
32 .await
33}
34
35#[must_use]
37pub fn reads_so_far() -> Option<u64> {
38 READS
39 .try_with(|reads| {
40 let reads = reads.borrow();
41 (!reads.untracked).then(|| reads.hasher.clone().finish())
42 })
43 .ok()
44 .flatten()
45}
46
47pub fn read_untracked() {
49 let _ = READS.try_with(|reads| reads.borrow_mut().untracked = true);
50}
51
52pub fn read_cached(value: &str) {
54 let _ = READS.try_with(|reads| value.hash(&mut reads.borrow_mut().hasher));
55}
56
57pub(crate) fn tracking() -> bool {
58 READS.try_with(|_| ()).is_ok()
59}
60
61pub(crate) struct Fingerprinted<'s> {
63 rows: &'s mut dyn RowSink,
64 hasher: DefaultHasher,
65 count: u64,
66}
67
68impl<'s> Fingerprinted<'s> {
69 pub(crate) fn new(rows: &'s mut dyn RowSink) -> Self {
70 Self {
71 rows,
72 hasher: DefaultHasher::new(),
73 count: 0,
74 }
75 }
76
77 pub(crate) fn record(self) {
78 let _ = READS.try_with(|reads| {
79 let hasher = &mut reads.borrow_mut().hasher;
80 self.count.hash(hasher);
81 self.hasher.finish().hash(hasher);
82 });
83 }
84}
85
86impl RowSink for Fingerprinted<'_> {
87 fn expects(&self) -> &[Option<SqlType>] {
88 self.rows.expects()
89 }
90
91 fn row(&mut self, row: &mut dyn ColumnReader) -> Result<()> {
92 self.count += 1;
93 self.rows.row(&mut Hashing {
94 row,
95 hasher: &mut self.hasher,
96 })
97 }
98}
99
100struct Hashing<'r> {
101 row: &'r mut dyn ColumnReader,
102 hasher: &'r mut DefaultHasher,
103}
104
105macro_rules! hashed {
106 ($($method:ident -> $type:ty),*) => {
107 $(fn $method(&mut self) -> Result<$type> {
108 let value = self.row.$method()?;
109 value.hash(self.hasher);
110 Ok(value)
111 })*
112 };
113}
114
115impl ColumnReader for Hashing<'_> {
116 fn remaining(&self) -> usize {
117 self.row.remaining()
118 }
119
120 fn is_null_at(&self, ahead: usize) -> Result<bool> {
121 self.row.is_null_at(ahead)
122 }
123
124 fn skip(&mut self) -> Result<()> {
125 self.value().map(drop)
126 }
127
128 hashed!(bool -> bool, int -> i64, uuid -> Uuid, timestamp -> Timestamp, date -> Date,
129 date_time -> sofie_core::DateTime, text -> &str, bytes -> &[u8]);
130
131 fn float(&mut self) -> Result<f64> {
132 let value = self.row.float()?;
133 value.to_bits().hash(self.hasher);
134 Ok(value)
135 }
136
137 fn json(&mut self) -> Result<serde_json::Value> {
138 let value = self.row.json()?;
139 value.to_string().hash(self.hasher);
140 Ok(value)
141 }
142
143 fn value(&mut self) -> Result<SqlValue> {
144 let value = self.row.value()?;
145 hash_value(&value, self.hasher);
146 Ok(value)
147 }
148}
149
150fn hash_value(value: &SqlValue, hasher: &mut DefaultHasher) {
151 std::mem::discriminant(value).hash(hasher);
152 match value {
153 SqlValue::Null => {}
154 SqlValue::Bool(value) => value.hash(hasher),
155 SqlValue::Int(value) => value.hash(hasher),
156 SqlValue::Float(value) => value.to_bits().hash(hasher),
157 SqlValue::Text(value) => value.hash(hasher),
158 SqlValue::Bytes(value) => value.hash(hasher),
159 SqlValue::Uuid(value) => value.hash(hasher),
160 SqlValue::Timestamp(value) => value.hash(hasher),
161 SqlValue::Date(value) => value.hash(hasher),
162 SqlValue::DateTime(value) => value.hash(hasher),
163 SqlValue::Json(value) => value.to_string().hash(hasher),
164 SqlValue::Array(values) => {
165 values.len().hash(hasher);
166 for value in values {
167 hash_value(value, hasher);
168 }
169 }
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use sofie_core::RowReader;
176
177 use super::*;
178 use crate::Decoding;
179
180 fn row(version: i64) -> Vec<SqlValue> {
181 vec![SqlValue::Int(version), SqlValue::Text("open".to_owned())]
182 }
183
184 fn read(rows: &[Vec<SqlValue>], skip_version: bool) {
185 let mut decoding = Decoding::new(Vec::new(), |row: &mut dyn ColumnReader| {
186 if skip_version {
187 row.skip()?;
188 } else {
189 row.int()?;
190 }
191 row.text().map(str::to_owned)
192 });
193 let mut fingerprinted = Fingerprinted::new(&mut decoding);
194 for values in rows {
195 fingerprinted.row(&mut RowReader::new(values)).unwrap();
196 }
197 fingerprinted.record();
198 }
199
200 fn fingerprint(seed: u64, rows: &[Vec<SqlValue>], skip_version: bool) -> u64 {
201 let runtime = tokio::runtime::Builder::new_current_thread()
202 .build()
203 .unwrap();
204 let reading = async { read(rows, skip_version) };
205 runtime.block_on(track_reads(seed, reading)).1.unwrap()
206 }
207
208 fn tracked(reading: impl FnOnce()) -> Option<u64> {
209 let runtime = tokio::runtime::Builder::new_current_thread()
210 .build()
211 .unwrap();
212 runtime.block_on(track_reads(1, async { reading() })).1
213 }
214
215 #[test]
216 fn the_fingerprint_follows_the_values_read_and_the_seed() {
217 let first = fingerprint(1, &[row(1)], false);
218
219 assert_eq!(first, fingerprint(1, &[row(1)], false));
220 assert_ne!(first, fingerprint(1, &[row(2)], false));
221 assert_ne!(first, fingerprint(2, &[row(1)], false));
222 assert_ne!(first, fingerprint(1, &[row(1), row(1)], false));
223 }
224
225 #[test]
226 fn a_skipped_column_still_counts() {
227 assert_ne!(
228 fingerprint(1, &[row(1)], true),
229 fingerprint(1, &[row(2)], true)
230 );
231 }
232
233 #[test]
234 fn an_answer_from_outside_is_not_fingerprinted() {
235 assert_eq!(tracked(read_untracked), None);
236 }
237
238 #[test]
239 fn a_cached_value_read_changes_the_fingerprint() {
240 let first = tracked(|| read_cached("{\"total\":1}"));
241
242 assert!(first.is_some());
243 assert_eq!(first, tracked(|| read_cached("{\"total\":1}")));
244 assert_ne!(first, tracked(|| read_cached("{\"total\":2}")));
245 }
246
247 #[test]
248 fn nothing_is_tracked_outside_a_scope() {
249 read(&[row(1)], false);
250
251 assert_eq!(reads_so_far(), None);
252 }
253}