colonylib 0.6.0

A library implementing the Colony metadata framework on Autonomi
Documentation
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
use alloc::string::FromUtf8Error;
use chrono::Utc;
use oxigraph::io::{RdfFormat, RdfParseError, RdfParser};
use oxigraph::model::{
    GraphNameRef, IriParseError, LiteralRef, NamedNodeRef, Quad, QuadRef, TermRef,
};
use oxigraph::sparql::results::QueryResultsFormat;
use oxigraph::sparql::{EvaluationError, QueryResults, QuerySolutionIter};
use oxigraph::store::{LoaderError, SerializerError, StorageError, Store};
use oxjsonld::{JsonLdProfile, JsonLdProfileSet};
use oxttl::TriGParser;
use serde;
use serde_json::Error as SerdeError;
use std::collections::HashMap;
use std::io::Cursor;
use std::path::PathBuf;
use thiserror;
use tracing::{debug, error, info};

//////////////////////////////////////////////
// Vocabulary
//////////////////////////////////////////////
macro_rules! PREDICATE {
    ($e:expr) => {
        concat!("ant://colonylib/", "v1/", $e)
    };
}

macro_rules! OBJECT {
    ($e:expr) => {
        concat!("ant://colonylib/", "v1/", $e)
    };
}

//////////////////////////////////////////////
// Predicates
//////////////////////////////////////////////

/// Address Type
/// Defines the type of pod component at the address
/// Object must be one of the address type objects
pub const HAS_ADDR_TYPE: &str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";

/// Name
/// The name of the resource pod
/// Object is a string literal
pub const HAS_NAME: &str = "http://schema.org/name";

/// Pod Depth
/// The depth of the pod in the reference tree
/// Only valid for POD and POD_REF address types
/// Object is a literal representing the depth, local pods are set to 0
pub const HAS_DEPTH: &str = PREDICATE!("depth");

/// Scratchpad Index
/// The index of the scratchpad in a pod
/// This is used to sequentially build up a pod from multiple scratchpads
/// Object is a literal representing the index
pub const HAS_INDEX: &str = PREDICATE!("index");

/// Key Count
/// The number of derived keys
/// This is used during refresh to pregenerate the keys we need
/// Object is a literal representing the count
pub const KEY_COUNT: &str = PREDICATE!("count");

/// Creation Date
/// The date when the pod was created
/// Object is a literal representing the date
pub const HAS_CREATION_DATE: &str = PREDICATE!("creation");

/// Modified Date
/// The date when the pod was modified
/// Object is a literal representing the date
pub const HAS_MODIFIED_DATE: &str = PREDICATE!("modified");

//////////////////////////////////////////////
// Objects
//////////////////////////////////////////////

/// Address Type Objects
/// Defines what kind of object the address is pointing to
pub const POD: &str = OBJECT!("pod"); // pointer for a pod
pub const POD_REF: &str = OBJECT!("ref"); // pointer for a pod reference
pub const DATA: &str = OBJECT!("data"); //scratchpad containing data for a pod
pub const FREED_POD: &str = OBJECT!("free_pod"); //Unused pod pointer or scratchpad
pub const FREED_DATA: &str = OBJECT!("free_data"); //Unused pod pointer or scratchpad
pub const ABANDONED: &str = OBJECT!("abandoned"); //Abandoned pod pointer or scratchpad address, never use again

