Skip to main content

aquadoggo/db/models/
utils.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3//! Utility methods for parsing database rows into p2panda data types.
4use std::collections::BTreeMap;
5
6use p2panda_rs::document::{DocumentId, DocumentViewFields, DocumentViewId, DocumentViewValue};
7use p2panda_rs::identity::PublicKey;
8use p2panda_rs::operation::traits::AsOperation;
9use p2panda_rs::operation::{
10    OperationAction, OperationBuilder, OperationId, OperationValue, PinnedRelation,
11    PinnedRelationList, Relation, RelationList,
12};
13use p2panda_rs::schema::SchemaId;
14
15use crate::db::models::DocumentViewFieldRow;
16use crate::db::models::OperationFieldsJoinedRow;
17use crate::db::types::StorageOperation;
18
19/// Takes a vector of `OperationFieldsJoinedRow` and parses them into an `VerifiedOperation`
20/// struct.
21///
22/// Operation fields which contain lists of values (RelationList & PinnedRelationList) are
23/// flattened and inserted as indiviual rows. This means we need to reconstruct these fields when
24/// retrieving an operation from the db.
25pub fn parse_operation_rows(
26    operation_rows: Vec<OperationFieldsJoinedRow>,
27) -> Option<StorageOperation> {
28    let first_row = match operation_rows.first() {
29        Some(row) => row,
30        None => return None,
31    };
32
33    // Unwrapping as we assume values coming from the db are valid
34    let schema_id: SchemaId = first_row.schema_id.parse().unwrap();
35    let public_key = PublicKey::new(&first_row.public_key).unwrap();
36    let operation_id = first_row.operation_id.parse().unwrap();
37    let document_id = first_row.document_id.parse().unwrap();
38    let sorted_index = first_row.sorted_index;
39
40    let mut relation_lists: BTreeMap<String, Vec<DocumentId>> = BTreeMap::new();
41    let mut pinned_relation_lists: BTreeMap<String, Vec<DocumentViewId>> = BTreeMap::new();
42
43    let mut operation_fields = Vec::new();
44
45    // Iterate over returned field values, for each value:
46    // * if it is a simple value type, parse it into an OperationValue and add it to the
47    // operation_fields
48    // * if it is a relation list value type: if the row.value is None then this list is empty and
49    // we should create a relation list with no items, otherwise safely unwrap each item and parse
50    // into a DocumentId/DocumentViewId then push to the suitable list vec
51    if first_row.action != "delete" {
52        operation_rows.iter().for_each(|row| {
53            let field_type = row.field_type.as_ref().unwrap().as_str();
54            let field_name = row.name.as_ref().unwrap();
55            // We don't unwrap the value as for empty relation lists this may be `None`
56            // Below we safely unwrap all values which are _not_ part of a relation list
57            let field_value = row.value.as_ref();
58
59            match field_type {
60                "bool" => {
61                    operation_fields.push((
62                        field_name.to_string(),
63                        OperationValue::Boolean(field_value.unwrap().parse::<bool>().unwrap()),
64                    ));
65                }
66                "int" => {
67                    operation_fields.push((
68                        field_name.to_string(),
69                        OperationValue::Integer(field_value.unwrap().parse::<i64>().unwrap()),
70                    ));
71                }
72                "float" => {
73                    operation_fields.push((
74                        field_name.to_string(),
75                        OperationValue::Float(field_value.unwrap().parse::<f64>().unwrap()),
76                    ));
77                }
78                "str" => {
79                    operation_fields.push((
80                        field_name.to_string(),
81                        OperationValue::String(field_value.unwrap().clone()),
82                    ));
83                }
84                "bytes" => {
85                    operation_fields.push((
86                        field_name.to_string(),
87                        OperationValue::Bytes(hex::decode(field_value.unwrap()).expect(
88                            "bytes coming from the store are encoded in valid hex strings",
89                        )),
90                    ));
91                }
92                "relation" => {
93                    operation_fields.push((
94                        field_name.to_string(),
95                        OperationValue::Relation(Relation::new(
96                            field_value.unwrap().parse::<DocumentId>().unwrap(),
97                        )),
98                    ));
99                }
100                // This is a list item, so we push it to a vec but _don't_ add it
101                // to the operation_fields yet.
102                "relation_list" => {
103                    match relation_lists.get_mut(field_name) {
104                        // We unwrap the field value here as if the list already exists then we can
105                        // assume this next item contains a value
106                        Some(list) => {
107                            list.push(field_value.unwrap().parse::<DocumentId>().unwrap())
108                        }
109                        None => {
110                            let list = match field_value {
111                                Some(document_id) => {
112                                    vec![document_id.parse::<DocumentId>().unwrap()]
113                                }
114                                None => vec![],
115                            };
116                            relation_lists.insert(field_name.to_string(), list);
117                        }
118                    };
119                }
120                "pinned_relation" => {
121                    operation_fields.push((
122                        field_name.to_string(),
123                        OperationValue::PinnedRelation(PinnedRelation::new(
124                            field_value.unwrap().parse::<DocumentViewId>().unwrap(),
125                        )),
126                    ));
127                }
128                // This is a list item, so we push it to a vec but _don't_ add it
129                // to the operation_fields yet.
130                "pinned_relation_list" => {
131                    match pinned_relation_lists.get_mut(field_name) {
132                        // We unwrap the field value here as if the list already exists then we can
133                        // assume this next item contains a value
134                        Some(list) => {
135                            list.push(field_value.unwrap().parse::<DocumentViewId>().unwrap())
136                        }
137                        None => {
138                            let list = match field_value {
139                                Some(document_view_id) => {
140                                    vec![document_view_id.parse::<DocumentViewId>().unwrap()]
141                                }
142                                None => vec![],
143                            };
144                            pinned_relation_lists.insert(field_name.to_string(), list);
145                        }
146                    };
147                }
148                _ => (),
149            };
150        })
151    };
152
153    for (field_name, relation_list) in relation_lists {
154        operation_fields.push((
155            field_name,
156            OperationValue::RelationList(RelationList::new(relation_list)),
157        ));
158    }
159
160    for (field_name, pinned_relation_list) in pinned_relation_lists {
161        operation_fields.push((
162            field_name,
163            OperationValue::PinnedRelationList(PinnedRelationList::new(pinned_relation_list)),
164        ));
165    }
166
167    let operation_builder = OperationBuilder::new(&schema_id);
168    let previous = first_row.previous.clone();
169    let previous = previous.map(|previous| previous.parse().unwrap());
170    let fields: Vec<(&str, OperationValue)> = operation_fields
171        .iter()
172        .map(|(name, value)| (name.as_str(), value.to_owned()))
173        .collect();
174
175    let operation = match first_row.action.as_str() {
176        "create" => operation_builder.fields(fields.as_slice()).build(),
177        "update" => operation_builder
178            .action(OperationAction::Update)
179            .fields(fields.as_slice())
180            .previous(previous.as_ref().unwrap())
181            .build(),
182        "delete" => operation_builder
183            .action(OperationAction::Delete)
184            .previous(previous.as_ref().unwrap())
185            .build(),
186        _ => panic!("Operation which was not CREATE, UPDATE or DELETE found."),
187    }
188    // Unwrap as we are sure values coming from the db are validated
189    .unwrap();
190
191    let operation = StorageOperation {
192        document_id,
193        id: operation_id,
194        version: operation.version(),
195        action: operation.action(),
196        schema_id,
197        previous: operation.previous(),
198        fields: operation.fields(),
199        public_key,
200        sorted_index,
201    };
202
203    Some(operation)
204}
205
206/// Takes a single `OperationValue` and parses it into a vector of string values.
207///
208/// OperationValues are inserted into the database as strings. If a value is a list type
209/// (`RelationList` & `PinnedRelationList`) we insert one row for each value. This method
210/// transforms a single operation into a list of string values, if the is not a list, it will only
211/// contain a single item.
212pub fn parse_value_to_string_vec(value: &OperationValue) -> Vec<Option<String>> {
213    match value {
214        OperationValue::Boolean(bool) => vec![Some(bool.to_string())],
215        OperationValue::Integer(int) => vec![Some(int.to_string())],
216        OperationValue::Float(float) => vec![Some(float.to_string())],
217        OperationValue::String(str) => vec![Some(str.to_string())],
218        OperationValue::Relation(relation) => {
219            vec![Some(relation.document_id().as_str().to_string())]
220        }
221        OperationValue::RelationList(relation_list) => {
222            let mut db_values = Vec::new();
223            if relation_list.len() == 0 {
224                db_values.push(None);
225            } else {
226                for document_id in relation_list.iter() {
227                    db_values.push(Some(document_id.to_string()))
228                }
229            }
230            db_values
231        }
232        OperationValue::PinnedRelation(pinned_relation) => {
233            vec![Some(pinned_relation.view_id().to_string())]
234        }
235        OperationValue::PinnedRelationList(pinned_relation_list) => {
236            let mut db_values = Vec::new();
237            if pinned_relation_list.len() == 0 {
238                db_values.push(None);
239            } else {
240                for document_view_id in pinned_relation_list.iter() {
241                    db_values.push(Some(document_view_id.to_string()))
242                }
243            }
244            db_values
245        }
246        OperationValue::Bytes(bytes) => {
247            // bytes are stored in the db as hex strings
248            vec![Some(hex::encode(bytes))]
249        }
250    }
251}
252
253/// Takes a vector of `DocumentViewFieldRow` and parses them into an `DocumentViewFields` struct.
254///
255/// Document fields which contain lists of values (RelationList & PinnedRelationList) are flattened
256/// and inserted as indiviual rows. This means we need to reconstruct these fields when retrieving
257/// an document view from the db.
258pub fn parse_document_view_field_rows(
259    document_field_rows: Vec<DocumentViewFieldRow>,
260) -> DocumentViewFields {
261    let mut relation_lists: BTreeMap<String, (OperationId, Vec<DocumentId>)> = BTreeMap::new();
262    let mut pinned_relation_lists: BTreeMap<String, (OperationId, Vec<DocumentViewId>)> =
263        BTreeMap::new();
264
265    let mut document_view_fields = DocumentViewFields::new();
266
267    // Iterate over returned field values, for each value:
268    // - if it is a simple value type, safely unwrap it, parse it into an DocumentViewValue and add
269    // it to the document_view_fields
270    // - if it is a relation list value type: if the row.value is None then this list is empty and
271    // we should create a relation list with no items, otherwise safely unwrap each item and parse
272    // into a DocumentId/DocumentViewId then push to the suitable list vec
273    document_field_rows.iter().for_each(|row| {
274        match row.field_type.as_str() {
275            "bool" => {
276                document_view_fields.insert(
277                    &row.name,
278                    DocumentViewValue::new(
279                        &row.operation_id.parse::<OperationId>().unwrap(),
280                        &OperationValue::Boolean(
281                            row.value.as_ref().unwrap().parse::<bool>().unwrap(),
282                        ),
283                    ),
284                );
285            }
286            "int" => {
287                document_view_fields.insert(
288                    &row.name,
289                    DocumentViewValue::new(
290                        &row.operation_id.parse::<OperationId>().unwrap(),
291                        &OperationValue::Integer(
292                            row.value.as_ref().unwrap().parse::<i64>().unwrap(),
293                        ),
294                    ),
295                );
296            }
297            "float" => {
298                document_view_fields.insert(
299                    &row.name,
300                    DocumentViewValue::new(
301                        &row.operation_id.parse::<OperationId>().unwrap(),
302                        &OperationValue::Float(row.value.as_ref().unwrap().parse::<f64>().unwrap()),
303                    ),
304                );
305            }
306            "str" => {
307                document_view_fields.insert(
308                    &row.name,
309                    DocumentViewValue::new(
310                        &row.operation_id.parse::<OperationId>().unwrap(),
311                        &OperationValue::String(row.value.as_ref().unwrap().clone()),
312                    ),
313                );
314            }
315            "bytes" => {
316                document_view_fields.insert(
317                    &row.name,
318                    DocumentViewValue::new(
319                        &row.operation_id.parse::<OperationId>().unwrap(),
320                        &OperationValue::Bytes(
321                            hex::decode(row.value.as_ref().unwrap())
322                                .expect("bytes coming from the db to be hex encoded"),
323                        ),
324                    ),
325                );
326            }
327            "relation" => {
328                document_view_fields.insert(
329                    &row.name,
330                    DocumentViewValue::new(
331                        &row.operation_id.parse::<OperationId>().unwrap(),
332                        &OperationValue::Relation(Relation::new(
333                            row.value.as_ref().unwrap().parse::<DocumentId>().unwrap(),
334                        )),
335                    ),
336                );
337            }
338            // This is a list item, so we push it to a vec but _don't_ add it
339            // to the document_view_fields yet.
340            "relation_list" => {
341                match relation_lists.get_mut(&row.name) {
342                    Some((_, list)) => {
343                        list.push(row.value.as_ref().unwrap().parse::<DocumentId>().unwrap())
344                    }
345                    None => {
346                        let list = match row.value.as_ref() {
347                            Some(document_id) => {
348                                vec![document_id.parse::<DocumentId>().unwrap()]
349                            }
350                            None => vec![],
351                        };
352                        relation_lists
353                            .insert(row.name.clone(), (row.operation_id.parse().unwrap(), list));
354                    }
355                };
356            }
357            "pinned_relation" => {
358                document_view_fields.insert(
359                    &row.name,
360                    DocumentViewValue::new(
361                        &row.operation_id.parse::<OperationId>().unwrap(),
362                        &OperationValue::PinnedRelation(PinnedRelation::new(
363                            row.value
364                                .as_ref()
365                                .unwrap()
366                                .parse::<DocumentViewId>()
367                                .unwrap(),
368                        )),
369                    ),
370                );
371            }
372            // This is a list item, so we push it to a vec but _don't_ add it to the
373            // document_view_fields yet.
374            "pinned_relation_list" => {
375                match pinned_relation_lists.get_mut(&row.name) {
376                    Some((_, list)) => list.push(
377                        row.value
378                            .as_ref()
379                            .unwrap()
380                            .parse::<DocumentViewId>()
381                            .unwrap(),
382                    ),
383                    None => {
384                        let list = match row.value.as_ref() {
385                            Some(document_view_id) => {
386                                vec![document_view_id.parse::<DocumentViewId>().unwrap()]
387                            }
388                            None => vec![],
389                        };
390                        pinned_relation_lists
391                            .insert(row.name.clone(), (row.operation_id.parse().unwrap(), list));
392                    }
393                };
394            }
395            _ => (),
396        };
397    });
398
399    for (field_name, (operation_id, relation_list)) in relation_lists {
400        document_view_fields.insert(
401            &field_name,
402            DocumentViewValue::new(
403                &operation_id,
404                &OperationValue::RelationList(RelationList::new(relation_list)),
405            ),
406        );
407    }
408
409    for (field_name, (operation_id, pinned_relation_list)) in pinned_relation_lists {
410        document_view_fields.insert(
411            &field_name,
412            DocumentViewValue::new(
413                &operation_id,
414                &OperationValue::PinnedRelationList(PinnedRelationList::new(pinned_relation_list)),
415            ),
416        );
417    }
418
419    document_view_fields
420}
421
422#[cfg(test)]
423mod tests {
424    use std::vec;
425
426    use p2panda_rs::document::DocumentViewValue;
427    use p2panda_rs::operation::traits::AsOperation;
428    use p2panda_rs::operation::{
429        OperationId, OperationValue, PinnedRelation, PinnedRelationList, Relation, RelationList,
430    };
431    use p2panda_rs::schema::SchemaId;
432    use p2panda_rs::test_utils::fixtures::{create_operation, schema_id};
433    use rstest::rstest;
434
435    use crate::db::models::{DocumentViewFieldRow, OperationFieldsJoinedRow};
436    use crate::test_utils::doggo_fields;
437
438    use super::{parse_document_view_field_rows, parse_operation_rows, parse_value_to_string_vec};
439
440    #[test]
441    fn parses_operation_rows() {
442        let operation_rows = vec![
443            OperationFieldsJoinedRow {
444                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
445                    .to_string(),
446                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
447                    .to_string(),
448                operation_id:
449                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
450                        .to_string(),
451                action: "create".to_string(),
452                schema_id:
453                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
454                        .to_string(),
455                previous: None,
456                name: Some("age".to_string()),
457                field_type: Some("int".to_string()),
458                value: Some("28".to_string()),
459                list_index: Some(0),
460                sorted_index: None,
461            },
462            OperationFieldsJoinedRow {
463                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
464                    .to_string(),
465                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
466                    .to_string(),
467                operation_id:
468                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
469                        .to_string(),
470                action: "create".to_string(),
471                schema_id:
472                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
473                        .to_string(),
474                previous: None,
475                name: Some("data".to_string()),
476                field_type: Some("bytes".to_string()),
477                value: Some("00010203".to_string()),
478                list_index: Some(0),
479                sorted_index: None,
480            },
481            OperationFieldsJoinedRow {
482                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
483                    .to_string(),
484                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
485                    .to_string(),
486                operation_id:
487                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
488                        .to_string(),
489                action: "create".to_string(),
490                schema_id:
491                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
492                        .to_string(),
493                previous: None,
494                name: Some("height".to_string()),
495                field_type: Some("float".to_string()),
496                value: Some("3.5".to_string()),
497                list_index: Some(0),
498                sorted_index: None,
499            },
500            OperationFieldsJoinedRow {
501                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
502                    .to_string(),
503                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
504                    .to_string(),
505                operation_id:
506                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
507                        .to_string(),
508                action: "create".to_string(),
509                schema_id:
510                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
511                        .to_string(),
512                previous: None,
513                name: Some("is_admin".to_string()),
514                field_type: Some("bool".to_string()),
515                value: Some("false".to_string()),
516                list_index: Some(0),
517                sorted_index: None,
518            },
519            OperationFieldsJoinedRow {
520                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
521                    .to_string(),
522                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
523                    .to_string(),
524                operation_id:
525                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
526                        .to_string(),
527                action: "create".to_string(),
528                schema_id:
529                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
530                        .to_string(),
531                previous: None,
532                name: Some("many_profile_pictures".to_string()),
533                field_type: Some("relation_list".to_string()),
534                value: Some(
535                    "0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
536                        .to_string(),
537                ),
538                list_index: Some(0),
539                sorted_index: None,
540            },
541            OperationFieldsJoinedRow {
542                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
543                    .to_string(),
544                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
545                    .to_string(),
546                operation_id:
547                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
548                        .to_string(),
549                action: "create".to_string(),
550                schema_id:
551                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
552                        .to_string(),
553                previous: None,
554                name: Some("many_profile_pictures".to_string()),
555                field_type: Some("relation_list".to_string()),
556                value: Some(
557                    "0020bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
558                        .to_string(),
559                ),
560                list_index: Some(1),
561                sorted_index: None,
562            },
563            OperationFieldsJoinedRow {
564                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
565                    .to_string(),
566                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
567                    .to_string(),
568                operation_id:
569                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
570                        .to_string(),
571                action: "create".to_string(),
572                schema_id:
573                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
574                        .to_string(),
575                previous: None,
576                name: Some("many_special_profile_pictures".to_string()),
577                field_type: Some("pinned_relation_list".to_string()),
578                value: Some(
579                    "0020cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
580                        .to_string(),
581                ),
582                list_index: Some(0),
583                sorted_index: None,
584            },
585            OperationFieldsJoinedRow {
586                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
587                    .to_string(),
588                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
589                    .to_string(),
590                operation_id:
591                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
592                        .to_string(),
593                action: "create".to_string(),
594                schema_id:
595                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
596                        .to_string(),
597                previous: None,
598                name: Some("many_special_profile_pictures".to_string()),
599                field_type: Some("pinned_relation_list".to_string()),
600                value: Some(
601                    "0020dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
602                        .to_string(),
603                ),
604                list_index: Some(1),
605                sorted_index: None,
606            },
607            OperationFieldsJoinedRow {
608                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
609                    .to_string(),
610                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
611                    .to_string(),
612                operation_id:
613                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
614                        .to_string(),
615                action: "create".to_string(),
616                schema_id:
617                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
618                        .to_string(),
619                previous: None,
620                name: Some("many_special_dog_pictures".to_string()),
621                field_type: Some("pinned_relation_list".to_string()),
622                value: Some(
623                    "0020bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc"
624                        .to_string(),
625                ),
626                list_index: Some(0),
627                sorted_index: None,
628            },
629            OperationFieldsJoinedRow {
630                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
631                    .to_string(),
632                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
633                    .to_string(),
634                operation_id:
635                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
636                        .to_string(),
637                action: "create".to_string(),
638                schema_id:
639                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
640                        .to_string(),
641                previous: None,
642                name: Some("many_special_dog_pictures".to_string()),
643                field_type: Some("pinned_relation_list".to_string()),
644                value: Some(
645                    "0020abababababababababababababababababababababababababababababababab"
646                        .to_string(),
647                ),
648                list_index: Some(1),
649                sorted_index: None,
650            },
651            OperationFieldsJoinedRow {
652                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
653                    .to_string(),
654                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
655                    .to_string(),
656                operation_id:
657                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
658                        .to_string(),
659                action: "create".to_string(),
660                schema_id:
661                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
662                        .to_string(),
663                previous: None,
664                name: Some("profile_picture".to_string()),
665                field_type: Some("relation".to_string()),
666                value: Some(
667                    "0020eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
668                        .to_string(),
669                ),
670                list_index: Some(0),
671                sorted_index: None,
672            },
673            OperationFieldsJoinedRow {
674                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
675                    .to_string(),
676                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
677                    .to_string(),
678                operation_id:
679                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
680                        .to_string(),
681                action: "create".to_string(),
682                schema_id:
683                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
684                        .to_string(),
685                previous: None,
686                name: Some("special_profile_picture".to_string()),
687                field_type: Some("pinned_relation".to_string()),
688                value: Some(
689                    "0020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
690                        .to_string(),
691                ),
692                list_index: Some(0),
693                sorted_index: None,
694            },
695            OperationFieldsJoinedRow {
696                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
697                    .to_string(),
698                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
699                    .to_string(),
700                operation_id:
701                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
702                        .to_string(),
703                action: "create".to_string(),
704                schema_id:
705                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
706                        .to_string(),
707                previous: None,
708                name: Some("username".to_string()),
709                field_type: Some("str".to_string()),
710                value: Some("bubu".to_string()),
711                list_index: Some(0),
712                sorted_index: None,
713            },
714            OperationFieldsJoinedRow {
715                public_key: "2f8e50c2ede6d936ecc3144187ff1c273808185cfbc5ff3d3748d1ff7353fc96"
716                    .to_string(),
717                document_id: "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
718                    .to_string(),
719                operation_id:
720                    "0020b177ec1bf26dfb3b7010d473e6d44713b29b765b99c6e60ecbfae742de496543"
721                        .to_string(),
722                action: "create".to_string(),
723                schema_id:
724                    "venue_0020c65567ae37efea293e34a9c7d13f8f2bf23dbdc3b5c7b9ab46293111c48fc78b"
725                        .to_string(),
726                previous: None,
727                name: Some("an_empty_relation_list".to_string()),
728                field_type: Some("pinned_relation_list".to_string()),
729                value: None,
730                list_index: Some(0),
731                sorted_index: None,
732            },
733        ];
734
735        let operation = parse_operation_rows(operation_rows).unwrap();
736
737        assert_eq!(
738            operation.fields().unwrap().get("username").unwrap(),
739            &OperationValue::String("bubu".to_string())
740        );
741        assert_eq!(
742            operation.fields().unwrap().get("data").unwrap(),
743            &OperationValue::Bytes(vec![0, 1, 2, 3])
744        );
745        assert_eq!(
746            operation.fields().unwrap().get("age").unwrap(),
747            &OperationValue::Integer(28)
748        );
749        assert_eq!(
750            operation.fields().unwrap().get("height").unwrap(),
751            &OperationValue::Float(3.5)
752        );
753        assert_eq!(
754            operation.fields().unwrap().get("is_admin").unwrap(),
755            &OperationValue::Boolean(false)
756        );
757        assert_eq!(
758            operation
759                .fields()
760                .unwrap()
761                .get("many_profile_pictures")
762                .unwrap(),
763            &OperationValue::RelationList(RelationList::new(vec![
764                "0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
765                    .parse()
766                    .unwrap(),
767                "0020bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
768                    .parse()
769                    .unwrap(),
770            ]))
771        );
772        assert_eq!(
773            operation
774                .fields()
775                .unwrap()
776                .get("many_special_profile_pictures")
777                .unwrap(),
778            &OperationValue::PinnedRelationList(PinnedRelationList::new(vec![
779                "0020cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
780                    .parse()
781                    .unwrap(),
782                "0020dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
783                    .parse()
784                    .unwrap(),
785            ]))
786        );
787        assert_eq!(
788            operation
789                .fields()
790                .unwrap()
791                .get("many_special_dog_pictures")
792                .unwrap(),
793            &OperationValue::PinnedRelationList(PinnedRelationList::new(vec![
794                "0020bcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbcbc"
795                    .parse()
796                    .unwrap(),
797                "0020abababababababababababababababababababababababababababababababab"
798                    .parse()
799                    .unwrap(),
800            ]))
801        );
802
803        assert_eq!(
804            operation.fields().unwrap().get("profile_picture").unwrap(),
805            &OperationValue::Relation(Relation::new(
806                "0020eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
807                    .parse()
808                    .unwrap()
809            ))
810        );
811        assert_eq!(
812            operation
813                .fields()
814                .unwrap()
815                .get("special_profile_picture")
816                .unwrap(),
817            &OperationValue::PinnedRelation(PinnedRelation::new(
818                "0020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
819                    .parse()
820                    .unwrap()
821            ))
822        );
823        assert_eq!(
824            operation
825                .fields()
826                .unwrap()
827                .get("an_empty_relation_list")
828                .unwrap(),
829            &OperationValue::PinnedRelationList(PinnedRelationList::new(vec![]))
830        )
831    }
832
833    #[rstest]
834    fn operation_values_to_string_vec(schema_id: SchemaId) {
835        let expected_list = vec![
836            Some("28".into()),
837            None, // This is an empty relation list
838            Some("0020abababababababababababababababababababababababababababababababab".into()),
839            Some("0020cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd".into()),
840            Some("00010203".into()),
841            Some("3.5".into()),
842            Some("false".into()),
843            Some("0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into()),
844            Some("0020bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".into()),
845            Some("0020cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc".into()),
846            Some("0020dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd".into()),
847            Some("0020eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".into()),
848            Some("0020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff".into()),
849            Some("bubu".into()),
850        ];
851
852        let operation = create_operation(doggo_fields(), schema_id);
853
854        let mut string_value_list = vec![];
855        for (_, value) in operation.fields().unwrap().iter() {
856            string_value_list.push(parse_value_to_string_vec(value));
857        }
858
859        let string_value_list: Vec<Option<String>> =
860            string_value_list.into_iter().flatten().collect();
861        assert_eq!(expected_list, string_value_list)
862    }
863
864    #[rstest]
865    fn parses_empty_relation_lists_correctly(schema_id: SchemaId) {
866        let expected_list = vec![None];
867
868        let operation = create_operation(
869            vec![(
870                "field_name",
871                OperationValue::RelationList(RelationList::new(vec![])),
872            )],
873            schema_id,
874        );
875
876        let mut string_value_list = vec![];
877        for (_, value) in operation.fields().unwrap().iter() {
878            string_value_list.push(parse_value_to_string_vec(value));
879        }
880
881        let string_value_list: Vec<Option<String>> =
882            string_value_list.into_iter().flatten().collect();
883        assert_eq!(expected_list, string_value_list)
884    }
885
886    #[test]
887    fn parses_document_field_rows() {
888        let document_id =
889            "0020713b2777f1222660291cb528d220c358920b4beddc1aea9df88a69cec45a10c0".to_string();
890        let operation_id =
891            "0020dc8fe1cbacac4d411ae25ea264369a7b2dabdfb617129dec03b6661edd963770".to_string();
892        let document_view_id = operation_id.clone();
893
894        let document_field_rows = vec![
895            DocumentViewFieldRow {
896                document_id: document_id.clone(),
897                document_view_id: document_view_id.clone(),
898                operation_id: operation_id.clone(),
899                name: "age".to_string(),
900                list_index: 0,
901                field_type: "int".to_string(),
902                value: Some("28".to_string()),
903            },
904            DocumentViewFieldRow {
905                document_id: document_id.clone(),
906                document_view_id: document_view_id.clone(),
907                operation_id: operation_id.clone(),
908                name: "height".to_string(),
909                list_index: 0,
910                field_type: "float".to_string(),
911                value: Some("3.5".to_string()),
912            },
913            DocumentViewFieldRow {
914                document_id: document_id.clone(),
915                document_view_id: document_view_id.clone(),
916                operation_id: operation_id.clone(),
917                name: "is_admin".to_string(),
918                list_index: 0,
919                field_type: "bool".to_string(),
920                value: Some("false".to_string()),
921            },
922            DocumentViewFieldRow {
923                document_id: document_id.clone(),
924                document_view_id: document_view_id.clone(),
925                operation_id: operation_id.clone(),
926                name: "many_profile_pictures".to_string(),
927                list_index: 0,
928                field_type: "relation_list".to_string(),
929                value: Some(
930                    "0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
931                        .to_string(),
932                ),
933            },
934            DocumentViewFieldRow {
935                document_id: document_id.clone(),
936                document_view_id: document_view_id.clone(),
937                operation_id: operation_id.clone(),
938                name: "many_profile_pictures".to_string(),
939                list_index: 1,
940                field_type: "relation_list".to_string(),
941                value: Some(
942                    "0020bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
943                        .to_string(),
944                ),
945            },
946            DocumentViewFieldRow {
947                document_id: document_id.clone(),
948                document_view_id: document_view_id.clone(),
949                operation_id: operation_id.clone(),
950                name: "many_special_profile_pictures".to_string(),
951                list_index: 0,
952                field_type: "pinned_relation_list".to_string(),
953                value: Some(
954                    "0020cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
955                        .to_string(),
956                ),
957            },
958            DocumentViewFieldRow {
959                document_id: document_id.clone(),
960                document_view_id: document_view_id.clone(),
961                operation_id: operation_id.clone(),
962                name: "many_special_profile_pictures".to_string(),
963                list_index: 1,
964                field_type: "pinned_relation_list".to_string(),
965                value: Some(
966                    "0020dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
967                        .to_string(),
968                ),
969            },
970            DocumentViewFieldRow {
971                document_id: document_id.clone(),
972                document_view_id: document_view_id.clone(),
973                operation_id: operation_id.clone(),
974                name: "profile_picture".to_string(),
975                list_index: 0,
976                field_type: "relation".to_string(),
977                value: Some(
978                    "0020eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
979                        .to_string(),
980                ),
981            },
982            DocumentViewFieldRow {
983                document_id: document_id.clone(),
984                document_view_id: document_view_id.clone(),
985                operation_id: operation_id.clone(),
986                name: "special_profile_picture".to_string(),
987                list_index: 0,
988                field_type: "pinned_relation".to_string(),
989                value: Some(
990                    "0020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
991                        .to_string(),
992                ),
993            },
994            DocumentViewFieldRow {
995                document_id: document_id.clone(),
996                document_view_id: document_view_id.clone(),
997                operation_id: operation_id.clone(),
998                name: "username".to_string(),
999                list_index: 0,
1000                field_type: "str".to_string(),
1001                value: Some("bubu".to_string()),
1002            },
1003            DocumentViewFieldRow {
1004                document_id: document_id.clone(),
1005                document_view_id: document_view_id.clone(),
1006                operation_id: operation_id.clone(),
1007                name: "data".to_string(),
1008                list_index: 0,
1009                field_type: "bytes".to_string(),
1010                value: Some("00010203".to_string()),
1011            },
1012            DocumentViewFieldRow {
1013                document_id: document_id.clone(),
1014                document_view_id: document_view_id.clone(),
1015                operation_id: operation_id.clone(),
1016                name: "an_empty_relation_list".to_string(),
1017                list_index: 0,
1018                field_type: "pinned_relation_list".to_string(),
1019                value: None,
1020            },
1021        ];
1022
1023        let document_fields = parse_document_view_field_rows(document_field_rows);
1024        let operation_id: OperationId =
1025            "0020dc8fe1cbacac4d411ae25ea264369a7b2dabdfb617129dec03b6661edd963770"
1026                .parse()
1027                .unwrap();
1028
1029        assert_eq!(
1030            document_fields.get("username").unwrap(),
1031            &DocumentViewValue::new(&operation_id, &OperationValue::String("bubu".to_string()))
1032        );
1033        assert_eq!(
1034            document_fields.get("data").unwrap(),
1035            &DocumentViewValue::new(&operation_id, &OperationValue::Bytes(vec![0, 1, 2, 3]))
1036        );
1037        assert_eq!(
1038            document_fields.get("age").unwrap(),
1039            &DocumentViewValue::new(&operation_id, &OperationValue::Integer(28))
1040        );
1041        assert_eq!(
1042            document_fields.get("height").unwrap(),
1043            &DocumentViewValue::new(&operation_id, &OperationValue::Float(3.5))
1044        );
1045        assert_eq!(
1046            document_fields.get("is_admin").unwrap(),
1047            &DocumentViewValue::new(&operation_id, &OperationValue::Boolean(false))
1048        );
1049        assert_eq!(
1050            document_fields.get("many_profile_pictures").unwrap(),
1051            &DocumentViewValue::new(
1052                &operation_id,
1053                &OperationValue::RelationList(RelationList::new(vec![
1054                    "0020aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1055                        .parse()
1056                        .unwrap(),
1057                    "0020bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1058                        .parse()
1059                        .unwrap(),
1060                ]))
1061            )
1062        );
1063        assert_eq!(
1064            document_fields
1065                .get("many_special_profile_pictures")
1066                .unwrap(),
1067            &DocumentViewValue::new(
1068                &operation_id,
1069                &OperationValue::PinnedRelationList(PinnedRelationList::new(vec![
1070                    "0020cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1071                        .parse()
1072                        .unwrap(),
1073                    "0020dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
1074                        .parse()
1075                        .unwrap(),
1076                ]))
1077            )
1078        );
1079        assert_eq!(
1080            document_fields.get("profile_picture").unwrap(),
1081            &DocumentViewValue::new(
1082                &operation_id,
1083                &OperationValue::Relation(Relation::new(
1084                    "0020eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
1085                        .parse()
1086                        .unwrap()
1087                ))
1088            )
1089        );
1090        assert_eq!(
1091            document_fields.get("special_profile_picture").unwrap(),
1092            &DocumentViewValue::new(
1093                &operation_id,
1094                &OperationValue::PinnedRelation(PinnedRelation::new(
1095                    "0020ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
1096                        .parse()
1097                        .unwrap()
1098                ))
1099            )
1100        );
1101        assert_eq!(
1102            document_fields.get("an_empty_relation_list").unwrap(),
1103            &DocumentViewValue::new(
1104                &operation_id,
1105                &OperationValue::PinnedRelationList(PinnedRelationList::new(vec![]))
1106            )
1107        )
1108    }
1109}