1use crate::value::{compare_values, OwnedValue, RelDef, Schema};
24
25pub const COMPARATOR_VERSION: u32 = 2;
35
36#[cfg_attr(
44 any(feature = "testkit", feature = "serde"),
45 derive(serde::Serialize, serde::Deserialize)
46)]
47#[derive(Clone, Debug)]
48pub struct WireProjection {
49 pub col: u32,
50 pub identity: OwnedValue,
51}
52
53impl PartialEq for WireProjection {
57 fn eq(&self, other: &Self) -> bool {
58 self.col == other.col
59 && compare_values(self.identity.as_ref(), other.identity.as_ref())
60 == std::cmp::Ordering::Equal
61 }
62}
63impl Eq for WireProjection {}
64
65#[cfg_attr(
70 any(feature = "testkit", feature = "serde"),
71 derive(serde::Serialize, serde::Deserialize)
72)]
73#[derive(Clone, Debug, PartialEq, Eq)]
74pub struct WireRel {
75 pub name: Box<str>,
76 pub slot: u32,
77 pub child: Option<WireSchema>,
78 pub project: Option<WireProjection>,
81}
82
83#[cfg_attr(
85 any(feature = "testkit", feature = "serde"),
86 derive(serde::Serialize, serde::Deserialize)
87)]
88#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct WireSchema {
90 pub columns: Vec<Box<str>>,
92 pub primary_key: Vec<u32>,
94 pub sort: Vec<(u32, bool)>,
97 pub singular: bool,
101 pub relationships: Vec<WireRel>,
103}
104
105impl WireSchema {
106 pub fn fingerprint(&self) -> SchemaFp {
108 schema_fp(self)
109 }
110}
111
112pub fn to_wire(schema: &Schema) -> WireSchema {
116 WireSchema {
117 columns: schema.columns.clone(),
118 primary_key: schema.primary_key.iter().map(|&c| c as u32).collect(),
119 sort: schema
120 .sort
121 .iter()
122 .map(|&(c, asc)| (c as u32, asc))
123 .collect(),
124 singular: schema.singular,
125 relationships: schema
126 .relationships
127 .iter()
128 .enumerate()
129 .map(|(slot, rd)| WireRel {
130 name: rd.name.clone(),
131 slot: slot as u32,
132 child: rd.child.as_deref().map(to_wire),
133 project: rd.project.as_ref().map(|p| WireProjection {
134 col: p.col as u32,
135 identity: p.identity.clone(),
136 }),
137 })
138 .collect(),
139 }
140}
141
142pub fn to_schema(ws: &WireSchema) -> Schema {
147 let cols: Vec<&str> = ws.columns.iter().map(|c| &**c).collect();
148 let pk: Vec<usize> = ws.primary_key.iter().map(|&c| c as usize).collect();
149 let sort: Vec<(usize, bool)> = ws.sort.iter().map(|&(c, asc)| (c as usize, asc)).collect();
150 let mut schema = Schema::new(cols, pk, sort);
151 schema.singular = ws.singular;
152 if !ws.relationships.is_empty() {
153 let rels: Vec<RelDef> = ws
154 .relationships
155 .iter()
156 .map(|r| match &r.child {
157 Some(child) => {
158 let rd = RelDef::related(&r.name, to_schema(child));
159 match &r.project {
160 Some(p) => rd.project_scalar(p.col as usize, p.identity.clone()),
161 None => rd,
162 }
163 }
164 None => RelDef::new(&r.name),
165 })
166 .collect();
167 schema = schema.with_relationships(rels);
168 }
169 schema
170}
171
172#[cfg_attr(
175 any(feature = "testkit", feature = "serde"),
176 derive(serde::Serialize, serde::Deserialize)
177)]
178#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
179pub struct SchemaFp(pub u64);
180
181impl std::fmt::Display for SchemaFp {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 write!(f, "{:016x}", self.0)
184 }
185}
186
187struct Fnv(u64);
190
191impl Fnv {
192 fn new() -> Fnv {
193 Fnv(0xcbf29ce484222325) }
195 #[inline]
196 fn byte(&mut self, b: u8) {
197 self.0 ^= b as u64;
198 self.0 = self.0.wrapping_mul(0x0000_0100_0000_01b3); }
200 fn bytes(&mut self, bs: &[u8]) {
201 for &b in bs {
202 self.byte(b);
203 }
204 }
205 fn u8(&mut self, v: u8) {
206 self.byte(v);
207 }
208 fn u32(&mut self, v: u32) {
209 self.bytes(&v.to_le_bytes());
210 }
211 fn s(&mut self, s: &str) {
213 self.u32(s.len() as u32);
214 self.bytes(s.as_bytes());
215 }
216}
217
218pub fn schema_fp(ws: &WireSchema) -> SchemaFp {
222 let mut h = Fnv::new();
223 hash_level(&mut h, ws);
224 SchemaFp(h.0)
225}
226
227fn hash_level(h: &mut Fnv, ws: &WireSchema) {
228 h.u8(b'S');
229 h.u32(ws.columns.len() as u32);
230 for c in &ws.columns {
231 h.s(c);
232 }
233 h.u32(ws.primary_key.len() as u32);
235 for &pk in &ws.primary_key {
236 h.s(&ws.columns[pk as usize]);
237 }
238 h.u32(ws.sort.len() as u32);
239 for &(c, asc) in &ws.sort {
240 h.s(&ws.columns[c as usize]);
241 h.u8(asc as u8);
242 }
243 h.u8(ws.singular as u8);
244 h.u32(ws.relationships.len() as u32);
246 for r in &ws.relationships {
247 h.s(&r.name);
248 match &r.child {
249 Some(child) => {
250 h.u8(1);
251 hash_level(h, child);
252 match &r.project {
259 Some(p) => {
260 h.u8(1);
261 h.s(&child.columns[p.col as usize]);
262 }
263 None => h.u8(0),
264 }
265 }
266 None => h.u8(0),
267 }
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use crate::value::{RelDef, Schema};
275
276 fn comment_schema() -> Schema {
277 Schema::new(vec!["id", "issue", "val"], vec![0], vec![(0, true)])
278 }
279 fn issue_schema() -> Schema {
280 Schema::new(vec!["id", "val"], vec![0], vec![(1, true), (0, true)]).with_relationships(
281 vec![
282 RelDef::related("comments", comment_schema()),
283 RelDef::new("gate"),
284 ],
285 )
286 }
287
288 #[test]
289 fn to_wire_shapes_columns_sort_and_relationships() {
290 let w = to_wire(&issue_schema());
291 assert_eq!(
292 w.columns.iter().map(|c| &**c).collect::<Vec<_>>(),
293 vec!["id", "val"]
294 );
295 assert_eq!(w.primary_key, vec![0]);
296 assert_eq!(w.sort, vec![(1, true), (0, true)]); assert!(!w.singular);
298 assert_eq!(w.relationships.len(), 2);
299 assert_eq!(&*w.relationships[0].name, "comments");
301 assert_eq!(w.relationships[0].slot, 0);
302 assert!(w.relationships[0].child.is_some());
303 assert_eq!(&*w.relationships[1].name, "gate");
304 assert!(w.relationships[1].child.is_none());
305 let child = w.relationships[0].child.as_ref().unwrap();
307 assert_eq!(child.sort, vec![(0, true)]);
308 assert!(child.relationships.is_empty());
309 }
310
311 #[test]
312 fn schema_round_trips_through_wire() {
313 let w = to_wire(&issue_schema());
314 let rebuilt = to_schema(&w);
315 assert_eq!(to_wire(&rebuilt), w);
318 }
319
320 #[test]
321 fn fingerprint_is_deterministic_and_round_trip_stable() {
322 let w = to_wire(&issue_schema());
323 assert_eq!(schema_fp(&w), schema_fp(&w));
324 assert_eq!(schema_fp(&to_wire(&to_schema(&w))), schema_fp(&w));
326 }
327
328 #[test]
329 fn fingerprint_detects_drift() {
330 let base = schema_fp(&to_wire(&issue_schema()));
331
332 let renamed = Schema::new(vec!["id", "value"], vec![0], vec![(1, true), (0, true)])
334 .with_relationships(vec![
335 RelDef::related("comments", comment_schema()),
336 RelDef::new("gate"),
337 ]);
338 assert_ne!(schema_fp(&to_wire(&renamed)), base, "column rename");
339
340 let resorted = Schema::new(vec!["id", "val"], vec![0], vec![(1, false), (0, true)])
342 .with_relationships(vec![
343 RelDef::related("comments", comment_schema()),
344 RelDef::new("gate"),
345 ]);
346 assert_ne!(schema_fp(&to_wire(&resorted)), base, "sort dir change");
347
348 let mut singular = issue_schema();
350 singular.singular = true;
351 assert_ne!(schema_fp(&to_wire(&singular)), base, "singular flip");
352
353 let gated = Schema::new(vec!["id", "val"], vec![0], vec![(1, true), (0, true)])
355 .with_relationships(vec![RelDef::new("comments"), RelDef::new("gate")]);
356 assert_ne!(schema_fp(&to_wire(&gated)), base, "rel in-view->gating");
357
358 let child_drift = Schema::new(vec!["id", "val"], vec![0], vec![(1, true), (0, true)])
360 .with_relationships(vec![
361 RelDef::related(
362 "comments",
363 Schema::new(vec!["id", "issue", "body"], vec![0], vec![(0, true)]),
364 ),
365 RelDef::new("gate"),
366 ]);
367 assert_ne!(
368 schema_fp(&to_wire(&child_drift)),
369 base,
370 "child column rename"
371 );
372 }
373
374 #[cfg(any(feature = "testkit", feature = "serde"))]
375 #[test]
376 fn wire_schema_serde_round_trips() {
377 let w = to_wire(&issue_schema());
378 let json = serde_json::to_value(&w).expect("serialize");
379 let back: WireSchema = serde_json::from_value(json).expect("deserialize");
380 assert_eq!(back, w);
381 assert_eq!(schema_fp(&back), schema_fp(&w));
382 }
383
384 use crate::value::OwnedValue as V;
387
388 fn agg_child() -> Schema {
392 let mut s = Schema::new(vec!["issueID", "count"], vec![0], vec![(0, true)]);
393 s.singular = true;
394 s
395 }
396 fn projected_schema() -> Schema {
397 Schema::new(vec!["id", "val"], vec![0], vec![(0, true)]).with_relationships(vec![
398 RelDef::related("commentCount", agg_child()).project_scalar(1, V::Int(0)),
399 ])
400 }
401
402 #[test]
403 fn projection_round_trips_through_wire() {
404 let w = to_wire(&projected_schema());
405 let p = w.relationships[0]
406 .project
407 .as_ref()
408 .expect("projection on wire");
409 assert_eq!(p.col, 1);
410 assert_eq!(to_wire(&to_schema(&w)), w);
412 }
413
414 #[test]
415 fn projection_perturbs_fingerprint() {
416 let singular_only = Schema::new(vec!["id", "val"], vec![0], vec![(0, true)])
420 .with_relationships(vec![RelDef::related("commentCount", agg_child())]);
421 assert_ne!(
422 schema_fp(&to_wire(&projected_schema())),
423 schema_fp(&to_wire(&singular_only)),
424 "projection marker must perturb the fingerprint"
425 );
426 }
427
428 #[test]
429 fn fingerprint_detects_projected_column_change() {
430 let base = schema_fp(&to_wire(&projected_schema()));
431 let other_col = Schema::new(vec!["id", "val"], vec![0], vec![(0, true)])
433 .with_relationships(vec![
434 RelDef::related("commentCount", agg_child()).project_scalar(0, V::Int(0))
435 ]);
436 assert_ne!(
437 schema_fp(&to_wire(&other_col)),
438 base,
439 "projected column name is part of the fingerprint"
440 );
441 }
442
443 #[cfg(any(feature = "testkit", feature = "serde"))]
444 #[test]
445 fn projection_serde_round_trips() {
446 let w = to_wire(&projected_schema());
447 let json = serde_json::to_value(&w).expect("serialize");
448 let back: WireSchema = serde_json::from_value(json).expect("deserialize");
449 assert_eq!(back, w);
450 assert_eq!(schema_fp(&back), schema_fp(&w));
451 }
452}