// Error handling
#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error(transparent)]
    Graph(#[from] StorageError),
    #[error(transparent)]
    Iri(#[from] IriParseError),
    #[error(transparent)]
    Serializer(#[from] SerializerError),
    #[error(transparent)]
    Evaluation(#[from] EvaluationError),
    #[error(transparent)]
    Serde(#[from] SerdeError),
    #[error(transparent)]
    FromUtf8(#[from] FromUtf8Error),
    #[error(transparent)]
    Loader(#[from] LoaderError),
    #[error(transparent)]
    RdfParse(#[from] RdfParseError),
}

#[derive(serde::Serialize)]
#[serde(tag = "kind", content = "message")]
#[serde(rename_all = "camelCase")]
pub enum ErrorKind {
    Graph(String),
    Iri(String),
    Serializer(String),
    Evaluation(String),
    Serde(String),
    FromUtf8(String),
    Loader(String),
    RdfParse(String),
}

impl serde::Serialize for Error {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::ser::Serializer,
    {
        let error_message = self.to_string();
        let error_kind = match self {
            Self::Graph(_) => ErrorKind::Graph(error_message),
            Self::Iri(_) => ErrorKind::Iri(error_message),
            Self::Serializer(_) => ErrorKind::Serializer(error_message),
            Self::Evaluation(_) => ErrorKind::Evaluation(error_message),
            Self::Serde(_) => ErrorKind::Serde(error_message),
            Self::FromUtf8(_) => ErrorKind::FromUtf8(error_message),
            Self::Loader(_) => ErrorKind::Loader(error_message),
            Self::RdfParse(_) => ErrorKind::RdfParse(error_message),
        };
        error_kind.serialize(serializer)
    }
}

#[derive(Clone)]
pub struct Graph {
    store: Store,
}

impl Graph {
    pub fn open(db: &PathBuf) -> Result<Self, Error> {
        let store = Store::open(db)?;
        info!("Opened graph store at {:?}", db);
        Ok(Graph { store })
    }

    pub fn put_quad(
        &self,
        subject: &str,
        predicate: &str,
        object: &str,
        graph_name: Option<&str>,
    ) -> Result<Quad, Error> {
        let subject_node = NamedNodeRef::new(subject)?;
        let predicate_node = NamedNodeRef::new(predicate)?;
        let object_node = match object {
            // If the object is a URI (starts with http:// or https:// or ant://), create a NamedNodeRef
            _ if object.starts_with("http://")
                || object.starts_with("https://")
                || object.starts_with("ant://") =>
            {
                TermRef::NamedNode(NamedNodeRef::new(object)?)
            }
            // Otherwise, treat it as a simple literal
            _ => TermRef::Literal(LiteralRef::new_simple_literal(object)),
        };
        let graph_name_ref = match graph_name {
            Some(name) => GraphNameRef::NamedNode(NamedNodeRef::new(name)?),
            None => GraphNameRef::DefaultGraph,
        };
        let quad = QuadRef::new(subject_node, predicate_node, object_node, graph_name_ref);
        debug!("Creating quad: {:?}", quad);
        self.store.remove(quad)?;
        self.store.insert(quad)?;
        Ok(quad.into_owned())
    }

    pub fn add_pod_entry(
        &mut self,
        pod_name: &str,
        pod_address: &str,
        scratchpad_address: &str,
        configuration_address: &str,
        configuration_scratchpad_address: &str,
        num_keys: u64,
    ) -> Result<(Vec<u8>, Vec<u8>), Error> {
        // Add a new pod
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();
        let pod = NamedNodeRef::new(pod_iri)?;
        self.store.insert_named_graph(pod)?;

        // Get the configuration IRI and create a configuration graph if it doesn't exist
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();
        let config = NamedNodeRef::new(configuration_iri)?;
        self.store.insert_named_graph(config)?;

        // Update the key count
        self.update_key_count(configuration_address, num_keys)?;

        // Enter in scratchpad quad
        let scratchpad_iri = format!("ant://{scratchpad_address}");
        let scratchpad_iri = scratchpad_iri.as_str();
        let configuration_scratchpad_iri = format!("ant://{configuration_scratchpad_address}");
        let configuration_scratchpad_iri = configuration_scratchpad_iri.as_str();
        let date = Utc::now().to_rfc3339();
        let date = date.as_str();
        // Pod metadata
        let _quad = self.put_quad(pod_iri, HAS_ADDR_TYPE, POD, Some(configuration_iri))?;
        let _quad = self.put_quad(pod_iri, HAS_DEPTH, "0", Some(configuration_iri))?;
        let _quad = self.put_quad(pod_iri, HAS_NAME, pod_name, Some(pod_iri))?;
        let _quad = self.put_quad(pod_iri, HAS_CREATION_DATE, date, Some(pod_iri))?;
        let _quad = self.put_quad(pod_iri, HAS_MODIFIED_DATE, date, Some(pod_iri))?;
        // Scratchpad metadata
        let _quad = self.put_quad(scratchpad_iri, HAS_ADDR_TYPE, DATA, Some(configuration_iri))?;
        let _quad = self.put_quad(scratchpad_iri, HAS_INDEX, "0", Some(pod_iri))?;
        let _quad = self.put_quad(scratchpad_iri, HAS_MODIFIED_DATE, date, Some(pod_iri))?;

        let _quad = self.put_quad(
            configuration_scratchpad_iri,
            HAS_INDEX,
            "0",
            Some(configuration_iri),
        )?;
        let _quad = self.put_quad(
            configuration_scratchpad_iri,
            HAS_ADDR_TYPE,
            DATA,
            Some(configuration_iri),
        )?;
        let _quad = self.put_quad(
            configuration_scratchpad_iri,
            HAS_MODIFIED_DATE,
            date,
            Some(configuration_iri),
        )?;
        let _quad = self.put_quad(
            configuration_iri,
            HAS_ADDR_TYPE,
            POD,
            Some(configuration_iri),
        )?;
        let _quad = self.put_quad(
            configuration_iri,
            HAS_NAME,
            "User Configuration",
            Some(configuration_iri),
        )?;
        debug!("Pod entries added");

        // Dump newly created graph in TriG format
        let mut buffer = Vec::new();
        self.store
            .dump_graph_to_writer(pod, RdfFormat::TriG, &mut buffer)?;

        // Dump the updated configuration graph in TriG format
        let mut configuration = Vec::new();
        self.store
            .dump_graph_to_writer(config, RdfFormat::TriG, &mut configuration)?;

        Ok((buffer, configuration))
    }

    pub fn check_pod_exists(&self, pod_address: &str) -> Result<String, Error> {
        // check if the given pod_address is actually the NAME of a pod
        // if so, get the pod's address from the graph
        let query = format!(
            "SELECT ?pod WHERE {{ GRAPH ?graph {{ ?pod <{HAS_NAME}> \"{pod_address}\" . }} }}"
        );
        debug!("Pod exists query: {}", query);

        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(pod_node)) = solution.get("pod") {
                    let pod_iri = pod_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = pod_iri.strip_prefix("ant://") {
                        debug!(
                            "Found address for pod alias \"{}\": {}",
                            pod_address, address
                        );
                        return Ok(address.to_string());
                    }
                }
            }
        }

        // Otherwise check to make sure the pod graph exists and pass it through
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();
        let pod = NamedNodeRef::new(pod_iri)?;
        if self.store.contains_named_graph(pod)? {
            debug!("Pod address exists: {}", pod_address);
            Ok(pod_address.to_string())
        } else {
            Err(Error::Graph(StorageError::Io(std::io::Error::new(
                std::io::ErrorKind::NotFound,
                "Pod not found",
            ))))
        }
    }

    pub fn remove_pod_entry(
        &mut self,
        pod_address: &str,
        pod_scratchpads: Vec<String>,
        configuration_address: &str,
    ) -> Result<Vec<u8>, Error> {
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();
        let pod = NamedNodeRef::new(pod_iri)?;

        // Get the configuration IRI
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();
        let config = NamedNodeRef::new(configuration_iri)?;

        // Remove the pod graph
        self.store.clear_graph(pod)?;

        // Remove the pod from the configuration graph
        let update =
            format!("DELETE WHERE {{ GRAPH <{configuration_iri}> {{ <{pod_iri}> ?p ?o . }} }}");
        self.store.update(update.as_str())?;

        for scratchpad in pod_scratchpads.clone() {
            let scratchpad_iri = format!("ant://{scratchpad}");
            let scratchpad_iri = scratchpad_iri.as_str();
            let update = format!(
                "DELETE WHERE {{ GRAPH <{configuration_iri}> {{ <{scratchpad_iri}> ?p ?o . }} }}"
            );
            self.store.update(update.as_str())?;
        }

        // Set the pod_address and pod_scratchpads to UNUSED in the configuration graph
        let _quad = self.put_quad(pod_iri, HAS_ADDR_TYPE, FREED_POD, Some(configuration_iri))?;
        for scratchpad in pod_scratchpads {
            let scratchpad_iri = format!("ant://{scratchpad}");
            let scratchpad_iri = scratchpad_iri.as_str();
            let _quad = self.put_quad(
                scratchpad_iri,
                HAS_ADDR_TYPE,
                FREED_DATA,
                Some(configuration_iri),
            )?;
        }

        // Dump the updated configuration graph in TriG format
        let mut configuration = Vec::new();
        self.store
            .dump_graph_to_writer(config, RdfFormat::TriG, &mut configuration)?;

        Ok(configuration)
    }

    pub fn rename_pod_entry(
        &mut self,
        pod_address: &str,
        new_name: &str,
    ) -> Result<Vec<u8>, Error> {
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();
        let pod = NamedNodeRef::new(pod_iri)?;

        // Update the pod name
        let update =
            format!("DELETE WHERE {{ GRAPH <{pod_iri}> {{ <{pod_iri}> <{HAS_NAME}> ?o . }} }}");
        debug!("Delete existing pod name string: {}", update);
        self.store.update(update.as_str())?;

        // Enter in new pod name quad
        let _quad = self.put_quad(pod_iri, HAS_NAME, new_name, Some(pod_iri))?;
        debug!("Pod name updated to {}", new_name);

        // Dump the updated graph in TriG format
        let mut buffer = Vec::new();
        self.store
            .dump_graph_to_writer(pod, RdfFormat::TriG, &mut buffer)?;

        Ok(buffer)
    }

    pub fn remove_scratchpad_entry(
        &mut self,
        pod_address: &str,
        scratchpad_address: &str,
    ) -> Result<(), Error> {
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();

        let scratchpad_iri = format!("ant://{scratchpad_address}");
        let scratchpad_iri = scratchpad_iri.as_str();

        // Remove the depth object if it already exists in the configuration graph
        let update =
            format!("DELETE WHERE {{ GRAPH <{pod_iri}> {{ <{scratchpad_iri}> ?p ?o . }} }}");
        debug!("Delete unused scratchpad from pod string: {}", update);
        self.store.update(update.as_str())?;

        Ok(())
    }

    pub fn pod_ref_entry(
        &mut self,
        pod_address: &str,
        pod_ref_address: &str,
        configuration_address: &str,
        add: bool,
        is_local: bool,
    ) -> Result<(Vec<u8>, Vec<u8>), Error> {
        let pod_ref_iri = format!("ant://{pod_ref_address}");
        let pod_ref_iri = pod_ref_iri.as_str();
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();

        // Get the configuration IRI
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();

        // Remove the depth object if it already exists in the configuration graph
        let update =
            format!("DELETE WHERE {{ GRAPH <{configuration_iri}> {{ <{pod_ref_iri}> ?p ?o . }} }}");
        debug!("Delete pod_ref from configuration graph string: {}", update);
        self.store.update(update.as_str())?;

        // Delete existing data for the subject in the pod graph
        // This query deletes all triples for the subject in the specified pod graph
        let update = format!("DELETE WHERE {{ GRAPH <{pod_iri}> {{ <{pod_ref_iri}> ?p ?o . }} }}");
        debug!("Delete pod_ref from pod string: {}", update);

        self.store.update(update.as_str())?;

        if add {
            // Enter in pod ref quad
            let depth = if is_local { "0" } else { "1" };
            let _quad = self.put_quad(pod_ref_iri, HAS_DEPTH, depth, Some(configuration_iri))?;
            let _quad = self.put_quad(pod_ref_iri, HAS_ADDR_TYPE, POD_REF, Some(pod_iri))?;
            debug!("Pod ref {} added to pod {}", pod_ref_address, pod_address);
        } else {
            debug!(
                "Pod ref {} removed from pod {}",
                pod_ref_address, pod_address
            );
            // If the pod is local, we need to put the depth information back (since it was deleted above)
            if is_local {
                let _quad = self.put_quad(pod_ref_iri, HAS_DEPTH, "0", Some(configuration_iri))?;
            } else {
                // If the pod isn't local, but is referenced in my other pods, we need to put the depth of 1 back into the configuration graph
                // "SELECT ?g WHERE {{ GRAPH ?g {{ <{pod_ref_iri}> ?p ?o . }} }}"
                let query = format!(
                    r#"
                    SELECT ?g WHERE {{
                        GRAPH ?g {{ <{pod_ref_iri}> ?p ?o . }}
                        GRAPH <{configuration_iri}> {{ ?g <{HAS_DEPTH}> "0" . }}
                    }}
                    "#
                );
                let results = self.store.query(query.as_str())?;
                // If 1 or more results found, set the depth to 1
                if let QueryResults::Solutions(solutions) = results {
                    for solution in solutions.flatten() {
                        if let Some(oxigraph::model::Term::NamedNode(graph)) = solution.get("g") {
                            debug!("Pod ref {} found in graph {}", pod_ref_address, graph);
                            let _quad = self.put_quad(
                                pod_ref_iri,
                                HAS_DEPTH,
                                "1",
                                Some(configuration_iri),
                            )?;
                            // if found, exit loop
                            break;
                        }
                    }
                }
            }
        }

        // If the pod is local, we need to put the POD address type back (since it was deleted above)
        if is_local {
            let _quad = self.put_quad(pod_ref_iri, HAS_ADDR_TYPE, POD, Some(configuration_iri))?;
            debug!("Pod ref {} set as local pod", pod_ref_address);
        }

        // Update the modified date in the configuration pod
        let delete_query = format!(
            "DELETE WHERE {{ GRAPH <{configuration_iri}> {{ ?subject <{HAS_MODIFIED_DATE}> ?date . }} }}"
        );
        debug!("Delete existing modified date query: {}", delete_query);
        self.store.update(delete_query.as_str())?;

        let date = Utc::now().to_rfc3339();
        let date = date.as_str();
        let _quad = self.put_quad(
            configuration_iri,
            HAS_MODIFIED_DATE,
            date,
            Some(configuration_iri),
        )?;

        // Update all of the scratchpads within the configuration pod to have the same modified date
        let scratchpads = self.get_scratchpads(configuration_address)?;
        for scratchpad in scratchpads {
            let scratchpad_iri = format!("ant://{scratchpad}");
            let scratchpad_iri = scratchpad_iri.as_str();
            let _quad = self.put_quad(
                scratchpad_iri,
                HAS_MODIFIED_DATE,
                date,
                Some(configuration_iri),
            )?;
        }

        // Update the modified date in the pod
        let delete_query = format!(
            "DELETE WHERE {{ GRAPH <{pod_iri}> {{ ?subject <{HAS_MODIFIED_DATE}> ?date . }} }}"
        );
        debug!("Delete existing modified date query: {}", delete_query);
        self.store.update(delete_query.as_str())?;

        let _quad = self.put_quad(pod_iri, HAS_MODIFIED_DATE, date, Some(pod_iri))?;

        // Update all of the scratchpads within the pod to have the same modified date
        let scratchpads = self.get_pod_scratchpads(pod_address)?;
        for scratchpad in scratchpads {
            let scratchpad_iri = format!("ant://{scratchpad}");
            let scratchpad_iri = scratchpad_iri.as_str();
            let _quad = self.put_quad(scratchpad_iri, HAS_MODIFIED_DATE, date, Some(pod_iri))?;
        }

        // Dump the updated graph in TriG format
        let pod = oxigraph::model::NamedNodeRef::new(pod_iri)?;
        let mut buffer = Vec::new();
        self.store
            .dump_graph_to_writer(pod, RdfFormat::TriG, &mut buffer)?;

        // Dump the updated configuration graph in TriG format
        let pod = oxigraph::model::NamedNodeRef::new(configuration_iri)?;
        let mut configuration = Vec::new();
        self.store
            .dump_graph_to_writer(pod, RdfFormat::TriG, &mut configuration)?;

        Ok((buffer, configuration))
    }

    pub fn update_key_count(
        &mut self,
        configuration_address: &str,
        num_keys: u64,
    ) -> Result<(), Error> {
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();

        // Remove the key count object if it already exists in the configuration graph
        let update = format!(
            "DELETE WHERE {{ GRAPH <{configuration_iri}> {{ <{configuration_iri}> <{KEY_COUNT}> ?o . }} }}"
        );
        self.store.update(update.as_str())?;

        // Enter in key count quad
        let _quad = self.put_quad(
            configuration_iri,
            KEY_COUNT,
            &num_keys.to_string(),
            Some(configuration_iri),
        )?;
        Ok(())
    }

    // Input is a JSON-LD string
    pub fn put_subject_data(
        &mut self,
        pod_address: &str,
        subject_address: &str,
        configuration_address: &str,
        data: &str,
    ) -> Result<(Vec<u8>, Vec<u8>), Error> {
        let pod_iri = format!("ant://{pod_address}");
        let pod_iri = pod_iri.as_str();
        let pod = NamedNodeRef::new(pod_iri)?;
        let subject_iri = format!("ant://{subject_address}");
        let subject_iri = subject_iri.as_str();

        // Get the configuration IRI
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();
        let config = NamedNodeRef::new(configuration_iri)?;

        // Delete existing data for the subject in the pod graph
        // This query deletes all triples for the subject in the specified pod graph
        let update = format!("DELETE WHERE {{ GRAPH <{pod_iri}> {{ <{subject_iri}> ?p ?o . }} }}");
        debug!("Delete string: {}", update);

        self.store.update(update.as_str())?;

        // Convert the data &str to a Reader
        let data_reader = Cursor::new(data);

        // Insert the new data using the Reader
        //FIXME: may need the streaming profile option here?
        let mut profile = JsonLdProfileSet::empty();
        profile |= JsonLdProfile::Compacted;
        profile |= JsonLdProfile::Context;
        // Load the data into the pod graph
        self.store.load_from_reader(
            RdfParser::from_format(RdfFormat::JsonLd { profile })
                //.with_base_iri("https://schema.org/")? // don't need this
                .without_named_graphs() // No named graphs allowed in the input
                .with_default_graph(pod), // we put the file default graph inside of a named graph
            data_reader,
        )?;

        // Update modified date
        let delete_query = format!(
            "DELETE WHERE {{ GRAPH <{pod_iri}> {{ ?subject <{HAS_MODIFIED_DATE}> ?date . }} }}"
        );
        debug!("Delete existing modified date query: {}", delete_query);
        self.store.update(delete_query.as_str())?;

        let date = Utc::now().to_rfc3339();
        let date = date.as_str();
        let _quad = self.put_quad(pod_iri, HAS_MODIFIED_DATE, date, Some(pod_iri))?;

        // Update all of the scratchpads within the pod to have the same modified date
        let scratchpads = self.get_pod_scratchpads(pod_address)?;
        for scratchpad in scratchpads {
            let scratchpad_iri = format!("ant://{scratchpad}");
            let scratchpad_iri = scratchpad_iri.as_str();
            let _quad = self.put_quad(scratchpad_iri, HAS_MODIFIED_DATE, date, Some(pod_iri))?;
        }

        // Dump newly created graph in TriG format
        let mut buffer = Vec::new();
        self.store
            .dump_graph_to_writer(pod, RdfFormat::TriG, &mut buffer)?;

        // Update the modified date in the configuration pod
        let delete_query = format!(
            "DELETE WHERE {{ GRAPH <{configuration_iri}> {{ ?subject <{HAS_MODIFIED_DATE}> ?date . }} }}"
        );
        debug!("Delete existing modified date query: {}", delete_query);
        self.store.update(delete_query.as_str())?;

        let _quad = self.put_quad(
            configuration_iri,
            HAS_MODIFIED_DATE,
            date,
            Some(configuration_iri),
        )?;

        // Update all of the scratchpads within the configuration pod to have the same modified date
        let scratchpads = self.get_scratchpads(configuration_address)?;
        for scratchpad in scratchpads {
            let scratchpad_iri = format!("ant://{scratchpad}");
            let scratchpad_iri = scratchpad_iri.as_str();
            let _quad = self.put_quad(
                scratchpad_iri,
                HAS_MODIFIED_DATE,
                date,
                Some(configuration_iri),
            )?;
        }

        // Dump the updated configuration graph in TriG format
        let mut configuration = Vec::new();
        self.store
            .dump_graph_to_writer(config, RdfFormat::TriG, &mut configuration)?;

        Ok((buffer, configuration))
    }

    pub fn get_subject_data(&self, subject_address: &str) -> Result<String, Error> {
        let subject_iri = format!("ant://{subject_address}");

        let query = format!(
            "SELECT ?graph ?predicate ?object WHERE {{ GRAPH ?graph {{ <{}> ?predicate ?object . }} }}",
            subject_iri.as_str()
        );
        debug!("Query string: {}", query);

        let results = self.store.query(query.as_str())?;
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;

        // Map the vector buffer to a Value JSON object
        let json_str = String::from_utf8(buffer)?;
        debug!("Query results: {}", json_str);
        // This is output in the W3C JSON SPARQL format.
        // Can use the JavaScript `sparqljson-parse` library to parse it
        Ok(json_str)
    }

    // Get the depth of a pod from the graph database
    pub fn get_pod_depth(&self, pod_address: &str) -> Result<u64, Error> {
        let pod_iri = format!("ant://{pod_address}");

        let query = format!(
            "SELECT ?depth WHERE {{ GRAPH ?graph {{ <{pod_iri}> <{HAS_DEPTH}> ?depth . }} }}"
        );
        debug!("Depth query: {}", query);

        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::Literal(literal)) = solution.get("depth")
                    && let Ok(depth_value) = literal.value().parse::<u64>()
                {
                    debug!("Found depth {} for pod {}", depth_value, pod_address);
                    return Ok(depth_value);
                }
            }
        }

        // If no depth found, return a high value to indicate unknown depth
        debug!("No depth found for pod {}, returning default", pod_address);
        Ok(u64::MAX)
    }

    // Update or set the depth of a pod in the graph database
    pub fn update_pod_depth(
        &mut self,
        pod_address: &str,
        configuration_address: &str,
        new_depth: u64,
    ) -> Result<(), Error> {
        self.update_pod_depth_internal(pod_address, configuration_address, new_depth, false)
    }

    // Force set the depth of a pod in the graph database (for testing)
    pub fn force_set_pod_depth(
        &mut self,
        pod_address: &str,
        configuration_address: &str,
        new_depth: u64,
    ) -> Result<(), Error> {
        self.update_pod_depth_internal(pod_address, configuration_address, new_depth, true)
    }

    // Internal method to update pod depth with optional force parameter
    fn update_pod_depth_internal(
        &mut self,
        pod_address: &str,
        configuration_address: &str,
        new_depth: u64,
        force: bool,
    ) -> Result<(), Error> {
        let pod_iri = format!("ant://{pod_address}");
        let configuration_iri = format!("ant://{configuration_address}");

        // First, check if there's an existing depth
        let current_depth = self.get_pod_depth(pod_address)?;

        // Only update if the new depth is smaller (closer to root), if no depth exists, or if forced
        if force || new_depth < current_depth {
            info!(
                "Updating depth for pod {} from {} to {}",
                pod_address, current_depth, new_depth
            );

            let delete_query =
                format!("DELETE WHERE {{ GRAPH ?graph {{ <{pod_iri}> <{HAS_DEPTH}> ?depth . }} }}");
            debug!("Delete depth query: {}", delete_query);
            self.store.update(delete_query.as_str())?;

            // Insert new depth
            let _quad = self.put_quad(
                &pod_iri,
                HAS_DEPTH,
                &new_depth.to_string(),
                Some(&configuration_iri),
            )?;
            info!("Set depth {} for pod {}", new_depth, pod_address);
        } else {
            debug!(
                "Not updating depth for pod {} (current: {}, new: {})",
                pod_address, current_depth, new_depth
            );
        }

        Ok(())
    }

    // Get the largest pod depth in the graph database
    pub fn get_max_pod_depth(&self) -> Result<u64, Error> {
        let query = format!(
            "SELECT (MAX(?depth) AS ?max_depth) WHERE {{ GRAPH ?graph {{ ?pod <{HAS_DEPTH}> ?depth . }} }}"
        );
        debug!("Max depth query: {}", query);

        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::Literal(literal)) = solution.get("max_depth")
                    && let Ok(max_depth_value) = literal.value().parse::<u64>()
                {
                    debug!("Max pod depth found: {}", max_depth_value);
                    return Ok(max_depth_value);
                }
            }
        }

        // If no pods found, return 0
        debug!("No pods found, returning depth 0");
        Ok(0)
    }

    // Get all pods at a specific depth
    pub fn get_pods_at_depth(&self, depth: u64) -> Result<Vec<String>, Error> {
        let query =
            format!("SELECT ?pod WHERE {{ GRAPH ?graph {{ ?pod <{HAS_DEPTH}> \"{depth}\" . }} }}");
        debug!("Pods at depth query: {}", query);

        let mut pods = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(pod_node)) = solution.get("pod") {
                    let pod_iri = pod_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = pod_iri.strip_prefix("ant://") {
                        pods.push(address.to_string());
                    }
                }
            }
        }

        debug!("Found {} pods at depth {}", pods.len(), depth);
        Ok(pods)
    }

    // Get all pod references from the graph data
    pub fn get_pod_references(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all objects in the pod's named graph that are ant:// URIs
        let query = format!(
            "SELECT DISTINCT ?pod_ref WHERE {{ GRAPH <{pod_iri}> {{ ?pod_ref <{HAS_ADDR_TYPE}> <{POD_REF}> . }} }}"
        );
        debug!("Pod references query: {}", query);

        let mut references = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(ref_node)) = solution.get("pod_ref") {
                    let ref_iri = ref_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = ref_iri.strip_prefix("ant://") {
                        references.push(address.to_string());
                    }
                }
            }
        }

        debug!(
            "Found {} references in pod {}",
            references.len(),
            pod_address
        );
        Ok(references)
    }

    // Get all free pointers from the graph data
    pub fn get_free_pointers(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all objects in the pod's named graph that are ant:// URIs
        let query = format!(
            "SELECT DISTINCT ?pod_ref WHERE {{ GRAPH <{pod_iri}> {{ ?pod_ref <{HAS_ADDR_TYPE}> <{FREED_POD}> . }} }}"
        );
        debug!("Free pointers query: {}", query);

        let mut pointers = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(ref_node)) = solution.get("pod_ref") {
                    let ref_iri = ref_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = ref_iri.strip_prefix("ant://") {
                        pointers.push(address.to_string());
                    }
                }
            }
        }

        debug!(
            "Found {} free pointers in pod {}",
            pointers.len(),
            pod_address
        );
        Ok(pointers)
    }

    // Get all free scratchpads from the graph data
    pub fn get_free_scratchpads(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all objects in the pod's named graph that are ant:// URIs
        let query = format!(
            "SELECT DISTINCT ?pod_ref WHERE {{ GRAPH <{pod_iri}> {{ ?pod_ref <{HAS_ADDR_TYPE}> <{FREED_DATA}> . }} }}"
        );
        debug!("Free scratchpads query: {}", query);

        let mut scratchpads = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(ref_node)) = solution.get("pod_ref") {
                    let ref_iri = ref_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = ref_iri.strip_prefix("ant://") {
                        scratchpads.push(address.to_string());
                    }
                }
            }
        }

        debug!(
            "Found {} free scratchpads in pod {}",
            scratchpads.len(),
            pod_address
        );
        Ok(scratchpads)
    }

    // Get all pointers from the graph data
    pub fn get_pointers(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all objects in the pod's named graph that are ant:// URIs
        let query = format!(
            "SELECT DISTINCT ?pod_ref WHERE {{ GRAPH <{pod_iri}> {{ ?pod_ref <{HAS_ADDR_TYPE}> <{POD}> . }} }}"
        );
        debug!("Pointers query: {}", query);

        let mut pointers = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(ref_node)) = solution.get("pod_ref") {
                    let ref_iri = ref_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = ref_iri.strip_prefix("ant://") {
                        pointers.push(address.to_string());
                    }
                }
            }
        }

        debug!("Found {} pointers in pod {}", pointers.len(), pod_address);
        Ok(pointers)
    }

    // Get all scratchpads from the graph data
    pub fn get_scratchpads(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all objects in the pod's named graph that are ant:// URIs
        let query = format!(
            "SELECT DISTINCT ?pod_ref WHERE {{ GRAPH <{pod_iri}> {{ ?pod_ref <{HAS_ADDR_TYPE}> <{DATA}> . }} }}"
        );
        debug!("Scratchpads query: {}", query);

        let mut scratchpads = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(ref_node)) = solution.get("pod_ref") {
                    let ref_iri = ref_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = ref_iri.strip_prefix("ant://") {
                        scratchpads.push(address.to_string());
                    }
                }
            }
        }

        debug!(
            "Found {} scratchpads in pod {}",
            scratchpads.len(),
            pod_address
        );
        Ok(scratchpads)
    }

    // Get the key count from the graph data
    pub fn get_key_count(&self, pod_address: &str) -> Result<u64, Error> {
        let pod_iri = format!("ant://{pod_address}");

        let query = format!(
            "SELECT ?count WHERE {{ GRAPH <{pod_iri}> {{ <{pod_iri}> <{KEY_COUNT}> ?count . }} }}"
        );
        debug!("Key count query: {}", query);

        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::Literal(literal)) = solution.get("count")
                    && let Ok(count_value) = literal.value().parse::<u64>()
                {
                    debug!("Found key count {} for pod {}", count_value, pod_address);
                    return Ok(count_value);
                }
            }
        }

        Ok(0)
    }

    // Get all subjects in a pod
    pub fn get_pod_subjects(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all objects in the pod's named graph that are ant:// URIs
        let query = format!(
            r#"
            SELECT DISTINCT ?subject WHERE {{
                GRAPH <{pod_iri}> {{
                    ?subject ?p ?o .
                    FILTER(STRSTARTS(STR(?subject), "ant://"))
                }}
            }}
            "#
        );
        debug!("Pod subjects query: {}", query);

        let mut subjects = Vec::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(ref_node)) = solution.get("subject") {
                    let ref_iri = ref_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = ref_iri.strip_prefix("ant://") {
                        subjects.push(address.to_string());
                    }
                }
            }
        }

        debug!("Found {} subjects in pod {}", subjects.len(), pod_address);
        Ok(subjects)
    }

    // Get all of the user's pods
    pub fn get_my_pods(&self, configuration_address: &str) -> Result<String, Error> {
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();

        // Query for all subjects that have HAS_ADDR_TYPE predicate with POD object
        // Returns all information stored in all graphs for these subjects
        let query = format!(
            r#"
            SELECT DISTINCT ?subject ?predicate ?object ?graph WHERE {{
                {{
                    SELECT DISTINCT ?subject WHERE {{
                        GRAPH <{configuration_iri}> {{
                            ?subject <{HAS_ADDR_TYPE}> <{POD}> .
                        }}
                    }}
                }}
                GRAPH ?graph {{
                    ?subject ?predicate ?object .
                }}
            }}
            ORDER BY ?subject ?predicate
            "#
        );
        debug!("My pods query: {}", query);
        let results = self.store.query(query.as_str()).unwrap_or_else(|e| {
            error!("Error executing advanced search query: {}", e);
            QueryResults::Solutions(QuerySolutionIter::new(
                std::sync::Arc::new([]),
                std::iter::empty(),
            ))
        });
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("My pods results: {}", json_str);

        Ok(json_str)
    }

    // Load TriG data into the graph database
    pub fn load_pod_into_graph(&mut self, pod_address: &str, trig_data: &str) -> Result<(), Error> {
        if !trig_data.trim().is_empty() {
            let pod_iri = format!("ant://{pod_address}");
            let pod_iri = pod_iri.as_str();
            let pod = NamedNodeRef::new(pod_iri)?;

            // Insert the graph if it wasn't already there
            self.store.insert_named_graph(pod)?;

            // Clear graph to receive new data
            self.store.clear_graph(pod)?;

            let data_reader = Cursor::new(trig_data);

            // Load the TriG data into the graph store
            self.store.load_from_reader(
                RdfParser::from_format(RdfFormat::TriG)
                    .without_named_graphs() // No named graphs allowed in the input
                    .with_default_graph(pod), // we put the file default graph inside of a named graph
                data_reader,
            )?;

            debug!("Successfully loaded TriG data into graph database");
        }

        Ok(())
    }

    // Browse all subjects on the network and return their name, @type, and description
    // ordered by pod depth
    pub fn browse(&self, limit: Option<u64>) -> Result<String, Error> {
        let limit_clause = if let Some(l) = limit {
            format!("LIMIT {l}")
        } else {
            String::new()
        };

        let query = format!(
            r#"
            SELECT DISTINCT ?subject ?name ?type ?description ?size ?graph ?depth WHERE {{
                GRAPH ?graph {{
                    ?subject <{HAS_NAME}> ?name .
                    OPTIONAL {{ ?subject <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> ?type . }}
                    OPTIONAL {{ ?subject <http://schema.org/description> ?description . }}
                    OPTIONAL {{ ?subject <http://schema.org/contentSize> ?size . }}
                }}
                OPTIONAL {{
                    # Look for depth in any graph (typically configuration graphs)
                    GRAPH ?config_graph {{
                        ?graph <{HAS_DEPTH}> ?depth .
                    }}
                }}
            }}
            ORDER BY ASC(COALESCE(?depth, 999999)) ?graph ?subject
            {limit_clause}
            "#
        );

        debug!("Browse query: {}", query);

        let results = self.store.query(query.as_str())?;
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("Browse results: {}", json_str);
        Ok(json_str)
    }

    // Search for content across all graphs
    pub fn search_content(&self, search_text: &str, limit: Option<u64>) -> Result<String, Error> {
        let limit_clause = if let Some(l) = limit {
            format!("LIMIT {l}")
        } else {
            String::new()
        };

        // Parse search text to handle quoted phrases and individual words
        let search_terms = Self::parse_search_terms(search_text);

        if search_terms.is_empty() {
            return Ok("[]".to_string()); // Return empty results for empty search
        }

        // Create filter conditions for subquery (OR logic)
        let mut subquery_term_filters = Vec::new();
        for term in &search_terms {
            let escaped_term = term.replace("\"", "\\\"");
            subquery_term_filters.push(format!(
                "CONTAINS(LCASE(STR(?filter_object)), LCASE(\"{escaped_term}\"))"
            ));
        }
        let subquery_combined_filter = subquery_term_filters.join(" || ");

        // Create individual term match expressions for counting
        let mut match_expressions = Vec::new();
        for term in &search_terms {
            let escaped_term = term.replace("\"", "\\\"");
            match_expressions.push(format!(
                "IF(CONTAINS(LCASE(STR(?object)), LCASE(\"{escaped_term}\")), 1, 0)"
            ));
        }
        let match_count_expr = match_expressions.join(" + ");

        let query = format!(
            r#"
            SELECT ?subject ?predicate ?object ?graph ?depth
                   (({match_count_expr}) AS ?match_count) WHERE {{
                {{
                    SELECT DISTINCT ?subject WHERE {{
                        GRAPH ?filter_graph {{
                            ?subject ?filter_predicate ?filter_object .
                            FILTER(isLiteral(?filter_object) && ({subquery_combined_filter}))
                        }}
                    }}
                }}
                GRAPH ?graph {{
                    ?subject ?predicate ?object .
                }}
                OPTIONAL {{
                    # Look for depth in any graph (typically configuration graphs)
                    GRAPH ?config_graph {{
                        ?graph <{HAS_DEPTH}> ?depth .
                    }}
                }}
            }}
            ORDER BY DESC(?match_count) ASC(COALESCE(?depth, 999999)) ?graph ?subject
            {limit_clause}
            "#
        );

        debug!("Enhanced search query: {}", query);

        let results = self.store.query(query.as_str()).unwrap_or_else(|e| {
            error!("Error executing enhanced search query: {}", e);
            QueryResults::Solutions(QuerySolutionIter::new(
                std::sync::Arc::new([]),
                std::iter::empty(),
            ))
        });
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("Enhanced search results: {}", json_str);
        Ok(json_str)
    }

    // Helper function to parse search terms, handling quoted phrases
    fn parse_search_terms(search_text: &str) -> Vec<String> {
        let mut terms = Vec::new();
        let chars = search_text.chars();
        let mut current_term = String::new();
        let mut in_quotes = false;

        for ch in chars {
            match ch {
                '"' => {
                    if in_quotes {
                        // End of quoted phrase
                        if !current_term.is_empty() {
                            terms.push(current_term.trim().to_string());
                            current_term.clear();
                        }
                        in_quotes = false;
                    } else {
                        // Start of quoted phrase - save any current term first
                        if !current_term.trim().is_empty() {
                            // Split any accumulated unquoted text into individual words
                            for word in current_term.split_whitespace() {
                                if !word.is_empty() {
                                    terms.push(word.to_string());
                                }
                            }
                            current_term.clear();
                        }
                        in_quotes = true;
                    }
                }
                ' ' | '\t' | '\n' | '\r' => {
                    if in_quotes {
                        // Inside quotes, preserve spaces
                        current_term.push(ch);
                    } else {
                        // Outside quotes, space separates terms
                        if !current_term.trim().is_empty() {
                            terms.push(current_term.trim().to_string());
                            current_term.clear();
                        }
                    }
                }
                _ => {
                    current_term.push(ch);
                }
            }
        }

        // Handle any remaining term
        if !current_term.trim().is_empty() {
            if in_quotes {
                // Unclosed quote - treat as quoted phrase anyway
                terms.push(current_term.trim().to_string());
            } else {
                // Split remaining unquoted text into words
                for word in current_term.split_whitespace() {
                    if !word.is_empty() {
                        terms.push(word.to_string());
                    }
                }
            }
        }

        terms
    }

    // Search for subjects by type
    //FIXME: order the results by pod depth
    pub fn search_by_type(&self, type_uri: &str, limit: Option<u64>) -> Result<String, Error> {
        let limit_clause = if let Some(l) = limit {
            format!("LIMIT {l}")
        } else {
            String::new()
        };

        let query = format!(
            r#"
            SELECT DISTINCT ?subject ?graph WHERE {{
                GRAPH ?graph {{
                    ?subject <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <{type_uri}> .
                }}
            }}
            ORDER BY ?graph ?subject
            {limit_clause}
            "#
        );

        debug!("Type search query: {}", query);

        let results = self.store.query(query.as_str()).unwrap_or_else(|e| {
            error!("Error executing advanced search query: {}", e);
            QueryResults::Solutions(QuerySolutionIter::new(
                std::sync::Arc::new([]),
                std::iter::empty(),
            ))
        });
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("Type search results: {}", json_str);
        Ok(json_str)
    }

    // Search for subjects with a specific predicate
    pub fn search_by_predicate(
        &self,
        predicate_uri: &str,
        limit: Option<u64>,
    ) -> Result<String, Error> {
        let limit_clause = if let Some(l) = limit {
            format!("LIMIT {l}")
        } else {
            String::new()
        };

        let query = format!(
            r#"
            SELECT DISTINCT ?subject ?object ?graph WHERE {{
                GRAPH ?graph {{
                    ?subject <{predicate_uri}> ?object .
                }}
            }}
            ORDER BY ?graph ?subject
            {limit_clause}
            "#
        );

        debug!("Predicate search query: {}", query);

        let results = self.store.query(query.as_str()).unwrap_or_else(|e| {
            error!("Error executing advanced search query: {}", e);
            QueryResults::Solutions(QuerySolutionIter::new(
                std::sync::Arc::new([]),
                std::iter::empty(),
            ))
        });
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("Predicate search results: {}", json_str);
        Ok(json_str)
    }

    // Advanced search with multiple criteria
    pub fn advanced_search(&self, query: &str) -> Result<String, Error> {
        debug!("Advanced search query: {}", query);

        let results = self.store.query(query).unwrap_or_else(|e| {
            error!("Error executing advanced search query: {}", e);
            QueryResults::Solutions(QuerySolutionIter::new(
                std::sync::Arc::new([]),
                std::iter::empty(),
            ))
        });
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("Advanced search results: {}", json_str);
        Ok(json_str)
    }

    // Advanced search with multiple criteria
    // FIXME: will need something like this for text search to handle fuzzy search and regexes
    pub fn query_builder(&self, criteria: &serde_json::Value) -> Result<String, Error> {
        // Build SPARQL query based on criteria
        let mut where_clauses = Vec::new();
        let mut filters = Vec::new();

        // Handle text search
        if let Some(text) = criteria.get("text").and_then(|v| v.as_str())
            && !text.is_empty()
        {
            where_clauses.push("?subject ?predicate ?object .".to_string());
            filters.push(format!(
                "FILTER(isLiteral(?object) && CONTAINS(LCASE(STR(?object)), LCASE(\"{}\")))",
                text.replace("\"", "\\\"")
            ));
        }

        // Handle type filter
        if let Some(type_uri) = criteria.get("type").and_then(|v| v.as_str())
            && !type_uri.is_empty()
        {
            where_clauses.push(format!(
                "?subject <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <{type_uri}> ."
            ));
        }

        // Handle predicate filter
        if let Some(predicate) = criteria.get("predicate").and_then(|v| v.as_str())
            && !predicate.is_empty()
        {
            where_clauses.push(format!("?subject <{predicate}> ?object ."));
        }

        // Handle pod filter (specific graph)
        if let Some(pod_address) = criteria.get("pod").and_then(|v| v.as_str())
            && !pod_address.is_empty()
        {
            let _pod_iri = if pod_address.starts_with("ant://") {
                pod_address.to_string()
            } else {
                format!("ant://{pod_address}")
            };
            // This will be used in the GRAPH clause
        }

        // Default to basic search if no criteria
        if where_clauses.is_empty() {
            where_clauses.push("?subject ?predicate ?object .".to_string());
        }

        let limit = criteria
            .get("limit")
            .and_then(|v| v.as_u64())
            .unwrap_or(100);

        let where_clause = where_clauses.join(" ");
        let filter_clause = if filters.is_empty() {
            "".to_string()
        } else {
            filters.join(" ")
        };

        let query = format!(
            r#"
            SELECT DISTINCT ?subject ?predicate ?object ?graph WHERE {{
                GRAPH ?graph {{
                    {where_clause}
                    {filter_clause}
                }}
            }}
            ORDER BY ?graph ?subject
            LIMIT {limit}
            "#
        );

        debug!("Advanced search query: {}", query);

        let results = self.store.query(query.as_str())?;
        let buffer = results.write(Vec::new(), QueryResultsFormat::Json)?;
        let json_str = String::from_utf8(buffer)?;

        debug!("Advanced search results: {}", json_str);
        Ok(json_str)
    }

    // Get all scratchpad addresses for a pod
    pub fn get_pod_scratchpads(&self, pod_address: &str) -> Result<Vec<String>, Error> {
        let pod_iri = format!("ant://{pod_address}");

        // Query for all scratchpad addresses in the pod's named graph
        let query = format!(
            "SELECT DISTINCT ?scratchpad ?index WHERE {{ GRAPH <{pod_iri}> {{ ?scratchpad <{HAS_INDEX}> ?index . }} }}"
        );
        debug!("Pod scratchpads query: {}", query);

        let mut triples = HashMap::new();
        let results = self.store.query(query.as_str())?;
        if let QueryResults::Solutions(solutions) = results {
            for solution in solutions.flatten() {
                if let Some(oxigraph::model::Term::NamedNode(scratchpad_node)) =
                    solution.get("scratchpad")
                {
                    let scratchpad_iri = scratchpad_node.as_str();
                    // Extract the address from the ant:// URI
                    if let Some(address) = scratchpad_iri.strip_prefix("ant://")
                        && let Some(oxigraph::model::Term::Literal(literal)) = solution.get("index")
                        && let Ok(index) = literal.value().parse::<u64>()
                    {
                        triples.insert(index, address.to_string());
                    }
                }
            }
        }

        // take the ordered addresses from the hashmap and map them to the scratchpads vector
        let mut scratchpads = Vec::new();
        for i in 0..triples.len() {
            if let Some(scratchpad) = triples.get(&(i as u64)) {
                // The address is already stripped of "ant://" prefix in line 661
                scratchpads.push(scratchpad.clone());
            } else {
                error!("Missing scratchpad at index {}", i);
            }
        }

        debug!(
            "Found {} scratchpads for pod {}",
            scratchpads.len(),
            pod_address
        );
        Ok(scratchpads)
    }

    pub fn get_pod_scratchpads_from_string(&self, data: &str) -> Result<Vec<String>, Error> {
        // Parse the TriG data and return a hashmap of the scratchpad addresses and their pod index
        let mut triples = HashMap::new();
        for triple in TriGParser::new().for_reader(data.as_bytes()) {
            // The last line will be garbage, so we just ignore it by passing a default quad
            let triple = triple.unwrap_or_else(|_e| {
                Quad::new(
                    NamedNodeRef::new("http://example.org/subject").unwrap(),
                    NamedNodeRef::new("http://example.org/predicate").unwrap(),
                    NamedNodeRef::new("http://example.org/object").unwrap(),
                    GraphNameRef::DefaultGraph,
                )
            });

            if triple.predicate == HAS_INDEX {
                // Convert the triple.object into a u64
                if let oxigraph::model::Term::Literal(literal) = triple.object
                    && let Ok(index) = literal.value().parse::<u64>()
                    && let oxigraph::model::Subject::NamedNode(scratchpad) = triple.subject
                {
                    triples.insert(index, scratchpad.into_string());
                }
            }
        }

        // take the ordered addresses from the hashmap and map them to the scratchpads vector
        let mut scratchpads = Vec::new();
        for i in 0..triples.len() {
            if let Some(scratchpad) = triples.get(&(i as u64)) {
                let address = scratchpad
                    .as_str()
                    .strip_prefix("ant://")
                    .unwrap_or_default();
                scratchpads.push(address.to_string());
            } else {
                error!("Missing scratchpad at index {}", i);
            }
        }

        Ok(scratchpads)
    }

    // Clear a specific pod graph
    pub fn clear_pod_graph(&mut self, pod_address: &str) -> Result<(), Error> {
        let pod_iri = format!("ant://{pod_address}");
        let pod_node = NamedNodeRef::new(&pod_iri)?;
        self.store.clear_graph(pod_node)?;
        debug!("Cleared graph for pod: {}", pod_address);
        Ok(())
    }

    pub fn use_free_pointer(
        &mut self,
        address: &str,
        configuration_address: &str,
    ) -> Result<(), Error> {
        let address_iri = format!("ant://{address}");
        let address_iri = address_iri.as_str();
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();

        // Remove the pod from the configuration graph
        let update = format!(
            "DELETE WHERE {{ GRAPH <{configuration_iri}> {{ <{address_iri}> <{HAS_ADDR_TYPE}> <{FREED_POD}> . }} }}"
        );
        debug!("Delete pod from configuration graph string: {}", update);
        self.store.update(update.as_str())?;
        Ok(())
    }

    pub fn use_free_scratchpad(
        &mut self,
        address: &str,
        configuration_address: &str,
    ) -> Result<(), Error> {
        let address_iri = format!("ant://{address}");
        let address_iri = address_iri.as_str();
        let configuration_iri = format!("ant://{configuration_address}");
        let configuration_iri = configuration_iri.as_str();

        // Remove the pod from the configuration graph
        let update = format!(
            "DELETE WHERE {{ GRAPH <{configuration_iri}> {{ <{address_iri}> <{HAS_ADDR_TYPE}> <{FREED_DATA}> . }} }}"
        );
        debug!("Delete pod from configuration graph string: {}", update);
        self.store.update(update.as_str())?;
        Ok(())
    }
}